feat: add session subagent cli control

This commit is contained in:
Codex
2026-06-03 11:27:55 +08:00
parent a40fdf6ab1
commit b761ef6713
12 changed files with 833 additions and 34 deletions
+50 -2
View File
@@ -1,10 +1,10 @@
import type { Server } from "node:http";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import type { AgentRunStore, ListQueueTasksInput } from "./store.js";
import type { AgentRunStore, ListQueueTasksInput, ListSessionsInput, SessionEventPageInput } from "./store.js";
import { openAgentRunStoreFromEnv } from "./store.js";
import { AgentRunError, errorToJson } from "../common/errors.js";
import { asRecord, validateCreateCommand, validateCreateQueueTask, validateCreateRun, validateQueueTaskState } from "../common/validation.js";
import { asRecord, validateBackendProfile, validateCreateCommand, validateCreateQueueTask, validateCreateRun, validateQueueTaskState, validateSessionListState } from "../common/validation.js";
import type { ApiErrorBody, ApiOkBody, JsonRecord, JsonValue, RunEvent } from "../common/types.js";
import { createKubernetesRunnerJob } from "./kubernetes-runner-job.js";
import { dispatchQueueTask, refreshQueueTaskFromCore } from "./queue-dispatch.js";
@@ -69,6 +69,54 @@ async function route({ method, url, body, store, sourceCommit, runnerJobDefaults
return { serviceId: "agentrun-mgr", live: true, ready, database, sourceCommit, secretRefs: { databaseUrl: database.adapter === "postgres" ? "redacted" : "not-used", valuesPrinted: false } };
}
if (method === "GET" && path === "/api/v1/backends") return { items: await store.backends() as unknown as JsonValue };
if (method === "GET" && path === "/api/v1/sessions") {
const input: ListSessionsInput = { limit: integerQuery(url, "limit", 50) };
const state = url.searchParams.get("state");
const backendProfile = url.searchParams.get("backendProfile") ?? url.searchParams.get("profile");
const readerId = url.searchParams.get("readerId");
const cursor = url.searchParams.get("cursor");
if (state) input.state = validateSessionListState(state);
if (backendProfile) input.backendProfile = validateBackendProfile(backendProfile);
if (readerId) input.readerId = readerId;
if (cursor) input.cursor = cursor;
return await store.listSessions(input) as unknown as JsonValue;
}
const sessionMatch = path.match(/^\/api\/v1\/sessions\/([^/]+)$/u);
if (method === "GET" && sessionMatch) return await store.getSessionSummary(sessionMatch[1] ?? "", url.searchParams.get("readerId")) as unknown as JsonValue;
const sessionTraceMatch = path.match(/^\/api\/v1\/sessions\/([^/]+)\/trace$/u);
if (method === "GET" && sessionTraceMatch) {
const input: SessionEventPageInput = { afterSeq: integerQuery(url, "afterSeq", 0), limit: integerQuery(url, "limit", 100) };
const runId = url.searchParams.get("runId");
if (runId) input.runId = runId;
return await store.listSessionTrace(sessionTraceMatch[1] ?? "", input) as unknown as JsonValue;
}
const sessionOutputMatch = path.match(/^\/api\/v1\/sessions\/([^/]+)\/output$/u);
if (method === "GET" && sessionOutputMatch) {
const input: SessionEventPageInput = { afterSeq: integerQuery(url, "afterSeq", 0), limit: integerQuery(url, "limit", 100) };
const runId = url.searchParams.get("runId");
if (runId) input.runId = runId;
return await store.listSessionOutput(sessionOutputMatch[1] ?? "", input) as unknown as JsonValue;
}
const sessionReadMatch = path.match(/^\/api\/v1\/sessions\/([^/]+)\/read$/u);
if (method === "POST" && sessionReadMatch) {
const record = body === null ? {} : asRecord(body, "read");
const readerId = typeof record.readerId === "string" && record.readerId.trim().length > 0 ? record.readerId.trim() : "cli";
return await store.markSessionRead(sessionReadMatch[1] ?? "", readerId) as unknown as JsonValue;
}
const sessionControlMatch = path.match(/^\/api\/v1\/sessions\/([^/]+)\/control$/u);
if (method === "POST" && sessionControlMatch) {
const record = asRecord(body ?? {}, "sessionControl");
const action = typeof record.action === "string" ? record.action : "";
const session = await store.getSessionSummary(sessionControlMatch[1] ?? "", typeof record.readerId === "string" ? record.readerId : null);
if (action === "read") return await store.markSessionRead(session.sessionId, typeof record.readerId === "string" && record.readerId.trim().length > 0 ? record.readerId.trim() : "cli") as unknown as JsonValue;
if (action === "cancel") {
const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : undefined;
if (session.activeCommandId) return await store.cancelCommand(session.activeCommandId, reason) as unknown as JsonValue;
if (session.activeRunId) return await store.cancelRun(session.activeRunId, reason) as unknown as JsonValue;
throw new AgentRunError("schema-invalid", `session ${session.sessionId} has no active run or command`, { httpStatus: 409 });
}
throw new AgentRunError("schema-invalid", `session control action ${action} is not supported`, { httpStatus: 400 });
}
if (method === "POST" && path === "/api/v1/queue/tasks") return await store.createQueueTask(validateCreateQueueTask(body)) as unknown as JsonValue;
if (method === "GET" && path === "/api/v1/queue/tasks") {
const state = url.searchParams.get("state");