@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.
Files changed (89) hide show
  1. package/dist/boot/stage-07-capability-layer.js +2 -1
  2. package/dist/capabilities/skills.d.ts +10 -0
  3. package/dist/capabilities/skills.js +3 -1
  4. package/dist/http/admission.js +3 -1
  5. package/dist/http/dispatch.js +28 -8
  6. package/dist/http/resume-legs.js +4 -3
  7. package/dist/http/route-ctx.d.ts +101 -4
  8. package/dist/http/route-ctx.js +4 -0
  9. package/dist/http/route-table.d.ts +71 -43
  10. package/dist/http/route-table.js +111 -187
  11. package/dist/http/routes/a2a-serve.d.ts +18 -3
  12. package/dist/http/routes/a2a-serve.js +25 -23
  13. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  14. package/dist/http/routes/admin-config-refresh.js +32 -31
  15. package/dist/http/routes/admin-drain.d.ts +9 -2
  16. package/dist/http/routes/admin-drain.js +32 -31
  17. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  18. package/dist/http/routes/admin-memory-optout.js +27 -40
  19. package/dist/http/routes/adoption.d.ts +15 -2
  20. package/dist/http/routes/adoption.js +52 -52
  21. package/dist/http/routes/agents-roster.d.ts +9 -2
  22. package/dist/http/routes/agents-roster.js +82 -80
  23. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  24. package/dist/http/routes/approvals-assistant.js +38 -39
  25. package/dist/http/routes/attachments.d.ts +22 -2
  26. package/dist/http/routes/attachments.js +81 -72
  27. package/dist/http/routes/capabilities.d.ts +21 -2
  28. package/dist/http/routes/capabilities.js +174 -170
  29. package/dist/http/routes/config-catalog.d.ts +9 -2
  30. package/dist/http/routes/config-catalog.js +28 -31
  31. package/dist/http/routes/devices.d.ts +24 -2
  32. package/dist/http/routes/devices.js +19 -21
  33. package/dist/http/routes/diagnostics.d.ts +9 -2
  34. package/dist/http/routes/diagnostics.js +41 -43
  35. package/dist/http/routes/fleet.d.ts +11 -2
  36. package/dist/http/routes/fleet.js +23 -22
  37. package/dist/http/routes/images.d.ts +63 -9
  38. package/dist/http/routes/images.js +53 -46
  39. package/dist/http/routes/leader.d.ts +20 -2
  40. package/dist/http/routes/leader.js +43 -40
  41. package/dist/http/routes/memory-bundle.d.ts +15 -2
  42. package/dist/http/routes/memory-bundle.js +38 -38
  43. package/dist/http/routes/memory-compliance.d.ts +16 -4
  44. package/dist/http/routes/memory-compliance.js +44 -45
  45. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  46. package/dist/http/routes/memory-consolidation.js +66 -66
  47. package/dist/http/routes/memory-origin.d.ts +23 -4
  48. package/dist/http/routes/memory-origin.js +65 -65
  49. package/dist/http/routes/memory-policy.d.ts +21 -2
  50. package/dist/http/routes/memory-policy.js +119 -115
  51. package/dist/http/routes/notify-wake.d.ts +21 -11
  52. package/dist/http/routes/notify-wake.js +118 -116
  53. package/dist/http/routes/observability.d.ts +16 -2
  54. package/dist/http/routes/observability.js +54 -51
  55. package/dist/http/routes/retention-ops.d.ts +19 -2
  56. package/dist/http/routes/retention-ops.js +20 -22
  57. package/dist/http/routes/rules.d.ts +36 -2
  58. package/dist/http/routes/rules.js +22 -24
  59. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  60. package/dist/http/routes/run-memory-capture.js +84 -84
  61. package/dist/http/routes/runs.d.ts +102 -22
  62. package/dist/http/routes/runs.js +79 -96
  63. package/dist/http/routes/session-sync.d.ts +41 -2
  64. package/dist/http/routes/session-sync.js +26 -24
  65. package/dist/http/routes/sessions-list.d.ts +16 -2
  66. package/dist/http/routes/sessions-list.js +78 -75
  67. package/dist/http/routes/sessions.d.ts +77 -2
  68. package/dist/http/routes/sessions.js +50 -47
  69. package/dist/http/routes/shared-memory.d.ts +19 -2
  70. package/dist/http/routes/shared-memory.js +65 -63
  71. package/dist/http/routes/side-query.d.ts +11 -2
  72. package/dist/http/routes/side-query.js +81 -79
  73. package/dist/http/routes/tasks.d.ts +17 -2
  74. package/dist/http/routes/tasks.js +18 -11
  75. package/dist/http/routes/trace-usage.d.ts +40 -2
  76. package/dist/http/routes/trace-usage.js +46 -35
  77. package/dist/http/routes/workflows.d.ts +35 -3
  78. package/dist/http/routes/workflows.js +29 -29
  79. package/dist/http/wire-types.d.ts +15 -9
  80. package/dist/parked-decide.d.ts +9 -0
  81. package/dist/parked-decide.js +3 -3
  82. package/dist/run-local.js +2 -1
  83. package/dist/spec-fields.js +33 -30
  84. package/dist/task-cwd.d.ts +21 -0
  85. package/dist/task-cwd.js +3 -0
  86. package/dist/trace/core-keyset-guard.d.ts +6 -1
  87. package/dist/trace/project.d.ts +20 -0
  88. package/dist/trace/project.js +15 -1
  89. 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, url, ctx) {
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
- if (req.method === "POST" && url === "/v1/side-query") {
38
- const ac = new AbortController();
39
- const onClose = () => ac.abort();
40
- res.on("close", onClose);
41
- try {
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
- const raw = await readJson(req);
52
- if (raw === null || typeof raw !== "object" || Array.isArray(raw))
53
- throw new HttpError(400, "body must be a JSON object (SideQuerySpec shape)");
54
- body = raw;
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
- if (ac.signal.aborted)
82
- return;
83
- const spec = {
84
- messages: body.messages,
85
- ...(typeof body.model === "string" ? { model: body.model } : {}),
86
- ...(typeof body.modelRole === "string" ? { modelRole: passthroughModelRole(body.modelRole) } : {}),
87
- ...(isThinkingLevel(body.thinking) ? { thinking: body.thinking } : {}),
88
- ...(typeof body.systemPrompt === "string" ? { systemPrompt: body.systemPrompt } : {}),
89
- ...(Array.isArray(body.tools) ? { tools: body.tools } : {}),
90
- ...(typeof body.maxOutputTokens === "number" && Number.isFinite(body.maxOutputTokens) && body.maxOutputTokens > 0 ? { maxOutputTokens: Math.floor(body.maxOutputTokens) } : {}),
91
- signal: ac.signal,
92
- };
93
- try {
94
- const result = await deps.sideQuery(spec);
95
- const rr = result;
96
- const modelId = typeof rr.model === "string" ? rr.model : "unknown";
97
- deps.sideQueryAccounting?.(principal, { model: modelId, usage: rr.usage });
98
- deps.metrics?.inc?.("side_query_total", { result: "ok" });
99
- if (!res.writableEnded)
100
- sendJson(res, 200, result);
101
- }
102
- catch (e) {
103
- deps.metrics?.inc?.("side_query_total", { result: "error" });
104
- if (!res.writableEnded && !ac.signal.aborted) {
105
- const m = msg(e);
106
- const isValidation = isCorePreflightValidationError(m);
107
- sendError(res, isValidation ? 400 : 500, isValidation ? "request.field_invalid" : "internal.error", redactSecrets(m));
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
- finally {
112
- res.off("close", onClose);
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
- export declare function handleTasks(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
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, url, ctx) {
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
- if (req.method === "POST" && (url === "/v1/tasks" || url === "/v1/tasks/stream")) {
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 (url === "/v1/tasks/stream") {
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: url === "/v1/tasks/stream" });
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 (url === "/v1/tasks/stream") {
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
- export declare function handleTraceUsage(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
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
- const TRACE_RE = /^\/v1\/tasks(?:\/([^/]+)(\/turns|\/stream|\/artifacts|\/tool-results\/[^/]+))?$/;
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, url, ctx) {
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 (req.method === "GET" && url === "/v1/tasks/source-summary") {
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 URL(req.url ?? "", "http://x").searchParams;
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
- const traceMatch = req.method === "GET" ? TRACE_RE.exec(url) : null;
36
- if (traceMatch) {
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 URL(req.url ?? "", "http://x").searchParams;
48
- const taskId = traceMatch[1];
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
- if (!taskId)
60
- await handleTaskList(res, deps.runStore, query, gateOwner);
61
- else if (sub === "/turns")
62
- await handleTaskTurns(res, deps.runStore, taskId, query);
63
- else if (sub === "/stream")
64
- await streamTaskTrace(req, res, deps.runStore, taskId, deps.config.runStaleSec * 1000);
65
- else if (sub === "/artifacts")
66
- await handleTaskArtifacts(res, deps.runStore, taskId);
67
- else if (sub?.startsWith(TOOL_RESULTS_SUB)) {
68
- await handleToolResult(res, deps.toolResultStore, deps.runStore, taskId, ownedRun, safeDecode(sub.slice(TOOL_RESULTS_SUB.length)), query);
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 (url.startsWith("/v1/usage/") && req.method === "GET") {
75
- const sub = url.slice("/v1/usage/".length).split("?")[0];
76
- if (sub === "summary" || sub === "series" || sub === "breakdown") {
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 URL(req.url ?? "", "http://x").searchParams;
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 URL(req.url ?? "", "http://x").searchParams.get("from") ?? 0);
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
- export declare function handleWorkflowAgentSteer(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
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
- const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/;
14
- const WORKFLOW_AGENT_STEER_RE = /^\/v1\/workflows\/([^/]+)\/agents\/([^/]+)\/steer$/;
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
- const wfMatch = req.method === "GET" ? WORKFLOWS_RE.exec(url) : null;
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 = wfMatch[1];
41
- const isStream = wfMatch[2] === "/stream";
42
- if (!wfId) {
43
- const q = new URL(req.url ?? "", "http://x").searchParams;
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 }, wfMatch[2] === "/journal" ? "workflow.journal" : isStream ? "workflow.stream" : "workflow.detail", "workflow not found"))
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 (wfMatch[2] === "/journal") {
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 URL(req.url ?? "", "http://x").searchParams;
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 handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
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
- const wfSteerMatch = req.method === "POST" ? WORKFLOW_AGENT_STEER_RE.exec(url) : null;
157
- if (wfSteerMatch) {
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(wfSteerMatch[1]);
170
- const label = safeDecode(wfSteerMatch[2]);
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