@tangle-network/agent-runtime 0.193.1 → 0.194.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/dist/{activation-BismttgN.js → activation-BQFIiyUG.js} +3 -3
- package/dist/{activation-BismttgN.js.map → activation-BQFIiyUG.js.map} +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/candidate-execution/index.js +4 -4
- package/dist/{candidate-execution-BSkZErxD.js → candidate-execution-B6CDW-fp.js} +4 -4
- package/dist/{candidate-execution-BSkZErxD.js.map → candidate-execution-B6CDW-fp.js.map} +1 -1
- package/dist/{conversation-DILnmy84.js → conversation-BXPWMOSd.js} +4 -5
- package/dist/{conversation-DILnmy84.js.map → conversation-BXPWMOSd.js.map} +1 -1
- package/dist/conversation.d.ts +1 -1
- package/dist/conversation.js +1 -1
- package/dist/{coordination-driver-DujHiIGa.js → coordination-driver-iSN-m3VX.js} +281 -244
- package/dist/coordination-driver-iSN-m3VX.js.map +1 -0
- package/dist/{authoring-DWiqKSDY.js → delegate-DCC2muUd.js} +56 -13
- package/dist/delegate-DCC2muUd.js.map +1 -0
- package/dist/delegation-status-BlsbSeOB.js +358 -0
- package/dist/delegation-status-BlsbSeOB.js.map +1 -0
- package/dist/durable.d.ts +2 -2
- package/dist/durable.js +3 -3
- package/dist/{environment-provider-DEwsolvi.js → environment-provider-Dr-wfnHg.js} +422 -8
- package/dist/environment-provider-Dr-wfnHg.js.map +1 -0
- package/dist/{environment-provider-DpPGJfK-.d.ts → environment-provider-adVa6X7a.d.ts} +2 -2
- package/dist/environment-provider.d.ts +1 -1
- package/dist/environment-provider.js +1 -1
- package/dist/{graph-DORfp9WX.js → graph-DghidDx5.js} +138 -5
- package/dist/graph-DghidDx5.js.map +1 -0
- package/dist/graph.d.ts +3 -3
- package/dist/graph.js +4 -5
- package/dist/graph.js.map +1 -1
- package/dist/{improve-Bn1zpWHF.d.ts → improve-COGzLCiY.d.ts} +3 -3
- package/dist/{improvement-cycle-BbsiYcpH.js → improvement-cycle-DeS1ZC5B.js} +6 -6
- package/dist/{improvement-cycle-BbsiYcpH.js.map → improvement-cycle-DeS1ZC5B.js.map} +1 -1
- package/dist/{index-DV1lzp9p.d.ts → index-BEPjOPwH.d.ts} +49 -50
- package/dist/{index-Vz-13VZq.d.ts → index-CrBgLCIf.d.ts} +4 -4
- package/dist/{index-RM2oRI9s.d.ts → index-DO6DHgRP.d.ts} +2 -2
- package/dist/index.d.ts +8 -8
- package/dist/index.js +13 -13
- package/dist/intelligence.d.ts +5 -5
- package/dist/intelligence.js +6 -6
- package/dist/{executable-spec-DxuiDFAU.js → jsonl-file-Bh1Q9nt0.js} +59 -3
- package/dist/jsonl-file-Bh1Q9nt0.js.map +1 -0
- package/dist/kernel.d.ts +6 -6
- package/dist/kernel.js +11 -12
- package/dist/{knowledge-RW1pbZCH.js → knowledge-B5okOzBQ.js} +6 -6
- package/dist/{knowledge-RW1pbZCH.js.map → knowledge-B5okOzBQ.js.map} +1 -1
- package/dist/knowledge.d.ts +1 -1
- package/dist/knowledge.js +1 -1
- package/dist/{loop-runner-bin-CGraqNiA.d.ts → loop-runner-bin-Clc5IAwu.d.ts} +3 -3
- package/dist/{loop-runner-bin-sBwcyQTc.js → loop-runner-bin-U1J6x_UF.js} +3 -3
- package/dist/{loop-runner-bin-sBwcyQTc.js.map → loop-runner-bin-U1J6x_UF.js.map} +1 -1
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/{materialization-CekWK6OO.js → materialization-vZssF9Nx.js} +28 -3
- package/dist/materialization-vZssF9Nx.js.map +1 -0
- package/dist/mcp/bin.js +3 -3
- package/dist/mcp/index.d.ts +4 -4
- package/dist/mcp/index.js +8 -8
- package/dist/mcp/index.js.map +1 -1
- package/dist/{openai-tools-6jUqkGG7.js → openai-tools-DmvmaG0a.js} +2 -2
- package/dist/{openai-tools-6jUqkGG7.js.map → openai-tools-DmvmaG0a.js.map} +1 -1
- package/dist/{prepare-B5fJ7mLS.js → prepare-C29kNAon.js} +9 -14
- package/dist/prepare-C29kNAon.js.map +1 -0
- package/dist/primeintellect/index.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/{protected-model-port-B0xFSAjt.js → protected-model-port-CKYND416.js} +2 -2
- package/dist/{protected-model-port-B0xFSAjt.js.map → protected-model-port-CKYND416.js.map} +1 -1
- package/dist/{provision-supervisor-CddfH9mg.js → provision-supervisor-CRPbnJiB.js} +6 -8
- package/dist/{provision-supervisor-CddfH9mg.js.map → provision-supervisor-CRPbnJiB.js.map} +1 -1
- package/dist/{redact-D0OzQsyt.js → redact-BOU77QfQ.js} +1013 -24
- package/dist/redact-BOU77QfQ.js.map +1 -0
- package/dist/{researcher-hgPa5p9i.js → researcher-Cp4JRbFp.js} +2 -2
- package/dist/researcher-Cp4JRbFp.js.map +1 -0
- package/dist/{runtime-V91N6sPc.d.ts → runtime-B8x43nGP.d.ts} +5 -4
- package/dist/{runtime-ZM4i-jEF.js → runtime-BVrqSu0-.js} +22 -21
- package/dist/{runtime-ZM4i-jEF.js.map → runtime-BVrqSu0-.js.map} +1 -1
- package/dist/server-D6XmE1Du.js +1311 -0
- package/dist/server-D6XmE1Du.js.map +1 -0
- package/dist/{stream-agent-turn-1ahpdeIo.d.ts → stream-agent-turn-B-gtBlOy.d.ts} +2 -2
- package/dist/{stream-agent-turn-DWWe_7bh.js → stream-agent-turn-DOtEsvCV.js} +3 -3
- package/dist/{stream-agent-turn-DWWe_7bh.js.map → stream-agent-turn-DOtEsvCV.js.map} +1 -1
- package/dist/{structural-rollout-CktnUTCC.js → structural-rollout-BcPXFzVb.js} +16 -43
- package/dist/structural-rollout-BcPXFzVb.js.map +1 -0
- package/dist/{supervise-Dj6zd2C7.js → supervise-C0V3pZXK.js} +154 -1961
- package/dist/supervise-C0V3pZXK.js.map +1 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +13 -13
- package/dist/tui/index.d.ts +1 -1
- package/dist/tui/index.js +1 -1
- package/dist/{types-CGez_ZHQ.d.ts → types-B_pNTBjK.d.ts} +5 -2
- package/dist/{workspace-archive-yAPDklLV.js → workspace-archive-BCezkeIk.js} +2 -2
- package/dist/{workspace-archive-yAPDklLV.js.map → workspace-archive-BCezkeIk.js.map} +1 -1
- package/package.json +1 -1
- package/skills/codemode/SKILL.md +2 -2
- package/skills/supervise/SKILL.md +24 -6
- package/dist/authoring-DWiqKSDY.js.map +0 -1
- package/dist/coordination-driver-DujHiIGa.js.map +0 -1
- package/dist/environment-provider-DEwsolvi.js.map +0 -1
- package/dist/executable-spec-DxuiDFAU.js.map +0 -1
- package/dist/graph-DORfp9WX.js.map +0 -1
- package/dist/jsonl-file-CDfsCI5s.js +0 -59
- package/dist/jsonl-file-CDfsCI5s.js.map +0 -1
- package/dist/materialization-CekWK6OO.js.map +0 -1
- package/dist/prepare-B5fJ7mLS.js.map +0 -1
- package/dist/redact-D0OzQsyt.js.map +0 -1
- package/dist/researcher-hgPa5p9i.js.map +0 -1
- package/dist/snapshot-CTAf4uuA.js +0 -26
- package/dist/snapshot-CTAf4uuA.js.map +0 -1
- package/dist/spawn-journal-B9eTmVAn.js +0 -999
- package/dist/spawn-journal-B9eTmVAn.js.map +0 -1
- package/dist/structural-rollout-CktnUTCC.js.map +0 -1
- package/dist/supervise-Dj6zd2C7.js.map +0 -1
- package/dist/util-mCNUeboW.js +0 -419
- package/dist/util-mCNUeboW.js.map +0 -1
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
import { m as ValidationError, t as AgentEvalError } from "./errors-CDZ8XsVj.js";
|
|
2
|
+
import { Tn as buildLoopSpanNodes } from "./redact-BOU77QfQ.js";
|
|
3
|
+
import { t as createStdioToolServer } from "./tool-server-DEmLr9YY.js";
|
|
4
|
+
import { n as delegate } from "./delegate-DCC2muUd.js";
|
|
5
|
+
import { _ as InMemoryFeedbackStore, c as DELEGATION_HISTORY_TOOL_NAME, f as DELEGATE_FEEDBACK_DESCRIPTION, h as createDelegateFeedbackHandler, i as createDelegationStatusHandler, l as createDelegationHistoryHandler, m as DELEGATE_FEEDBACK_TOOL_NAME, n as DELEGATION_STATUS_INPUT_SCHEMA, o as DELEGATION_HISTORY_DESCRIPTION, p as DELEGATE_FEEDBACK_INPUT_SCHEMA, r as DELEGATION_STATUS_TOOL_NAME, s as DELEGATION_HISTORY_INPUT_SCHEMA, t as DELEGATION_STATUS_DESCRIPTION } from "./delegation-status-BlsbSeOB.js";
|
|
6
|
+
import { n as UI_LENSES } from "./substrate-B0TYNrXn.js";
|
|
7
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
8
|
+
import path, { dirname } from "node:path";
|
|
9
|
+
import { Readable, Writable } from "node:stream";
|
|
10
|
+
//#region src/mcp/delegation-trace.ts
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* Compact loop-trace tee for the delegation journal.
|
|
14
|
+
*
|
|
15
|
+
* The OTEL exporter ({@link createPropagatingTraceEmitter}) is a no-op
|
|
16
|
+
* without `OTEL_EXPORTER_OTLP_ENDPOINT`, which leaves delegated work streams
|
|
17
|
+
* dark in practice. This module derives the same loop → round → branch span
|
|
18
|
+
* tree (via the shared {@link buildLoopSpanNodes} builder) into a small,
|
|
19
|
+
* JSON-safe shape persisted directly on the `DelegationRecord` — observable
|
|
20
|
+
* through `delegation_status` with no collector infrastructure. Both sinks
|
|
21
|
+
* coexist: the OTEL export path is unchanged.
|
|
22
|
+
*
|
|
23
|
+
* Payload discipline: a record's trace is hard-capped (spans + serialized
|
|
24
|
+
* bytes). Past the cap the OLDEST spans are dropped and the record carries a
|
|
25
|
+
* `traceTruncated: true` marker — truncation is never silent.
|
|
26
|
+
*
|
|
27
|
+
* @experimental
|
|
28
|
+
*/
|
|
29
|
+
/** Default cap on spans retained per delegation record. @experimental */
|
|
30
|
+
const DELEGATION_TRACE_MAX_SPANS = 512;
|
|
31
|
+
/** Default cap on the serialized trace payload per record, in bytes. @experimental */
|
|
32
|
+
const DELEGATION_TRACE_MAX_BYTES = 256 * 1024;
|
|
33
|
+
/**
|
|
34
|
+
* Derive the compact span tree for ONE loop run from its buffered
|
|
35
|
+
* `LoopTraceEvent` stream. Same reconstruction as the OTEL exporter
|
|
36
|
+
* ({@link buildLoopSpanNodes}); tolerates partial streams.
|
|
37
|
+
*
|
|
38
|
+
* @experimental
|
|
39
|
+
*/
|
|
40
|
+
function buildDelegationTraceSpans(events) {
|
|
41
|
+
return buildLoopSpanNodes(events).map((node) => ({
|
|
42
|
+
spanId: node.spanId,
|
|
43
|
+
...node.parentSpanId !== void 0 ? { parentSpanId: node.parentSpanId } : {},
|
|
44
|
+
name: node.name,
|
|
45
|
+
kind: node.kind,
|
|
46
|
+
startMs: node.startMs,
|
|
47
|
+
endMs: node.endMs,
|
|
48
|
+
...Object.keys(node.attrs).length > 0 ? { meta: node.attrs } : {}
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Enforce the trace caps over an ordered (oldest-first) span list. Drops the
|
|
53
|
+
* OLDEST spans first and reports `truncated: true` when anything was dropped;
|
|
54
|
+
* the newest span always survives, so a non-empty input never caps to empty.
|
|
55
|
+
* Dropping a parent may orphan surviving children's `parentSpanId` references
|
|
56
|
+
* — acceptable for the flat journal shape; consumers treat unresolved parents
|
|
57
|
+
* as roots.
|
|
58
|
+
*
|
|
59
|
+
* @experimental
|
|
60
|
+
*/
|
|
61
|
+
function capDelegationTrace(spans, caps) {
|
|
62
|
+
const maxSpans = caps?.maxSpans ?? 512;
|
|
63
|
+
const maxBytes = caps?.maxBytes ?? 262144;
|
|
64
|
+
let start = Math.max(0, spans.length - maxSpans);
|
|
65
|
+
const sizes = spans.map((span) => JSON.stringify(span).length + 1);
|
|
66
|
+
let total = 0;
|
|
67
|
+
for (let i = start; i < sizes.length; i += 1) total += sizes[i];
|
|
68
|
+
while (start < spans.length - 1 && total > maxBytes) {
|
|
69
|
+
total -= sizes[start];
|
|
70
|
+
start += 1;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
trace: spans.slice(start),
|
|
74
|
+
truncated: start > 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Build a `DelegationTraceCollector` that buffers loop-trace events and converts them to spans on settle. @experimental */
|
|
78
|
+
function createDelegationTraceCollector(onSpans) {
|
|
79
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
80
|
+
const flush = (events) => {
|
|
81
|
+
const spans = buildDelegationTraceSpans(events);
|
|
82
|
+
if (spans.length > 0) onSpans(spans);
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
emitter: { emit(event) {
|
|
86
|
+
const buf = buffers.get(event.runId);
|
|
87
|
+
if (buf) buf.push(event);
|
|
88
|
+
else buffers.set(event.runId, [event]);
|
|
89
|
+
if (event.kind === "loop.ended") {
|
|
90
|
+
const events = buffers.get(event.runId) ?? [event];
|
|
91
|
+
buffers.delete(event.runId);
|
|
92
|
+
flush(events);
|
|
93
|
+
}
|
|
94
|
+
} },
|
|
95
|
+
settle() {
|
|
96
|
+
for (const events of buffers.values()) flush(events);
|
|
97
|
+
buffers.clear();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 16-hex-char span id for journal spans synthesized outside the shared loop
|
|
103
|
+
* builder (e.g. the queue's detached-resume segment).
|
|
104
|
+
*
|
|
105
|
+
* @experimental
|
|
106
|
+
*/
|
|
107
|
+
function generateDelegationSpanId() {
|
|
108
|
+
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
109
|
+
if (typeof globalThis.crypto?.getRandomValues === "function") globalThis.crypto.getRandomValues(bytes);
|
|
110
|
+
else for (let i = 0; i < 8; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
111
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Fan one `LoopTraceEvent` stream into several emitters — e.g. the
|
|
115
|
+
* process-wide OTEL exporter AND the per-delegation journal collector.
|
|
116
|
+
* `undefined` entries are skipped; returns `undefined` when nothing is left
|
|
117
|
+
* so callers keep the kernel's "no emitter, no events" fast path.
|
|
118
|
+
*
|
|
119
|
+
* @experimental
|
|
120
|
+
*/
|
|
121
|
+
function composeLoopTraceEmitters(...emitters) {
|
|
122
|
+
const live = emitters.filter((e) => e !== void 0);
|
|
123
|
+
if (live.length === 0) return void 0;
|
|
124
|
+
if (live.length === 1) return live[0];
|
|
125
|
+
return { emit(event) {
|
|
126
|
+
const pending = [];
|
|
127
|
+
for (const emitter of live) {
|
|
128
|
+
const result = emitter.emit(event);
|
|
129
|
+
if (result) pending.push(result);
|
|
130
|
+
}
|
|
131
|
+
if (pending.length > 0) return Promise.all(pending).then(() => void 0);
|
|
132
|
+
} };
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/mcp/delegation-store.ts
|
|
136
|
+
/**
|
|
137
|
+
*
|
|
138
|
+
* Persistence port for the MCP delegation queue.
|
|
139
|
+
*
|
|
140
|
+
* `DelegationTaskQueue` keeps its working set in memory (status/history
|
|
141
|
+
* reads stay synchronous) and journals every record mutation through a
|
|
142
|
+
* `DelegationStore`. `DelegationTaskQueue.restore({ store })` is the load
|
|
143
|
+
* path: it reads the full record set once at construction and rehydrates
|
|
144
|
+
* the queue from it. After that the store only sees writes.
|
|
145
|
+
*
|
|
146
|
+
* Records MUST be JSON-safe — `FileDelegationStore` round-trips them
|
|
147
|
+
* through `JSON.stringify`/`JSON.parse`, so a `Date`, `Map`, or function
|
|
148
|
+
* smuggled into `args`/`result` would corrupt the journal.
|
|
149
|
+
*
|
|
150
|
+
* @stable
|
|
151
|
+
*/
|
|
152
|
+
/**
|
|
153
|
+
* The persisted delegation state exists but cannot be parsed into
|
|
154
|
+
* records. Fail loud: silently starting empty over a corrupt journal
|
|
155
|
+
* would erase delegation history and re-run idempotent work. Opt into
|
|
156
|
+
* recovery explicitly via `FileDelegationStoreOptions.recoverCorrupt`
|
|
157
|
+
* (the bin maps `AGENT_RUNTIME_DELEGATION_STATE_RECOVER=1` onto it),
|
|
158
|
+
* which archives the corrupt file and starts fresh.
|
|
159
|
+
*
|
|
160
|
+
* @stable
|
|
161
|
+
*/
|
|
162
|
+
var DelegationStateCorruptError = class extends AgentEvalError {
|
|
163
|
+
constructor(message, options) {
|
|
164
|
+
super("validation", message, options);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* A delegation-store read or write failed (filesystem error, store
|
|
169
|
+
* called before `loadAll`, ...). Once the queue observes one, it stops
|
|
170
|
+
* accepting new submissions — accepting work it cannot journal would
|
|
171
|
+
* silently demote durable mode to in-memory mode.
|
|
172
|
+
*
|
|
173
|
+
* @stable
|
|
174
|
+
*/
|
|
175
|
+
var DelegationPersistenceError = class extends AgentEvalError {
|
|
176
|
+
constructor(message, options) {
|
|
177
|
+
super("config", message, options);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/** In-memory `DelegationStore` — suitable for single-process use and tests. @stable */
|
|
181
|
+
var InMemoryDelegationStore = class {
|
|
182
|
+
records = /* @__PURE__ */ new Map();
|
|
183
|
+
async loadAll() {
|
|
184
|
+
return [...this.records.values()].map(cloneRecord);
|
|
185
|
+
}
|
|
186
|
+
async upsert(record) {
|
|
187
|
+
this.records.set(record.taskId, cloneRecord(record));
|
|
188
|
+
}
|
|
189
|
+
async lookupIdempotencyKey(key) {
|
|
190
|
+
for (const record of this.records.values()) if (record.idempotencyKey === key) return record.taskId;
|
|
191
|
+
}
|
|
192
|
+
async remove(taskIds) {
|
|
193
|
+
for (const taskId of taskIds) this.records.delete(taskId);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const STATE_FORMAT_VERSION = 1;
|
|
197
|
+
/**
|
|
198
|
+
* JSON-file persistence for the delegation queue. Each write serializes
|
|
199
|
+
* the full record set and lands it atomically (write to a sibling tmp
|
|
200
|
+
* file, then `rename`), so readers never observe a torn file — a crash
|
|
201
|
+
* mid-write leaves the previous snapshot intact. Writes are serialized
|
|
202
|
+
* internally; concurrent `upsert`/`remove` calls cannot interleave.
|
|
203
|
+
*
|
|
204
|
+
* Built for the MCP server's scale (one stdio process, hundreds of
|
|
205
|
+
* records): full-snapshot writes keep the format trivially inspectable
|
|
206
|
+
* and corruption-detectable without a database dependency.
|
|
207
|
+
*
|
|
208
|
+
* @stable
|
|
209
|
+
*/
|
|
210
|
+
var FileDelegationStore = class {
|
|
211
|
+
filePath;
|
|
212
|
+
recoverCorrupt;
|
|
213
|
+
records = /* @__PURE__ */ new Map();
|
|
214
|
+
loaded = false;
|
|
215
|
+
writeTail = Promise.resolve();
|
|
216
|
+
tmpSeq = 0;
|
|
217
|
+
constructor(options) {
|
|
218
|
+
this.filePath = options.filePath;
|
|
219
|
+
this.recoverCorrupt = options.recoverCorrupt ?? false;
|
|
220
|
+
}
|
|
221
|
+
async loadAll() {
|
|
222
|
+
let raw;
|
|
223
|
+
try {
|
|
224
|
+
raw = await readFile(this.filePath, "utf8");
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err.code === "ENOENT") {
|
|
227
|
+
this.loaded = true;
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
throw new DelegationPersistenceError(`FileDelegationStore: failed to read ${this.filePath}: ${errorMessage(err)}`, { cause: err });
|
|
231
|
+
}
|
|
232
|
+
let state;
|
|
233
|
+
try {
|
|
234
|
+
state = parsePersistedState(raw);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (!this.recoverCorrupt) throw new DelegationStateCorruptError(`FileDelegationStore: state file ${this.filePath} is corrupt (${errorMessage(err)}). Repair or archive the file, or opt into automatic recovery (recoverCorrupt / AGENT_RUNTIME_DELEGATION_STATE_RECOVER=1) to archive it and start empty.`, { cause: err });
|
|
237
|
+
const archivePath = `${this.filePath}.corrupt-${Date.now()}`;
|
|
238
|
+
await rename(this.filePath, archivePath);
|
|
239
|
+
this.loaded = true;
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
this.records.clear();
|
|
243
|
+
for (const record of state.records) this.records.set(record.taskId, record);
|
|
244
|
+
this.loaded = true;
|
|
245
|
+
return [...this.records.values()].map(cloneRecord);
|
|
246
|
+
}
|
|
247
|
+
async upsert(record) {
|
|
248
|
+
this.assertLoaded("upsert");
|
|
249
|
+
this.records.set(record.taskId, cloneRecord(record));
|
|
250
|
+
await this.enqueueWrite();
|
|
251
|
+
}
|
|
252
|
+
async lookupIdempotencyKey(key) {
|
|
253
|
+
this.assertLoaded("lookupIdempotencyKey");
|
|
254
|
+
for (const record of this.records.values()) if (record.idempotencyKey === key) return record.taskId;
|
|
255
|
+
}
|
|
256
|
+
async remove(taskIds) {
|
|
257
|
+
this.assertLoaded("remove");
|
|
258
|
+
let changed = false;
|
|
259
|
+
for (const taskId of taskIds) if (this.records.delete(taskId)) changed = true;
|
|
260
|
+
if (changed) await this.enqueueWrite();
|
|
261
|
+
}
|
|
262
|
+
assertLoaded(op) {
|
|
263
|
+
if (this.loaded) return;
|
|
264
|
+
throw new DelegationPersistenceError(`FileDelegationStore: ${op} called before loadAll() — the on-disk state has not been read yet`);
|
|
265
|
+
}
|
|
266
|
+
enqueueWrite() {
|
|
267
|
+
const write = this.writeTail.then(() => this.writeSnapshot());
|
|
268
|
+
this.writeTail = write.catch(() => {});
|
|
269
|
+
return write;
|
|
270
|
+
}
|
|
271
|
+
async writeSnapshot() {
|
|
272
|
+
const state = {
|
|
273
|
+
version: STATE_FORMAT_VERSION,
|
|
274
|
+
records: [...this.records.values()]
|
|
275
|
+
};
|
|
276
|
+
const payload = `${JSON.stringify(state)}\n`;
|
|
277
|
+
this.tmpSeq += 1;
|
|
278
|
+
const tmpPath = `${this.filePath}.tmp-${process.pid}-${this.tmpSeq}`;
|
|
279
|
+
try {
|
|
280
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
281
|
+
await writeFile(tmpPath, payload, "utf8");
|
|
282
|
+
await rename(tmpPath, this.filePath);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
throw new DelegationPersistenceError(`FileDelegationStore: failed to write ${this.filePath}: ${errorMessage(err)}`, { cause: err });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
function parsePersistedState(raw) {
|
|
289
|
+
const parsed = JSON.parse(raw);
|
|
290
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("top-level value is not an object");
|
|
291
|
+
const state = parsed;
|
|
292
|
+
if (state.version !== STATE_FORMAT_VERSION) throw new Error(`unsupported state version ${JSON.stringify(state.version)}`);
|
|
293
|
+
if (!Array.isArray(state.records)) throw new Error("`records` is not an array");
|
|
294
|
+
for (const record of state.records) {
|
|
295
|
+
if (record === null || typeof record !== "object") throw new Error("a record entry is not an object");
|
|
296
|
+
const candidate = record;
|
|
297
|
+
if (typeof candidate.taskId !== "string" || typeof candidate.status !== "string") throw new Error("a record entry is missing `taskId`/`status`");
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
version: STATE_FORMAT_VERSION,
|
|
301
|
+
records: state.records
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function cloneRecord(record) {
|
|
305
|
+
return structuredClone(record);
|
|
306
|
+
}
|
|
307
|
+
function errorMessage(err) {
|
|
308
|
+
return err instanceof Error ? err.message : String(err);
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/mcp/task-queue.ts
|
|
312
|
+
/**
|
|
313
|
+
*
|
|
314
|
+
* State machine for async MCP delegations:
|
|
315
|
+
*
|
|
316
|
+
* pending → running → completed | failed
|
|
317
|
+
* ↘ cancelled (from any non-terminal state via cancel())
|
|
318
|
+
*
|
|
319
|
+
* Each `submit` returns a `taskId` immediately and kicks the work off in the
|
|
320
|
+
* background. The work function receives an `AbortSignal` the queue fires
|
|
321
|
+
* when `cancel(taskId)` is called. The queue does NOT supervise runtime
|
|
322
|
+
* timeouts — the underlying `runAgentRounds` driver / sandbox imposes those.
|
|
323
|
+
*
|
|
324
|
+
* Idempotency: callers may supply an `idempotencyKey` (hash of the input).
|
|
325
|
+
* A duplicate `submit` with a known key returns the existing task instead of
|
|
326
|
+
* starting a new one. Mutated input → different key → different task.
|
|
327
|
+
*
|
|
328
|
+
* Durability: the working set lives in memory (reads stay synchronous) and
|
|
329
|
+
* every record mutation is journaled through a `DelegationStore`. The default
|
|
330
|
+
* `InMemoryDelegationStore` keeps today's semantics — a process restart drops
|
|
331
|
+
* all state. Construct via `DelegationTaskQueue.restore({ store })` with a
|
|
332
|
+
* `FileDelegationStore` to reload prior records on startup: terminal records
|
|
333
|
+
* stay queryable, in-flight records either re-attach through the
|
|
334
|
+
* `resumeDelegate` seam (when they carry a `detachedSessionRef`) or fail
|
|
335
|
+
* loud with a driver-restart error so `delegation_status` tells the truth.
|
|
336
|
+
*
|
|
337
|
+
* @stable
|
|
338
|
+
*/
|
|
339
|
+
/** In-process queue for async delegation tasks — submit, cancel, poll status, and read history. @stable */
|
|
340
|
+
var DelegationTaskQueue = class DelegationTaskQueue {
|
|
341
|
+
records = /* @__PURE__ */ new Map();
|
|
342
|
+
controllers = /* @__PURE__ */ new Map();
|
|
343
|
+
byIdempotencyKey = /* @__PURE__ */ new Map();
|
|
344
|
+
generateId;
|
|
345
|
+
now;
|
|
346
|
+
store;
|
|
347
|
+
resumeDelegate;
|
|
348
|
+
maxTerminalRecords;
|
|
349
|
+
onPersistError;
|
|
350
|
+
traceContext;
|
|
351
|
+
persistTail = Promise.resolve();
|
|
352
|
+
persistFailure;
|
|
353
|
+
constructor(options = {}) {
|
|
354
|
+
this.generateId = options.generateId ?? randomTaskId;
|
|
355
|
+
this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
356
|
+
this.store = options.store ?? new InMemoryDelegationStore();
|
|
357
|
+
this.resumeDelegate = options.resumeDelegate;
|
|
358
|
+
if (options.maxTerminalRecords !== void 0) {
|
|
359
|
+
if (!Number.isInteger(options.maxTerminalRecords) || options.maxTerminalRecords < 1) throw new ValidationError(`DelegationTaskQueue: maxTerminalRecords must be a positive integer, got ${String(options.maxTerminalRecords)}`);
|
|
360
|
+
}
|
|
361
|
+
this.maxTerminalRecords = options.maxTerminalRecords ?? Number.POSITIVE_INFINITY;
|
|
362
|
+
this.traceContext = options.traceContext;
|
|
363
|
+
this.onPersistError = options.onPersistError ?? ((error) => {
|
|
364
|
+
queueMicrotask(() => {
|
|
365
|
+
throw error;
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Construct a queue from previously-persisted state. Loads every record
|
|
371
|
+
* from `options.store`, rebuilds the idempotency index (so a re-submitted
|
|
372
|
+
* identical task returns the prior taskId and its terminal state), then:
|
|
373
|
+
*
|
|
374
|
+
* - terminal records stay queryable via `status()` / `history()`
|
|
375
|
+
* - in-flight records with a `detachedSessionRef` re-attach through
|
|
376
|
+
* `options.resumeDelegate` and report `running`
|
|
377
|
+
* - other in-flight records settle as failed — their driver died with
|
|
378
|
+
* the previous process and the result is unrecoverable
|
|
379
|
+
*
|
|
380
|
+
* The retention cap applies to the loaded set as well.
|
|
381
|
+
*/
|
|
382
|
+
static async restore(options = {}) {
|
|
383
|
+
const queue = new DelegationTaskQueue(options);
|
|
384
|
+
const loaded = await queue.store.loadAll();
|
|
385
|
+
await queue.rehydrate(loaded);
|
|
386
|
+
return queue;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Kick off a delegation in the background. Returns immediately. The
|
|
390
|
+
* `taskId` is queryable via `status` once this method returns. Throws
|
|
391
|
+
* the recorded `DelegationPersistenceError` once the store has failed —
|
|
392
|
+
* the queue does not accept work it cannot journal.
|
|
393
|
+
*/
|
|
394
|
+
submit(input) {
|
|
395
|
+
if (this.persistFailure) throw this.persistFailure;
|
|
396
|
+
if (input.idempotencyKey) {
|
|
397
|
+
const existing = this.byIdempotencyKey.get(input.idempotencyKey);
|
|
398
|
+
if (existing && this.records.has(existing)) return {
|
|
399
|
+
taskId: existing,
|
|
400
|
+
reused: true
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const taskId = this.generateId();
|
|
404
|
+
const controller = new AbortController();
|
|
405
|
+
const record = {
|
|
406
|
+
taskId,
|
|
407
|
+
profile: input.profile,
|
|
408
|
+
namespace: input.namespace,
|
|
409
|
+
args: input.args,
|
|
410
|
+
status: "pending",
|
|
411
|
+
startedAt: this.now(),
|
|
412
|
+
feedback: [],
|
|
413
|
+
idempotencyKey: input.idempotencyKey,
|
|
414
|
+
detachedSessionRef: input.detachedSessionRef,
|
|
415
|
+
...this.traceContext !== void 0 ? {
|
|
416
|
+
traceId: this.traceContext.traceId,
|
|
417
|
+
...this.traceContext.parentSpanId !== void 0 ? { parentSpanId: this.traceContext.parentSpanId } : {}
|
|
418
|
+
} : {}
|
|
419
|
+
};
|
|
420
|
+
this.records.set(taskId, record);
|
|
421
|
+
this.controllers.set(taskId, controller);
|
|
422
|
+
if (input.idempotencyKey) this.byIdempotencyKey.set(input.idempotencyKey, taskId);
|
|
423
|
+
this.persist(record);
|
|
424
|
+
queueMicrotask(() => {
|
|
425
|
+
this.execute(taskId, input, controller);
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
taskId,
|
|
429
|
+
reused: false
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Snapshot the current state of a delegation. Returns `undefined` for
|
|
434
|
+
* unknown ids so callers can distinguish missing from terminal.
|
|
435
|
+
* `includeTrace` attaches the journaled loop-trace span tree — off by
|
|
436
|
+
* default so status polls stay light.
|
|
437
|
+
*/
|
|
438
|
+
status(taskId, opts) {
|
|
439
|
+
const record = this.records.get(taskId);
|
|
440
|
+
if (!record) return void 0;
|
|
441
|
+
return toStatusResult(record, opts);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Abort an in-flight delegation. Returns `false` if the task is unknown
|
|
445
|
+
* or already terminal. The underlying `run` function MUST honor the
|
|
446
|
+
* abort signal for the cancel to take effect; the queue marks the
|
|
447
|
+
* record `cancelled` regardless so a misbehaving runner cannot pin the
|
|
448
|
+
* UI on `running` forever.
|
|
449
|
+
*/
|
|
450
|
+
cancel(taskId) {
|
|
451
|
+
const record = this.records.get(taskId);
|
|
452
|
+
if (!record) return false;
|
|
453
|
+
if (isTerminal(record.status)) return false;
|
|
454
|
+
this.controllers.get(taskId)?.abort();
|
|
455
|
+
record.status = "cancelled";
|
|
456
|
+
record.completedAt = this.now();
|
|
457
|
+
record.error = {
|
|
458
|
+
message: "cancelled by caller",
|
|
459
|
+
kind: "CancelledError"
|
|
460
|
+
};
|
|
461
|
+
this.persist(record);
|
|
462
|
+
this.enforceRetention();
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Append a feedback event to the matching delegation. Returns `false`
|
|
467
|
+
* when `ref` does not name a known taskId — the caller should still
|
|
468
|
+
* record the feedback through a different surface (artifact/outcome
|
|
469
|
+
* kinds are not queue-bound).
|
|
470
|
+
*/
|
|
471
|
+
attachFeedback(taskId, snapshot) {
|
|
472
|
+
const record = this.records.get(taskId);
|
|
473
|
+
if (!record) return false;
|
|
474
|
+
record.feedback.push(snapshot);
|
|
475
|
+
this.persist(record);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Query the recorded delegations. Returns entries newest-first (by
|
|
480
|
+
* `startedAt`), truncated to `limit`.
|
|
481
|
+
*/
|
|
482
|
+
history(args = {}) {
|
|
483
|
+
const limit = clampLimit(args.limit);
|
|
484
|
+
const since = args.since ? Date.parse(args.since) : Number.NEGATIVE_INFINITY;
|
|
485
|
+
const out = [];
|
|
486
|
+
for (const record of this.records.values()) {
|
|
487
|
+
if (args.namespace && record.namespace !== args.namespace) continue;
|
|
488
|
+
if (args.profile && record.profile !== args.profile) continue;
|
|
489
|
+
if (Number.isFinite(since) && Date.parse(record.startedAt) < since) continue;
|
|
490
|
+
out.push(toHistoryEntry(record));
|
|
491
|
+
}
|
|
492
|
+
out.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
493
|
+
return out.slice(0, limit);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Await every journal write issued so far. Rejects with the recorded
|
|
497
|
+
* `DelegationPersistenceError` when any of them failed. Call before
|
|
498
|
+
* handing the store's backing file to another process.
|
|
499
|
+
*/
|
|
500
|
+
async flush() {
|
|
501
|
+
let tail;
|
|
502
|
+
while (this.persistTail !== tail) {
|
|
503
|
+
tail = this.persistTail;
|
|
504
|
+
await tail;
|
|
505
|
+
}
|
|
506
|
+
if (this.persistFailure) throw this.persistFailure;
|
|
507
|
+
}
|
|
508
|
+
/** Test-only — number of in-flight (non-terminal) records. */
|
|
509
|
+
inflightCount() {
|
|
510
|
+
let n = 0;
|
|
511
|
+
for (const record of this.records.values()) if (!isTerminal(record.status)) n += 1;
|
|
512
|
+
return n;
|
|
513
|
+
}
|
|
514
|
+
async execute(taskId, input, controller) {
|
|
515
|
+
const record = this.records.get(taskId);
|
|
516
|
+
if (!record) return;
|
|
517
|
+
record.status = "running";
|
|
518
|
+
this.persist(record);
|
|
519
|
+
const traceCollector = createDelegationTraceCollector((spans) => {
|
|
520
|
+
if (isTerminal(currentStatus(record))) return;
|
|
521
|
+
this.appendTrace(record, spans);
|
|
522
|
+
this.persist(record);
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
const output = await input.run({
|
|
526
|
+
signal: controller.signal,
|
|
527
|
+
report: (progress) => {
|
|
528
|
+
if (record.status === "running") {
|
|
529
|
+
record.progress = progress;
|
|
530
|
+
this.persist(record);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
traceEmitter: traceCollector.emitter,
|
|
534
|
+
...record.detachedSessionRef !== void 0 ? { detachedSessionRef: record.detachedSessionRef } : {},
|
|
535
|
+
updateDetachedSessionRef: (ref) => {
|
|
536
|
+
if (typeof ref !== "string" || ref.length === 0) throw new ValidationError("DelegationTaskQueue: updateDetachedSessionRef requires a non-empty ref");
|
|
537
|
+
if (isTerminal(currentStatus(record))) return;
|
|
538
|
+
record.detachedSessionRef = ref;
|
|
539
|
+
this.persist(record);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
traceCollector.settle();
|
|
543
|
+
if (currentStatus(record) === "cancelled") return;
|
|
544
|
+
record.status = "completed";
|
|
545
|
+
record.completedAt = this.now();
|
|
546
|
+
record.result = {
|
|
547
|
+
profile: input.profile,
|
|
548
|
+
output
|
|
549
|
+
};
|
|
550
|
+
this.persist(record);
|
|
551
|
+
this.enforceRetention();
|
|
552
|
+
} catch (err) {
|
|
553
|
+
traceCollector.settle();
|
|
554
|
+
if (currentStatus(record) === "cancelled") return;
|
|
555
|
+
record.status = "failed";
|
|
556
|
+
record.completedAt = this.now();
|
|
557
|
+
record.error = errorToShape(err);
|
|
558
|
+
this.persist(record);
|
|
559
|
+
this.enforceRetention();
|
|
560
|
+
} finally {
|
|
561
|
+
this.controllers.delete(taskId);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
appendTrace(record, spans) {
|
|
565
|
+
if (spans.length === 0) return;
|
|
566
|
+
const { trace, truncated } = capDelegationTrace([...record.trace ?? [], ...spans]);
|
|
567
|
+
record.trace = trace;
|
|
568
|
+
if (truncated) record.traceTruncated = true;
|
|
569
|
+
}
|
|
570
|
+
async rehydrate(loaded) {
|
|
571
|
+
const records = [...loaded].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
572
|
+
for (const record of records) {
|
|
573
|
+
this.records.set(record.taskId, record);
|
|
574
|
+
if (record.idempotencyKey) this.byIdempotencyKey.set(record.idempotencyKey, record.taskId);
|
|
575
|
+
}
|
|
576
|
+
const restoreWrites = [];
|
|
577
|
+
for (const record of this.records.values()) {
|
|
578
|
+
if (isTerminal(record.status)) continue;
|
|
579
|
+
if (record.detachedSessionRef && this.resumeDelegate) {
|
|
580
|
+
record.status = "running";
|
|
581
|
+
restoreWrites.push(this.persist(record));
|
|
582
|
+
this.startResume(record, record.detachedSessionRef, this.resumeDelegate);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
record.status = "failed";
|
|
586
|
+
record.completedAt = this.now();
|
|
587
|
+
record.error = {
|
|
588
|
+
message: record.detachedSessionRef ? `delegation driver restarted while the task was in flight; detached session "${record.detachedSessionRef}" needs a resumeDelegate to be resumed` : "delegation driver restarted while the task was in flight; the run was not detached and cannot be resumed",
|
|
589
|
+
kind: "DriverRestartError"
|
|
590
|
+
};
|
|
591
|
+
restoreWrites.push(this.persist(record));
|
|
592
|
+
}
|
|
593
|
+
const retentionWrite = this.enforceRetention();
|
|
594
|
+
if (retentionWrite) restoreWrites.push(retentionWrite);
|
|
595
|
+
await Promise.all(restoreWrites);
|
|
596
|
+
if (this.persistFailure) throw this.persistFailure;
|
|
597
|
+
}
|
|
598
|
+
startResume(record, detachedSessionRef, driver) {
|
|
599
|
+
const controller = new AbortController();
|
|
600
|
+
this.controllers.set(record.taskId, controller);
|
|
601
|
+
this.driveResume(record, detachedSessionRef, driver, controller);
|
|
602
|
+
}
|
|
603
|
+
async driveResume(record, detachedSessionRef, driver, controller) {
|
|
604
|
+
const intervalMs = driver.intervalMs ?? 5e3;
|
|
605
|
+
const resumeStartMs = Date.parse(this.now());
|
|
606
|
+
const ctx = {
|
|
607
|
+
signal: controller.signal,
|
|
608
|
+
report: (progress) => {
|
|
609
|
+
if (currentStatus(record) !== "running") return;
|
|
610
|
+
record.progress = progress;
|
|
611
|
+
this.persist(record);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
try {
|
|
615
|
+
while (!controller.signal.aborted && currentStatus(record) === "running") {
|
|
616
|
+
const tick = await driver.tick({
|
|
617
|
+
record: structuredClone(record),
|
|
618
|
+
detachedSessionRef
|
|
619
|
+
}, ctx);
|
|
620
|
+
if (currentStatus(record) === "cancelled") return;
|
|
621
|
+
if (tick.state === "completed") {
|
|
622
|
+
this.appendResumeSpan(record, detachedSessionRef, resumeStartMs);
|
|
623
|
+
record.status = "completed";
|
|
624
|
+
record.completedAt = this.now();
|
|
625
|
+
record.result = {
|
|
626
|
+
profile: record.profile,
|
|
627
|
+
output: tick.output
|
|
628
|
+
};
|
|
629
|
+
if (tick.costUsd !== void 0) record.costUsd = tick.costUsd;
|
|
630
|
+
this.persist(record);
|
|
631
|
+
this.enforceRetention();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (tick.state === "failed") {
|
|
635
|
+
this.appendResumeSpan(record, detachedSessionRef, resumeStartMs, tick.error.message);
|
|
636
|
+
record.status = "failed";
|
|
637
|
+
record.completedAt = this.now();
|
|
638
|
+
record.error = tick.error;
|
|
639
|
+
this.persist(record);
|
|
640
|
+
this.enforceRetention();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
await abortableDelay(intervalMs, controller.signal);
|
|
644
|
+
}
|
|
645
|
+
} catch (err) {
|
|
646
|
+
if (currentStatus(record) === "cancelled") return;
|
|
647
|
+
this.appendResumeSpan(record, detachedSessionRef, resumeStartMs, errorToShape(err).message);
|
|
648
|
+
record.status = "failed";
|
|
649
|
+
record.completedAt = this.now();
|
|
650
|
+
record.error = errorToShape(err);
|
|
651
|
+
this.persist(record);
|
|
652
|
+
this.enforceRetention();
|
|
653
|
+
} finally {
|
|
654
|
+
this.controllers.delete(record.taskId);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Journal the resumed segment of a detached run as one compact span. The
|
|
659
|
+
* resume driver re-attaches after a process restart, so the original
|
|
660
|
+
* process's loop events are gone — this span records the post-restart
|
|
661
|
+
* observation window (re-attach → terminal tick) under the
|
|
662
|
+
* `'detached-resume'` driver tag, keeping restored delegations observable
|
|
663
|
+
* in the journal alongside trace-carrying live runs.
|
|
664
|
+
*/
|
|
665
|
+
appendResumeSpan(record, detachedSessionRef, startMs, error) {
|
|
666
|
+
this.appendTrace(record, [{
|
|
667
|
+
spanId: generateDelegationSpanId(),
|
|
668
|
+
name: "loop",
|
|
669
|
+
kind: "loop",
|
|
670
|
+
startMs,
|
|
671
|
+
endMs: Date.parse(this.now()),
|
|
672
|
+
meta: {
|
|
673
|
+
"tangle.loop.driver": "detached-resume",
|
|
674
|
+
"tangle.loop.detached_session_ref": detachedSessionRef,
|
|
675
|
+
...error !== void 0 ? { "tangle.loop.error": error } : {}
|
|
676
|
+
}
|
|
677
|
+
}]);
|
|
678
|
+
}
|
|
679
|
+
persist(record) {
|
|
680
|
+
if (this.persistFailure) return Promise.resolve();
|
|
681
|
+
const snapshot = structuredClone(record);
|
|
682
|
+
this.persistTail = this.persistTail.then(async () => {
|
|
683
|
+
if (this.persistFailure) return;
|
|
684
|
+
try {
|
|
685
|
+
await this.store.upsert(snapshot);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
this.failPersistence(err);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
return this.persistTail;
|
|
691
|
+
}
|
|
692
|
+
persistRemoval(taskIds) {
|
|
693
|
+
if (this.persistFailure || taskIds.length === 0) return void 0;
|
|
694
|
+
this.persistTail = this.persistTail.then(async () => {
|
|
695
|
+
if (this.persistFailure) return;
|
|
696
|
+
try {
|
|
697
|
+
await this.store.remove(taskIds);
|
|
698
|
+
} catch (err) {
|
|
699
|
+
this.failPersistence(err);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
return this.persistTail;
|
|
703
|
+
}
|
|
704
|
+
failPersistence(cause) {
|
|
705
|
+
if (this.persistFailure) return;
|
|
706
|
+
const error = cause instanceof DelegationPersistenceError ? cause : new DelegationPersistenceError(`DelegationTaskQueue: store write failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
707
|
+
this.persistFailure = error;
|
|
708
|
+
this.onPersistError(error);
|
|
709
|
+
}
|
|
710
|
+
enforceRetention() {
|
|
711
|
+
if (!Number.isFinite(this.maxTerminalRecords)) return void 0;
|
|
712
|
+
const terminal = [];
|
|
713
|
+
for (const record of this.records.values()) if (isTerminal(record.status)) terminal.push(record);
|
|
714
|
+
const excess = terminal.length - this.maxTerminalRecords;
|
|
715
|
+
if (excess <= 0) return void 0;
|
|
716
|
+
terminal.sort((a, b) => (a.completedAt ?? a.startedAt).localeCompare(b.completedAt ?? b.startedAt));
|
|
717
|
+
const evicted = terminal.slice(0, excess);
|
|
718
|
+
for (const record of evicted) {
|
|
719
|
+
this.records.delete(record.taskId);
|
|
720
|
+
if (record.idempotencyKey && this.byIdempotencyKey.get(record.idempotencyKey) === record.taskId) this.byIdempotencyKey.delete(record.idempotencyKey);
|
|
721
|
+
}
|
|
722
|
+
return this.persistRemoval(evicted.map((record) => record.taskId));
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
function isTerminal(status) {
|
|
726
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
727
|
+
}
|
|
728
|
+
function currentStatus(record) {
|
|
729
|
+
return record.status;
|
|
730
|
+
}
|
|
731
|
+
function clampLimit(raw) {
|
|
732
|
+
if (!Number.isFinite(raw)) return 50;
|
|
733
|
+
const n = Math.trunc(raw);
|
|
734
|
+
if (n <= 0) return 50;
|
|
735
|
+
return Math.min(n, 500);
|
|
736
|
+
}
|
|
737
|
+
function abortableDelay(ms, signal) {
|
|
738
|
+
return new Promise((resolve) => {
|
|
739
|
+
if (signal.aborted) {
|
|
740
|
+
resolve();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const onAbort = () => {
|
|
744
|
+
clearTimeout(timer);
|
|
745
|
+
resolve();
|
|
746
|
+
};
|
|
747
|
+
const timer = setTimeout(() => {
|
|
748
|
+
signal.removeEventListener("abort", onAbort);
|
|
749
|
+
resolve();
|
|
750
|
+
}, ms);
|
|
751
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
function toStatusResult(record, opts) {
|
|
755
|
+
const out = {
|
|
756
|
+
taskId: record.taskId,
|
|
757
|
+
profile: record.profile,
|
|
758
|
+
status: record.status,
|
|
759
|
+
startedAt: record.startedAt
|
|
760
|
+
};
|
|
761
|
+
if (record.progress) out.progress = record.progress;
|
|
762
|
+
if (record.result) out.result = record.result;
|
|
763
|
+
if (record.error) out.error = record.error;
|
|
764
|
+
if (record.costUsd !== void 0) out.costUsd = record.costUsd;
|
|
765
|
+
if (record.completedAt) out.completedAt = record.completedAt;
|
|
766
|
+
if (record.traceId !== void 0) out.traceId = record.traceId;
|
|
767
|
+
if (record.parentSpanId !== void 0) out.parentSpanId = record.parentSpanId;
|
|
768
|
+
if (opts?.includeTrace === true && record.trace && record.trace.length > 0) {
|
|
769
|
+
out.trace = record.trace.map((span) => ({ ...span }));
|
|
770
|
+
if (record.traceTruncated) out.traceTruncated = true;
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
function toHistoryEntry(record) {
|
|
775
|
+
const entry = {
|
|
776
|
+
taskId: record.taskId,
|
|
777
|
+
profile: record.profile,
|
|
778
|
+
args: record.args,
|
|
779
|
+
status: record.status,
|
|
780
|
+
startedAt: record.startedAt,
|
|
781
|
+
hasTrace: record.trace !== void 0 && record.trace.length > 0
|
|
782
|
+
};
|
|
783
|
+
if (record.namespace) entry.namespace = record.namespace;
|
|
784
|
+
if (record.completedAt) entry.completedAt = record.completedAt;
|
|
785
|
+
if (record.costUsd !== void 0) entry.costUsd = record.costUsd;
|
|
786
|
+
if (record.feedback.length > 0) entry.feedback = [...record.feedback];
|
|
787
|
+
if (record.traceId !== void 0) entry.traceId = record.traceId;
|
|
788
|
+
return entry;
|
|
789
|
+
}
|
|
790
|
+
function errorToShape(err) {
|
|
791
|
+
if (err instanceof Error) return {
|
|
792
|
+
message: err.message,
|
|
793
|
+
kind: err.name || "Error"
|
|
794
|
+
};
|
|
795
|
+
return {
|
|
796
|
+
message: String(err),
|
|
797
|
+
kind: "NonError"
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function randomTaskId() {
|
|
801
|
+
return `dlg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Best-effort stable hash for use as `idempotencyKey`. Not cryptographic;
|
|
805
|
+
* collisions only affect dedupe, never correctness.
|
|
806
|
+
*
|
|
807
|
+
* @stable
|
|
808
|
+
*/
|
|
809
|
+
function hashIdempotencyInput(value) {
|
|
810
|
+
let str;
|
|
811
|
+
try {
|
|
812
|
+
str = JSON.stringify(canonicalize(value));
|
|
813
|
+
} catch {
|
|
814
|
+
str = String(value);
|
|
815
|
+
}
|
|
816
|
+
let h = 2166136261;
|
|
817
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
818
|
+
h ^= str.charCodeAt(i);
|
|
819
|
+
h = Math.imul(h, 16777619);
|
|
820
|
+
}
|
|
821
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
822
|
+
}
|
|
823
|
+
function canonicalize(value) {
|
|
824
|
+
if (value === null || typeof value !== "object") return value;
|
|
825
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
826
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b));
|
|
827
|
+
const out = {};
|
|
828
|
+
for (const [k, v] of entries) out[k] = canonicalize(v);
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region src/mcp/tools/delegate.ts
|
|
833
|
+
/** MCP tool name for the `delegate` generic-delegation tool. @stable */
|
|
834
|
+
const DELEGATE_TOOL_NAME = "delegate";
|
|
835
|
+
/** Human-readable description of the `delegate` MCP tool, injected into the tool manifest. @stable */
|
|
836
|
+
const DELEGATE_DESCRIPTION = [
|
|
837
|
+
"Delegate an INTENT to a supervisor that AUTHORS and drives whatever worker the intent needs.",
|
|
838
|
+
"",
|
|
839
|
+
"Use when: you want a task done but do not want to specify HOW. State the outcome — \"fix the",
|
|
840
|
+
"failing auth test\", \"research competitor pricing with citations\", \"refactor the parser for",
|
|
841
|
+
"clarity\" — and the supervisor decomposes it, writes a tailored worker profile per sub-task, runs",
|
|
842
|
+
"the workers over a conserved compute budget, and settles only when a deployable check passes.",
|
|
843
|
+
"",
|
|
844
|
+
"There is no fixed worker type: this ONE verb replaces separate code / research delegation. The",
|
|
845
|
+
"supervisor picks the worker shape from your intent.",
|
|
846
|
+
"",
|
|
847
|
+
"Returns synchronously with the delivered result AND the real cost of the whole delegation",
|
|
848
|
+
"(spentTotal: iterations, input/output tokens, usd, ms) — so you always know what it spent. A run",
|
|
849
|
+
"that produced no delivered worker returns status \"no-winner\" with the reason; it never fabricates",
|
|
850
|
+
"a success."
|
|
851
|
+
].join("\n");
|
|
852
|
+
/** JSON Schema for `delegate` tool arguments (`intent` + optional trace id). @stable */
|
|
853
|
+
const DELEGATE_INPUT_SCHEMA = {
|
|
854
|
+
type: "object",
|
|
855
|
+
properties: {
|
|
856
|
+
intent: {
|
|
857
|
+
type: "string",
|
|
858
|
+
description: "What you want accomplished, as an outcome. The supervisor authors the worker."
|
|
859
|
+
},
|
|
860
|
+
runId: {
|
|
861
|
+
type: "string",
|
|
862
|
+
description: "Optional trace-correlation id for this delegation."
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
required: ["intent"],
|
|
866
|
+
additionalProperties: false
|
|
867
|
+
};
|
|
868
|
+
/** Parse and validate raw MCP tool input into typed `DelegateArgs`; throws `TypeError` on bad input. @stable */
|
|
869
|
+
function validateDelegateArgs(raw) {
|
|
870
|
+
if (raw === null || typeof raw !== "object") throw new TypeError("delegate: arguments must be an object");
|
|
871
|
+
const value = raw;
|
|
872
|
+
const unknown = Object.keys(value).filter((key) => key !== "intent" && key !== "runId");
|
|
873
|
+
if (unknown.length > 0) throw new TypeError(`delegate: unknown arguments: ${unknown.join(", ")}`);
|
|
874
|
+
const intent = value.intent;
|
|
875
|
+
if (typeof intent !== "string" || intent.trim().length === 0) throw new TypeError("delegate: `intent` must be a non-empty string");
|
|
876
|
+
const args = { intent: intent.trim() };
|
|
877
|
+
if (value.runId !== void 0) {
|
|
878
|
+
if (typeof value.runId !== "string") throw new TypeError("delegate: `runId` must be a string");
|
|
879
|
+
args.runId = value.runId;
|
|
880
|
+
}
|
|
881
|
+
return args;
|
|
882
|
+
}
|
|
883
|
+
/** Project a `SupervisedResult` onto the tool's flat `DelegateResult`. Both variants carry the real
|
|
884
|
+
* conserved `spentTotal`, so the agent always learns the cost — even on a no-winner, never a faked
|
|
885
|
+
* output and never a fabricated zero spend. */
|
|
886
|
+
function toDelegateResult(result) {
|
|
887
|
+
if (result.kind === "no-winner") {
|
|
888
|
+
const rejection = result.error;
|
|
889
|
+
const error = typeof rejection?.name === "string" && typeof rejection.message === "string" ? {
|
|
890
|
+
name: rejection.name,
|
|
891
|
+
message: rejection.message
|
|
892
|
+
} : void 0;
|
|
893
|
+
return {
|
|
894
|
+
status: "no-winner",
|
|
895
|
+
reason: result.reason,
|
|
896
|
+
...error ? { error } : {},
|
|
897
|
+
spentTotal: result.spentTotal
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
status: "winner",
|
|
902
|
+
out: result.out,
|
|
903
|
+
outRef: result.outRef,
|
|
904
|
+
spentTotal: result.spentTotal
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Build the `delegate` tool handler. Closes over the injected supervisor substrate (`router` /
|
|
909
|
+
* `backend` / `deliverable`); each call routes the agent's intent to `delegate()` and returns the
|
|
910
|
+
* delivered output with its conserved cost.
|
|
911
|
+
*/
|
|
912
|
+
function createDelegateHandler(options) {
|
|
913
|
+
return async (raw) => {
|
|
914
|
+
const args = validateDelegateArgs(raw);
|
|
915
|
+
const opts = {
|
|
916
|
+
backend: options.backend,
|
|
917
|
+
router: options.router,
|
|
918
|
+
supervisorProfile: options.supervisorProfile,
|
|
919
|
+
...options.deliverable ? { deliverable: options.deliverable } : {},
|
|
920
|
+
...options.allowedModels ? { allowedModels: options.allowedModels } : {},
|
|
921
|
+
...args.runId ? { runId: args.runId } : {}
|
|
922
|
+
};
|
|
923
|
+
return toDelegateResult(await delegate(args.intent, opts));
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/mcp/tools/delegate-ui-audit.ts
|
|
928
|
+
/**
|
|
929
|
+
*
|
|
930
|
+
* `delegate_ui_audit` MCP tool — async kickoff for UI audit runs. Validates
|
|
931
|
+
* the input, computes an idempotency key over the canonical fields, hands
|
|
932
|
+
* the task to the queue, and returns a taskId. Identical inputs return
|
|
933
|
+
* the same taskId.
|
|
934
|
+
*
|
|
935
|
+
* The handler does not import the auditor profile directly — consumers
|
|
936
|
+
* inject a `UiAuditorDelegate` via `createMcpServer({ uiAuditorDelegate })`.
|
|
937
|
+
* The delegate is the seam where the consumer chooses the judge (vision
|
|
938
|
+
* model) and the `SandboxClient` (in-process Playwright vs fleet vs
|
|
939
|
+
* remote browser). agent-runtime ships the in-process client under
|
|
940
|
+
* `./profiles` so consumers who want the canonical setup can wire it
|
|
941
|
+
* with a few lines.
|
|
942
|
+
*
|
|
943
|
+
* @experimental
|
|
944
|
+
*/
|
|
945
|
+
/** MCP tool name for the `delegate_ui_audit` async kickoff tool. @experimental */
|
|
946
|
+
const DELEGATE_UI_AUDIT_TOOL_NAME = "delegate_ui_audit";
|
|
947
|
+
/** Human-readable description of the `delegate_ui_audit` MCP tool, injected into the tool manifest. @experimental */
|
|
948
|
+
const DELEGATE_UI_AUDIT_DESCRIPTION = [
|
|
949
|
+
"Delegate a UI/UX audit to a vision-driven auditor that produces self-contained",
|
|
950
|
+
"GitHub-issue-ready Markdown findings — one file per finding, with embedded",
|
|
951
|
+
"screenshot evidence and a suggested fix.",
|
|
952
|
+
"",
|
|
953
|
+
"Use when: you want a thorough pass over a running web app for consistency,",
|
|
954
|
+
"hierarchy, layout, ux-flow, duplication, accessibility, responsive, states,",
|
|
955
|
+
"content, interaction, or perceived-performance issues. The auditor iterates",
|
|
956
|
+
"lens-by-lens so each pass finds new classes of issues; the workspace registry",
|
|
957
|
+
"deduplicates across iterations.",
|
|
958
|
+
"",
|
|
959
|
+
"Returns immediately with a taskId. Poll delegation_status to retrieve the",
|
|
960
|
+
"workspace path + indexed findings (typically minutes per audited route).",
|
|
961
|
+
"Identical inputs return the same taskId — safe to retry.",
|
|
962
|
+
"",
|
|
963
|
+
"Output layout under workspaceDir:",
|
|
964
|
+
" registry.json — finding index + capture sidecar",
|
|
965
|
+
" index.md — human-readable rollup",
|
|
966
|
+
" issues/NNN--<lens>--<slug>.md — one self-contained GitHub-issue",
|
|
967
|
+
" screenshots/<route>--<viewport>.png — capture archive",
|
|
968
|
+
"",
|
|
969
|
+
"Multi-tenant isolation: every finding is scoped to `namespace` when set.",
|
|
970
|
+
"Never pass another tenant's namespace."
|
|
971
|
+
].join("\n");
|
|
972
|
+
/** JSON Schema for `delegate_ui_audit` tool arguments (`workspaceDir`, `routes`, optional config). @experimental */
|
|
973
|
+
const DELEGATE_UI_AUDIT_INPUT_SCHEMA = {
|
|
974
|
+
type: "object",
|
|
975
|
+
properties: {
|
|
976
|
+
workspaceDir: {
|
|
977
|
+
type: "string",
|
|
978
|
+
description: "Absolute path for the audit workspace."
|
|
979
|
+
},
|
|
980
|
+
routes: {
|
|
981
|
+
type: "array",
|
|
982
|
+
items: {
|
|
983
|
+
type: "object",
|
|
984
|
+
properties: {
|
|
985
|
+
name: {
|
|
986
|
+
type: "string",
|
|
987
|
+
description: "Stable route name (used in screenshot filenames)."
|
|
988
|
+
},
|
|
989
|
+
url: {
|
|
990
|
+
type: "string",
|
|
991
|
+
description: "Fully-qualified URL."
|
|
992
|
+
},
|
|
993
|
+
viewports: {
|
|
994
|
+
type: "array",
|
|
995
|
+
items: {
|
|
996
|
+
type: "object",
|
|
997
|
+
properties: {
|
|
998
|
+
width: {
|
|
999
|
+
type: "integer",
|
|
1000
|
+
minimum: 1
|
|
1001
|
+
},
|
|
1002
|
+
height: {
|
|
1003
|
+
type: "integer",
|
|
1004
|
+
minimum: 1
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
required: ["width", "height"],
|
|
1008
|
+
additionalProperties: false
|
|
1009
|
+
},
|
|
1010
|
+
description: "Viewports to capture at. Default [{1280, 800}]."
|
|
1011
|
+
},
|
|
1012
|
+
fullPage: { type: "boolean" },
|
|
1013
|
+
waitFor: {
|
|
1014
|
+
type: "string",
|
|
1015
|
+
description: "CSS selector to wait for before capturing."
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
required: ["name", "url"],
|
|
1019
|
+
additionalProperties: false
|
|
1020
|
+
},
|
|
1021
|
+
minItems: 1
|
|
1022
|
+
},
|
|
1023
|
+
namespace: {
|
|
1024
|
+
type: "string",
|
|
1025
|
+
description: "Multi-tenant scope."
|
|
1026
|
+
},
|
|
1027
|
+
config: {
|
|
1028
|
+
type: "object",
|
|
1029
|
+
properties: {
|
|
1030
|
+
lenses: {
|
|
1031
|
+
type: "array",
|
|
1032
|
+
items: {
|
|
1033
|
+
type: "string",
|
|
1034
|
+
enum: [...UI_LENSES]
|
|
1035
|
+
},
|
|
1036
|
+
description: "Lenses to iterate. Default: every lens except \"other\"."
|
|
1037
|
+
},
|
|
1038
|
+
maxIterations: {
|
|
1039
|
+
type: "integer",
|
|
1040
|
+
minimum: 1
|
|
1041
|
+
},
|
|
1042
|
+
maxConcurrency: {
|
|
1043
|
+
type: "integer",
|
|
1044
|
+
minimum: 1
|
|
1045
|
+
},
|
|
1046
|
+
productContext: { type: "string" }
|
|
1047
|
+
},
|
|
1048
|
+
additionalProperties: false
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
required: ["workspaceDir", "routes"],
|
|
1052
|
+
additionalProperties: false
|
|
1053
|
+
};
|
|
1054
|
+
const PER_LENS_PER_ROUTE_ESTIMATE_MS = 45e3;
|
|
1055
|
+
/** Parse and validate raw MCP tool input into typed `DelegateUiAuditArgs`; throws `TypeError` on bad input. @experimental */
|
|
1056
|
+
function validateDelegateUiAuditArgs(raw) {
|
|
1057
|
+
if (raw === null || typeof raw !== "object") throw new TypeError("delegate_ui_audit: arguments must be an object");
|
|
1058
|
+
const value = raw;
|
|
1059
|
+
const workspaceDir = value.workspaceDir;
|
|
1060
|
+
if (typeof workspaceDir !== "string" || workspaceDir.trim().length === 0) throw new TypeError("delegate_ui_audit: `workspaceDir` must be a non-empty string");
|
|
1061
|
+
const trimmedWs = workspaceDir.trim();
|
|
1062
|
+
if (!path.isAbsolute(trimmedWs)) throw new TypeError(`delegate_ui_audit: \`workspaceDir\` must be an absolute path (got ${JSON.stringify(workspaceDir)})`);
|
|
1063
|
+
if (trimmedWs.split(path.sep).includes("..")) throw new TypeError(`delegate_ui_audit: \`workspaceDir\` must not contain '..' segments (got ${JSON.stringify(workspaceDir)})`);
|
|
1064
|
+
const routesRaw = value.routes;
|
|
1065
|
+
if (!Array.isArray(routesRaw) || routesRaw.length === 0) throw new TypeError("delegate_ui_audit: `routes` must be a non-empty array");
|
|
1066
|
+
const routes = routesRaw.map((r, i) => validateRoute(r, i));
|
|
1067
|
+
const args = {
|
|
1068
|
+
workspaceDir: workspaceDir.trim(),
|
|
1069
|
+
routes
|
|
1070
|
+
};
|
|
1071
|
+
if (value.namespace !== void 0) {
|
|
1072
|
+
if (typeof value.namespace !== "string" || value.namespace.trim().length === 0) throw new TypeError("delegate_ui_audit: `namespace` must be a non-empty string when set");
|
|
1073
|
+
args.namespace = value.namespace.trim();
|
|
1074
|
+
}
|
|
1075
|
+
if (value.config !== void 0) args.config = validateConfig(value.config);
|
|
1076
|
+
return args;
|
|
1077
|
+
}
|
|
1078
|
+
function validateRoute(raw, index) {
|
|
1079
|
+
if (raw === null || typeof raw !== "object") throw new TypeError(`delegate_ui_audit: routes[${index}] must be an object`);
|
|
1080
|
+
const v = raw;
|
|
1081
|
+
if (typeof v.name !== "string" || v.name.trim().length === 0) throw new TypeError(`delegate_ui_audit: routes[${index}].name must be a non-empty string`);
|
|
1082
|
+
const trimmedName = v.name.trim();
|
|
1083
|
+
if (/[./\\]/.test(trimmedName) || trimmedName.includes("\0")) throw new TypeError(`delegate_ui_audit: routes[${index}].name must not contain path separators, dots, or NUL (got ${JSON.stringify(v.name)})`);
|
|
1084
|
+
if (typeof v.url !== "string" || v.url.trim().length === 0) throw new TypeError(`delegate_ui_audit: routes[${index}].url must be a non-empty string`);
|
|
1085
|
+
let parsedUrl;
|
|
1086
|
+
try {
|
|
1087
|
+
parsedUrl = new URL(v.url);
|
|
1088
|
+
} catch {
|
|
1089
|
+
throw new TypeError(`delegate_ui_audit: routes[${index}].url is not a parseable URL (got ${JSON.stringify(v.url)})`);
|
|
1090
|
+
}
|
|
1091
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new TypeError(`delegate_ui_audit: routes[${index}].url must use http or https (got ${parsedUrl.protocol})`);
|
|
1092
|
+
const out = {
|
|
1093
|
+
name: v.name.trim(),
|
|
1094
|
+
url: v.url.trim()
|
|
1095
|
+
};
|
|
1096
|
+
if (v.viewports !== void 0) {
|
|
1097
|
+
if (!Array.isArray(v.viewports) || v.viewports.length === 0) throw new TypeError(`delegate_ui_audit: routes[${index}].viewports must be a non-empty array when set`);
|
|
1098
|
+
out.viewports = v.viewports.map((vp, j) => validateViewport(vp, index, j));
|
|
1099
|
+
}
|
|
1100
|
+
if (v.fullPage !== void 0) {
|
|
1101
|
+
if (typeof v.fullPage !== "boolean") throw new TypeError(`delegate_ui_audit: routes[${index}].fullPage must be a boolean`);
|
|
1102
|
+
out.fullPage = v.fullPage;
|
|
1103
|
+
}
|
|
1104
|
+
if (v.waitFor !== void 0) {
|
|
1105
|
+
if (typeof v.waitFor !== "string" || v.waitFor.trim().length === 0) throw new TypeError(`delegate_ui_audit: routes[${index}].waitFor must be a non-empty string when set`);
|
|
1106
|
+
out.waitFor = v.waitFor.trim();
|
|
1107
|
+
}
|
|
1108
|
+
return out;
|
|
1109
|
+
}
|
|
1110
|
+
function validateViewport(raw, routeIndex, viewportIndex) {
|
|
1111
|
+
if (raw === null || typeof raw !== "object") throw new TypeError(`delegate_ui_audit: routes[${routeIndex}].viewports[${viewportIndex}] must be an object`);
|
|
1112
|
+
const v = raw;
|
|
1113
|
+
const w = Number(v.width);
|
|
1114
|
+
const h = Number(v.height);
|
|
1115
|
+
if (!Number.isInteger(w) || w <= 0 || !Number.isInteger(h) || h <= 0) throw new RangeError(`delegate_ui_audit: routes[${routeIndex}].viewports[${viewportIndex}] must have positive integer width/height`);
|
|
1116
|
+
return {
|
|
1117
|
+
width: w,
|
|
1118
|
+
height: h
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function validateConfig(raw) {
|
|
1122
|
+
if (raw === null || typeof raw !== "object") throw new TypeError("delegate_ui_audit: `config` must be an object");
|
|
1123
|
+
const v = raw;
|
|
1124
|
+
const out = {};
|
|
1125
|
+
if (v.lenses !== void 0) {
|
|
1126
|
+
if (!Array.isArray(v.lenses) || v.lenses.length === 0) throw new TypeError("delegate_ui_audit: `config.lenses` must be a non-empty array when set");
|
|
1127
|
+
const knownSet = new Set(UI_LENSES);
|
|
1128
|
+
const lenses = [];
|
|
1129
|
+
for (let i = 0; i < v.lenses.length; i += 1) {
|
|
1130
|
+
const lens = v.lenses[i];
|
|
1131
|
+
if (typeof lens !== "string" || !knownSet.has(lens)) throw new TypeError(`delegate_ui_audit: config.lenses[${i}] must be one of ${UI_LENSES.join("|")}`);
|
|
1132
|
+
lenses.push(lens);
|
|
1133
|
+
}
|
|
1134
|
+
out.lenses = lenses;
|
|
1135
|
+
}
|
|
1136
|
+
if (v.maxIterations !== void 0) {
|
|
1137
|
+
const n = Number(v.maxIterations);
|
|
1138
|
+
if (!Number.isInteger(n) || n < 1) throw new RangeError("delegate_ui_audit: `config.maxIterations` must be a positive integer");
|
|
1139
|
+
out.maxIterations = n;
|
|
1140
|
+
}
|
|
1141
|
+
if (v.maxConcurrency !== void 0) {
|
|
1142
|
+
const n = Number(v.maxConcurrency);
|
|
1143
|
+
if (!Number.isInteger(n) || n < 1) throw new RangeError("delegate_ui_audit: `config.maxConcurrency` must be a positive integer");
|
|
1144
|
+
out.maxConcurrency = n;
|
|
1145
|
+
}
|
|
1146
|
+
if (v.productContext !== void 0) {
|
|
1147
|
+
if (typeof v.productContext !== "string") throw new TypeError("delegate_ui_audit: `config.productContext` must be a string");
|
|
1148
|
+
out.productContext = v.productContext;
|
|
1149
|
+
}
|
|
1150
|
+
return out;
|
|
1151
|
+
}
|
|
1152
|
+
/** Build the MCP tool handler that validates input, deduplicates via idempotency key, and enqueues a UI audit. @experimental */
|
|
1153
|
+
function createDelegateUiAuditHandler(options) {
|
|
1154
|
+
const estimateDurationMs = options.estimateDurationMs ?? defaultEstimate;
|
|
1155
|
+
return async (raw) => {
|
|
1156
|
+
const args = validateDelegateUiAuditArgs(raw);
|
|
1157
|
+
const idempotencyKey = hashIdempotencyInput({
|
|
1158
|
+
profile: "ui-auditor",
|
|
1159
|
+
workspaceDir: args.workspaceDir,
|
|
1160
|
+
routes: args.routes,
|
|
1161
|
+
namespace: args.namespace,
|
|
1162
|
+
config: args.config
|
|
1163
|
+
});
|
|
1164
|
+
return {
|
|
1165
|
+
taskId: options.queue.submit({
|
|
1166
|
+
profile: "ui-auditor",
|
|
1167
|
+
args,
|
|
1168
|
+
namespace: args.namespace,
|
|
1169
|
+
idempotencyKey,
|
|
1170
|
+
run: async (ctx) => options.delegate(args, ctx)
|
|
1171
|
+
}).taskId,
|
|
1172
|
+
estimatedDurationMs: estimateDurationMs(args)
|
|
1173
|
+
};
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function defaultEstimate(args) {
|
|
1177
|
+
const lenses = args.config?.lenses?.length ?? UI_LENSES.length - 1;
|
|
1178
|
+
const routes = args.routes.length;
|
|
1179
|
+
return PER_LENS_PER_ROUTE_ESTIMATE_MS * lenses * routes;
|
|
1180
|
+
}
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/mcp/server.ts
|
|
1183
|
+
/**
|
|
1184
|
+
*
|
|
1185
|
+
* Stdio JSON-RPC MCP server exposing the delegation tools to sandbox
|
|
1186
|
+
* coding-harness agents (claude-code, codex, opencode, ...): the generic
|
|
1187
|
+
* `delegate` verb plus the queue-bound `delegate_feedback`,
|
|
1188
|
+
* `delegation_status`, and `delegation_history`. `delegate_ui_audit` is served
|
|
1189
|
+
* when a `uiAuditorDelegate` is wired.
|
|
1190
|
+
*
|
|
1191
|
+
* The server is transport-bound but topology-free: tool execution is
|
|
1192
|
+
* delegated to handler functions composed from a queue, a feedback
|
|
1193
|
+
* store, and the wired run delegates. Consumers wire those at
|
|
1194
|
+
* construction time. The `agent-runtime-mcp` bin serves the generic
|
|
1195
|
+
* `delegate` verb over a real sandbox client when `MCP_ENABLE_DELEGATE=1`.
|
|
1196
|
+
*
|
|
1197
|
+
* Wire protocol: line-delimited JSON-RPC 2.0 over stdio. Each line is
|
|
1198
|
+
* one request; each response is one line. `tools/list` and `tools/call`
|
|
1199
|
+
* mirror the MCP 2024-11-05 spec. The production server does not depend on
|
|
1200
|
+
* `@modelcontextprotocol/sdk`; integration tests validate this wire with the official client.
|
|
1201
|
+
*
|
|
1202
|
+
* @experimental
|
|
1203
|
+
*/
|
|
1204
|
+
const DEFAULT_SERVER_NAME = "agent-runtime-mcp";
|
|
1205
|
+
const DEFAULT_SERVER_VERSION = "0.22.0";
|
|
1206
|
+
/**
|
|
1207
|
+
* Stdio JSON-RPC MCP server exposing the delegation tools (`delegate`, `delegate_feedback`, `delegation_status`, `delegation_history`, optional `delegate_ui_audit`) to sandbox coding-harness agents.
|
|
1208
|
+
*
|
|
1209
|
+
* @experimental
|
|
1210
|
+
*/
|
|
1211
|
+
function createMcpServer(options = {}) {
|
|
1212
|
+
const queue = options.queue ?? new DelegationTaskQueue(options.traceContext !== void 0 ? { traceContext: options.traceContext } : {});
|
|
1213
|
+
const feedbackStore = options.feedbackStore ?? new InMemoryFeedbackStore();
|
|
1214
|
+
const serverName = options.serverName ?? DEFAULT_SERVER_NAME;
|
|
1215
|
+
const serverVersion = options.serverVersion ?? DEFAULT_SERVER_VERSION;
|
|
1216
|
+
const tools = /* @__PURE__ */ new Map();
|
|
1217
|
+
if (options.delegateSupervisor) tools.set(DELEGATE_TOOL_NAME, {
|
|
1218
|
+
name: DELEGATE_TOOL_NAME,
|
|
1219
|
+
description: DELEGATE_DESCRIPTION,
|
|
1220
|
+
inputSchema: DELEGATE_INPUT_SCHEMA,
|
|
1221
|
+
handler: createDelegateHandler(options.delegateSupervisor)
|
|
1222
|
+
});
|
|
1223
|
+
if (options.uiAuditorDelegate) tools.set(DELEGATE_UI_AUDIT_TOOL_NAME, {
|
|
1224
|
+
name: DELEGATE_UI_AUDIT_TOOL_NAME,
|
|
1225
|
+
description: DELEGATE_UI_AUDIT_DESCRIPTION,
|
|
1226
|
+
inputSchema: DELEGATE_UI_AUDIT_INPUT_SCHEMA,
|
|
1227
|
+
handler: createDelegateUiAuditHandler({
|
|
1228
|
+
queue,
|
|
1229
|
+
delegate: options.uiAuditorDelegate
|
|
1230
|
+
})
|
|
1231
|
+
});
|
|
1232
|
+
tools.set(DELEGATE_FEEDBACK_TOOL_NAME, {
|
|
1233
|
+
name: DELEGATE_FEEDBACK_TOOL_NAME,
|
|
1234
|
+
description: DELEGATE_FEEDBACK_DESCRIPTION,
|
|
1235
|
+
inputSchema: DELEGATE_FEEDBACK_INPUT_SCHEMA,
|
|
1236
|
+
handler: createDelegateFeedbackHandler({
|
|
1237
|
+
queue,
|
|
1238
|
+
store: feedbackStore
|
|
1239
|
+
})
|
|
1240
|
+
});
|
|
1241
|
+
tools.set(DELEGATION_STATUS_TOOL_NAME, {
|
|
1242
|
+
name: DELEGATION_STATUS_TOOL_NAME,
|
|
1243
|
+
description: DELEGATION_STATUS_DESCRIPTION,
|
|
1244
|
+
inputSchema: DELEGATION_STATUS_INPUT_SCHEMA,
|
|
1245
|
+
handler: createDelegationStatusHandler({ queue })
|
|
1246
|
+
});
|
|
1247
|
+
tools.set(DELEGATION_HISTORY_TOOL_NAME, {
|
|
1248
|
+
name: DELEGATION_HISTORY_TOOL_NAME,
|
|
1249
|
+
description: DELEGATION_HISTORY_DESCRIPTION,
|
|
1250
|
+
inputSchema: DELEGATION_HISTORY_INPUT_SCHEMA,
|
|
1251
|
+
handler: createDelegationHistoryHandler({ queue })
|
|
1252
|
+
});
|
|
1253
|
+
for (const tool of options.extraTools ?? []) {
|
|
1254
|
+
if (tools.has(tool.name)) throw new ValidationError(`createMcpServer: extra tool "${tool.name}" shadows a built-in tool`);
|
|
1255
|
+
tools.set(tool.name, tool);
|
|
1256
|
+
}
|
|
1257
|
+
const stdio = createStdioToolServer({
|
|
1258
|
+
serverName,
|
|
1259
|
+
serverVersion,
|
|
1260
|
+
tools: [...tools.values()]
|
|
1261
|
+
});
|
|
1262
|
+
return {
|
|
1263
|
+
tools: stdio.tools,
|
|
1264
|
+
queue,
|
|
1265
|
+
feedbackStore,
|
|
1266
|
+
handle: stdio.handle,
|
|
1267
|
+
serve: stdio.serve,
|
|
1268
|
+
stop: stdio.stop
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* In-process pair of `Readable` + `Writable` streams suitable for driving
|
|
1273
|
+
* `server.serve(...)` from a test. Returns the agent-side stream (the
|
|
1274
|
+
* client writes to it) and the server-side stream (the test reads from it).
|
|
1275
|
+
*
|
|
1276
|
+
* @experimental
|
|
1277
|
+
*/
|
|
1278
|
+
function createInProcessTransport() {
|
|
1279
|
+
const responses = [];
|
|
1280
|
+
const input = new Readable({ read() {} });
|
|
1281
|
+
return {
|
|
1282
|
+
transport: {
|
|
1283
|
+
input,
|
|
1284
|
+
output: new Writable({ write(chunk, _enc, cb) {
|
|
1285
|
+
const text = chunk.toString("utf8");
|
|
1286
|
+
for (const line of text.split("\n")) {
|
|
1287
|
+
const trimmed = line.trim();
|
|
1288
|
+
if (!trimmed) continue;
|
|
1289
|
+
try {
|
|
1290
|
+
responses.push(JSON.parse(trimmed));
|
|
1291
|
+
} catch {}
|
|
1292
|
+
}
|
|
1293
|
+
cb();
|
|
1294
|
+
} })
|
|
1295
|
+
},
|
|
1296
|
+
clientWrite(line) {
|
|
1297
|
+
input.push(`${line}\n`);
|
|
1298
|
+
},
|
|
1299
|
+
clientClose() {
|
|
1300
|
+
input.push(null);
|
|
1301
|
+
},
|
|
1302
|
+
async readServer() {
|
|
1303
|
+
for (let i = 0; i < 5; i += 1) await new Promise((r) => setImmediate(r));
|
|
1304
|
+
return [...responses];
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1309
|
+
export { composeLoopTraceEmitters as C, capDelegationTrace as S, FileDelegationStore as _, DELEGATE_UI_AUDIT_TOOL_NAME as a, DELEGATION_TRACE_MAX_SPANS as b, DELEGATE_DESCRIPTION as c, createDelegateHandler as d, validateDelegateArgs as f, DelegationStateCorruptError as g, DelegationPersistenceError as h, DELEGATE_UI_AUDIT_INPUT_SCHEMA as i, DELEGATE_INPUT_SCHEMA as l, hashIdempotencyInput as m, createMcpServer as n, createDelegateUiAuditHandler as o, DelegationTaskQueue as p, DELEGATE_UI_AUDIT_DESCRIPTION as r, validateDelegateUiAuditArgs as s, createInProcessTransport as t, DELEGATE_TOOL_NAME as u, InMemoryDelegationStore as v, createDelegationTraceCollector as w, buildDelegationTraceSpans as x, DELEGATION_TRACE_MAX_BYTES as y };
|
|
1310
|
+
|
|
1311
|
+
//# sourceMappingURL=server-D6XmE1Du.js.map
|