@sema-agent/server 1.316.0 → 1.317.0
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/README.md +9 -0
- package/README.zh-CN.md +7 -0
- package/dist/config-types.d.ts +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.js +148 -45
- package/dist/elicitation.js +2 -2
- package/dist/http/route-ctx.d.ts +51 -2
- package/dist/http/routes/approvals-assistant.d.ts +11 -0
- package/dist/http/routes/approvals-assistant.js +530 -0
- package/dist/http/routes/attachments.js +7 -7
- package/dist/http/routes/fleet.d.ts +4 -0
- package/dist/http/routes/fleet.js +147 -0
- package/dist/http/routes/images.js +29 -29
- package/dist/http/routes/leader.d.ts +4 -0
- package/dist/http/routes/leader.js +48 -0
- package/dist/http/routes/memory-policy.js +10 -10
- package/dist/http/routes/notify-wake.d.ts +4 -0
- package/dist/http/routes/notify-wake.js +133 -0
- package/dist/http/routes/observability.js +5 -5
- package/dist/http/routes/runs.d.ts +19 -0
- package/dist/http/routes/runs.js +967 -0
- package/dist/http/routes/session-sync.js +28 -28
- package/dist/http/routes/sessions-list.js +8 -8
- package/dist/http/routes/sessions.js +47 -47
- package/dist/http/routes/side-query.d.ts +4 -0
- package/dist/http/routes/side-query.js +88 -0
- package/dist/http/routes/tasks.d.ts +4 -0
- package/dist/http/routes/tasks.js +632 -0
- package/dist/http/routes/trace-usage.d.ts +4 -0
- package/dist/http/routes/trace-usage.js +239 -0
- package/dist/http/routes/workflows.d.ts +5 -0
- package/dist/http/routes/workflows.js +337 -0
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +16 -0
- package/dist/http/send.d.ts +1 -0
- package/dist/http/send.js +15 -0
- package/dist/http/server.d.ts +6 -5
- package/dist/http/server.js +241 -3166
- package/dist/http/sse-log.js +2 -2
- package/dist/http/wire-types.d.ts +6 -0
- package/dist/leader/endpoint.js +4 -4
- package/dist/main.js +5 -5
- package/dist/plugins/remote-env-host.js +4 -3
- package/dist/question.js +2 -2
- package/dist/run-local.js +1 -1
- package/dist/tool-approval.js +2 -2
- package/dist/trace/ledger-sink.js +1 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { projectEvents, runSummary, mapTraceEvent } from "../../trace/project.js";
|
|
2
|
+
import { projectArtifacts } from "../../trace/artifacts.js";
|
|
3
|
+
import { usageSummary, usageSeries, usageBreakdown } from "../../usage-analytics.js";
|
|
4
|
+
import { sleep } from "../sse-log.js";
|
|
5
|
+
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
6
|
+
import { gatedPrincipal } from "../principal-gate.js";
|
|
7
|
+
const TRACE_RE = /^\/v1\/tasks(?:\/([^/]+)(\/turns|\/stream|\/artifacts))?$/;
|
|
8
|
+
export async function handleTraceUsage(req, res, url, ctx) {
|
|
9
|
+
const miss = { fell: false };
|
|
10
|
+
await handleTraceUsageBody(req, res, url, ctx, miss);
|
|
11
|
+
return !miss.fell;
|
|
12
|
+
}
|
|
13
|
+
async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
14
|
+
const { deps } = ctx;
|
|
15
|
+
const { isFleetWide } = ctx.helpers;
|
|
16
|
+
if (req.method === "GET" && url === "/v1/tasks/source-summary") {
|
|
17
|
+
if (!isFleetWide(req)) {
|
|
18
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!deps.runStore) {
|
|
22
|
+
sendError(res, 501, "capability.run_store_required", "trace API requires the TiDB run store (SESSION_BACKEND=tidb)");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
26
|
+
const sinceSec = Math.min(30 * 86_400, Math.max(60, Number(q.get("sinceSec") ?? 86_400) || 86_400));
|
|
27
|
+
sendJson(res, 200, { sinceSec, rows: await deps.runStore.sourceSummary(Date.now() - sinceSec * 1000) });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const traceMatch = req.method === "GET" ? TRACE_RE.exec(url) : null;
|
|
31
|
+
if (traceMatch) {
|
|
32
|
+
const fleetWide = isFleetWide(req);
|
|
33
|
+
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
34
|
+
if (!fleetWide && gateOwner === null) {
|
|
35
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!deps.runStore) {
|
|
39
|
+
sendError(res, 501, "capability.run_store_required", "trace API requires the TiDB run store (SESSION_BACKEND=tidb)");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const query = new URL(req.url ?? "", "http://x").searchParams;
|
|
43
|
+
const taskId = traceMatch[1];
|
|
44
|
+
const sub = traceMatch[2];
|
|
45
|
+
if (taskId && gateOwner !== null) {
|
|
46
|
+
const run = await deps.runStore.getRun(taskId);
|
|
47
|
+
if (!run || (run.owner !== null && run.owner !== gateOwner)) {
|
|
48
|
+
sendError(res, 404, "not_found.run", "task not found");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!taskId)
|
|
53
|
+
await handleTaskList(res, deps.runStore, query, gateOwner);
|
|
54
|
+
else if (sub === "/turns")
|
|
55
|
+
await handleTaskTurns(res, deps.runStore, taskId, query);
|
|
56
|
+
else if (sub === "/stream")
|
|
57
|
+
await streamTaskTrace(req, res, deps.runStore, taskId, deps.config.runStaleSec * 1000);
|
|
58
|
+
else if (sub === "/artifacts")
|
|
59
|
+
await handleTaskArtifacts(res, deps.runStore, taskId);
|
|
60
|
+
else
|
|
61
|
+
sendError(res, 404, "not_found.route", "not found");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (url.startsWith("/v1/usage/") && req.method === "GET") {
|
|
65
|
+
const sub = url.slice("/v1/usage/".length).split("?")[0];
|
|
66
|
+
if (sub === "summary" || sub === "series" || sub === "breakdown") {
|
|
67
|
+
if (!deps.runStore) {
|
|
68
|
+
sendError(res, 501, "capability.run_store_required", "usage analytics requires a run store");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const fleetWide = isFleetWide(req);
|
|
72
|
+
const jwtOwner = fleetWide ? undefined : gatedPrincipal(req, deps.config);
|
|
73
|
+
if (!fleetWide && !jwtOwner) {
|
|
74
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
78
|
+
const parseT = (v, dflt) => {
|
|
79
|
+
if (!v)
|
|
80
|
+
return dflt;
|
|
81
|
+
const n = /^\d+$/.test(v) ? Number(v) : Date.parse(v);
|
|
82
|
+
return Number.isFinite(n) ? n : NaN;
|
|
83
|
+
};
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const from = parseT(q.get("from"), now - 7 * 86_400_000);
|
|
86
|
+
const to = parseT(q.get("to"), now);
|
|
87
|
+
if (Number.isNaN(from) || Number.isNaN(to) || from >= to) {
|
|
88
|
+
sendError(res, 400, "request.query_invalid", "invalid from/to (ISO-8601 or epoch-ms; from < to)");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const owner = jwtOwner ?? (q.get("principal") ?? undefined);
|
|
92
|
+
try {
|
|
93
|
+
const scan = await deps.runStore.usageScan(from, to, owner !== undefined ? { owner } : undefined);
|
|
94
|
+
const base = { from: new Date(from).toISOString(), to: new Date(to).toISOString(), ...(scan.truncated ? { truncated: true } : {}) };
|
|
95
|
+
if (sub === "summary") {
|
|
96
|
+
sendJson(res, 200, { ...base, ...usageSummary(scan.rows) });
|
|
97
|
+
}
|
|
98
|
+
else if (sub === "series") {
|
|
99
|
+
const metric = (q.get("metric") ?? "costUsd");
|
|
100
|
+
const granularity = (q.get("granularity") ?? "day");
|
|
101
|
+
if (!["tasks", "tokensIn", "tokensOut", "costUsd"].includes(metric) || !["hour", "day"].includes(granularity)) {
|
|
102
|
+
sendError(res, 400, "request.query_invalid", "metric=tasks|tokensIn|tokensOut|costUsd, granularity=hour|day");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
sendJson(res, 200, { ...base, metric, granularity, series: usageSeries(scan.rows, metric, granularity) });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const dimension = (q.get("dimension") ?? "principal");
|
|
109
|
+
if (!["principal", "model"].includes(dimension)) {
|
|
110
|
+
sendError(res, 400, "request.query_invalid", "dimension=principal|model");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
sendJson(res, 200, { ...base, dimension, breakdown: usageBreakdown(scan.rows, dimension) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
deps.logger?.warn?.("usage_analytics_failed", { err: String(err) });
|
|
118
|
+
sendError(res, 500, "internal.error", "usage analytics failed");
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
miss.fell = true;
|
|
124
|
+
}
|
|
125
|
+
function encodeCursor(c) {
|
|
126
|
+
return Buffer.from(JSON.stringify(c)).toString("base64url");
|
|
127
|
+
}
|
|
128
|
+
function decodeCursor(s) {
|
|
129
|
+
if (!s)
|
|
130
|
+
return undefined;
|
|
131
|
+
try {
|
|
132
|
+
const o = JSON.parse(Buffer.from(s, "base64url").toString());
|
|
133
|
+
return o.createdAt && o.taskId && !Number.isNaN(Date.parse(o.createdAt)) ? { createdAt: o.createdAt, taskId: o.taskId } : undefined;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function handleTaskList(res, runStore, query, forceOwner) {
|
|
140
|
+
const status = query.get("status") ?? undefined;
|
|
141
|
+
const jobId = query.get("jobId") ?? undefined;
|
|
142
|
+
const source = query.get("source") ?? undefined;
|
|
143
|
+
const owner = forceOwner != null ? forceOwner : (query.get("owner") ?? undefined);
|
|
144
|
+
const limit = Math.min(100, Math.max(1, Number(query.get("limit") ?? 50) || 50));
|
|
145
|
+
const cursor = decodeCursor(query.get("cursor"));
|
|
146
|
+
const rows = await runStore.listRuns({ ...(status ? { status } : {}), ...(jobId ? { jobId } : {}), ...(source ? { source } : {}), ...(owner ? { owner } : {}), ...(cursor ? { cursor } : {}), limit: limit + 1 });
|
|
147
|
+
const hasMore = rows.length > limit;
|
|
148
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
149
|
+
const tasks = page.map((r) => runSummary(r));
|
|
150
|
+
const last = page[page.length - 1];
|
|
151
|
+
const nextCursor = hasMore && last ? encodeCursor({ createdAt: last.createdAt, taskId: last.taskId }) : undefined;
|
|
152
|
+
sendJson(res, 200, { tasks, ...(nextCursor ? { nextCursor } : {}) });
|
|
153
|
+
}
|
|
154
|
+
async function handleTaskArtifacts(res, runStore, taskId) {
|
|
155
|
+
const run = await runStore.getRun(taskId);
|
|
156
|
+
if (!run) {
|
|
157
|
+
sendError(res, 404, "not_found.run", "task not found");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const events = await runStore.getEvents(taskId, 0);
|
|
161
|
+
sendJson(res, 200, { artifacts: projectArtifacts(events, { taskId, jobId: run.jobId }) });
|
|
162
|
+
}
|
|
163
|
+
async function handleTaskTurns(res, runStore, taskId, query) {
|
|
164
|
+
const run = await runStore.getRun(taskId);
|
|
165
|
+
if (!run) {
|
|
166
|
+
sendError(res, 404, "not_found.run", "task not found");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const limit = Math.min(100, Math.max(1, Number(query.get("limit") ?? 20) || 20));
|
|
170
|
+
const cursorSeq = Number(query.get("cursor") ?? 0) || 0;
|
|
171
|
+
const [events, retainedFrom] = await Promise.all([runStore.getEvents(taskId, 0), runStore.retainedFrom(taskId)]);
|
|
172
|
+
const all = projectEvents(events);
|
|
173
|
+
const eligible = cursorSeq > 0 ? all.filter((t) => t.seq < cursorSeq) : all;
|
|
174
|
+
const turns = eligible.slice(Math.max(0, eligible.length - limit));
|
|
175
|
+
const oldest = turns[0];
|
|
176
|
+
const nextCursor = eligible.length > limit && oldest ? String(oldest.seq) : undefined;
|
|
177
|
+
sendJson(res, 200, { turns, ...(nextCursor ? { nextCursor } : {}), retainedFrom });
|
|
178
|
+
}
|
|
179
|
+
async function streamTaskTrace(req, res, runStore, taskId, staleMs) {
|
|
180
|
+
const run0 = await runStore.getRun(taskId);
|
|
181
|
+
if (!run0) {
|
|
182
|
+
sendError(res, 404, "not_found.run", "task not found");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const lastId = Number(req.headers["last-event-id"] ?? new URL(req.url ?? "", "http://x").searchParams.get("from") ?? 0);
|
|
186
|
+
let from = Number.isFinite(lastId) ? lastId : 0;
|
|
187
|
+
if (from > 0) {
|
|
188
|
+
const retainedFrom = await runStore.retainedFrom(taskId);
|
|
189
|
+
if (retainedFrom > from + 1) {
|
|
190
|
+
sendError(res, 416, "limit.retention_evicted", "resume point evicted past retention", { retainedFrom });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
sseHeaders(res);
|
|
195
|
+
res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, mode: "delta", resumeFrom: from })}\n\n`);
|
|
196
|
+
let closed = false;
|
|
197
|
+
req.on("close", () => {
|
|
198
|
+
closed = true;
|
|
199
|
+
});
|
|
200
|
+
const start = Date.now();
|
|
201
|
+
const MAX_MS = 15 * 60 * 1000;
|
|
202
|
+
let lastBeat = Date.now();
|
|
203
|
+
const write = (events) => {
|
|
204
|
+
for (const ev of events) {
|
|
205
|
+
const out = mapTraceEvent(ev.type, ev.seq, (ev.data ?? {}));
|
|
206
|
+
if (out)
|
|
207
|
+
res.write(`id: ${ev.seq}\nevent: ${out.event}\ndata: ${JSON.stringify(out.data)}\n\n`);
|
|
208
|
+
from = ev.seq;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
while (!closed) {
|
|
212
|
+
const [events, run] = await Promise.all([runStore.getEvents(taskId, from), runStore.getRun(taskId)]);
|
|
213
|
+
write(events);
|
|
214
|
+
if (!run)
|
|
215
|
+
break;
|
|
216
|
+
if (run.status !== "running") {
|
|
217
|
+
write(await runStore.getEvents(taskId, from));
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
const stale = Date.now() - new Date(run.updatedAt).getTime() > staleMs;
|
|
221
|
+
if (stale && events.length === 0) {
|
|
222
|
+
res.write(`event: error\ndata: ${JSON.stringify({ code: "WORKER_DOWN", message: "run stalled (instance lost?)" })}\n\n`);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (Date.now() - start > MAX_MS) {
|
|
226
|
+
res.write(`event: error\ndata: ${JSON.stringify({ code: "STREAM_MAX_DURATION", message: "trace stream reached its 15-minute cap — reconnect with Last-Event-ID to continue (the run is still active)" })}\n\n`);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (events.length === 0) {
|
|
230
|
+
if (Date.now() - lastBeat > 15_000) {
|
|
231
|
+
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
232
|
+
lastBeat = Date.now();
|
|
233
|
+
}
|
|
234
|
+
await sleep(250);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
res.end();
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=trace-usage.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
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>;
|
|
4
|
+
export declare function handleWorkflowAgentSteer(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
5
|
+
//# sourceMappingURL=workflows.d.ts.map
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus, callKeyOrdinal } from "@sema-agent/core";
|
|
2
|
+
import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
|
|
3
|
+
import { redactSecrets } from "../../trace/redact.js";
|
|
4
|
+
import { HEARTBEAT_MS } from "../../runs.js";
|
|
5
|
+
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
6
|
+
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
7
|
+
const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/;
|
|
8
|
+
const WORKFLOW_AGENT_STEER_RE = /^\/v1\/workflows\/([^/]+)\/agents\/([^/]+)\/steer$/;
|
|
9
|
+
export async function handleWorkflows(req, res, url, ctx) {
|
|
10
|
+
const miss = { fell: false };
|
|
11
|
+
await handleWorkflowsReadBody(req, res, url, ctx, miss);
|
|
12
|
+
return !miss.fell;
|
|
13
|
+
}
|
|
14
|
+
export async function handleWorkflowAgentSteer(req, res, url, ctx) {
|
|
15
|
+
const miss = { fell: false };
|
|
16
|
+
await handleWorkflowAgentSteerBody(req, res, url, ctx, miss);
|
|
17
|
+
return !miss.fell;
|
|
18
|
+
}
|
|
19
|
+
async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
|
|
20
|
+
const { deps } = ctx;
|
|
21
|
+
const { runSessionAcceptOk } = ctx.helpers;
|
|
22
|
+
const wfMatch = req.method === "GET" ? WORKFLOWS_RE.exec(url) : null;
|
|
23
|
+
if (wfMatch) {
|
|
24
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
25
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
26
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!deps.workflowRunStore) {
|
|
30
|
+
sendError(res, 501, "capability.self_orchestration_required", "workflow runs require self-orchestration (SELF_ORCHESTRATION_ENABLED)");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const scope = principal ?? "default";
|
|
34
|
+
const wfId = wfMatch[1];
|
|
35
|
+
const isStream = wfMatch[2] === "/stream";
|
|
36
|
+
if (!wfId) {
|
|
37
|
+
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
38
|
+
const statusFilter = q.get("status");
|
|
39
|
+
const limit = Math.min(100, Math.max(1, Number(q.get("limit") ?? 50) || 50));
|
|
40
|
+
const sessionFilter = q.get("session");
|
|
41
|
+
const sessionRuns = await deps.workflowRunStore.listByScope(scope, {
|
|
42
|
+
...(statusFilter ? { status: statusFilter } : {}),
|
|
43
|
+
limit,
|
|
44
|
+
...(sessionFilter !== null ? { session: sessionFilter } : {}),
|
|
45
|
+
});
|
|
46
|
+
const redacted = sessionRuns.map((r) => ({
|
|
47
|
+
...r,
|
|
48
|
+
...(r.name !== undefined ? { name: redactSecrets(r.name) } : {}),
|
|
49
|
+
...(r.description !== undefined ? { description: redactSecrets(r.description) } : {}),
|
|
50
|
+
...(r.currentPhase !== undefined ? { currentPhase: redactSecrets(r.currentPhase) } : {}),
|
|
51
|
+
}));
|
|
52
|
+
sendJson(res, 200, { workflows: redacted });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const run = await getWorkflowRun(deps.workflowRunStore, wfId, scope);
|
|
56
|
+
if (!run || run.scope !== scope) {
|
|
57
|
+
sendError(res, 404, "not_found.workflow", "workflow not found");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!runSessionAcceptOk(req, res, { sessionId: run.originatingSessionId ?? null }, wfMatch[2] === "/journal" ? "workflow.journal" : isStream ? "workflow.stream" : "workflow.detail", "workflow not found"))
|
|
61
|
+
return;
|
|
62
|
+
if (wfMatch[2] === "/journal") {
|
|
63
|
+
if (!deps.workflowJournalStore) {
|
|
64
|
+
sendError(res, 501, "capability.workflow_store_required", "workflow journal requires the journal store (SELF_ORCHESTRATION_ENABLED + a store backend)");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const jq = new URL(req.url ?? "", "http://x").searchParams;
|
|
68
|
+
const jLimit = Math.min(50, Math.max(1, Number(jq.get("limit") ?? 20) || 20));
|
|
69
|
+
const jOffset = Math.max(0, Number(jq.get("offset") ?? 0) || 0);
|
|
70
|
+
const MAX_ROW_BYTES = 64 * 1024;
|
|
71
|
+
const firstLine = (t) => (t ? redactSecrets(t.split("\n")[0].slice(0, 300)) : undefined);
|
|
72
|
+
const projectResult = (callKey, r) => ({
|
|
73
|
+
callKey,
|
|
74
|
+
ordinal: callKeyOrdinal(callKey),
|
|
75
|
+
status: r.status,
|
|
76
|
+
...(r.errorMessage ? { error: firstLine(r.errorMessage) } : {}),
|
|
77
|
+
...(r.result ? { result: redactSecrets(r.result.slice(0, 2000)) } : {}),
|
|
78
|
+
...(r.stats ? { tokens: r.stats.tokens, turns: r.stats.turns } : {}),
|
|
79
|
+
});
|
|
80
|
+
const js = deps.workflowJournalStore;
|
|
81
|
+
let projected;
|
|
82
|
+
if (js.loadPage) {
|
|
83
|
+
const page = await js.loadPage(wfId, scope, { offset: jOffset, limit: jLimit, maxResultBytes: MAX_ROW_BYTES });
|
|
84
|
+
projected = page.map((row) => {
|
|
85
|
+
if (row.resultJson === null) {
|
|
86
|
+
return { callKey: row.callKey, ordinal: callKeyOrdinal(row.callKey), truncated: true, resultBytes: row.resultBytes };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return projectResult(row.callKey, JSON.parse(row.resultJson));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { callKey: row.callKey, ordinal: callKeyOrdinal(row.callKey), truncated: true, resultBytes: row.resultBytes };
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const entries = await deps.workflowJournalStore.load(wfId, scope);
|
|
98
|
+
projected = entries
|
|
99
|
+
.sort((a, b) => callKeyOrdinal(a.callKey) - callKeyOrdinal(b.callKey))
|
|
100
|
+
.slice(jOffset, jOffset + jLimit)
|
|
101
|
+
.map((en) => {
|
|
102
|
+
const bytes = Buffer.byteLength(JSON.stringify(en.result));
|
|
103
|
+
return bytes > MAX_ROW_BYTES
|
|
104
|
+
? { callKey: en.callKey, ordinal: callKeyOrdinal(en.callKey), truncated: true, resultBytes: bytes }
|
|
105
|
+
: projectResult(en.callKey, en.result);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
sendJson(res, 200, { runId: wfId, entries: projected, ...(projected.length === jLimit ? { nextOffset: jOffset + jLimit } : {}) });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (isStream)
|
|
112
|
+
await streamWorkflowRun(req, res, wfId, scope);
|
|
113
|
+
else
|
|
114
|
+
sendJson(res, 200, summarizeWorkflowDetail(run));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
miss.fell = true;
|
|
118
|
+
}
|
|
119
|
+
async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
|
|
120
|
+
const { deps } = ctx;
|
|
121
|
+
const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
|
|
122
|
+
const wfSteerMatch = req.method === "POST" ? WORKFLOW_AGENT_STEER_RE.exec(url) : null;
|
|
123
|
+
if (wfSteerMatch) {
|
|
124
|
+
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
125
|
+
return;
|
|
126
|
+
if (!deps.workflowRunStore) {
|
|
127
|
+
sendError(res, 501, "capability.self_orchestration_required", "workflow runs require self-orchestration (SELF_ORCHESTRATION_ENABLED)");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
131
|
+
if (deps.config.requirePrincipal && principal === undefined) {
|
|
132
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const wfRunId = safeDecode(wfSteerMatch[1]);
|
|
136
|
+
const label = safeDecode(wfSteerMatch[2]);
|
|
137
|
+
if (wfRunId === null || label === null) {
|
|
138
|
+
sendError(res, 400, "request.path_malformed", "malformed workflow path (invalid percent-encoding)");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let body;
|
|
142
|
+
try {
|
|
143
|
+
body = (await readJson(req));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (typeof body.content !== "string" || body.content.length === 0) {
|
|
150
|
+
sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
|
|
154
|
+
sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
158
|
+
const ownerScope = principal ?? "default";
|
|
159
|
+
const wfRun = await getWorkflowRun(deps.workflowRunStore, wfRunId, ownerScope);
|
|
160
|
+
if (!trusted && !wfRun) {
|
|
161
|
+
sendError(res, 404, "not_found.workflow", "workflow not found");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (wfRun && !runSessionAcceptOk(req, res, { sessionId: wfRun.originatingSessionId ?? null }, "workflow.agent-steer", "workflow not found"))
|
|
165
|
+
return;
|
|
166
|
+
const sendNotRunningWf = (error) => sendError(res, 409, "steering.not_running", error);
|
|
167
|
+
const redacted = redactSteerIn(body.content, label);
|
|
168
|
+
const resolution = deps.workflowAgentRegistry?.resolve(wfRunId, label);
|
|
169
|
+
if (resolution && resolution.count > 1) {
|
|
170
|
+
sendError(res, 409, "steering.ambiguous_label", `${resolution.count} live agents share label '${label}' in this run — steer target is ambiguous`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const handle = resolution?.handle;
|
|
174
|
+
if (handle) {
|
|
175
|
+
try {
|
|
176
|
+
const marker = await handle.steer(redacted);
|
|
177
|
+
sendJson(res, 200, { runId: wfRunId, label, status: "running", delivery: "applied", marker });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
const code = e.code;
|
|
182
|
+
if (code === "steering.not_running") {
|
|
183
|
+
sendNotRunningWf("agent just finished — no longer accepting steers");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
throw e;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
sendNotRunningWf(!wfRun
|
|
190
|
+
? "workflow agent is not running on this replica (no live handle)"
|
|
191
|
+
: wfRun.status === "running"
|
|
192
|
+
? "workflow agent is active on another replica — cross-replica live-steer is not yet supported"
|
|
193
|
+
: `workflow is ${wfRun.status} — agent is not running`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
miss.fell = true;
|
|
197
|
+
}
|
|
198
|
+
function summarizeWorkflowDetail(run) {
|
|
199
|
+
const isDone = (s) => s === "completed" || s === "failed";
|
|
200
|
+
const durationOf = (a) => a.startedAt === undefined || a.endedAt === undefined ? undefined : a.endedAt - a.startedAt;
|
|
201
|
+
const runPhases = run.phases ?? [];
|
|
202
|
+
const runAgents = run.agents ?? [];
|
|
203
|
+
const runGroups = run.groups ?? [];
|
|
204
|
+
const phases = runPhases.map((p) => {
|
|
205
|
+
const inPhase = runAgents.filter((a) => a.phase === p.title);
|
|
206
|
+
return {
|
|
207
|
+
title: redactSecrets(p.title),
|
|
208
|
+
status: p.status,
|
|
209
|
+
startedAt: p.startedAt,
|
|
210
|
+
endedAt: p.endedAt,
|
|
211
|
+
durationMs: durationOf(p),
|
|
212
|
+
done: inPhase.filter((a) => isDone(a.status)).length,
|
|
213
|
+
total: inPhase.length,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
const phaseTitleSet = new Set(runPhases.map((p) => p.title));
|
|
217
|
+
const unphased = runAgents.filter((a) => a.phase === undefined || a.phase === "" || !phaseTitleSet.has(a.phase));
|
|
218
|
+
const unphasedView = unphased.length > 0
|
|
219
|
+
? { done: unphased.filter((a) => isDone(a.status)).length, total: unphased.length }
|
|
220
|
+
: undefined;
|
|
221
|
+
const agents = runAgents.map((a) => ({
|
|
222
|
+
label: redactSecrets(a.label),
|
|
223
|
+
status: a.status,
|
|
224
|
+
displayStatus: deriveAgentDisplayStatus(a, run.status),
|
|
225
|
+
taskStatus: a.taskStatus,
|
|
226
|
+
callKey: a.callKey,
|
|
227
|
+
groupId: a.groupId,
|
|
228
|
+
phase: a.phase !== undefined ? redactSecrets(a.phase) : undefined,
|
|
229
|
+
model: a.model,
|
|
230
|
+
tokens: a.stats?.tokens,
|
|
231
|
+
turns: a.stats?.turns,
|
|
232
|
+
toolCalls: a.toolCalls,
|
|
233
|
+
activity: a.activity,
|
|
234
|
+
lastActivityAt: a.activity && a.activity.length > 0 ? a.activity[a.activity.length - 1].at : undefined,
|
|
235
|
+
durationMs: durationOf(a),
|
|
236
|
+
queuedAt: a.queuedAt,
|
|
237
|
+
startedAt: a.startedAt,
|
|
238
|
+
endedAt: a.endedAt,
|
|
239
|
+
replayed: a.replayed,
|
|
240
|
+
prompt: a.prompt,
|
|
241
|
+
output: a.output,
|
|
242
|
+
}));
|
|
243
|
+
const byId = new Map();
|
|
244
|
+
for (const g of runGroups) {
|
|
245
|
+
byId.set(g.groupId, {
|
|
246
|
+
groupId: g.groupId,
|
|
247
|
+
parentGroupId: g.parentGroupId,
|
|
248
|
+
status: g.status,
|
|
249
|
+
startedAt: g.startedAt,
|
|
250
|
+
endedAt: g.endedAt,
|
|
251
|
+
durationMs: durationOf(g),
|
|
252
|
+
agentCallKeys: runAgents.filter((a) => a.groupId === g.groupId).map((a) => a.callKey),
|
|
253
|
+
children: [],
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const inCycle = (start) => {
|
|
257
|
+
const seen = new Set();
|
|
258
|
+
let cur = start;
|
|
259
|
+
while (cur && cur.parentGroupId !== undefined) {
|
|
260
|
+
if (seen.has(cur.groupId))
|
|
261
|
+
return true;
|
|
262
|
+
seen.add(cur.groupId);
|
|
263
|
+
cur = byId.get(cur.parentGroupId);
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
};
|
|
267
|
+
const roots = [];
|
|
268
|
+
for (const node of byId.values()) {
|
|
269
|
+
const parent = node.parentGroupId !== undefined ? byId.get(node.parentGroupId) : undefined;
|
|
270
|
+
if (parent && parent !== node && !inCycle(node))
|
|
271
|
+
parent.children.push(node);
|
|
272
|
+
else
|
|
273
|
+
roots.push(node);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
id: run.id,
|
|
277
|
+
scope: run.scope,
|
|
278
|
+
status: run.status,
|
|
279
|
+
...(run.name !== undefined ? { name: redactSecrets(run.name) } : {}),
|
|
280
|
+
...(run.description !== undefined ? { description: redactSecrets(run.description) } : {}),
|
|
281
|
+
stats: run.stats,
|
|
282
|
+
...(run.agentFailures !== undefined ? { agentFailures: run.agentFailures } : {}),
|
|
283
|
+
startedAt: run.startedAt,
|
|
284
|
+
endedAt: run.endedAt,
|
|
285
|
+
createdAt: run.createdAt,
|
|
286
|
+
durationMs: durationOf(run),
|
|
287
|
+
rev: run.rev,
|
|
288
|
+
error: run.error,
|
|
289
|
+
...(run.result !== undefined ? { result: redactSecrets(run.result) } : {}),
|
|
290
|
+
phases,
|
|
291
|
+
...(unphasedView ? { unphased: unphasedView } : {}),
|
|
292
|
+
agents,
|
|
293
|
+
groups: roots,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async function streamWorkflowRun(req, res, runId, scope) {
|
|
297
|
+
sseHeaders(res);
|
|
298
|
+
res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, runId })}\n\n`);
|
|
299
|
+
let closed = false;
|
|
300
|
+
const it = subscribeWorkflow(runId, scope)[Symbol.asyncIterator]();
|
|
301
|
+
const hb = setInterval(() => {
|
|
302
|
+
if (!res.writableEnded)
|
|
303
|
+
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
304
|
+
}, 15_000);
|
|
305
|
+
if (typeof hb.unref === "function")
|
|
306
|
+
hb.unref();
|
|
307
|
+
const shutdown = () => {
|
|
308
|
+
if (closed)
|
|
309
|
+
return;
|
|
310
|
+
closed = true;
|
|
311
|
+
clearInterval(hb);
|
|
312
|
+
void it.return?.(undefined);
|
|
313
|
+
if (!res.writableEnded)
|
|
314
|
+
res.end();
|
|
315
|
+
};
|
|
316
|
+
req.on("close", shutdown);
|
|
317
|
+
res.on("close", shutdown);
|
|
318
|
+
try {
|
|
319
|
+
for (let next = await it.next(); !next.done; next = await it.next()) {
|
|
320
|
+
if (closed || res.writableEnded)
|
|
321
|
+
break;
|
|
322
|
+
const ev = next.value;
|
|
323
|
+
res.write(`event: ${ev.type ?? "event"}\ndata: ${JSON.stringify(ev)}\n\n`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
if (!closed && !res.writableEnded)
|
|
328
|
+
res.write(`event: error\ndata: ${JSON.stringify({ message: "workflow stream error" })}\n\n`);
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
clearInterval(hb);
|
|
332
|
+
void it.return?.(undefined);
|
|
333
|
+
}
|
|
334
|
+
if (!res.writableEnded)
|
|
335
|
+
res.end();
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=workflows.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TaskSpec, CascadeConfig } from "@sema-agent/core";
|
|
2
|
+
export declare function runMeta(prepared: {
|
|
3
|
+
jobId?: string;
|
|
4
|
+
spec: TaskSpec;
|
|
5
|
+
}, source: string | null): {
|
|
6
|
+
jobId: string | null;
|
|
7
|
+
source: string | null;
|
|
8
|
+
objectivePreview: string | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function cascadeConfig(ladder: readonly string[], maxCostUsd?: number): CascadeConfig;
|
|
11
|
+
//# sourceMappingURL=run-meta.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { redactSecrets } from "../trace/redact.js";
|
|
2
|
+
export function runMeta(prepared, source) {
|
|
3
|
+
const obj = prepared.spec.objective ?? "";
|
|
4
|
+
return {
|
|
5
|
+
jobId: prepared.jobId ?? null,
|
|
6
|
+
source,
|
|
7
|
+
objectivePreview: obj ? redactSecrets(obj).slice(0, 120) : null,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function cascadeConfig(ladder, maxCostUsd) {
|
|
11
|
+
return {
|
|
12
|
+
ladder: ladder.map((model) => ({ model })),
|
|
13
|
+
...(maxCostUsd && maxCostUsd > 0 ? { costCeilingMicroUsd: Math.round(maxCostUsd * 1e6) } : {}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=run-meta.js.map
|
package/dist/http/send.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import type { ServerResponse } from "node:http";
|
|
|
2
2
|
export declare function sseHeaders(res: ServerResponse, extra?: Record<string, string>): void;
|
|
3
3
|
export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
|
|
4
4
|
export declare function sendError(res: ServerResponse, status: number, errorCode: string, message: string, extra?: Record<string, unknown>): void;
|
|
5
|
+
export declare function httpErrorCode(status: number, code?: string): string;
|
|
5
6
|
export declare function msg(e: unknown): string;
|
|
6
7
|
//# sourceMappingURL=send.d.ts.map
|
package/dist/http/send.js
CHANGED
|
@@ -13,6 +13,21 @@ export function sendJson(res, status, body) {
|
|
|
13
13
|
export function sendError(res, status, errorCode, message, extra) {
|
|
14
14
|
sendJson(res, status, { error: message, errorCode, ...extra });
|
|
15
15
|
}
|
|
16
|
+
export function httpErrorCode(status, code) {
|
|
17
|
+
if (code)
|
|
18
|
+
return code;
|
|
19
|
+
switch (status) {
|
|
20
|
+
case 400: return "request.rejected";
|
|
21
|
+
case 401: return "auth.unauthorized";
|
|
22
|
+
case 403: return "auth.forbidden";
|
|
23
|
+
case 404: return "not_found.resource";
|
|
24
|
+
case 413: return "request.payload_too_large";
|
|
25
|
+
case 422: return "request.unprocessable";
|
|
26
|
+
case 429: return "limit.rate_exceeded";
|
|
27
|
+
case 501: return "capability.unavailable";
|
|
28
|
+
default: return status >= 500 ? "internal.error" : "request.rejected";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
16
31
|
export function msg(e) {
|
|
17
32
|
return e instanceof Error ? e.message : String(e);
|
|
18
33
|
}
|
package/dist/http/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import type { IncomingMessage
|
|
3
|
-
import { type Runner, type TaskSpec, type TaskResult, type
|
|
2
|
+
import type { IncomingMessage } from "node:http";
|
|
3
|
+
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry } from "@sema-agent/core";
|
|
4
4
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
5
5
|
import type { ServiceConfig } from "../config-types.js";
|
|
6
6
|
import { type RestartSignal, type SessionMirrorRuling } from "../sema-registry.js";
|
|
@@ -25,6 +25,10 @@ import type { QuotaTracker } from "../observability/cost-quota.js";
|
|
|
25
25
|
import type { ModelUsageTracker, PromptManifestTracker } from "../budget.js";
|
|
26
26
|
import { scopedIdempotencyKey } from "./idempotency.js";
|
|
27
27
|
export { scopedIdempotencyKey };
|
|
28
|
+
import { streamApprovals, isQuestionAnswer } from "./routes/approvals-assistant.js";
|
|
29
|
+
export { streamApprovals, isQuestionAnswer };
|
|
30
|
+
import { cascadeConfig } from "./run-meta.js";
|
|
31
|
+
export { cascadeConfig };
|
|
28
32
|
import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
|
|
29
33
|
export { coarseStatusForState, errorCodeForExit };
|
|
30
34
|
import { explicitOperatorOk, isOperator, explicitOperator } from "./principal-gate.js";
|
|
@@ -197,11 +201,8 @@ export declare const MAX_SKILL_CONTENT_CHARS = 1048576;
|
|
|
197
201
|
export declare const MAX_SYSTEM_PROMPT_CHARS = 16384;
|
|
198
202
|
export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
|
|
199
203
|
export declare function validateUserSkills(skills: unknown): string | null;
|
|
200
|
-
export declare function cascadeConfig(ladder: readonly string[], maxCostUsd?: number): CascadeConfig;
|
|
201
204
|
export declare function clampVerifyRounds(v: unknown): number;
|
|
202
|
-
export declare function isQuestionAnswer(v: unknown): v is QuestionAnswer;
|
|
203
205
|
export declare function createHttpServer(rawDeps: ServiceDeps): http.Server & {
|
|
204
206
|
denyExpiredApprovals: (now: number) => Promise<void>;
|
|
205
207
|
};
|
|
206
|
-
export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">, scope: string | undefined, pollMs?: number): Promise<void>;
|
|
207
208
|
//# sourceMappingURL=server.d.ts.map
|