Task 007 add agent job admin runner follow-up

This commit is contained in:
2026-05-21 19:33:24 +03:00
parent 4fd837349f
commit 8aa92ea728
16 changed files with 826 additions and 29 deletions
@@ -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",
+1 -1
View File
@@ -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": {
@@ -0,0 +1,3 @@
import AdminJobsPage from "@/pages/admin-jobs";
export default AdminJobsPage;
@@ -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 ?? "",
}));
}
@@ -0,0 +1,51 @@
import type { AgentJobRow } from "./model";
type AgentJobsTableProps = {
rows: readonly AgentJobRow[];
};
export function AgentJobsTable({ rows }: AgentJobsTableProps) {
if (rows.length === 0) {
return <p className="emptyState">No agent jobs yet.</p>;
}
return (
<table className="table">
<thead>
<tr>
<th>Job</th>
<th>Status</th>
<th>Attempt</th>
<th>Runtime</th>
<th>Workspace</th>
<th>Logs</th>
<th>Outputs</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>
<div>{row.type}</div>
<small>{row.id}</small>
{row.parentJobId ? <div>retry of {row.parentJobId}</div> : null}
</td>
<td>{row.status}</td>
<td>{row.attempt}</td>
<td>
<div>exit: {row.exitCode ?? "pending"}</div>
<div>{row.durationMs ?? 0} ms</div>
{row.error ? <div>{row.error}</div> : null}
</td>
<td>{row.workspacePath ?? "pending"}</td>
<td>
<pre className="logBlock">{row.stdout || "stdout pending"}</pre>
{row.stderr ? <pre className="logBlock">{row.stderr}</pre> : null}
</td>
<td>{row.outputFiles || "none"}</td>
</tr>
))}
</tbody>
</table>
);
}
@@ -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 (
<main>
<header className="pageHeader">
<div>
<h1>Agent Jobs</h1>
<p>Monitor runner status, logs, outputs, and retry lineage.</p>
</div>
<Link href="/">Back to dashboard</Link>
<span className="roleBadge">Admin demo</span>
</header>
<RoleNavigation role="ADMIN" />
<section className="panel">
<AgentJobsTable rows={rows} />
</section>
</main>
);
}
+42
View File
@@ -2,6 +2,9 @@ import type {
ArticleCreateRequest,
ArticleCreateResponse,
ArticleDetailResponse,
AgentJobListResponse,
AgentJobResponse,
AgentJobTestCodexRequest,
ArticleListResponse,
CurrentUserResponse,
ScriptConfigVersionCreateRequest,
@@ -149,6 +152,45 @@ export function fetchCurrentUser(): Promise<CurrentUserResponse> {
return apiGet<CurrentUserResponse>("/api/me");
}
export function fetchAgentJobs(
userEmail = DEMO_ADMIN_EMAIL,
): Promise<AgentJobListResponse> {
return apiGet<AgentJobListResponse>("/api/agent-jobs", { userEmail });
}
export function createTestCodexJob(
request: AgentJobTestCodexRequest = { fake_result: "valid" },
userEmail = DEMO_ADMIN_EMAIL,
): Promise<AgentJobResponse> {
return apiPost<AgentJobTestCodexRequest, AgentJobResponse>(
"/api/agent-jobs/test-codex",
request,
{ userEmail },
);
}
export function retryAgentJob(
jobId: string,
userEmail = DEMO_ADMIN_EMAIL,
): Promise<AgentJobResponse> {
return apiPost<unknown, AgentJobResponse>(
`/api/agent-jobs/${jobId}/retry`,
undefined,
{ userEmail },
);
}
export function cancelAgentJob(
jobId: string,
userEmail = DEMO_ADMIN_EMAIL,
): Promise<AgentJobResponse> {
return apiPost<unknown, AgentJobResponse>(
`/api/agent-jobs/${jobId}/cancel`,
undefined,
{ userEmail },
);
}
export function fetchTargetSites(): Promise<TargetSiteConfigResponse[]> {
return apiGet<TargetSiteConfigResponse[]>("/api/sites");
}
@@ -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[] {
@@ -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");
@@ -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");
@@ -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],
*,
+33 -1
View File
@@ -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
+89
View File
@@ -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()
+4
View File
@@ -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
+417 -21
View File
@@ -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,10 +124,37 @@
"type": "string"
},
"article_id": {
"anyOf": [
{
"format": "uuid",
"title": "Article Id",
"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",
+23 -4
View File
@@ -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<string, unknown>;
diff?: Record<string, unknown>;
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;
};