From 8aa92ea728a8fc60e73e07c7f2f3f80779a81c20 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 21 May 2026 19:33:24 +0300 Subject: [PATCH] Task 007 add agent job admin runner follow-up --- .../test_schema_storage_contracts.py | 12 + apps/frontend/package.json | 2 +- apps/frontend/src/app/admin/jobs/page.tsx | 3 + .../frontend/src/features/agent-jobs/model.ts | 33 ++ apps/frontend/src/features/agent-jobs/ui.tsx | 51 ++ apps/frontend/src/pages/admin-jobs/index.tsx | 28 ++ apps/frontend/src/shared/pipeline-api.ts | 42 ++ .../src/widgets/role-navigation/model.ts | 6 + apps/frontend/tests/agent_jobs.model.test.mjs | 49 ++ apps/frontend/tests/role-navigation.test.mjs | 4 + apps/runner/src/application/fake_runner.py | 29 ++ apps/runner/src/presentation/main.py | 34 +- apps/runner/tests/test_fake_runner.py | 89 ++++ docker-compose.yml | 4 + packages/shared/openapi.json | 442 +++++++++++++++++- packages/shared/src/api-types.ts | 27 +- 16 files changed, 826 insertions(+), 29 deletions(-) create mode 100644 apps/frontend/src/app/admin/jobs/page.tsx create mode 100644 apps/frontend/src/features/agent-jobs/model.ts create mode 100644 apps/frontend/src/features/agent-jobs/ui.tsx create mode 100644 apps/frontend/src/pages/admin-jobs/index.tsx create mode 100644 apps/frontend/tests/agent_jobs.model.test.mjs create mode 100644 apps/runner/tests/test_fake_runner.py diff --git a/apps/backend/tests/integration/test_schema_storage_contracts.py b/apps/backend/tests/integration/test_schema_storage_contracts.py index d345b36..5d29de6 100644 --- a/apps/backend/tests/integration/test_schema_storage_contracts.py +++ b/apps/backend/tests/integration/test_schema_storage_contracts.py @@ -75,6 +75,18 @@ class SchemaStorageContractsIntegrationTest(unittest.TestCase): "transform_script", }.issubset(repository.schema.list_columns("script_config_versions")) ) + self.assertTrue( + { + "parent_job_id", + "attempt", + "stdout", + "stderr", + "exit_code", + "duration_ms", + "workspace_path", + "output_files", + }.issubset(repository.schema.list_columns("agent_jobs")) + ) self.assertTrue( { "s3_prefix", diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 41b8229..932f6cf 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -6,7 +6,7 @@ "build": "next build", "start": "next start", "test:roles": "node tests/role-navigation.test.mjs", - "test:ui": "node tests/role-navigation.test.mjs && node tests/article_dashboard.test.mjs && node tests/article_form_validation.test.mjs && node tests/admin_script_versions.model.test.mjs", + "test:ui": "node tests/role-navigation.test.mjs && node tests/article_dashboard.test.mjs && node tests/article_form_validation.test.mjs && node tests/admin_script_versions.model.test.mjs && node tests/agent_jobs.model.test.mjs", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/apps/frontend/src/app/admin/jobs/page.tsx b/apps/frontend/src/app/admin/jobs/page.tsx new file mode 100644 index 0000000..f99ac85 --- /dev/null +++ b/apps/frontend/src/app/admin/jobs/page.tsx @@ -0,0 +1,3 @@ +import AdminJobsPage from "@/pages/admin-jobs"; + +export default AdminJobsPage; diff --git a/apps/frontend/src/features/agent-jobs/model.ts b/apps/frontend/src/features/agent-jobs/model.ts new file mode 100644 index 0000000..a859a3d --- /dev/null +++ b/apps/frontend/src/features/agent-jobs/model.ts @@ -0,0 +1,33 @@ +import type { AgentJobSummary } from "@pipeline/shared"; + +export type AgentJobRow = { + id: string; + type: string; + status: string; + attempt: number; + parentJobId: string | null; + workspacePath: string | null; + stdout: string; + stderr: string; + exitCode: number | null; + durationMs: number | null; + outputFiles: string; + error: string; +}; + +export function buildAgentJobRows(jobs: readonly AgentJobSummary[]): AgentJobRow[] { + return jobs.map((job) => ({ + id: job.id, + type: job.job_type, + status: job.status, + attempt: job.attempt, + parentJobId: job.parent_job_id ?? null, + workspacePath: job.workspace_path ?? null, + stdout: job.stdout, + stderr: job.stderr, + exitCode: job.exit_code ?? null, + durationMs: job.duration_ms ?? null, + outputFiles: job.output_files.map((file) => file.path).join(", "), + error: job.error_category ?? job.error_message ?? "", + })); +} diff --git a/apps/frontend/src/features/agent-jobs/ui.tsx b/apps/frontend/src/features/agent-jobs/ui.tsx new file mode 100644 index 0000000..4a33dcb --- /dev/null +++ b/apps/frontend/src/features/agent-jobs/ui.tsx @@ -0,0 +1,51 @@ +import type { AgentJobRow } from "./model"; + +type AgentJobsTableProps = { + rows: readonly AgentJobRow[]; +}; + +export function AgentJobsTable({ rows }: AgentJobsTableProps) { + if (rows.length === 0) { + return

No agent jobs yet.

; + } + + return ( + + + + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + + + ))} + +
JobStatusAttemptRuntimeWorkspaceLogsOutputs
+
{row.type}
+ {row.id} + {row.parentJobId ?
retry of {row.parentJobId}
: null} +
{row.status}{row.attempt} +
exit: {row.exitCode ?? "pending"}
+
{row.durationMs ?? 0} ms
+ {row.error ?
{row.error}
: null} +
{row.workspacePath ?? "pending"} +
{row.stdout || "stdout pending"}
+ {row.stderr ?
{row.stderr}
: null} +
{row.outputFiles || "none"}
+ ); +} diff --git a/apps/frontend/src/pages/admin-jobs/index.tsx b/apps/frontend/src/pages/admin-jobs/index.tsx new file mode 100644 index 0000000..11f2178 --- /dev/null +++ b/apps/frontend/src/pages/admin-jobs/index.tsx @@ -0,0 +1,28 @@ +import Link from "next/link"; + +import { AgentJobsTable } from "@/features/agent-jobs/ui"; +import { buildAgentJobRows } from "@/features/agent-jobs/model"; +import { fetchAgentJobs } from "@/shared/pipeline-api"; +import { RoleNavigation } from "@/widgets/role-navigation"; + +export default async function AdminJobsPage() { + const jobs = await fetchAgentJobs(); + const rows = buildAgentJobRows(jobs.jobs); + + return ( +
+
+
+

Agent Jobs

+

Monitor runner status, logs, outputs, and retry lineage.

+
+ Back to dashboard + Admin demo +
+ +
+ +
+
+ ); +} diff --git a/apps/frontend/src/shared/pipeline-api.ts b/apps/frontend/src/shared/pipeline-api.ts index 127234a..1f8393b 100644 --- a/apps/frontend/src/shared/pipeline-api.ts +++ b/apps/frontend/src/shared/pipeline-api.ts @@ -2,6 +2,9 @@ import type { ArticleCreateRequest, ArticleCreateResponse, ArticleDetailResponse, + AgentJobListResponse, + AgentJobResponse, + AgentJobTestCodexRequest, ArticleListResponse, CurrentUserResponse, ScriptConfigVersionCreateRequest, @@ -149,6 +152,45 @@ export function fetchCurrentUser(): Promise { return apiGet("/api/me"); } +export function fetchAgentJobs( + userEmail = DEMO_ADMIN_EMAIL, +): Promise { + return apiGet("/api/agent-jobs", { userEmail }); +} + +export function createTestCodexJob( + request: AgentJobTestCodexRequest = { fake_result: "valid" }, + userEmail = DEMO_ADMIN_EMAIL, +): Promise { + return apiPost( + "/api/agent-jobs/test-codex", + request, + { userEmail }, + ); +} + +export function retryAgentJob( + jobId: string, + userEmail = DEMO_ADMIN_EMAIL, +): Promise { + return apiPost( + `/api/agent-jobs/${jobId}/retry`, + undefined, + { userEmail }, + ); +} + +export function cancelAgentJob( + jobId: string, + userEmail = DEMO_ADMIN_EMAIL, +): Promise { + return apiPost( + `/api/agent-jobs/${jobId}/cancel`, + undefined, + { userEmail }, + ); +} + export function fetchTargetSites(): Promise { return apiGet("/api/sites"); } diff --git a/apps/frontend/src/widgets/role-navigation/model.ts b/apps/frontend/src/widgets/role-navigation/model.ts index 83f4fb6..d2b5e85 100644 --- a/apps/frontend/src/widgets/role-navigation/model.ts +++ b/apps/frontend/src/widgets/role-navigation/model.ts @@ -38,6 +38,12 @@ export const ROLE_NAVIGATION_ITEMS = [ href: "/admin/scripts", roles: ["ADMIN"], }, + { + id: "agent-jobs", + label: "Agent jobs", + href: "/admin/jobs", + roles: ["ADMIN"], + }, ] as const satisfies readonly NavigationItem[]; export function getRoleNavigationItems(role: Role): NavigationItem[] { diff --git a/apps/frontend/tests/agent_jobs.model.test.mjs b/apps/frontend/tests/agent_jobs.model.test.mjs new file mode 100644 index 0000000..b7fcb12 --- /dev/null +++ b/apps/frontend/tests/agent_jobs.model.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import ts from "typescript"; + +const testDir = path.dirname(fileURLToPath(import.meta.url)); +const sourcePath = path.resolve(testDir, "../src/features/agent-jobs/model.ts"); +const source = readFileSync(sourcePath, "utf8"); +const compiled = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, +}); + +const moduleExports = {}; +new Function("exports", compiled.outputText)(moduleExports); + +const { buildAgentJobRows } = moduleExports; + +const rows = buildAgentJobRows([ + { + id: "11111111-1111-1111-1111-111111111111", + article_id: null, + parent_job_id: "00000000-0000-0000-0000-000000000000", + attempt: 2, + job_type: "TEST_CODEX", + agent_profile: "fake-codex", + status: "SUCCEEDED", + workspace_path: "/tmp/job", + input_files: [{ path: "inputs/job.json" }], + output_files: [{ path: "outputs/result.json" }], + error_category: null, + error_message: null, + stdout: "done", + stderr: "", + exit_code: 0, + duration_ms: 5, + queued_at: "2026-05-21T00:00:00Z", + started_at: "2026-05-21T00:00:01Z", + finished_at: "2026-05-21T00:00:02Z", + }, +]); + +assert.equal(rows.length, 1); +assert.equal(rows[0].status, "SUCCEEDED"); +assert.equal(rows[0].attempt, 2); +assert.equal(rows[0].parentJobId, "00000000-0000-0000-0000-000000000000"); +assert.equal(rows[0].outputFiles, "outputs/result.json"); +assert.equal(rows[0].stdout, "done"); diff --git a/apps/frontend/tests/role-navigation.test.mjs b/apps/frontend/tests/role-navigation.test.mjs index 5521882..cde1428 100644 --- a/apps/frontend/tests/role-navigation.test.mjs +++ b/apps/frontend/tests/role-navigation.test.mjs @@ -36,5 +36,9 @@ assert.equal( adminItems.some((item) => item.id === "script-versions"), true, ); +assert.equal( + adminItems.some((item) => item.id === "agent-jobs"), + true, +); console.log("role navigation hides Admin-only items for Editors"); diff --git a/apps/runner/src/application/fake_runner.py b/apps/runner/src/application/fake_runner.py index eea0f35..8446bed 100644 --- a/apps/runner/src/application/fake_runner.py +++ b/apps/runner/src/application/fake_runner.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os import shutil import time from pathlib import Path @@ -90,6 +91,34 @@ def run_fake_runner_once( ) +def run_fake_runner_loop( + *, + backend_url: str, + workspace_root: Path, + poll_interval_seconds: float, +) -> None: + backend_client = HttpBackendJobClient(backend_url) + while True: + try: + run_fake_runner_once(backend_client, workspace_root=workspace_root) + except Exception: + pass + time.sleep(poll_interval_seconds) + + +def get_backend_url() -> str: + backend_url = os.environ.get("BACKEND_URL") + if not backend_url: + raise RuntimeError("BACKEND_URL is required for fake runner") + return backend_url + + +def get_workspace_root() -> Path: + return Path( + os.environ.get("RUNNER_WORKSPACE_ROOT", "/tmp/pipeline-runner-workspaces") + ) + + def materialize_fake_workspace( job: dict[str, Any], *, diff --git a/apps/runner/src/presentation/main.py b/apps/runner/src/presentation/main.py index 41ebbfb..30f0b4e 100644 --- a/apps/runner/src/presentation/main.py +++ b/apps/runner/src/presentation/main.py @@ -6,7 +6,13 @@ import threading from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from src.application.fake_runner import get_backend_url, get_workspace_root, run_fake_runner_loop +from src.application.fake_runner import ( + HttpBackendJobClient, + get_backend_url, + get_workspace_root, + run_fake_runner_loop, + run_fake_runner_once, +) class HealthHandler(BaseHTTPRequestHandler): @@ -22,6 +28,32 @@ class HealthHandler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body) + def do_POST(self) -> None: + if self.path != "/run-once": + self.send_error(HTTPStatus.NOT_FOUND) + return + + try: + result = run_fake_runner_once( + HttpBackendJobClient(get_backend_url()), + workspace_root=get_workspace_root(), + ) + except Exception as error: + body = json.dumps({"status": "error", "detail": str(error)}).encode("utf-8") + self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + body = json.dumps({"status": "ok", "job": result}, default=str).encode("utf-8") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + def log_message(self, format: str, *args: object) -> None: return diff --git a/apps/runner/tests/test_fake_runner.py b/apps/runner/tests/test_fake_runner.py new file mode 100644 index 0000000..0dfbef9 --- /dev/null +++ b/apps/runner/tests/test_fake_runner.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import tempfile +import sys +import unittest +from pathlib import Path +from typing import Any + + +REPO_ROOT = Path(__file__).resolve().parents[3] +RUNNER_ROOT = REPO_ROOT / "apps" / "runner" +sys.path.insert(0, str(RUNNER_ROOT)) + +from src.application.fake_runner import run_fake_runner_once + + +class FakeBackendJobClient: + def __init__(self, job: dict[str, Any] | None) -> None: + self.job = job + self.completed: dict[str, Any] | None = None + + def claim_job(self) -> dict[str, Any] | None: + job = self.job + self.job = None + return job + + def complete_job(self, job_id: str, payload: dict[str, Any]) -> dict[str, Any]: + self.completed = {"id": job_id, **payload} + return self.completed + + +class FakeRunnerTest(unittest.TestCase): + def test_fake_runner_claims_job_writes_workspace_and_completes(self) -> None: + backend = FakeBackendJobClient( + { + "id": "11111111-1111-1111-1111-111111111111", + "agent_profile": "fake-codex", + } + ) + + with tempfile.TemporaryDirectory() as temp_dir: + result = run_fake_runner_once( + backend, + workspace_root=Path(temp_dir), + ) + self.assertIsNotNone(result) + assert result is not None + workspace_path = Path(result["workspace_path"]) + self.assertTrue((workspace_path / "inputs" / "job.json").is_file()) + self.assertTrue((workspace_path / "logs" / "stdout.log").is_file()) + self.assertTrue((workspace_path / "logs" / "stderr.log").is_file()) + self.assertTrue((workspace_path / "outputs" / "result.json").is_file()) + self.assertEqual(0, result["exit_code"]) + self.assertEqual("SUCCEEDED", result["output"]["status"]) + self.assertEqual([{"path": "outputs/result.json"}], result["output"]["output_files"]) + + def test_fake_runner_can_emit_invalid_schema_fixture(self) -> None: + backend = FakeBackendJobClient( + { + "id": "22222222-2222-2222-2222-222222222222", + "agent_profile": "fake-invalid-schema", + } + ) + + with tempfile.TemporaryDirectory() as temp_dir: + result = run_fake_runner_once( + backend, + workspace_root=Path(temp_dir), + ) + + self.assertIsNotNone(result) + assert result is not None + self.assertEqual({"status": "NOT_A_STATUS"}, result["output"]) + + def test_fake_runner_noops_without_queued_job(self) -> None: + backend = FakeBackendJobClient(None) + + with tempfile.TemporaryDirectory() as temp_dir: + result = run_fake_runner_once( + backend, + workspace_root=Path(temp_dir), + ) + + self.assertIsNone(result) + self.assertIsNone(backend.completed) + + +if __name__ == "__main__": + unittest.main() diff --git a/docker-compose.yml b/docker-compose.yml index 17c54f6..18dd705 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,8 +41,12 @@ services: dockerfile: apps/runner/Dockerfile environment: RUNNER_MODE: fake + BACKEND_URL: http://backend:8000 ports: - "${RUNNER_PORT:-8010}:8010" + depends_on: + backend: + condition: service_started postgres: image: postgres:16-alpine diff --git a/packages/shared/openapi.json b/packages/shared/openapi.json index 3fc44b3..3ebcd10 100644 --- a/packages/shared/openapi.json +++ b/packages/shared/openapi.json @@ -19,6 +19,23 @@ "title": "AgentJobErrorCategory", "type": "string" }, + "AgentJobListResponse": { + "additionalProperties": false, + "properties": { + "jobs": { + "items": { + "$ref": "#/components/schemas/AgentJobSummary" + }, + "title": "Jobs", + "type": "array" + } + }, + "required": [ + "jobs" + ], + "title": "AgentJobListResponse", + "type": "object" + }, "AgentJobOutput": { "additionalProperties": false, "properties": { @@ -74,6 +91,19 @@ "title": "AgentJobOutput", "type": "object" }, + "AgentJobResponse": { + "additionalProperties": false, + "properties": { + "job": { + "$ref": "#/components/schemas/AgentJobSummary" + } + }, + "required": [ + "job" + ], + "title": "AgentJobResponse", + "type": "object" + }, "AgentJobStatus": { "enum": [ "QUEUED", @@ -94,9 +124,36 @@ "type": "string" }, "article_id": { - "format": "uuid", - "title": "Article Id", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Article Id" + }, + "attempt": { + "default": 1, + "minimum": 1, + "title": "Attempt", + "type": "integer" + }, + "duration_ms": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Duration Ms" }, "error_category": { "anyOf": [ @@ -121,6 +178,18 @@ "default": null, "title": "Error Message" }, + "exit_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Exit Code" + }, "finished_at": { "anyOf": [ { @@ -156,6 +225,24 @@ "title": "Output Files", "type": "array" }, + "parent_job_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parent Job Id" + }, + "queued_at": { + "format": "date-time", + "title": "Queued At", + "type": "string" + }, "started_at": { "anyOf": [ { @@ -172,6 +259,16 @@ "status": { "$ref": "#/components/schemas/AgentJobStatus" }, + "stderr": { + "default": "", + "title": "Stderr", + "type": "string" + }, + "stdout": { + "default": "", + "title": "Stdout", + "type": "string" + }, "workspace_path": { "anyOf": [ { @@ -187,14 +284,27 @@ }, "required": [ "id", - "article_id", "job_type", "agent_profile", - "status" + "status", + "queued_at" ], "title": "AgentJobSummary", "type": "object" }, + "AgentJobTestCodexRequest": { + "additionalProperties": false, + "properties": { + "fake_result": { + "default": "valid", + "pattern": "^(valid|invalid_schema)$", + "title": "Fake Result", + "type": "string" + } + }, + "title": "AgentJobTestCodexRequest", + "type": "object" + }, "AgentJobType": { "enum": [ "BOUNDARY_QUESTIONS", @@ -1563,16 +1673,6 @@ "ScriptConfigVersionSummary": { "additionalProperties": false, "properties": { - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" - }, - "created_by": { - "format": "uuid", - "title": "Created By", - "type": "string" - }, "activated_at": { "anyOf": [ { @@ -1583,8 +1683,19 @@ "type": "null" } ], + "default": null, "title": "Activated At" }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "created_by": { + "format": "uuid", + "title": "Created By", + "type": "string" + }, "diff": { "additionalProperties": true, "title": "Diff", @@ -1600,14 +1711,6 @@ "title": "Publishing Yaml Hash", "type": "string" }, - "status": { - "$ref": "#/components/schemas/ScriptConfigVersionStatus" - }, - "target_site_id": { - "format": "uuid", - "title": "Target Site Id", - "type": "string" - }, "rollback_target_version_id": { "anyOf": [ { @@ -1618,8 +1721,17 @@ "type": "null" } ], + "default": null, "title": "Rollback Target Version Id" }, + "status": { + "$ref": "#/components/schemas/ScriptConfigVersionStatus" + }, + "target_site_id": { + "format": "uuid", + "title": "Target Site Id", + "type": "string" + }, "transform_script_hash": { "minLength": 1, "title": "Transform Script Hash", @@ -2116,6 +2228,290 @@ }, "openapi": "3.1.0", "paths": { + "/api/agent-jobs": { + "get": { + "operationId": "get_agent_jobs_api_agent_jobs_get", + "parameters": [ + { + "in": "header", + "name": "X-Demo-User-Email", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Demo-User-Email" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentJobListResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Get Agent Jobs", + "tags": [ + "agent-jobs" + ] + } + }, + "/api/agent-jobs/test-codex": { + "post": { + "operationId": "post_test_codex_job_api_agent_jobs_test_codex_post", + "parameters": [ + { + "in": "header", + "name": "X-Demo-User-Email", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Demo-User-Email" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentJobTestCodexRequest" + } + } + } + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentJobResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Post Test Codex Job", + "tags": [ + "agent-jobs" + ] + } + }, + "/api/agent-jobs/{job_id}": { + "get": { + "operationId": "get_agent_job_route_api_agent_jobs__job_id__get", + "parameters": [ + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Demo-User-Email", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Demo-User-Email" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentJobResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Get Agent Job Route", + "tags": [ + "agent-jobs" + ] + } + }, + "/api/agent-jobs/{job_id}/cancel": { + "post": { + "operationId": "post_cancel_agent_job_api_agent_jobs__job_id__cancel_post", + "parameters": [ + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Demo-User-Email", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Demo-User-Email" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentJobResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Post Cancel Agent Job", + "tags": [ + "agent-jobs" + ] + } + }, + "/api/agent-jobs/{job_id}/retry": { + "post": { + "operationId": "post_retry_agent_job_api_agent_jobs__job_id__retry_post", + "parameters": [ + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Demo-User-Email", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Demo-User-Email" + } + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentJobResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Post Retry Agent Job", + "tags": [ + "agent-jobs" + ] + } + }, "/api/articles": { "get": { "operationId": "list_articles_route_api_articles_get", diff --git a/packages/shared/src/api-types.ts b/packages/shared/src/api-types.ts index 3c16450..9854aef 100644 --- a/packages/shared/src/api-types.ts +++ b/packages/shared/src/api-types.ts @@ -3,6 +3,10 @@ export type AgentJobErrorCategory = "FAILED_SCHEMA_VALIDATION" | "CLI_EXIT_CODE_FAILURE" | "TIMEOUT" | "MISSING_OUTPUT_FILE" | "UNSUPPORTED_CLAIMS_FOUND" | "SOURCE_RETRIEVAL_FAILED" | "RESEARCH_ARTIFACT_UPLOAD_FAILED" | "PUBLISH_DRY_RUN_FAILED" | "GIT_CHECKOUT_FAILED" | "GIT_COMMIT_FAILED" | "GIT_PUSH_NON_FAST_FORWARD" | "PUBLISH_VERIFICATION_FAILED"; +export type AgentJobListResponse = { + jobs: AgentJobSummary[]; +}; + export type AgentJobOutput = { artifacts?: ResearchArtifactSummary[]; error_category?: AgentJobErrorCategory | null; @@ -12,23 +16,38 @@ export type AgentJobOutput = { status: AgentJobStatus; }; +export type AgentJobResponse = { + job: AgentJobSummary; +}; + export type AgentJobStatus = "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | "CANCELLED"; export type AgentJobSummary = { agent_profile: string; - article_id: string; + article_id?: string | null; + attempt?: number; + duration_ms?: number | null; error_category?: AgentJobErrorCategory | null; error_message?: string | null; + exit_code?: number | null; finished_at?: string | null; id: string; input_files?: RunnerFileRef[]; job_type: AgentJobType; output_files?: RunnerFileRef[]; + parent_job_id?: string | null; + queued_at: string; started_at?: string | null; status: AgentJobStatus; + stderr?: string; + stdout?: string; workspace_path?: string | null; }; +export type AgentJobTestCodexRequest = { + fake_result?: string; +}; + export type AgentJobType = "BOUNDARY_QUESTIONS" | "PLAN_GENERATION" | "RESEARCH" | "EVIDENCE_MATRIX" | "SECTION_SCAFFOLD" | "DRAFT_ASSEMBLY" | "SEO_REVIEW" | "LANGUAGE_REVIEW" | "PUBLISH_DRY_RUN" | "PUBLISH_COMMIT" | "TEST_CODEX"; export type ArticleCreateRequest = { @@ -266,15 +285,15 @@ export type ScriptConfigVersionResponse = { export type ScriptConfigVersionStatus = "DRAFT" | "ACTIVE" | "DEPRECATED"; export type ScriptConfigVersionSummary = { + activated_at?: string | null; created_at: string; created_by: string; - activated_at?: string | null; - diff: Record; + diff?: Record; id: string; publishing_yaml_hash: string; + rollback_target_version_id?: string | null; status: ScriptConfigVersionStatus; target_site_id: string; - rollback_target_version_id?: string | null; transform_script_hash: string; version: number; };