@sema-agent/server 7.79.3 → 7.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/stage-07-capability-layer.js +2 -1
- package/dist/capabilities/skills.d.ts +10 -0
- package/dist/capabilities/skills.js +3 -1
- package/dist/http/admission.js +3 -1
- package/dist/http/dispatch.js +28 -8
- package/dist/http/resume-legs.js +4 -3
- package/dist/http/route-ctx.d.ts +101 -4
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/route-table.d.ts +71 -43
- package/dist/http/route-table.js +111 -187
- package/dist/http/routes/a2a-serve.d.ts +18 -3
- package/dist/http/routes/a2a-serve.js +25 -23
- package/dist/http/routes/admin-config-refresh.d.ts +9 -2
- package/dist/http/routes/admin-config-refresh.js +32 -31
- package/dist/http/routes/admin-drain.d.ts +9 -2
- package/dist/http/routes/admin-drain.js +32 -31
- package/dist/http/routes/admin-memory-optout.d.ts +28 -2
- package/dist/http/routes/admin-memory-optout.js +27 -40
- package/dist/http/routes/adoption.d.ts +15 -2
- package/dist/http/routes/adoption.js +52 -52
- package/dist/http/routes/agents-roster.d.ts +9 -2
- package/dist/http/routes/agents-roster.js +82 -80
- package/dist/http/routes/approvals-assistant.d.ts +58 -5
- package/dist/http/routes/approvals-assistant.js +38 -39
- package/dist/http/routes/attachments.d.ts +22 -2
- package/dist/http/routes/attachments.js +81 -72
- package/dist/http/routes/capabilities.d.ts +21 -2
- package/dist/http/routes/capabilities.js +174 -170
- package/dist/http/routes/config-catalog.d.ts +9 -2
- package/dist/http/routes/config-catalog.js +28 -31
- package/dist/http/routes/devices.d.ts +24 -2
- package/dist/http/routes/devices.js +19 -21
- package/dist/http/routes/diagnostics.d.ts +9 -2
- package/dist/http/routes/diagnostics.js +41 -43
- package/dist/http/routes/fleet.d.ts +11 -2
- package/dist/http/routes/fleet.js +23 -22
- package/dist/http/routes/images.d.ts +63 -9
- package/dist/http/routes/images.js +53 -46
- package/dist/http/routes/leader.d.ts +20 -2
- package/dist/http/routes/leader.js +43 -40
- package/dist/http/routes/memory-bundle.d.ts +15 -2
- package/dist/http/routes/memory-bundle.js +38 -38
- package/dist/http/routes/memory-compliance.d.ts +16 -4
- package/dist/http/routes/memory-compliance.js +44 -45
- package/dist/http/routes/memory-consolidation.d.ts +14 -2
- package/dist/http/routes/memory-consolidation.js +66 -66
- package/dist/http/routes/memory-origin.d.ts +23 -4
- package/dist/http/routes/memory-origin.js +65 -65
- package/dist/http/routes/memory-policy.d.ts +21 -2
- package/dist/http/routes/memory-policy.js +119 -115
- package/dist/http/routes/notify-wake.d.ts +21 -11
- package/dist/http/routes/notify-wake.js +118 -116
- package/dist/http/routes/observability.d.ts +16 -2
- package/dist/http/routes/observability.js +54 -51
- package/dist/http/routes/retention-ops.d.ts +19 -2
- package/dist/http/routes/retention-ops.js +20 -22
- package/dist/http/routes/rules.d.ts +36 -2
- package/dist/http/routes/rules.js +22 -24
- package/dist/http/routes/run-memory-capture.d.ts +10 -3
- package/dist/http/routes/run-memory-capture.js +84 -84
- package/dist/http/routes/runs.d.ts +102 -22
- package/dist/http/routes/runs.js +79 -96
- package/dist/http/routes/session-sync.d.ts +41 -2
- package/dist/http/routes/session-sync.js +26 -24
- package/dist/http/routes/sessions-list.d.ts +16 -2
- package/dist/http/routes/sessions-list.js +78 -75
- package/dist/http/routes/sessions.d.ts +77 -2
- package/dist/http/routes/sessions.js +50 -47
- package/dist/http/routes/shared-memory.d.ts +19 -2
- package/dist/http/routes/shared-memory.js +65 -63
- package/dist/http/routes/side-query.d.ts +11 -2
- package/dist/http/routes/side-query.js +81 -79
- package/dist/http/routes/tasks.d.ts +17 -2
- package/dist/http/routes/tasks.js +18 -11
- package/dist/http/routes/trace-usage.d.ts +40 -2
- package/dist/http/routes/trace-usage.js +46 -35
- package/dist/http/routes/workflows.d.ts +35 -3
- package/dist/http/routes/workflows.js +29 -29
- package/dist/http/wire-types.d.ts +15 -9
- package/dist/parked-decide.d.ts +9 -0
- package/dist/parked-decide.js +3 -3
- package/dist/run-local.js +2 -1
- package/dist/spec-fields.js +33 -30
- package/dist/task-cwd.d.ts +21 -0
- package/dist/task-cwd.js +3 -0
- package/dist/trace/core-keyset-guard.d.ts +6 -1
- package/dist/trace/project.d.ts +20 -0
- package/dist/trace/project.js +15 -1
- package/package.json +2 -2
|
@@ -4,6 +4,7 @@ import { isModelAllowlisted } from "../../model-select.js";
|
|
|
4
4
|
import { redactSecrets } from "../../trace/redact.js";
|
|
5
5
|
import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
|
|
6
6
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
7
|
+
import { assertNever } from "../route-ctx.js";
|
|
7
8
|
const CORE_PREFLIGHT_VALIDATION_PREFIXES = ["Unknown model ref ", "No model for role "];
|
|
8
9
|
export function isCorePreflightValidationError(message) {
|
|
9
10
|
return CORE_PREFLIGHT_VALIDATION_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
@@ -26,93 +27,94 @@ function isModelRole(value) {
|
|
|
26
27
|
function passthroughModelRole(value) {
|
|
27
28
|
return value;
|
|
28
29
|
}
|
|
29
|
-
export async function handleSideQuery(req, res,
|
|
30
|
-
const miss = { fell: false };
|
|
31
|
-
await handleSideQueryBody(req, res, url, ctx, miss);
|
|
32
|
-
return !miss.fell;
|
|
33
|
-
}
|
|
34
|
-
async function handleSideQueryBody(req, res, url, ctx, miss) {
|
|
30
|
+
export async function handleSideQuery(req, res, match, ctx) {
|
|
35
31
|
const { deps } = ctx;
|
|
36
32
|
const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
43
|
-
return;
|
|
44
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
45
|
-
if (deps.config.requirePrincipal && !principal) {
|
|
46
|
-
sendPrincipalRefusal(res, req, deps.config);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
let body;
|
|
33
|
+
switch (match.decl.id) {
|
|
34
|
+
case "side-query": {
|
|
35
|
+
const ac = new AbortController();
|
|
36
|
+
const onClose = () => ac.abort();
|
|
37
|
+
res.on("close", onClose);
|
|
50
38
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
catch (e) {
|
|
57
|
-
const he = e instanceof HttpError ? e : new HttpError(400, msg(e));
|
|
58
|
-
sendError(res, he.status, httpErrorCode(he.status, he.code), he.message);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (!Array.isArray(body.messages) || body.messages.length === 0) {
|
|
62
|
-
sendError(res, 400, "request.body_shape", "messages must be a non-empty array (SideQueryMessage[])");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (body.thinking !== undefined && !isThinkingLevel(body.thinking)) {
|
|
66
|
-
sendError(res, 400, "request.field_invalid", "invalid thinking level");
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (typeof body.model !== "string" && body.modelRole !== undefined && !isModelRole(body.modelRole)) {
|
|
70
|
-
sendError(res, 400, "request.field_invalid", `invalid modelRole (expected one of: ${Object.keys(MODEL_ROLE_TABLE).join(", ")}) — a role only resolves a model when 'model' is absent`);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (typeof body.model === "string" && body.model.length > 0) {
|
|
74
|
-
const bare = deps.config.models ?? {};
|
|
75
|
-
const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
|
|
76
|
-
if (!isModelAllowlisted(body.model, catalog, deps.config.atModelAllowlist)) {
|
|
77
|
-
sendError(res, 400, "request.model_not_allowed", `model is not in this deployment's @-mention allowlist: "${body.model.slice(0, 120)}" — the allowlist is configured by the operator (config-center models.atModelAllowlist)`);
|
|
39
|
+
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
40
|
+
return;
|
|
41
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
42
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
43
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
78
44
|
return;
|
|
79
45
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readJson(req);
|
|
49
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
50
|
+
throw new HttpError(400, "body must be a JSON object (SideQuerySpec shape)");
|
|
51
|
+
body = raw;
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
const he = e instanceof HttpError ? e : new HttpError(400, msg(e));
|
|
55
|
+
sendError(res, he.status, httpErrorCode(he.status, he.code), he.message);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(body.messages) || body.messages.length === 0) {
|
|
59
|
+
sendError(res, 400, "request.body_shape", "messages must be a non-empty array (SideQueryMessage[])");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (body.thinking !== undefined && !isThinkingLevel(body.thinking)) {
|
|
63
|
+
sendError(res, 400, "request.field_invalid", "invalid thinking level");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (typeof body.model !== "string" && body.modelRole !== undefined && !isModelRole(body.modelRole)) {
|
|
67
|
+
sendError(res, 400, "request.field_invalid", `invalid modelRole (expected one of: ${Object.keys(MODEL_ROLE_TABLE).join(", ")}) — a role only resolves a model when 'model' is absent`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof body.model === "string" && body.model.length > 0) {
|
|
71
|
+
const bare = deps.config.models ?? {};
|
|
72
|
+
const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
|
|
73
|
+
if (!isModelAllowlisted(body.model, catalog, deps.config.atModelAllowlist)) {
|
|
74
|
+
sendError(res, 400, "request.model_not_allowed", `model is not in this deployment's @-mention allowlist: "${body.model.slice(0, 120)}" — the allowlist is configured by the operator (config-center models.atModelAllowlist)`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (ac.signal.aborted)
|
|
79
|
+
return;
|
|
80
|
+
const spec = {
|
|
81
|
+
messages: body.messages,
|
|
82
|
+
...(typeof body.model === "string" ? { model: body.model } : {}),
|
|
83
|
+
...(typeof body.modelRole === "string" ? { modelRole: passthroughModelRole(body.modelRole) } : {}),
|
|
84
|
+
...(isThinkingLevel(body.thinking) ? { thinking: body.thinking } : {}),
|
|
85
|
+
...(typeof body.systemPrompt === "string" ? { systemPrompt: body.systemPrompt } : {}),
|
|
86
|
+
...(Array.isArray(body.tools) ? { tools: body.tools } : {}),
|
|
87
|
+
...(typeof body.maxOutputTokens === "number" && Number.isFinite(body.maxOutputTokens) && body.maxOutputTokens > 0 ? { maxOutputTokens: Math.floor(body.maxOutputTokens) } : {}),
|
|
88
|
+
signal: ac.signal,
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const result = await deps.sideQuery(spec);
|
|
92
|
+
const rr = result;
|
|
93
|
+
const modelId = typeof rr.model === "string" ? rr.model : "unknown";
|
|
94
|
+
deps.sideQueryAccounting?.(principal, { model: modelId, usage: rr.usage });
|
|
95
|
+
deps.metrics?.inc?.("side_query_total", { result: "ok" });
|
|
96
|
+
if (!res.writableEnded)
|
|
97
|
+
sendJson(res, 200, result);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
deps.metrics?.inc?.("side_query_total", { result: "error" });
|
|
101
|
+
if (!res.writableEnded && !ac.signal.aborted) {
|
|
102
|
+
const m = msg(e);
|
|
103
|
+
const isValidation = isCorePreflightValidationError(m);
|
|
104
|
+
sendError(res, isValidation ? 400 : 500, isValidation ? "request.field_invalid" : "internal.error", redactSecrets(m));
|
|
105
|
+
}
|
|
108
106
|
}
|
|
109
107
|
}
|
|
108
|
+
finally {
|
|
109
|
+
res.off("close", onClose);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
110
112
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
return;
|
|
113
|
+
default:
|
|
114
|
+
return assertNever(match.decl.id);
|
|
115
115
|
}
|
|
116
|
-
miss.fell = true;
|
|
117
116
|
}
|
|
117
|
+
export const SIDE_QUERY_ROUTES = [
|
|
118
|
+
{ id: "side-query", path: "/v1/side-query", billable: true, methods: ["POST"] },
|
|
119
|
+
];
|
|
118
120
|
//# sourceMappingURL=side-query.js.map
|
|
@@ -17,6 +17,21 @@
|
|
|
17
17
|
* 与 runs 域共用 `ctx.legs.prepareSpec`(读体/校验/建 spec 的同一条前段)。
|
|
18
18
|
*/
|
|
19
19
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
20
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
21
|
-
|
|
20
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
21
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(A9 的唯一改动:去缩进;B3 的唯一改动:
|
|
22
|
+
* 给两个匿名 `idemCache.run` 回调与三条驱动腿起名)。裸 `return;` = 「本域已应答」;走到函数尾才是「没匹配上」。 */
|
|
23
|
+
export declare function handleTasks(req: IncomingMessage, res: ServerResponse, match: RouteMatch<TasksRouteId>, ctx: RouteCtx): Promise<void>;
|
|
24
|
+
export declare const TASKS_ROUTES: readonly [{
|
|
25
|
+
readonly id: "tasks-submit";
|
|
26
|
+
readonly path: "/v1/tasks";
|
|
27
|
+
readonly billable: true;
|
|
28
|
+
readonly methods: readonly ["POST"];
|
|
29
|
+
}, {
|
|
30
|
+
readonly id: "tasks-submit-stream";
|
|
31
|
+
readonly path: "/v1/tasks/stream";
|
|
32
|
+
readonly billable: true;
|
|
33
|
+
readonly methods: readonly ["POST"];
|
|
34
|
+
}];
|
|
35
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
36
|
+
export type TasksRouteId = RouteIdsOf<typeof TASKS_ROUTES>;
|
|
22
37
|
//# sourceMappingURL=tasks.d.ts.map
|
|
@@ -23,6 +23,7 @@ import { createApprovalCardEmitter, resolveApprovalLeg, resolveStreamApprovalGat
|
|
|
23
23
|
import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflict.js";
|
|
24
24
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
25
25
|
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
26
|
+
import { assertNever } from "../route-ctx.js";
|
|
26
27
|
const DISCONNECT_PARK_GRACE_MS = 10_000;
|
|
27
28
|
function sseData(res, payload) {
|
|
28
29
|
const anchor = payload.eventId;
|
|
@@ -39,12 +40,7 @@ function sendSubmitResult(res, resp) {
|
|
|
39
40
|
sendJson(res, resp.status, resp.body);
|
|
40
41
|
}
|
|
41
42
|
const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For observable events submit via POST /v1/runs and read GET /v1/runs/:id/events.";
|
|
42
|
-
export async function handleTasks(req, res,
|
|
43
|
-
const miss = { fell: false };
|
|
44
|
-
await handleTasksBody(req, res, url, ctx, miss);
|
|
45
|
-
return !miss.fell;
|
|
46
|
-
}
|
|
47
|
-
async function handleTasksBody(req, res, url, ctx, miss) {
|
|
43
|
+
export async function handleTasks(req, res, match, ctx) {
|
|
48
44
|
const { deps } = ctx;
|
|
49
45
|
const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
|
|
50
46
|
const { rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
|
|
@@ -59,12 +55,20 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
59
55
|
if (plane.errorCode)
|
|
60
56
|
reqState.runErrorCode = plane.errorCode;
|
|
61
57
|
};
|
|
62
|
-
|
|
58
|
+
switch (match.decl.id) {
|
|
59
|
+
case "tasks-submit":
|
|
60
|
+
case "tasks-submit-stream":
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
return assertNever(match.decl.id);
|
|
64
|
+
}
|
|
65
|
+
const liveStream = match.decl.id === "tasks-submit-stream";
|
|
66
|
+
{
|
|
63
67
|
const rawIdem = headerStr(req.headers["idempotency-key"]);
|
|
64
68
|
const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
|
|
65
69
|
const cached = idemKey ? idemCache.peek(idemKey) : undefined;
|
|
66
70
|
if (cached) {
|
|
67
|
-
if (
|
|
71
|
+
if (liveStream) {
|
|
68
72
|
sseHeaders(res);
|
|
69
73
|
const hb = setInterval(() => {
|
|
70
74
|
if (!res.writableEnded)
|
|
@@ -90,11 +94,11 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
90
94
|
}
|
|
91
95
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
|
|
92
96
|
return;
|
|
93
|
-
const prepared = await prepareSpec(req, res, undefined, undefined, { liveStream
|
|
97
|
+
const prepared = await prepareSpec(req, res, undefined, undefined, { liveStream });
|
|
94
98
|
if (!prepared)
|
|
95
99
|
return;
|
|
96
100
|
const principal = prepared.auth?.principal;
|
|
97
|
-
if (
|
|
101
|
+
if (liveStream) {
|
|
98
102
|
if (prepared.verify || prepared.cascade) {
|
|
99
103
|
sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs");
|
|
100
104
|
return;
|
|
@@ -860,6 +864,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
860
864
|
}
|
|
861
865
|
return;
|
|
862
866
|
}
|
|
863
|
-
miss.fell = true;
|
|
864
867
|
}
|
|
868
|
+
export const TASKS_ROUTES = [
|
|
869
|
+
{ id: "tasks-submit", path: "/v1/tasks", billable: true, methods: ["POST"] },
|
|
870
|
+
{ id: "tasks-submit-stream", path: "/v1/tasks/stream", billable: true, methods: ["POST"] },
|
|
871
|
+
];
|
|
865
872
|
//# sourceMappingURL=tasks.js.map
|
|
@@ -9,6 +9,44 @@
|
|
|
9
9
|
* 因此本域在 `handle()` 里的位置就是契约的一部分:必须留在鉴权门上方,顺序不可与其他域对调。本域零可变状态。
|
|
10
10
|
*/
|
|
11
11
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
12
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
13
|
-
|
|
12
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
13
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
14
|
+
* = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
|
|
15
|
+
export declare function handleTraceUsage(req: IncomingMessage, res: ServerResponse, match: RouteMatch<TraceUsageRouteId>, ctx: RouteCtx): Promise<void>;
|
|
16
|
+
export declare const TRACE_USAGE_ROUTES: readonly [{
|
|
17
|
+
readonly id: "tasks-source-summary";
|
|
18
|
+
readonly path: "/v1/tasks/source-summary";
|
|
19
|
+
readonly methods: readonly ["GET"];
|
|
20
|
+
}, {
|
|
21
|
+
readonly id: "task-trace-list";
|
|
22
|
+
readonly path: "/v1/tasks";
|
|
23
|
+
readonly methods: readonly ["GET"];
|
|
24
|
+
}, {
|
|
25
|
+
readonly id: "task-tool-result";
|
|
26
|
+
readonly pattern: RegExp;
|
|
27
|
+
readonly label: "/v1/tasks/:id/tool-results/:ref";
|
|
28
|
+
readonly methods: readonly ["GET"];
|
|
29
|
+
}, {
|
|
30
|
+
readonly id: "task-trace-turns";
|
|
31
|
+
readonly pattern: RegExp;
|
|
32
|
+
readonly label: "/v1/tasks/:id/$sub";
|
|
33
|
+
readonly methods: readonly ["GET"];
|
|
34
|
+
}, {
|
|
35
|
+
readonly id: "task-trace-stream";
|
|
36
|
+
readonly pattern: RegExp;
|
|
37
|
+
readonly label: "/v1/tasks/:id/$sub";
|
|
38
|
+
readonly methods: readonly ["GET"];
|
|
39
|
+
}, {
|
|
40
|
+
readonly id: "task-trace-artifacts";
|
|
41
|
+
readonly pattern: RegExp;
|
|
42
|
+
readonly label: "/v1/tasks/:id/$sub";
|
|
43
|
+
readonly methods: readonly ["GET"];
|
|
44
|
+
}, {
|
|
45
|
+
readonly id: "usage-analytics";
|
|
46
|
+
readonly pattern: RegExp;
|
|
47
|
+
readonly label: "/v1/usage/$sub";
|
|
48
|
+
readonly methods: readonly ["GET"];
|
|
49
|
+
}];
|
|
50
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
51
|
+
export type TraceUsageRouteId = RouteIdsOf<typeof TRACE_USAGE_ROUTES>;
|
|
14
52
|
//# sourceMappingURL=trace-usage.d.ts.map
|
|
@@ -6,19 +6,13 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
|
6
6
|
import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
|
|
7
7
|
import { filterFingerprint } from "../cursor-fingerprint.js";
|
|
8
8
|
import { gatedPrincipal } from "../principal-gate.js";
|
|
9
|
-
|
|
10
|
-
const TOOL_RESULTS_SUB = "/tool-results/";
|
|
9
|
+
import { assertNever } from "../route-ctx.js";
|
|
11
10
|
const TOOL_RESULT_LIMIT_CAP = 262_144;
|
|
12
11
|
const TOOL_RESULT_OFFSET_CAP = 2_147_483_646;
|
|
13
|
-
export async function handleTraceUsage(req, res,
|
|
14
|
-
const miss = { fell: false };
|
|
15
|
-
await handleTraceUsageBody(req, res, url, ctx, miss);
|
|
16
|
-
return !miss.fell;
|
|
17
|
-
}
|
|
18
|
-
async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
12
|
+
export async function handleTraceUsage(req, res, match, ctx) {
|
|
19
13
|
const { deps } = ctx;
|
|
20
14
|
const { isFleetWide, safeDecode } = ctx.helpers;
|
|
21
|
-
if (
|
|
15
|
+
if (match.decl.id === "tasks-source-summary") {
|
|
22
16
|
if (!isFleetWide(req)) {
|
|
23
17
|
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
24
18
|
return;
|
|
@@ -27,13 +21,16 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
27
21
|
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
28
22
|
return;
|
|
29
23
|
}
|
|
30
|
-
const q = new
|
|
24
|
+
const q = new URLSearchParams(match.search);
|
|
31
25
|
const sinceSec = Math.min(30 * 86_400, Math.max(60, Number(q.get("sinceSec") ?? 86_400) || 86_400));
|
|
32
26
|
sendJson(res, 200, { sinceSec, rows: await deps.runStore.sourceSummary(Date.now() - sinceSec * 1000) });
|
|
33
27
|
return;
|
|
34
28
|
}
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
if (match.decl.id === "task-trace-list" ||
|
|
30
|
+
match.decl.id === "task-trace-turns" ||
|
|
31
|
+
match.decl.id === "task-trace-stream" ||
|
|
32
|
+
match.decl.id === "task-trace-artifacts" ||
|
|
33
|
+
match.decl.id === "task-tool-result") {
|
|
37
34
|
const fleetWide = isFleetWide(req);
|
|
38
35
|
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
39
36
|
if (!fleetWide && gateOwner === null) {
|
|
@@ -44,9 +41,8 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
44
41
|
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
45
42
|
return;
|
|
46
43
|
}
|
|
47
|
-
const query = new
|
|
48
|
-
const taskId =
|
|
49
|
-
const sub = traceMatch[2];
|
|
44
|
+
const query = new URLSearchParams(match.search);
|
|
45
|
+
const taskId = match.params.taskId;
|
|
50
46
|
let ownedRun;
|
|
51
47
|
if (taskId && gateOwner !== null) {
|
|
52
48
|
const run = await deps.runStore.getRun(taskId);
|
|
@@ -56,24 +52,31 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
56
52
|
}
|
|
57
53
|
ownedRun = run;
|
|
58
54
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
switch (match.decl.id) {
|
|
56
|
+
case "task-trace-list":
|
|
57
|
+
await handleTaskList(res, deps.runStore, query, gateOwner);
|
|
58
|
+
return;
|
|
59
|
+
case "task-trace-turns":
|
|
60
|
+
await handleTaskTurns(res, deps.runStore, taskId, query);
|
|
61
|
+
return;
|
|
62
|
+
case "task-trace-stream":
|
|
63
|
+
await streamTaskTrace(req, res, deps.runStore, taskId, deps.config.runStaleSec * 1000, match.search);
|
|
64
|
+
return;
|
|
65
|
+
case "task-trace-artifacts":
|
|
66
|
+
await handleTaskArtifacts(res, deps.runStore, taskId);
|
|
67
|
+
return;
|
|
68
|
+
case "task-tool-result":
|
|
69
|
+
await handleToolResult(res, deps.toolResultStore, deps.runStore, taskId, ownedRun, safeDecode(match.params.ref), query);
|
|
70
|
+
return;
|
|
71
|
+
default:
|
|
72
|
+
return assertNever(match.decl.id);
|
|
69
73
|
}
|
|
70
|
-
else
|
|
71
|
-
sendError(res, 404, "not_found.route", "not found");
|
|
72
|
-
return;
|
|
73
74
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
if (match.decl.id !== "usage-analytics")
|
|
76
|
+
return assertNever(match.decl.id);
|
|
77
|
+
{
|
|
78
|
+
const sub = match.params.sub;
|
|
79
|
+
{
|
|
77
80
|
if (!deps.runStore) {
|
|
78
81
|
sendError(res, 501, "capability.run_store_required", "usage analytics requires a run store");
|
|
79
82
|
return;
|
|
@@ -84,7 +87,7 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
84
87
|
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
87
|
-
const q = new
|
|
90
|
+
const q = new URLSearchParams(match.search);
|
|
88
91
|
const parseT = (v, dflt) => {
|
|
89
92
|
if (!v)
|
|
90
93
|
return dflt;
|
|
@@ -130,7 +133,6 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
130
133
|
return;
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
|
-
miss.fell = true;
|
|
134
136
|
}
|
|
135
137
|
function encodeCursor(c, f) {
|
|
136
138
|
return Buffer.from(JSON.stringify({ ...c, f })).toString("base64url");
|
|
@@ -234,13 +236,13 @@ async function handleTaskTurns(res, runStore, taskId, query) {
|
|
|
234
236
|
const nextCursor = eligible.length > limit && oldest ? String(oldest.seq) : undefined;
|
|
235
237
|
sendJson(res, 200, { turns, ...(nextCursor ? { nextCursor } : {}), retainedFrom });
|
|
236
238
|
}
|
|
237
|
-
async function streamTaskTrace(req, res, runStore, taskId, staleMs) {
|
|
239
|
+
async function streamTaskTrace(req, res, runStore, taskId, staleMs, search) {
|
|
238
240
|
const run0 = await runStore.getRun(taskId);
|
|
239
241
|
if (!run0) {
|
|
240
242
|
sendError(res, 404, "not_found.run", "task not found");
|
|
241
243
|
return;
|
|
242
244
|
}
|
|
243
|
-
const lastId = Number(req.headers["last-event-id"] ?? new
|
|
245
|
+
const lastId = Number(req.headers["last-event-id"] ?? new URLSearchParams(search).get("from") ?? 0);
|
|
244
246
|
let from = Number.isFinite(lastId) ? lastId : 0;
|
|
245
247
|
if (from > 0) {
|
|
246
248
|
const retainedFrom = await runStore.retainedFrom(taskId);
|
|
@@ -293,4 +295,13 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs) {
|
|
|
293
295
|
}
|
|
294
296
|
res.end();
|
|
295
297
|
}
|
|
298
|
+
export const TRACE_USAGE_ROUTES = [
|
|
299
|
+
{ id: "tasks-source-summary", path: "/v1/tasks/source-summary", methods: ["GET"] },
|
|
300
|
+
{ id: "task-trace-list", path: "/v1/tasks", methods: ["GET"] },
|
|
301
|
+
{ id: "task-tool-result", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/tool-results\/(?<ref>[^/]+)$/, label: "/v1/tasks/:id/tool-results/:ref", methods: ["GET"] },
|
|
302
|
+
{ id: "task-trace-turns", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/turns$/, label: "/v1/tasks/:id/$sub", methods: ["GET"] },
|
|
303
|
+
{ id: "task-trace-stream", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/stream$/, label: "/v1/tasks/:id/$sub", methods: ["GET"] },
|
|
304
|
+
{ id: "task-trace-artifacts", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/artifacts$/, label: "/v1/tasks/:id/$sub", methods: ["GET"] },
|
|
305
|
+
{ id: "usage-analytics", pattern: /^\/v1\/usage\/(?<sub>summary|series|breakdown)$/, label: "/v1/usage/$sub", methods: ["GET"] },
|
|
306
|
+
];
|
|
296
307
|
//# sourceMappingURL=trace-usage.js.map
|
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
3
|
-
export declare function handleWorkflows(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
2
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
4
3
|
/** SVC-5 的 steer 路——`handle()` 里位置在 tasks/runs 之后(鉴权门之后),故是**独立**的域入口。 */
|
|
5
|
-
|
|
4
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
5
|
+
* = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
|
|
6
|
+
export declare function handleWorkflows(req: IncomingMessage, res: ServerResponse, match: RouteMatch<WorkflowsRouteId>, ctx: RouteCtx): Promise<void>;
|
|
7
|
+
/** 同上纪律(SVC-5 steer 段)。 */
|
|
8
|
+
export declare function handleWorkflowAgentSteer(req: IncomingMessage, res: ServerResponse, match: RouteMatch<WorkflowAgentSteerRouteId>, ctx: RouteCtx): Promise<void>;
|
|
9
|
+
export declare const WORKFLOWS_ROUTES: readonly [{
|
|
10
|
+
readonly id: "workflows-list";
|
|
11
|
+
readonly path: "/v1/workflows";
|
|
12
|
+
readonly methods: readonly ["GET"];
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "workflow-stream";
|
|
15
|
+
readonly pattern: RegExp;
|
|
16
|
+
readonly label: "/v1/workflows/:id/$sub";
|
|
17
|
+
readonly methods: readonly ["GET"];
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "workflow-journal";
|
|
20
|
+
readonly pattern: RegExp;
|
|
21
|
+
readonly label: "/v1/workflows/:id/$sub";
|
|
22
|
+
readonly methods: readonly ["GET"];
|
|
23
|
+
}, {
|
|
24
|
+
readonly id: "workflow-detail";
|
|
25
|
+
readonly pattern: RegExp;
|
|
26
|
+
readonly label: "/v1/workflows/:id";
|
|
27
|
+
readonly methods: readonly ["GET"];
|
|
28
|
+
}];
|
|
29
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
30
|
+
export type WorkflowsRouteId = RouteIdsOf<typeof WORKFLOWS_ROUTES>;
|
|
31
|
+
export declare const WORKFLOW_AGENT_STEER_ROUTES: readonly [{
|
|
32
|
+
readonly id: "workflow-agent-steer";
|
|
33
|
+
readonly pattern: RegExp;
|
|
34
|
+
readonly label: "/v1/workflows/:id/agents/:aid/steer";
|
|
35
|
+
readonly methods: readonly ["POST"];
|
|
36
|
+
}];
|
|
37
|
+
export type WorkflowAgentSteerRouteId = RouteIdsOf<typeof WORKFLOW_AGENT_STEER_ROUTES>;
|
|
6
38
|
//# sourceMappingURL=workflows.d.ts.map
|
|
@@ -10,23 +10,11 @@ const JOURNAL_PROJECTION_MEMO_CAP = 1024;
|
|
|
10
10
|
const journalProjectionMemo = new Map();
|
|
11
11
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
12
12
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export async function handleWorkflows(req, res, url, ctx) {
|
|
16
|
-
const miss = { fell: false };
|
|
17
|
-
await handleWorkflowsReadBody(req, res, url, ctx, miss);
|
|
18
|
-
return !miss.fell;
|
|
19
|
-
}
|
|
20
|
-
export async function handleWorkflowAgentSteer(req, res, url, ctx) {
|
|
21
|
-
const miss = { fell: false };
|
|
22
|
-
await handleWorkflowAgentSteerBody(req, res, url, ctx, miss);
|
|
23
|
-
return !miss.fell;
|
|
24
|
-
}
|
|
25
|
-
async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
|
|
13
|
+
import { assertNever } from "../route-ctx.js";
|
|
14
|
+
export async function handleWorkflows(req, res, match, ctx) {
|
|
26
15
|
const { deps } = ctx;
|
|
27
16
|
const { runSessionAcceptOk } = ctx.helpers;
|
|
28
|
-
|
|
29
|
-
if (wfMatch) {
|
|
17
|
+
{
|
|
30
18
|
const principal = gatedPrincipal(req, deps.config);
|
|
31
19
|
if (deps.config.requirePrincipal && !principal) {
|
|
32
20
|
sendPrincipalRefusal(res, req, deps.config);
|
|
@@ -37,10 +25,10 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
|
|
|
37
25
|
return;
|
|
38
26
|
}
|
|
39
27
|
const scope = principal ?? "default";
|
|
40
|
-
const wfId =
|
|
41
|
-
const isStream =
|
|
42
|
-
if (
|
|
43
|
-
const q = new
|
|
28
|
+
const wfId = match.params.runId;
|
|
29
|
+
const isStream = match.decl.id === "workflow-stream";
|
|
30
|
+
if (match.decl.id === "workflows-list") {
|
|
31
|
+
const q = new URLSearchParams(match.search);
|
|
44
32
|
const statusFilter = q.get("status");
|
|
45
33
|
const limit = Math.min(100, Math.max(1, Number(q.get("limit") ?? 50) || 50));
|
|
46
34
|
const sessionFilter = q.get("session");
|
|
@@ -63,14 +51,14 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
|
|
|
63
51
|
sendError(res, 404, "not_found.workflow", "workflow not found");
|
|
64
52
|
return;
|
|
65
53
|
}
|
|
66
|
-
if (!runSessionAcceptOk(req, res, { sessionId: run.originatingSessionId ?? null },
|
|
54
|
+
if (!runSessionAcceptOk(req, res, { sessionId: run.originatingSessionId ?? null }, match.decl.id === "workflow-journal" ? "workflow.journal" : isStream ? "workflow.stream" : "workflow.detail", "workflow not found"))
|
|
67
55
|
return;
|
|
68
|
-
if (
|
|
56
|
+
if (match.decl.id === "workflow-journal") {
|
|
69
57
|
if (!deps.workflowJournalStore) {
|
|
70
58
|
sendError(res, 501, "capability.workflow_store_required", "workflow journal requires the journal store (SELF_ORCHESTRATION_ENABLED + a store backend)");
|
|
71
59
|
return;
|
|
72
60
|
}
|
|
73
|
-
const jq = new
|
|
61
|
+
const jq = new URLSearchParams(match.search);
|
|
74
62
|
const jLimit = Math.min(50, Math.max(1, Number(jq.get("limit") ?? 20) || 20));
|
|
75
63
|
const jOffset = Math.max(0, Number(jq.get("offset") ?? 0) || 0);
|
|
76
64
|
const MAX_ROW_BYTES = 64 * 1024;
|
|
@@ -148,13 +136,17 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
|
|
|
148
136
|
sendJson(res, 200, summarizeWorkflowDetail(run));
|
|
149
137
|
return;
|
|
150
138
|
}
|
|
151
|
-
miss.fell = true;
|
|
152
139
|
}
|
|
153
|
-
async function
|
|
140
|
+
export async function handleWorkflowAgentSteer(req, res, match, ctx) {
|
|
154
141
|
const { deps } = ctx;
|
|
155
142
|
const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
|
|
156
|
-
|
|
157
|
-
|
|
143
|
+
switch (match.decl.id) {
|
|
144
|
+
case "workflow-agent-steer":
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
return assertNever(match.decl.id);
|
|
148
|
+
}
|
|
149
|
+
{
|
|
158
150
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
159
151
|
return;
|
|
160
152
|
if (!deps.workflowRunStore) {
|
|
@@ -166,8 +158,8 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
|
|
|
166
158
|
sendPrincipalRefusal(res, req, deps.config);
|
|
167
159
|
return;
|
|
168
160
|
}
|
|
169
|
-
const wfRunId = safeDecode(
|
|
170
|
-
const label = safeDecode(
|
|
161
|
+
const wfRunId = safeDecode(match.params.runId);
|
|
162
|
+
const label = safeDecode(match.params.label);
|
|
171
163
|
if (wfRunId === null || label === null) {
|
|
172
164
|
sendError(res, 400, "request.path_malformed", "malformed workflow path (invalid percent-encoding)");
|
|
173
165
|
return;
|
|
@@ -227,7 +219,6 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
|
|
|
227
219
|
: `workflow is ${wfRun.status} — agent is not running`);
|
|
228
220
|
return;
|
|
229
221
|
}
|
|
230
|
-
miss.fell = true;
|
|
231
222
|
}
|
|
232
223
|
const LegApprovalKeysSchema = z
|
|
233
224
|
.object({
|
|
@@ -397,4 +388,13 @@ async function streamWorkflowRun(req, res, runId, scope) {
|
|
|
397
388
|
if (!res.writableEnded)
|
|
398
389
|
res.end();
|
|
399
390
|
}
|
|
391
|
+
export const WORKFLOWS_ROUTES = [
|
|
392
|
+
{ id: "workflows-list", path: "/v1/workflows", methods: ["GET"] },
|
|
393
|
+
{ id: "workflow-stream", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)\/stream$/, label: "/v1/workflows/:id/$sub", methods: ["GET"] },
|
|
394
|
+
{ id: "workflow-journal", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)\/journal$/, label: "/v1/workflows/:id/$sub", methods: ["GET"] },
|
|
395
|
+
{ id: "workflow-detail", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)$/, label: "/v1/workflows/:id", methods: ["GET"] },
|
|
396
|
+
];
|
|
397
|
+
export const WORKFLOW_AGENT_STEER_ROUTES = [
|
|
398
|
+
{ id: "workflow-agent-steer", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)\/agents\/(?<label>[^/]+)\/steer$/, label: "/v1/workflows/:id/agents/:aid/steer", methods: ["POST"] },
|
|
399
|
+
];
|
|
400
400
|
//# sourceMappingURL=workflows.js.map
|