@tangle-network/agent-runtime 0.104.0 → 0.106.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 +176 -42
- package/dist/activation-CRjVfOiy.js +203 -0
- package/dist/activation-CRjVfOiy.js.map +1 -0
- package/dist/activation-DyWB0K6E.d.ts +98 -0
- package/dist/agent.d.ts +216 -234
- package/dist/agent.js +615 -547
- package/dist/agent.js.map +1 -1
- package/dist/analyst-loop-DGdtVENg.js +517 -0
- package/dist/analyst-loop-DGdtVENg.js.map +1 -0
- package/dist/analyst-loop.d.ts +10 -43
- package/dist/analyst-loop.js +2 -11
- package/dist/backends-Dy8NujB4.js +713 -0
- package/dist/backends-Dy8NujB4.js.map +1 -0
- package/dist/candidate-execution/index.d.ts +4 -192
- package/dist/candidate-execution/index.js +5 -66
- package/dist/candidate-execution-DqMbv1rU.js +488 -0
- package/dist/candidate-execution-DqMbv1rU.js.map +1 -0
- package/dist/coder-yhVWbdWc.js +40 -0
- package/dist/coder-yhVWbdWc.js.map +1 -0
- package/dist/conversation-XucG_K3l.js +1265 -0
- package/dist/conversation-XucG_K3l.js.map +1 -0
- package/dist/conversation.d.ts +2 -804
- package/dist/conversation.js +2 -55
- package/dist/environment-provider-CfLUmKmf.js +1168 -0
- package/dist/environment-provider-CfLUmKmf.js.map +1 -0
- package/dist/environment-provider-IUGU3epE.d.ts +964 -0
- package/dist/environment-provider.d.ts +2 -78
- package/dist/environment-provider.js +2 -18
- package/dist/errors-DEAvWQPy.js +111 -0
- package/dist/errors-DEAvWQPy.js.map +1 -0
- package/dist/improvement-adapter-Cr5TBSji.d.ts +166 -0
- package/dist/improvement-cycle-C095kS_g.js +2624 -0
- package/dist/improvement-cycle-C095kS_g.js.map +1 -0
- package/dist/index-BGdxAwfO.d.ts +838 -0
- package/dist/index-C-FYUuFG.d.ts +193 -0
- package/dist/index-CbNM40xG.d.ts +140 -0
- package/dist/index-DruSKs3Z.d.ts +7224 -0
- package/dist/index-Efjb3nrQ.d.ts +671 -0
- package/dist/index.d.ts +14 -1134
- package/dist/index.js +2134 -1972
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +776 -888
- package/dist/intelligence.js +1466 -1410
- package/dist/intelligence.js.map +1 -1
- package/dist/{kb-gate-CwHO0vz6.d.ts → kb-gate-C8z2juK8.d.ts} +33 -31
- package/dist/kb-gate-DpaSwXVx.js +80 -0
- package/dist/kb-gate-DpaSwXVx.js.map +1 -0
- package/dist/knowledge-Ku-mPP4f.js +433 -0
- package/dist/knowledge-Ku-mPP4f.js.map +1 -0
- package/dist/knowledge.d.ts +2 -150
- package/dist/knowledge.js +2 -40
- package/dist/local-harness-Dh8PJ0ot.d.ts +455 -0
- package/dist/loop-runner-bin-BXsFEUcf.d.ts +162 -0
- package/dist/loop-runner-bin-BwjQ1NfH.js +268 -0
- package/dist/loop-runner-bin-BwjQ1NfH.js.map +1 -0
- package/dist/loop-runner-bin.d.ts +2 -17
- package/dist/loop-runner-bin.js +2 -24
- package/dist/loops.d.ts +5 -4164
- package/dist/loops.js +8 -419
- package/dist/mcp/bin.d.ts +1 -1
- package/dist/mcp/bin.js +116 -146
- package/dist/mcp/bin.js.map +1 -1
- package/dist/mcp/index.d.ts +295 -437
- package/dist/mcp/index.js +1242 -1177
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/memory-bin.d.ts +1 -1
- package/dist/mcp/memory-bin.js +37 -16
- package/dist/mcp/memory-bin.js.map +1 -1
- package/dist/memory-server-DL6cE2Ag.js +234 -0
- package/dist/memory-server-DL6cE2Ag.js.map +1 -0
- package/dist/model-resolution-Btd9iIKV.js +98 -0
- package/dist/model-resolution-Btd9iIKV.js.map +1 -0
- package/dist/openai-tools-B68JaOCx.d.ts +24 -0
- package/dist/openai-tools-CKLy1C7M.js +44 -0
- package/dist/openai-tools-CKLy1C7M.js.map +1 -0
- package/dist/otel-export-D34IQW1C.js +843 -0
- package/dist/otel-export-D34IQW1C.js.map +1 -0
- package/dist/platform.d.ts +188 -185
- package/dist/platform.js +185 -218
- package/dist/platform.js.map +1 -1
- package/dist/prepare-B53sPV7M.js +4292 -0
- package/dist/prepare-B53sPV7M.js.map +1 -0
- package/dist/primeintellect/index.d.ts +150 -147
- package/dist/primeintellect/index.js +626 -1004
- package/dist/primeintellect/index.js.map +1 -1
- package/dist/profiles.d.ts +296 -490
- package/dist/profiles.js +722 -749
- package/dist/profiles.js.map +1 -1
- package/dist/protected-model-port-BLPH5VVv.js +722 -0
- package/dist/protected-model-port-BLPH5VVv.js.map +1 -0
- package/dist/protected-model-port-BP6Z4eau.d.ts +283 -0
- package/dist/protected-redaction--F3v1oo8.js +200 -0
- package/dist/protected-redaction--F3v1oo8.js.map +1 -0
- package/dist/redact-D-u-rrcn.js +82 -0
- package/dist/redact-D-u-rrcn.js.map +1 -0
- package/dist/redact-kbIOjfjM.d.ts +452 -0
- package/dist/researcher-CoVqNhfI.js +433 -0
- package/dist/researcher-CoVqNhfI.js.map +1 -0
- package/dist/runtime-BG0BziaP.js +5408 -0
- package/dist/runtime-BG0BziaP.js.map +1 -0
- package/dist/sandbox-events-DeI5xX8P.js +316 -0
- package/dist/sandbox-events-DeI5xX8P.js.map +1 -0
- package/dist/spawn-journal-CwPvKUTa.js +456 -0
- package/dist/spawn-journal-CwPvKUTa.js.map +1 -0
- package/dist/structural-rollout-IXUEplky.js +1445 -0
- package/dist/structural-rollout-IXUEplky.js.map +1 -0
- package/dist/substrate-B0TYNrXn.js +27 -0
- package/dist/substrate-B0TYNrXn.js.map +1 -0
- package/dist/{substrate-Dyiqmd3o.d.ts → substrate-BcnuSHXm.d.ts} +57 -54
- package/dist/supervise-BiRutHS9.js +7321 -0
- package/dist/supervise-BiRutHS9.js.map +1 -0
- package/dist/supervisor-DTKhF-RV.js +3826 -0
- package/dist/supervisor-DTKhF-RV.js.map +1 -0
- package/dist/testing.d.ts +4 -3
- package/dist/testing.js +2478 -2637
- package/dist/testing.js.map +1 -1
- package/dist/tool-server-RcWgLIsL.js +141 -0
- package/dist/tool-server-RcWgLIsL.js.map +1 -0
- package/dist/trace-propagation-CJJC7SVB.js +57 -0
- package/dist/trace-propagation-CJJC7SVB.js.map +1 -0
- package/dist/types-BaIpb0XT.d.ts +492 -0
- package/dist/types-BevOjfTY.d.ts +765 -0
- package/dist/types-C9j4qg6l.d.ts +500 -0
- package/dist/types-Cja5OTGh.d.ts +190 -0
- package/dist/util-MVgdwuIS.js +145 -0
- package/dist/util-MVgdwuIS.js.map +1 -0
- package/dist/workspace-archive-BosQiotA.js +575 -0
- package/dist/workspace-archive-BosQiotA.js.map +1 -0
- package/package.json +43 -55
- package/skills/build-with-agent-runtime/SKILL.md +16 -9
- package/skills/loop-writer/SKILL.md +1 -1
- package/dist/activation-Cm-9MMX1.d.ts +0 -69
- package/dist/analyst-loop.js.map +0 -1
- package/dist/backends-BO4Jqoe2.d.ts +0 -134
- package/dist/candidate-execution/index.js.map +0 -1
- package/dist/chunk-2KGAN2HM.js +0 -1256
- package/dist/chunk-2KGAN2HM.js.map +0 -1
- package/dist/chunk-2QJSNOGK.js +0 -48
- package/dist/chunk-2QJSNOGK.js.map +0 -1
- package/dist/chunk-2UABGGUB.js +0 -474
- package/dist/chunk-2UABGGUB.js.map +0 -1
- package/dist/chunk-2VZYRB55.js +0 -834
- package/dist/chunk-2VZYRB55.js.map +0 -1
- package/dist/chunk-2X65HYMI.js +0 -3526
- package/dist/chunk-2X65HYMI.js.map +0 -1
- package/dist/chunk-3MDZX7YU.js +0 -125
- package/dist/chunk-3MDZX7YU.js.map +0 -1
- package/dist/chunk-3PPVCS75.js +0 -199
- package/dist/chunk-3PPVCS75.js.map +0 -1
- package/dist/chunk-5HNDYWUT.js +0 -6424
- package/dist/chunk-5HNDYWUT.js.map +0 -1
- package/dist/chunk-5IBQAPVB.js +0 -29
- package/dist/chunk-5IBQAPVB.js.map +0 -1
- package/dist/chunk-5QOB7H74.js +0 -387
- package/dist/chunk-5QOB7H74.js.map +0 -1
- package/dist/chunk-ATDFUJYI.js +0 -4728
- package/dist/chunk-ATDFUJYI.js.map +0 -1
- package/dist/chunk-B7K7V22Y.js +0 -699
- package/dist/chunk-B7K7V22Y.js.map +0 -1
- package/dist/chunk-DPEUKJRO.js +0 -27
- package/dist/chunk-DPEUKJRO.js.map +0 -1
- package/dist/chunk-FD2MBMOH.js +0 -494
- package/dist/chunk-FD2MBMOH.js.map +0 -1
- package/dist/chunk-FJI3EXU3.js +0 -2402
- package/dist/chunk-FJI3EXU3.js.map +0 -1
- package/dist/chunk-HAO4CXHP.js +0 -200
- package/dist/chunk-HAO4CXHP.js.map +0 -1
- package/dist/chunk-ISPWRSEC.js +0 -408
- package/dist/chunk-ISPWRSEC.js.map +0 -1
- package/dist/chunk-KRBFHMV6.js +0 -1228
- package/dist/chunk-KRBFHMV6.js.map +0 -1
- package/dist/chunk-L5DST3QC.js +0 -351
- package/dist/chunk-L5DST3QC.js.map +0 -1
- package/dist/chunk-M22PL4GH.js +0 -171
- package/dist/chunk-M22PL4GH.js.map +0 -1
- package/dist/chunk-NURIQBJQ.js +0 -72
- package/dist/chunk-NURIQBJQ.js.map +0 -1
- package/dist/chunk-PZZKQVQV.js +0 -52
- package/dist/chunk-PZZKQVQV.js.map +0 -1
- package/dist/chunk-Q2JSAVQ3.js +0 -671
- package/dist/chunk-Q2JSAVQ3.js.map +0 -1
- package/dist/chunk-SBTWKPVR.js +0 -5576
- package/dist/chunk-SBTWKPVR.js.map +0 -1
- package/dist/chunk-SGKPNBXE.js +0 -60
- package/dist/chunk-SGKPNBXE.js.map +0 -1
- package/dist/chunk-SNSMRT6H.js +0 -680
- package/dist/chunk-SNSMRT6H.js.map +0 -1
- package/dist/chunk-STJEHM5F.js +0 -1338
- package/dist/chunk-STJEHM5F.js.map +0 -1
- package/dist/chunk-UPWGXKXB.js +0 -254
- package/dist/chunk-UPWGXKXB.js.map +0 -1
- package/dist/chunk-YEJR7IXO.js +0 -68
- package/dist/chunk-YEJR7IXO.js.map +0 -1
- package/dist/chunk-ZXICDSAK.js +0 -907
- package/dist/chunk-ZXICDSAK.js.map +0 -1
- package/dist/completion-gate-Bv47cOnn.d.ts +0 -256
- package/dist/conversation.js.map +0 -1
- package/dist/coordination-CxtzMxSM.d.ts +0 -2570
- package/dist/environment-provider.js.map +0 -1
- package/dist/improve--2A0gK1-.d.ts +0 -440
- package/dist/improvement-adapter-HAZz-7vK.d.ts +0 -203
- package/dist/knowledge.js.map +0 -1
- package/dist/local-harness-CtK9dSny.d.ts +0 -175
- package/dist/loop-runner-bin-DUOGtvOl.d.ts +0 -207
- package/dist/loop-runner-bin.js.map +0 -1
- package/dist/loops.js.map +0 -1
- package/dist/openai-tools-fnj6SRVg.d.ts +0 -46
- package/dist/protected-model-port-DzkUY3HQ.d.ts +0 -271
- package/dist/sanitize-DEbPNtyI.d.ts +0 -328
- package/dist/structural-rollout-C4mabf87.d.ts +0 -446
- package/dist/supervise-7q9F5brx.d.ts +0 -379
- package/dist/types-BCoemcXU.d.ts +0 -802
- package/dist/types-BwoZWq-i.d.ts +0 -510
- package/dist/types-Cbp72qyu.d.ts +0 -491
- package/dist/types-CmYCMbFT.d.ts +0 -203
- package/dist/types-CvNwMzQt.d.ts +0 -957
- package/dist/worktree-fanout-WsOzt8qu.d.ts +0 -999
package/dist/mcp/index.js
CHANGED
|
@@ -1,1281 +1,1346 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "../
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "../
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
createKbGate
|
|
26
|
-
} from "../chunk-SGKPNBXE.js";
|
|
27
|
-
import {
|
|
28
|
-
assertTraceDerivedFindings,
|
|
29
|
-
runCoderChecks,
|
|
30
|
-
selectValidWinner
|
|
31
|
-
} from "../chunk-ATDFUJYI.js";
|
|
32
|
-
import "../chunk-Q2JSAVQ3.js";
|
|
33
|
-
import {
|
|
34
|
-
DELEGATE_DESCRIPTION,
|
|
35
|
-
DELEGATE_FEEDBACK_DESCRIPTION,
|
|
36
|
-
DELEGATE_FEEDBACK_INPUT_SCHEMA,
|
|
37
|
-
DELEGATE_FEEDBACK_TOOL_NAME,
|
|
38
|
-
DELEGATE_INPUT_SCHEMA,
|
|
39
|
-
DELEGATE_TOOL_NAME,
|
|
40
|
-
DELEGATE_UI_AUDIT_DESCRIPTION,
|
|
41
|
-
DELEGATE_UI_AUDIT_INPUT_SCHEMA,
|
|
42
|
-
DELEGATE_UI_AUDIT_TOOL_NAME,
|
|
43
|
-
DELEGATION_HISTORY_DESCRIPTION,
|
|
44
|
-
DELEGATION_HISTORY_INPUT_SCHEMA,
|
|
45
|
-
DELEGATION_HISTORY_TOOL_NAME,
|
|
46
|
-
DELEGATION_STATUS_DESCRIPTION,
|
|
47
|
-
DELEGATION_STATUS_INPUT_SCHEMA,
|
|
48
|
-
DELEGATION_STATUS_TOOL_NAME,
|
|
49
|
-
DELEGATION_TRACE_MAX_BYTES,
|
|
50
|
-
DELEGATION_TRACE_MAX_SPANS,
|
|
51
|
-
DelegationPersistenceError,
|
|
52
|
-
DelegationStateCorruptError,
|
|
53
|
-
DelegationTaskQueue,
|
|
54
|
-
FileDelegationStore,
|
|
55
|
-
InMemoryDelegationStore,
|
|
56
|
-
InMemoryFeedbackStore,
|
|
57
|
-
buildDelegationTraceSpans,
|
|
58
|
-
capDelegationTrace,
|
|
59
|
-
composeLoopTraceEmitters,
|
|
60
|
-
createCoordinationTools,
|
|
61
|
-
createDelegateFeedbackHandler,
|
|
62
|
-
createDelegateHandler,
|
|
63
|
-
createDelegateUiAuditHandler,
|
|
64
|
-
createDelegationHistoryHandler,
|
|
65
|
-
createDelegationStatusHandler,
|
|
66
|
-
createDelegationTraceCollector,
|
|
67
|
-
createInProcessTransport,
|
|
68
|
-
createMcpServer,
|
|
69
|
-
createSandboxForSpec,
|
|
70
|
-
eventToSnapshot,
|
|
71
|
-
hashIdempotencyInput,
|
|
72
|
-
runAgentRounds,
|
|
73
|
-
validateDelegateArgs,
|
|
74
|
-
validateDelegateFeedbackArgs,
|
|
75
|
-
validateDelegateUiAuditArgs,
|
|
76
|
-
validateDelegationHistoryArgs,
|
|
77
|
-
validateDelegationStatusArgs
|
|
78
|
-
} from "../chunk-5HNDYWUT.js";
|
|
79
|
-
import "../chunk-DPEUKJRO.js";
|
|
80
|
-
import "../chunk-STJEHM5F.js";
|
|
81
|
-
import "../chunk-UPWGXKXB.js";
|
|
82
|
-
import "../chunk-2VZYRB55.js";
|
|
83
|
-
import {
|
|
84
|
-
CodexExecutionDiagnosticError,
|
|
85
|
-
captureWorktreeDiff,
|
|
86
|
-
createWorktree,
|
|
87
|
-
parseCodexTokenUsage,
|
|
88
|
-
removeWorktree,
|
|
89
|
-
runLocalHarness,
|
|
90
|
-
runWorktreeHarness
|
|
91
|
-
} from "../chunk-2X65HYMI.js";
|
|
92
|
-
import "../chunk-ISPWRSEC.js";
|
|
93
|
-
import "../chunk-KRBFHMV6.js";
|
|
94
|
-
import {
|
|
95
|
-
deleteBoxSafe,
|
|
96
|
-
sleep,
|
|
97
|
-
throwAbort,
|
|
98
|
-
throwIfAborted
|
|
99
|
-
} from "../chunk-3MDZX7YU.js";
|
|
100
|
-
import "../chunk-M22PL4GH.js";
|
|
101
|
-
import {
|
|
102
|
-
ValidationError
|
|
103
|
-
} from "../chunk-YEJR7IXO.js";
|
|
104
|
-
|
|
105
|
-
// src/mcp/executor.ts
|
|
1
|
+
import { u as ValidationError } from "../errors-DEAvWQPy.js";
|
|
2
|
+
import { l as throwAbort, n as deleteBoxSafe, s as sleep, u as throwIfAborted } from "../util-MVgdwuIS.js";
|
|
3
|
+
import { A as removeWorktree, D as runWorktreeHarness, M as parseCodexTokenUsage, N as runLocalHarness, O as captureWorktreeDiff, P as CodexExecutionDiagnosticError, k as createWorktree } from "../supervisor-DTKhF-RV.js";
|
|
4
|
+
import { W as selectValidWinner, m as runCoderChecks, q as assertTraceDerivedFindings } from "../runtime-BG0BziaP.js";
|
|
5
|
+
import { $ as eventToSnapshot, A as createDelegateHandler, B as DelegationTaskQueue, Bt as runAgentRounds, C as DELEGATE_FEEDBACK_INPUT_SCHEMA, D as DELEGATE_DESCRIPTION, E as validateDelegateFeedbackArgs, G as capDelegationTrace, H as DELEGATION_TRACE_MAX_BYTES, J as DelegationPersistenceError, K as composeLoopTraceEmitters, O as DELEGATE_INPUT_SCHEMA, Q as InMemoryFeedbackStore, Rt as createSandboxForSpec, S as DELEGATE_FEEDBACK_DESCRIPTION, T as createDelegateFeedbackHandler, U as DELEGATION_TRACE_MAX_SPANS, V as hashIdempotencyInput, W as buildDelegationTraceSpans, X as FileDelegationStore, Y as DelegationStateCorruptError, Z as InMemoryDelegationStore, _ as DELEGATE_UI_AUDIT_DESCRIPTION, a as createInProcessTransport, b as createDelegateUiAuditHandler, c as DELEGATION_STATUS_INPUT_SCHEMA, d as validateDelegationStatusArgs, f as DELEGATION_HISTORY_DESCRIPTION, g as validateDelegationHistoryArgs, h as createDelegationHistoryHandler, ht as createCoordinationTools, j as validateDelegateArgs, k as DELEGATE_TOOL_NAME, l as DELEGATION_STATUS_TOOL_NAME, m as DELEGATION_HISTORY_TOOL_NAME, mt as DEFAULT_AWAIT_EVENT_TIMEOUT_MS, o as createMcpServer, p as DELEGATION_HISTORY_INPUT_SCHEMA, q as createDelegationTraceCollector, s as DELEGATION_STATUS_DESCRIPTION, u as createDelegationStatusHandler, v as DELEGATE_UI_AUDIT_INPUT_SCHEMA, w as DELEGATE_FEEDBACK_TOOL_NAME, x as validateDelegateUiAuditArgs, y as DELEGATE_UI_AUDIT_TOOL_NAME } from "../supervise-BiRutHS9.js";
|
|
6
|
+
import { t as createStdioToolServer } from "../tool-server-RcWgLIsL.js";
|
|
7
|
+
import { t as createKbGate } from "../kb-gate-DpaSwXVx.js";
|
|
8
|
+
import { n as mcpToolsForRuntimeMcpSubset, t as mcpToolsForRuntimeMcp } from "../openai-tools-CKLy1C7M.js";
|
|
9
|
+
import { t as coderTaskToPrompt } from "../coder-yhVWbdWc.js";
|
|
10
|
+
import { a as createMemoryToolServer, c as resolveMemoryFromEnv, i as MEMORY_NAME_ENV, n as MEMORY_ITEMS_ENV, o as parseMemoryItems, r as MEMORY_LOG_ENV, s as readMemoryItemsFile, t as MEMORY_FILE_ENV } from "../memory-server-DL6cE2Ag.js";
|
|
11
|
+
import { n as readTraceContextFromEnv, r as traceContextToEnv, t as createPropagatingTraceEmitter } from "../trace-propagation-CJJC7SVB.js";
|
|
12
|
+
import { makeFinding } from "@tangle-network/agent-eval";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
//#region src/mcp/executor.ts
|
|
15
|
+
/**
|
|
16
|
+
* Wrap a raw sandbox SDK client so the kernel emits
|
|
17
|
+
* `loop.iteration.dispatch` events with `{ placement: 'sibling', sandboxId }`.
|
|
18
|
+
*
|
|
19
|
+
* The returned client `.create()` delegates to the underlying client; the
|
|
20
|
+
* only added behavior is a `describePlacement` tag the kernel reads.
|
|
21
|
+
*
|
|
22
|
+
* @experimental
|
|
23
|
+
*/
|
|
106
24
|
function createSiblingSandboxExecutor(options) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
25
|
+
const underlying = options.client;
|
|
26
|
+
return {
|
|
27
|
+
client: {
|
|
28
|
+
create(opts) {
|
|
29
|
+
return underlying.create(opts);
|
|
30
|
+
},
|
|
31
|
+
describePlacement(box) {
|
|
32
|
+
return {
|
|
33
|
+
kind: "sibling",
|
|
34
|
+
sandboxId: readId(box)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
placement: "sibling",
|
|
39
|
+
describe() {
|
|
40
|
+
return "sibling-sandbox (each delegation = fresh sandbox via client.create)";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
123
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Build an executor that resolves each delegated iteration to an existing
|
|
46
|
+
* machine in `fleet`. The fleet's shared-workspace policy means the worker
|
|
47
|
+
* machine sees the caller's filesystem — diffs land in-place with no
|
|
48
|
+
* cross-sandbox copy step.
|
|
49
|
+
*
|
|
50
|
+
* @experimental
|
|
51
|
+
*/
|
|
124
52
|
function createFleetWorkspaceExecutor(options) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const excluded = exclude.size > 0 ? ` (excluded=[${[...exclude].join(",")}])` : "";
|
|
164
|
-
return `fleet-workspace (fleetId=${fleet.fleetId}, machines=[${fleet.ids.join(",")}]${excluded})`;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
53
|
+
const fleet = options.fleet;
|
|
54
|
+
const exclude = new Set(options.excludeMachineIds ?? []);
|
|
55
|
+
let callIndex = 0;
|
|
56
|
+
const placementBySandboxId = /* @__PURE__ */ new Map();
|
|
57
|
+
return {
|
|
58
|
+
client: {
|
|
59
|
+
async create() {
|
|
60
|
+
const ids = fleet.ids.filter((id) => !exclude.has(id));
|
|
61
|
+
if (ids.length === 0) throw new Error(`agent-runtime: fleet ${fleet.fleetId} has no eligible worker machines (ids=[${fleet.ids.join(",")}], excluded=[${[...exclude].join(",")}])`);
|
|
62
|
+
const selector = options.selectMachine;
|
|
63
|
+
const machineId = selector ? selector({
|
|
64
|
+
callIndex,
|
|
65
|
+
ids
|
|
66
|
+
}) : ids[callIndex % ids.length];
|
|
67
|
+
callIndex += 1;
|
|
68
|
+
if (typeof machineId !== "string" || machineId.length === 0) throw new Error("agent-runtime: fleet executor selectMachine returned an empty machine id");
|
|
69
|
+
const box = await fleet.sandbox(machineId);
|
|
70
|
+
const sandboxId = readId(box);
|
|
71
|
+
if (sandboxId) placementBySandboxId.set(sandboxId, { machineId });
|
|
72
|
+
return box;
|
|
73
|
+
},
|
|
74
|
+
describePlacement(box) {
|
|
75
|
+
const sandboxId = readId(box);
|
|
76
|
+
const recorded = sandboxId ? placementBySandboxId.get(sandboxId) : void 0;
|
|
77
|
+
return {
|
|
78
|
+
kind: "fleet",
|
|
79
|
+
sandboxId,
|
|
80
|
+
fleetId: fleet.fleetId,
|
|
81
|
+
machineId: recorded?.machineId
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
placement: "fleet",
|
|
86
|
+
describe() {
|
|
87
|
+
const excluded = exclude.size > 0 ? ` (excluded=[${[...exclude].join(",")}])` : "";
|
|
88
|
+
return `fleet-workspace (fleetId=${fleet.fleetId}, machines=[${fleet.ids.join(",")}]${excluded})`;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
167
91
|
}
|
|
168
92
|
function readId(box) {
|
|
169
|
-
|
|
170
|
-
|
|
93
|
+
const raw = box.id;
|
|
94
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
171
95
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/mcp/in-process-executor.ts
|
|
98
|
+
/**
|
|
99
|
+
*
|
|
100
|
+
* In-process delegation executor — when `agent-runtime-mcp` runs inside a sandbox whose image
|
|
101
|
+
* carries the local coding-harness CLIs (claude / codex / opencode), delegations spawn the harness
|
|
102
|
+
* AS A SUBPROCESS against a git worktree on the SAME filesystem instead of provisioning a sibling
|
|
103
|
+
* sandbox. Zero provisioning latency; worker diffs land in-place; multi-harness fanout = N parallel
|
|
104
|
+
* subprocesses in N parallel worktrees (round-robin `harnesses`).
|
|
105
|
+
*
|
|
106
|
+
* This is a THIN adapter over `runWorktreeHarness` (`./worktree-harness`) — the SAME core the
|
|
107
|
+
* `Scope` leaf `createWorktreeCliExecutor` uses. It only adapts the core to the `SandboxClient`
|
|
108
|
+
* port: `create()` reads the authored profile from `CreateSandboxOptions.backend.profile`, and
|
|
109
|
+
* `streamPrompt` runs the core then emits its raw `WorktreeHarnessResult` (the content-addressed
|
|
110
|
+
* patch artifact) on the `result` event. The sandbox-session decode layer
|
|
111
|
+
* (`./detached-coder`) projects that artifact onto `CoderOutput`; the generic `Scope` path settles
|
|
112
|
+
* the artifact directly. The §1.5 payload (systemPrompt + model) reaches the harness inside the core.
|
|
113
|
+
*
|
|
114
|
+
* @experimental
|
|
115
|
+
*/
|
|
116
|
+
const DEFAULT_HARNESS_TIMEOUT_MS = 300 * 1e3;
|
|
117
|
+
const DEFAULT_POSTCHECK_TIMEOUT_MS = 120 * 1e3;
|
|
118
|
+
/**
|
|
119
|
+
* Build an in-process executor. Returns a {@link DelegationExecutor} whose `client.create()`
|
|
120
|
+
* returns a minimal virtual `SandboxInstance`; the kernel calls `streamPrompt(msg)` on it, which
|
|
121
|
+
* runs the shared worktree-harness core and emits one `result` event whose `data.result` is the
|
|
122
|
+
* raw `WorktreeHarnessResult` (the content-addressed patch artifact). The authored profile
|
|
123
|
+
* (`backend.profile`) threads its systemPrompt + model into the harness via the core.
|
|
124
|
+
*
|
|
125
|
+
* @experimental
|
|
126
|
+
*/
|
|
177
127
|
function createInProcessExecutor(options) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
128
|
+
const harnesses = options.harnesses && options.harnesses.length > 0 ? [...options.harnesses] : ["claude"];
|
|
129
|
+
const runPostCheck = options.runPostCheck ?? defaultRunPostCheck;
|
|
130
|
+
const runCommand = async ({ command, cwd, signal }) => {
|
|
131
|
+
try {
|
|
132
|
+
const r = await runPostCheck(command, cwd, signal);
|
|
133
|
+
return {
|
|
134
|
+
exitCode: r.exitCode,
|
|
135
|
+
output: r.stderr || r.stdout
|
|
136
|
+
};
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return {
|
|
139
|
+
exitCode: -1,
|
|
140
|
+
output: err instanceof Error ? err.message : String(err)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
let callIndex = 0;
|
|
145
|
+
return {
|
|
146
|
+
client: {
|
|
147
|
+
async create(opts) {
|
|
148
|
+
const runId = randomUUID();
|
|
149
|
+
const harness = harnesses[callIndex % harnesses.length];
|
|
150
|
+
callIndex += 1;
|
|
151
|
+
const profile = (opts?.backend)?.profile ?? { name: `in-process-${harness}` };
|
|
152
|
+
return {
|
|
153
|
+
id: `in-process-${runId}`,
|
|
154
|
+
__inProcess: {
|
|
155
|
+
runId,
|
|
156
|
+
harness
|
|
157
|
+
},
|
|
158
|
+
async *streamPrompt(message, promptOpts) {
|
|
159
|
+
const taskPrompt = typeof message === "string" ? message : message.map((p) => typeof p === "object" && p && "text" in p ? String(p.text) : "").join("\n");
|
|
160
|
+
const run = await runWorktreeHarness({
|
|
161
|
+
repoRoot: options.repoRoot,
|
|
162
|
+
profile,
|
|
163
|
+
harness,
|
|
164
|
+
taskPrompt,
|
|
165
|
+
runId,
|
|
166
|
+
harnessTimeoutMs: options.harnessTimeoutMs ?? DEFAULT_HARNESS_TIMEOUT_MS,
|
|
167
|
+
checkTimeoutMs: options.postCheckTimeoutMs ?? DEFAULT_POSTCHECK_TIMEOUT_MS,
|
|
168
|
+
...options.testCmd !== void 0 ? { testCmd: options.testCmd } : {},
|
|
169
|
+
...options.typecheckCmd !== void 0 ? { typecheckCmd: options.typecheckCmd } : {},
|
|
170
|
+
...options.runGit ? { runGit: options.runGit } : {},
|
|
171
|
+
...options.runHarness ? { runHarness: options.runHarness } : {},
|
|
172
|
+
runCommand,
|
|
173
|
+
...promptOpts?.signal ? { signal: promptOpts.signal } : {}
|
|
174
|
+
});
|
|
175
|
+
this.__inProcess.worktree = run.worktree;
|
|
176
|
+
try {
|
|
177
|
+
yield {
|
|
178
|
+
type: "in_process.harness.started",
|
|
179
|
+
data: {
|
|
180
|
+
runId,
|
|
181
|
+
harness,
|
|
182
|
+
worktreePath: run.worktree.path,
|
|
183
|
+
command: harness
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const h = run.result.harness;
|
|
187
|
+
yield {
|
|
188
|
+
type: "in_process.harness.ended",
|
|
189
|
+
data: {
|
|
190
|
+
runId,
|
|
191
|
+
exitCode: h.exitCode,
|
|
192
|
+
durationMs: h.durationMs,
|
|
193
|
+
killedBySignal: h.killedBySignal,
|
|
194
|
+
timedOut: h.timedOut,
|
|
195
|
+
stdoutBytes: h.stdout.length,
|
|
196
|
+
stderrBytes: h.stderr.length
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
yield {
|
|
200
|
+
type: "result",
|
|
201
|
+
data: {
|
|
202
|
+
result: run.result,
|
|
203
|
+
source: "in-process-executor",
|
|
204
|
+
harness,
|
|
205
|
+
runId
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
} finally {
|
|
209
|
+
await run.cleanup();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
describePlacement(box) {
|
|
215
|
+
const sandboxId = box.id;
|
|
216
|
+
const meta = box.__inProcess;
|
|
217
|
+
return {
|
|
218
|
+
kind: "sibling",
|
|
219
|
+
sandboxId,
|
|
220
|
+
worktreePath: meta?.worktree?.path,
|
|
221
|
+
harness: meta?.harness
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
placement: "in-process",
|
|
226
|
+
describe() {
|
|
227
|
+
return `in-process (repoRoot=${options.repoRoot}, harnesses=[${harnesses.join(",")}]${options.testCmd ? `, testCmd="${options.testCmd}"` : ""}${options.typecheckCmd ? `, typecheckCmd="${options.typecheckCmd}"` : ""})`;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
274
230
|
}
|
|
275
231
|
async function defaultRunPostCheck(cmd, cwd, signal) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
232
|
+
const { spawn } = await import("node:child_process");
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const child = spawn("sh", ["-c", cmd], {
|
|
235
|
+
cwd,
|
|
236
|
+
stdio: "pipe"
|
|
237
|
+
});
|
|
238
|
+
let stdout = "";
|
|
239
|
+
let stderr = "";
|
|
240
|
+
child.stdout?.on("data", (c) => {
|
|
241
|
+
stdout += String(c);
|
|
242
|
+
});
|
|
243
|
+
child.stderr?.on("data", (c) => {
|
|
244
|
+
stderr += String(c);
|
|
245
|
+
});
|
|
246
|
+
if (signal) {
|
|
247
|
+
const onAbort = () => {
|
|
248
|
+
if (!child.killed) child.kill("SIGTERM");
|
|
249
|
+
};
|
|
250
|
+
if (signal.aborted) onAbort();
|
|
251
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
252
|
+
}
|
|
253
|
+
const killTimer = setTimeout(() => {
|
|
254
|
+
if (!child.killed) child.kill("SIGTERM");
|
|
255
|
+
}, DEFAULT_POSTCHECK_TIMEOUT_MS);
|
|
256
|
+
if (typeof killTimer.unref === "function") killTimer.unref();
|
|
257
|
+
child.on("error", (err) => {
|
|
258
|
+
clearTimeout(killTimer);
|
|
259
|
+
reject(err);
|
|
260
|
+
});
|
|
261
|
+
child.on("close", (code) => {
|
|
262
|
+
clearTimeout(killTimer);
|
|
263
|
+
resolve({
|
|
264
|
+
exitCode: code ?? -1,
|
|
265
|
+
stdout,
|
|
266
|
+
stderr
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
310
270
|
}
|
|
311
|
-
|
|
312
|
-
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/mcp/bin-helpers.ts
|
|
273
|
+
/**
|
|
274
|
+
* Pick the right executor for an MCP server invocation based on env vars.
|
|
275
|
+
*
|
|
276
|
+
* - `TANGLE_FLEET_ID` set → fleet-workspace placement; resolves the handle
|
|
277
|
+
* via `sandboxClient.fleets.get(...)`.
|
|
278
|
+
* - Otherwise → sibling-sandbox placement; each delegation creates a fresh
|
|
279
|
+
* sandbox via `sandboxClient.create(...)`.
|
|
280
|
+
*
|
|
281
|
+
* Fails loud (throws) when fleet mode is requested but the SDK shape is
|
|
282
|
+
* incompatible — the operator chose fleet semantics, silently degrading to
|
|
283
|
+
* sibling mode would lie about workspace topology.
|
|
284
|
+
*
|
|
285
|
+
* @experimental
|
|
286
|
+
*/
|
|
313
287
|
async function detectExecutor(args) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return createSiblingSandboxExecutor({ client: args.sandboxClient });
|
|
332
|
-
}
|
|
333
|
-
const resolveFleet = args.resolveFleet ?? defaultResolveFleet;
|
|
334
|
-
const fleet = await resolveFleet(args.sandboxClient, fleetId);
|
|
335
|
-
const excludeMachineIds = parseList(env.TANGLE_FLEET_EXCLUDE_MACHINES);
|
|
336
|
-
return createFleetWorkspaceExecutor({
|
|
337
|
-
fleet,
|
|
338
|
-
excludeMachineIds
|
|
339
|
-
});
|
|
288
|
+
const env = args.env ?? process.env;
|
|
289
|
+
if (env.AGENT_RUNTIME_IN_SANDBOX === "1") {
|
|
290
|
+
const repoRoot = env.AGENT_RUNTIME_REPO_ROOT?.trim();
|
|
291
|
+
if (!repoRoot) throw new Error("agent-runtime-mcp: AGENT_RUNTIME_IN_SANDBOX=1 requires AGENT_RUNTIME_REPO_ROOT to point at the workspace root");
|
|
292
|
+
return createInProcessExecutor({
|
|
293
|
+
repoRoot,
|
|
294
|
+
harnesses: parseHarnesses(env.AGENT_RUNTIME_LOCAL_HARNESSES),
|
|
295
|
+
testCmd: env.AGENT_RUNTIME_TEST_CMD?.trim() || void 0,
|
|
296
|
+
typecheckCmd: env.AGENT_RUNTIME_TYPECHECK_CMD?.trim() || void 0
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const fleetId = parseFleetId(env.TANGLE_FLEET_ID);
|
|
300
|
+
if (!fleetId) return createSiblingSandboxExecutor({ client: args.sandboxClient });
|
|
301
|
+
return createFleetWorkspaceExecutor({
|
|
302
|
+
fleet: await (args.resolveFleet ?? defaultResolveFleet)(args.sandboxClient, fleetId),
|
|
303
|
+
excludeMachineIds: parseList(env.TANGLE_FLEET_EXCLUDE_MACHINES)
|
|
304
|
+
});
|
|
340
305
|
}
|
|
341
|
-
|
|
306
|
+
const KNOWN_HARNESSES = [
|
|
307
|
+
"claude",
|
|
308
|
+
"codex",
|
|
309
|
+
"opencode"
|
|
310
|
+
];
|
|
342
311
|
function parseHarnesses(raw) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
throw new Error(
|
|
349
|
-
`agent-runtime-mcp: AGENT_RUNTIME_LOCAL_HARNESSES contains unknown harness "${part}". Expected: ${KNOWN_HARNESSES.join(", ")}.`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return parts;
|
|
312
|
+
if (!raw) return void 0;
|
|
313
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
314
|
+
if (parts.length === 0) return void 0;
|
|
315
|
+
for (const part of parts) if (!KNOWN_HARNESSES.includes(part)) throw new Error(`agent-runtime-mcp: AGENT_RUNTIME_LOCAL_HARNESSES contains unknown harness "${part}". Expected: ${KNOWN_HARNESSES.join(", ")}.`);
|
|
316
|
+
return parts;
|
|
354
317
|
}
|
|
355
318
|
async function defaultResolveFleet(sandboxClient, fleetId) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
throw new Error(`agent-runtime-mcp: fleets.get(${fleetId}) returned no handle`);
|
|
365
|
-
}
|
|
366
|
-
const handle = raw;
|
|
367
|
-
if (typeof handle.fleetId !== "string" || !Array.isArray(handle.ids)) {
|
|
368
|
-
throw new Error(
|
|
369
|
-
`agent-runtime-mcp: fleet handle for ${fleetId} is missing fleetId/ids \u2014 incompatible sandbox SDK shape`
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
if (typeof handle.sandbox !== "function") {
|
|
373
|
-
throw new Error(
|
|
374
|
-
`agent-runtime-mcp: fleet handle for ${fleetId} is missing sandbox(machineId) \u2014 incompatible sandbox SDK shape`
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
return handle;
|
|
319
|
+
const fleets = sandboxClient.fleets;
|
|
320
|
+
if (!fleets || typeof fleets.get !== "function") throw new Error("agent-runtime-mcp: the configured sandbox client does not expose `.fleets.get`; upgrade @tangle-network/sandbox to >= 0.2.1 or unset TANGLE_FLEET_ID.");
|
|
321
|
+
const raw = await fleets.get(fleetId);
|
|
322
|
+
if (!raw || typeof raw !== "object") throw new Error(`agent-runtime-mcp: fleets.get(${fleetId}) returned no handle`);
|
|
323
|
+
const handle = raw;
|
|
324
|
+
if (typeof handle.fleetId !== "string" || !Array.isArray(handle.ids)) throw new Error(`agent-runtime-mcp: fleet handle for ${fleetId} is missing fleetId/ids — incompatible sandbox SDK shape`);
|
|
325
|
+
if (typeof handle.sandbox !== "function") throw new Error(`agent-runtime-mcp: fleet handle for ${fleetId} is missing sandbox(machineId) — incompatible sandbox SDK shape`);
|
|
326
|
+
return handle;
|
|
378
327
|
}
|
|
379
328
|
function parseFleetId(raw) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
329
|
+
if (typeof raw !== "string") return void 0;
|
|
330
|
+
const trimmed = raw.trim();
|
|
331
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
383
332
|
}
|
|
384
333
|
function parseList(raw) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
334
|
+
if (!raw) return void 0;
|
|
335
|
+
const list = raw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
336
|
+
return list.length > 0 ? list : void 0;
|
|
388
337
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/mcp/detached-coder.ts
|
|
340
|
+
const DEFAULT_MAX_DIFF_LINES = 400;
|
|
341
|
+
/**
|
|
342
|
+
* The minimal default worker profile (§1.5: the system authors profiles — there is no hardcoded
|
|
343
|
+
* coder). Model-only by construction: no skills, no tool grants, no standing prompt. Callers that
|
|
344
|
+
* want a richer worker pass their own `AgentProfile` via `CoderRunSpecOptions.profile`. `harness` /
|
|
345
|
+
* `model` / `systemPrompt` are layered onto whichever profile is used.
|
|
346
|
+
*/
|
|
392
347
|
function minimalCoderProfile() {
|
|
393
|
-
|
|
348
|
+
return { name: "coder" };
|
|
394
349
|
}
|
|
350
|
+
/** Build the authored `AgentProfile` for one harness on the sandbox-session path: the caller's
|
|
351
|
+
* profile (or the minimal model-only default), with the per-run harness/model/prompt overrides. */
|
|
395
352
|
function coderRunProfile(options) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
353
|
+
const harness = options.harness ?? "claude-code";
|
|
354
|
+
const name = options.name ?? `coder-${harness}`;
|
|
355
|
+
const base = options.profile ?? minimalCoderProfile();
|
|
356
|
+
return {
|
|
357
|
+
...base,
|
|
358
|
+
name,
|
|
359
|
+
...options.systemPrompt ? { prompt: { systemPrompt: options.systemPrompt } } : {},
|
|
360
|
+
model: options.model ? { default: options.model } : base.model,
|
|
361
|
+
metadata: {
|
|
362
|
+
...base.metadata,
|
|
363
|
+
backendType: harness
|
|
364
|
+
}
|
|
365
|
+
};
|
|
406
366
|
}
|
|
367
|
+
/** @experimental Build the `AgentRunSpec<CoderTask>` the sandbox-session `runAgentRounds` path drives. */
|
|
407
368
|
function coderRunSpec(options = {}) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
369
|
+
return {
|
|
370
|
+
name: options.name ?? `coder-${options.harness ?? "claude-code"}`,
|
|
371
|
+
profile: coderRunProfile(options),
|
|
372
|
+
taskToPrompt: coderTaskToPrompt
|
|
373
|
+
};
|
|
413
374
|
}
|
|
414
|
-
|
|
375
|
+
/** @experimental The output adapter the sandbox-session path decodes the harness stream with. */
|
|
376
|
+
const coderOutputAdapter = { parse: parseCoderEvents };
|
|
377
|
+
/**
|
|
378
|
+
* The multi-harness coder fanout driving the sandbox-session delegate's `variants>1` path.
|
|
379
|
+
* (`worktreeFanout` is the local-repo generic counterpart for new code.)
|
|
380
|
+
*
|
|
381
|
+
* @experimental
|
|
382
|
+
*/
|
|
415
383
|
function multiHarnessCoderFanout(options = {}) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
384
|
+
const harnesses = options.harnesses && options.harnesses.length > 0 ? options.harnesses : [
|
|
385
|
+
"claude-code",
|
|
386
|
+
"codex",
|
|
387
|
+
"opencode/zai-coding-plan/glm-5.1"
|
|
388
|
+
];
|
|
389
|
+
const models = options.models ?? [];
|
|
390
|
+
const agentRuns = harnesses.map((harness, i) => coderRunSpec({
|
|
391
|
+
...options.profile ? { profile: options.profile } : {},
|
|
392
|
+
harness,
|
|
393
|
+
model: models[i]
|
|
394
|
+
}));
|
|
395
|
+
return {
|
|
396
|
+
agentRuns,
|
|
397
|
+
output: coderOutputAdapter,
|
|
398
|
+
validator: defaultCoderValidator(),
|
|
399
|
+
driver: {
|
|
400
|
+
name: "fanout",
|
|
401
|
+
plan: async (task, history) => history.length === 0 ? agentRuns.map(() => task) : [],
|
|
402
|
+
decide: (history) => history.some((i) => i.verdict?.valid === true) ? "pick-winner" : "fail"
|
|
403
|
+
}
|
|
404
|
+
};
|
|
431
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* The sandbox `CoderOutput` validator. A thin shim over the shared {@link runCoderChecks} gate,
|
|
408
|
+
* adapting the parsed `CoderOutput` into the gate inputs.
|
|
409
|
+
*
|
|
410
|
+
* @experimental
|
|
411
|
+
*/
|
|
432
412
|
function createCoderValidator(task) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
},
|
|
445
|
-
constraints
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
};
|
|
413
|
+
const constraints = {
|
|
414
|
+
maxDiffLines: task.maxDiffLines ?? DEFAULT_MAX_DIFF_LINES,
|
|
415
|
+
forbiddenPaths: task.forbiddenPaths ?? []
|
|
416
|
+
};
|
|
417
|
+
return { async validate(output) {
|
|
418
|
+
return runCoderChecks({
|
|
419
|
+
patch: output.patch,
|
|
420
|
+
testsPassed: output.testResult.passed,
|
|
421
|
+
typecheckPassed: output.typecheckResult.passed
|
|
422
|
+
}, constraints);
|
|
423
|
+
} };
|
|
449
424
|
}
|
|
450
425
|
function defaultCoderValidator() {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
426
|
+
return createCoderValidator({
|
|
427
|
+
goal: "",
|
|
428
|
+
repoRoot: "",
|
|
429
|
+
forbiddenPaths: [],
|
|
430
|
+
maxDiffLines: DEFAULT_MAX_DIFF_LINES
|
|
431
|
+
});
|
|
457
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Walk the event stream and return the structured coder payload.
|
|
435
|
+
*
|
|
436
|
+
* A `result` / `final` event lifts the structured payload onto `data.result`. That payload is
|
|
437
|
+
* either the in-process executor's raw worktree-harness result (projected onto `CoderOutput`) or an
|
|
438
|
+
* LLM-emitted `CoderOutput`-shaped JSON. When neither is present, the scan accumulates ALL assistant
|
|
439
|
+
* text in stream order (any harness shape) and takes the last fenced JSON block that coerces —
|
|
440
|
+
* claude-code lifts whole text onto `data.text`/`data.delta`; opencode streams `message.part.updated`
|
|
441
|
+
* fragments, so the final block is split across many events and never whole in one.
|
|
442
|
+
*/
|
|
458
443
|
function parseCoderEvents(events) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
444
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
445
|
+
const event = events[i];
|
|
446
|
+
if (!event) continue;
|
|
447
|
+
const type = String(event.type ?? "");
|
|
448
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
449
|
+
if (type === "result" || type === "final" || type === "coder.result") {
|
|
450
|
+
const payload = data.result ?? data.output ?? data;
|
|
451
|
+
const projected = projectWorktreeArtifact(payload);
|
|
452
|
+
if (projected) return projected;
|
|
453
|
+
const direct = coerceCoderOutput(payload);
|
|
454
|
+
if (direct) return direct;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const transcript = collectAssistantText(events);
|
|
458
|
+
for (const candidate of fencedJsonBlocks(transcript)) {
|
|
459
|
+
const coerced = coerceCoderOutput(candidate);
|
|
460
|
+
if (coerced) return coerced;
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
branch: "",
|
|
464
|
+
patch: "",
|
|
465
|
+
testResult: {
|
|
466
|
+
passed: false,
|
|
467
|
+
output: ""
|
|
468
|
+
},
|
|
469
|
+
typecheckResult: {
|
|
470
|
+
passed: false,
|
|
471
|
+
output: ""
|
|
472
|
+
},
|
|
473
|
+
diffStats: {
|
|
474
|
+
filesChanged: 0,
|
|
475
|
+
insertions: 0,
|
|
476
|
+
deletions: 0
|
|
477
|
+
}
|
|
478
|
+
};
|
|
484
479
|
}
|
|
480
|
+
/** Project the in-process executor's raw worktree-harness result (`{ branch, patch, stats, checks,
|
|
481
|
+
* harness }`) onto `CoderOutput`. A check that did not run is treated as passing (the executor
|
|
482
|
+
* simply didn't run that command). Returns undefined when the payload is not a worktree artifact. */
|
|
485
483
|
function projectWorktreeArtifact(value) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
} : {}
|
|
517
|
-
};
|
|
484
|
+
if (!isRecord(value)) return void 0;
|
|
485
|
+
const stats = value.stats;
|
|
486
|
+
if (!isRecord(stats)) return void 0;
|
|
487
|
+
const branch = pickString(value.branch) ?? "";
|
|
488
|
+
const patch = pickString(value.patch) ?? "";
|
|
489
|
+
const checks = isRecord(value.checks) ? value.checks : {};
|
|
490
|
+
const tests = isRecord(checks.tests) ? checks.tests : void 0;
|
|
491
|
+
const typecheck = isRecord(checks.typecheck) ? checks.typecheck : void 0;
|
|
492
|
+
const harness = isRecord(value.harness) ? value.harness : void 0;
|
|
493
|
+
const exitCode = harness ? toFiniteInt(harness.exitCode) : 0;
|
|
494
|
+
const timedOut = harness?.timedOut === true;
|
|
495
|
+
const harnessName = harness ? pickString(harness.name) ?? "harness" : "harness";
|
|
496
|
+
return {
|
|
497
|
+
branch,
|
|
498
|
+
patch,
|
|
499
|
+
testResult: {
|
|
500
|
+
passed: tests ? tests.passed === true : true,
|
|
501
|
+
output: tail(pickString(tests?.output) ?? "", 4e3)
|
|
502
|
+
},
|
|
503
|
+
typecheckResult: {
|
|
504
|
+
passed: typecheck ? typecheck.passed === true : true,
|
|
505
|
+
output: tail(pickString(typecheck?.output) ?? "", 4e3)
|
|
506
|
+
},
|
|
507
|
+
diffStats: {
|
|
508
|
+
filesChanged: toFiniteInt(stats.filesChanged),
|
|
509
|
+
insertions: toFiniteInt(stats.insertions),
|
|
510
|
+
deletions: toFiniteInt(stats.deletions)
|
|
511
|
+
},
|
|
512
|
+
...exitCode !== 0 ? { reviewerNotes: `harness ${harnessName} exited ${exitCode}${timedOut ? " (timed out)" : ""}` } : {}
|
|
513
|
+
};
|
|
518
514
|
}
|
|
519
515
|
function isRecord(value) {
|
|
520
|
-
|
|
516
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
521
517
|
}
|
|
518
|
+
/** Keep the last `max` chars of a diagnostic string — harness stdout can be large; the gate reads
|
|
519
|
+
* `passed`, not this text, so only the tail is retained for traces/logs. */
|
|
522
520
|
function tail(text, max) {
|
|
523
|
-
|
|
521
|
+
return text.length <= max ? text : text.slice(text.length - max);
|
|
524
522
|
}
|
|
525
523
|
function pickString(value) {
|
|
526
|
-
|
|
524
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
527
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Concatenate assistant text across the event stream in arrival order, tolerating every harness
|
|
528
|
+
* shape: claude-code lifts text onto `data.text`/`data.delta`; opencode streams
|
|
529
|
+
* `message.part.updated` with `data.part.type === 'text'` carrying `data.delta`/`data.part.text`.
|
|
530
|
+
* Reasoning/thinking parts are excluded — only the final answer text carries the result JSON.
|
|
531
|
+
*/
|
|
528
532
|
function collectAssistantText(events) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
533
|
+
const chunks = [];
|
|
534
|
+
for (const event of events) {
|
|
535
|
+
if (!event) continue;
|
|
536
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
537
|
+
if (String(event.type ?? "") === "message.part.updated") {
|
|
538
|
+
const part = isRecord(data.part) ? data.part : {};
|
|
539
|
+
const partType = String(part.type ?? "");
|
|
540
|
+
if (partType !== "text" && partType !== "") continue;
|
|
541
|
+
const text = pickString(data.delta) ?? pickString(part.text);
|
|
542
|
+
if (text) chunks.push(text);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const text = pickString(data.text) ?? pickString(data.delta);
|
|
546
|
+
if (text) chunks.push(text);
|
|
547
|
+
}
|
|
548
|
+
return chunks.join("");
|
|
545
549
|
}
|
|
550
|
+
/** All parseable fenced JSON blocks in `text`, last-first (the final result block the agent emits
|
|
551
|
+
* is the one we want). */
|
|
546
552
|
function fencedJsonBlocks(text) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
return out;
|
|
553
|
+
const out = [];
|
|
554
|
+
const matches = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)];
|
|
555
|
+
for (let i = matches.length - 1; i >= 0; i -= 1) {
|
|
556
|
+
const body = (matches[i]?.[1] ?? "").trim();
|
|
557
|
+
if (!body) continue;
|
|
558
|
+
try {
|
|
559
|
+
out.push(JSON.parse(body));
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
558
563
|
}
|
|
559
564
|
function coerceCoderOutput(value) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
565
|
+
if (!isRecord(value)) return void 0;
|
|
566
|
+
const branch = pickString(value.branch);
|
|
567
|
+
const patch = pickString(value.patch) ?? "";
|
|
568
|
+
if (branch === void 0) return void 0;
|
|
569
|
+
return {
|
|
570
|
+
branch,
|
|
571
|
+
patch,
|
|
572
|
+
testResult: coerceCmdResult(value.testResult),
|
|
573
|
+
typecheckResult: coerceCmdResult(value.typecheckResult),
|
|
574
|
+
diffStats: coerceDiffStats(value.diffStats),
|
|
575
|
+
reviewerNotes: pickString(value.reviewerNotes)
|
|
576
|
+
};
|
|
572
577
|
}
|
|
573
578
|
function coerceCmdResult(value) {
|
|
574
|
-
|
|
575
|
-
|
|
579
|
+
if (!isRecord(value)) return {
|
|
580
|
+
passed: false,
|
|
581
|
+
output: ""
|
|
582
|
+
};
|
|
583
|
+
return {
|
|
584
|
+
passed: value.passed === true,
|
|
585
|
+
output: pickString(value.output) ?? ""
|
|
586
|
+
};
|
|
576
587
|
}
|
|
577
588
|
function coerceDiffStats(value) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
589
|
+
if (!isRecord(value)) return {
|
|
590
|
+
filesChanged: 0,
|
|
591
|
+
insertions: 0,
|
|
592
|
+
deletions: 0
|
|
593
|
+
};
|
|
594
|
+
return {
|
|
595
|
+
filesChanged: toFiniteInt(value.filesChanged),
|
|
596
|
+
insertions: toFiniteInt(value.insertions),
|
|
597
|
+
deletions: toFiniteInt(value.deletions)
|
|
598
|
+
};
|
|
584
599
|
}
|
|
585
600
|
function toFiniteInt(value) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
601
|
+
if (typeof value !== "number") return 0;
|
|
602
|
+
if (!Number.isFinite(value)) return 0;
|
|
603
|
+
return Math.max(0, Math.trunc(value));
|
|
589
604
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/mcp/detached-turn.ts
|
|
607
|
+
const DEFAULT_TICK_INTERVAL_MS = 5e3;
|
|
608
|
+
/**
|
|
609
|
+
* Encode ref parts into the JSON-safe string stored on the record:
|
|
610
|
+
* `session=<id>` before the box exists, `sandbox=<id>;session=<id>` once
|
|
611
|
+
* bound. Ids must not contain the `;`/`=` delimiters.
|
|
612
|
+
*
|
|
613
|
+
* @experimental
|
|
614
|
+
*/
|
|
593
615
|
function formatDetachedSessionRef(parts) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
616
|
+
assertRefComponent("sessionId", parts.sessionId);
|
|
617
|
+
if (parts.sandboxId === void 0) return `session=${parts.sessionId}`;
|
|
618
|
+
assertRefComponent("sandboxId", parts.sandboxId);
|
|
619
|
+
return `sandbox=${parts.sandboxId};session=${parts.sessionId}`;
|
|
598
620
|
}
|
|
621
|
+
/** Parse a `detachedSessionRef` string back to parts; throws `ValidationError` on malformed input. @experimental */
|
|
599
622
|
function parseDetachedSessionRef(raw) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
`parseDetachedSessionRef: detachedSessionRef ${JSON.stringify(raw)} carries no session id`
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
const sandboxId = fields.get("sandbox");
|
|
619
|
-
return { sessionId, ...sandboxId !== void 0 ? { sandboxId } : {} };
|
|
623
|
+
const fields = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const pair of raw.split(";")) {
|
|
625
|
+
const eq = pair.indexOf("=");
|
|
626
|
+
const key = eq === -1 ? "" : pair.slice(0, eq);
|
|
627
|
+
const value = eq === -1 ? "" : pair.slice(eq + 1);
|
|
628
|
+
if (key !== "session" && key !== "sandbox" || value.length === 0 || fields.has(key)) throw new ValidationError(`parseDetachedSessionRef: malformed detachedSessionRef ${JSON.stringify(raw)} — expected "session=<id>" or "sandbox=<id>;session=<id>"`);
|
|
629
|
+
fields.set(key, value);
|
|
630
|
+
}
|
|
631
|
+
const sessionId = fields.get("session");
|
|
632
|
+
if (!sessionId) throw new ValidationError(`parseDetachedSessionRef: detachedSessionRef ${JSON.stringify(raw)} carries no session id`);
|
|
633
|
+
const sandboxId = fields.get("sandbox");
|
|
634
|
+
return {
|
|
635
|
+
sessionId,
|
|
636
|
+
...sandboxId !== void 0 ? { sandboxId } : {}
|
|
637
|
+
};
|
|
620
638
|
}
|
|
621
639
|
function assertRefComponent(name, value) {
|
|
622
|
-
|
|
623
|
-
throw new ValidationError(
|
|
624
|
-
`formatDetachedSessionRef: ${name} ${JSON.stringify(value)} must be non-empty and free of ";" / "="`
|
|
625
|
-
);
|
|
626
|
-
}
|
|
640
|
+
if (value.length === 0 || value.includes(";") || value.includes("=")) throw new ValidationError(`formatDetachedSessionRef: ${name} ${JSON.stringify(value)} must be non-empty and free of ";" / "="`);
|
|
627
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Synthesize the terminal event array a detached turn settles through. Shaped
|
|
644
|
+
* so the existing event-stream output adapters (coder, researcher) parse it:
|
|
645
|
+
* `data.result` for adapters that read a structured terminal record, `data.text`
|
|
646
|
+
* for adapters that scan assistant text for the fenced result block.
|
|
647
|
+
*
|
|
648
|
+
* @experimental
|
|
649
|
+
*/
|
|
628
650
|
function detachedTurnEvents(sessionId, turn) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
];
|
|
651
|
+
return [{
|
|
652
|
+
type: "result",
|
|
653
|
+
id: sessionId,
|
|
654
|
+
data: {
|
|
655
|
+
text: turn.text,
|
|
656
|
+
finalText: turn.text,
|
|
657
|
+
success: true,
|
|
658
|
+
result: turn.result
|
|
659
|
+
}
|
|
660
|
+
}];
|
|
641
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Dispatch one detached turn and advance it to a terminal state with
|
|
664
|
+
* `driveTurn` ticks. The first tick dispatches (idempotent on `sessionId`);
|
|
665
|
+
* subsequent ticks poll. On abort the remote session is cancelled via
|
|
666
|
+
* `_sessionCancel` when the box exposes it. The box is torn down on every
|
|
667
|
+
* in-process exit path (success, failure, abort) — only a process death skips
|
|
668
|
+
* teardown, which is exactly the case the resume driver re-attaches to.
|
|
669
|
+
*
|
|
670
|
+
* @experimental
|
|
671
|
+
*/
|
|
642
672
|
async function runDetachedTurn(options) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
trace.ended(err instanceof Error ? err.message : String(err));
|
|
692
|
-
throw err;
|
|
693
|
-
} finally {
|
|
694
|
-
options.signal.removeEventListener("abort", onAbort);
|
|
695
|
-
if (options.signal.aborted) onAbort();
|
|
696
|
-
await deleteBoxSafe(box);
|
|
697
|
-
}
|
|
673
|
+
const intervalMs = options.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS;
|
|
674
|
+
const trace = createDetachedTurnTrace(options);
|
|
675
|
+
trace.started();
|
|
676
|
+
const box = await createSandboxForSpec(options.client, options.spec, options.signal).catch((err) => {
|
|
677
|
+
trace.ended(err instanceof Error ? err.message : String(err));
|
|
678
|
+
throw err;
|
|
679
|
+
});
|
|
680
|
+
const drive = box;
|
|
681
|
+
const onAbort = () => {
|
|
682
|
+
drive._sessionCancel?.(options.sessionId).catch(() => {});
|
|
683
|
+
};
|
|
684
|
+
try {
|
|
685
|
+
if (typeof drive.driveTurn !== "function") throw new ValidationError("runDetachedTurn: the acquired sandbox exposes no driveTurn(message, { sessionId }) — detached dispatch requires @tangle-network/sandbox >= 0.6 and a session-backed placement (sibling/fleet); disable detached dispatch for this executor.");
|
|
686
|
+
const sandboxId = box.id;
|
|
687
|
+
if (typeof sandboxId !== "string" || sandboxId.length === 0) throw new ValidationError("runDetachedTurn: the acquired sandbox carries no id — without it the detached run cannot be resumed after a restart, so refusing to dispatch detached.");
|
|
688
|
+
options.bindSandbox(sandboxId);
|
|
689
|
+
trace.dispatched(sandboxId);
|
|
690
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
691
|
+
for (;;) {
|
|
692
|
+
throwIfAborted(options.signal);
|
|
693
|
+
const tick = await drive.driveTurn(options.prompt, {
|
|
694
|
+
sessionId: options.sessionId,
|
|
695
|
+
turnId: options.sessionId,
|
|
696
|
+
...options.wallCapMs !== void 0 ? { wallCapMs: options.wallCapMs } : {}
|
|
697
|
+
});
|
|
698
|
+
throwIfAborted(options.signal);
|
|
699
|
+
if (tick.state === "completed") {
|
|
700
|
+
trace.ended();
|
|
701
|
+
return {
|
|
702
|
+
text: tick.text,
|
|
703
|
+
result: tick.result
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
if (tick.state === "failed") throw new Error(`detached turn ${options.sessionId} failed: ${tick.error}`);
|
|
707
|
+
options.report({
|
|
708
|
+
iteration: 0,
|
|
709
|
+
phase: detachedRunningPhase(tick.elapsedMs)
|
|
710
|
+
});
|
|
711
|
+
await sleep(intervalMs, options.signal);
|
|
712
|
+
}
|
|
713
|
+
} catch (err) {
|
|
714
|
+
trace.ended(err instanceof Error ? err.message : String(err));
|
|
715
|
+
throw err;
|
|
716
|
+
} finally {
|
|
717
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
718
|
+
if (options.signal.aborted) onAbort();
|
|
719
|
+
await deleteBoxSafe(box);
|
|
720
|
+
}
|
|
698
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Synthesize the single-iteration loop event stream for one detached turn so
|
|
724
|
+
* the trace sinks (OTEL exporter, delegation journal) observe detached work
|
|
725
|
+
* exactly like a streamed `runAgentRounds` run. `runId` = the deterministic session
|
|
726
|
+
* id; cost/token figures are structurally unavailable on the `driveTurn`
|
|
727
|
+
* surface and emitted as 0 under the `'detached-turn'` driver tag.
|
|
728
|
+
*/
|
|
699
729
|
function createDetachedTurnTrace(options) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
730
|
+
const emitter = options.traceEmitter;
|
|
731
|
+
if (!emitter) return {
|
|
732
|
+
started() {},
|
|
733
|
+
dispatched() {},
|
|
734
|
+
ended() {}
|
|
735
|
+
};
|
|
736
|
+
const runId = options.sessionId;
|
|
737
|
+
const agentRunName = options.spec.name ?? options.spec.profile.name ?? "detached-turn";
|
|
738
|
+
const startMs = Date.now();
|
|
739
|
+
let done = false;
|
|
740
|
+
const emit = (event) => {
|
|
741
|
+
emitter.emit(event);
|
|
742
|
+
};
|
|
743
|
+
return {
|
|
744
|
+
started() {
|
|
745
|
+
emit({
|
|
746
|
+
kind: "loop.started",
|
|
747
|
+
runId,
|
|
748
|
+
timestamp: startMs,
|
|
749
|
+
payload: {
|
|
750
|
+
driver: "detached-turn",
|
|
751
|
+
agentRunNames: [agentRunName],
|
|
752
|
+
maxIterations: 1,
|
|
753
|
+
maxConcurrency: 1
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
emit({
|
|
757
|
+
kind: "loop.iteration.started",
|
|
758
|
+
runId,
|
|
759
|
+
timestamp: startMs,
|
|
760
|
+
payload: {
|
|
761
|
+
iterationIndex: 0,
|
|
762
|
+
agentRunName,
|
|
763
|
+
taskHash: options.sessionId
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
},
|
|
767
|
+
dispatched(sandboxId) {
|
|
768
|
+
emit({
|
|
769
|
+
kind: "loop.iteration.dispatch",
|
|
770
|
+
runId,
|
|
771
|
+
timestamp: Date.now(),
|
|
772
|
+
payload: {
|
|
773
|
+
iterationIndex: 0,
|
|
774
|
+
agentRunName,
|
|
775
|
+
placement: options.placement ?? "sibling",
|
|
776
|
+
sandboxId
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
},
|
|
780
|
+
ended(error) {
|
|
781
|
+
if (done) return;
|
|
782
|
+
done = true;
|
|
783
|
+
const endMs = Date.now();
|
|
784
|
+
emit({
|
|
785
|
+
kind: "loop.iteration.ended",
|
|
786
|
+
runId,
|
|
787
|
+
timestamp: endMs,
|
|
788
|
+
payload: {
|
|
789
|
+
iterationIndex: 0,
|
|
790
|
+
agentRunName,
|
|
791
|
+
costUsd: 0,
|
|
792
|
+
durationMs: endMs - startMs,
|
|
793
|
+
...error !== void 0 ? { error } : {}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
emit({
|
|
797
|
+
kind: "loop.ended",
|
|
798
|
+
runId,
|
|
799
|
+
timestamp: endMs,
|
|
800
|
+
payload: {
|
|
801
|
+
...error === void 0 ? { winnerIterationIndex: 0 } : {},
|
|
802
|
+
totalCostUsd: 0,
|
|
803
|
+
durationMs: endMs - startMs,
|
|
804
|
+
iterations: 1
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
};
|
|
776
809
|
}
|
|
777
810
|
function detachedRunningPhase(elapsedMs) {
|
|
778
|
-
|
|
811
|
+
return elapsedMs === void 0 ? "detached-running" : `detached-running ${Math.round(elapsedMs / 1e3)}s`;
|
|
779
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Build the `driveTurn`-backed {@link DelegationResumeDriver}. Each `tick()`
|
|
815
|
+
* is one settle/poll/dispatch pass:
|
|
816
|
+
*
|
|
817
|
+
* - ref without a sandbox binding → `failed` (`DetachedSessionUnboundError`):
|
|
818
|
+
* the previous process died before a box existed; there is nothing to resume.
|
|
819
|
+
* - `driveTurn` `completed` → `settleOutput` → `completed` tick.
|
|
820
|
+
* - `running` → progress via `ctx.report`, `running` tick (queue re-ticks
|
|
821
|
+
* after `intervalMs`).
|
|
822
|
+
* - `failed` → `failed` tick (`DetachedTurnFailedError`) — terminal per the
|
|
823
|
+
* SDK's deterministic-failure contract.
|
|
824
|
+
*
|
|
825
|
+
* Abort: the queue stops ticking once `cancel()` flips the record, so remote
|
|
826
|
+
* cancellation is hooked onto `ctx.signal` (once per task) and fires
|
|
827
|
+
* `_sessionCancel` when the SDK surface exposes it. The driver never deletes
|
|
828
|
+
* boxes — it cannot know whether `sandboxId` is a disposable sibling or a
|
|
829
|
+
* fleet machine, and destroying a fleet machine would be unrecoverable.
|
|
830
|
+
*
|
|
831
|
+
* @experimental
|
|
832
|
+
*/
|
|
780
833
|
function createDetachedTurnResumeDriver(options) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
message: `detached turn ${ref.sessionId} failed: ${tick.error}`,
|
|
828
|
-
kind: "DetachedTurnFailedError"
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
ctx.report({ iteration: 0, phase: detachedRunningPhase(tick.elapsedMs) });
|
|
833
|
-
return { state: "running" };
|
|
834
|
-
}
|
|
835
|
-
};
|
|
834
|
+
const cancelHooked = /* @__PURE__ */ new Set();
|
|
835
|
+
return {
|
|
836
|
+
intervalMs: options.intervalMs ?? DEFAULT_TICK_INTERVAL_MS,
|
|
837
|
+
async tick({ record, detachedSessionRef }, ctx) {
|
|
838
|
+
const ref = parseDetachedSessionRef(detachedSessionRef);
|
|
839
|
+
if (ref.sandboxId === void 0) return {
|
|
840
|
+
state: "failed",
|
|
841
|
+
error: {
|
|
842
|
+
message: `detached session "${ref.sessionId}" was never bound to a sandbox — the previous process died before the box was acquired, so the turn was never dispatched and cannot be resumed`,
|
|
843
|
+
kind: "DetachedSessionUnboundError"
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
const box = await options.resolveSandbox(ref.sandboxId);
|
|
847
|
+
if (!cancelHooked.has(record.taskId)) {
|
|
848
|
+
cancelHooked.add(record.taskId);
|
|
849
|
+
ctx.signal.addEventListener("abort", () => {
|
|
850
|
+
box._sessionCancel?.(ref.sessionId).catch(() => {});
|
|
851
|
+
}, { once: true });
|
|
852
|
+
}
|
|
853
|
+
if (ctx.signal.aborted) throwAbort();
|
|
854
|
+
const tick = await box.driveTurn(options.buildMessage(record), {
|
|
855
|
+
sessionId: ref.sessionId,
|
|
856
|
+
turnId: ref.sessionId,
|
|
857
|
+
...options.wallCapMs !== void 0 ? { wallCapMs: options.wallCapMs } : {}
|
|
858
|
+
});
|
|
859
|
+
if (tick.state === "completed") return {
|
|
860
|
+
state: "completed",
|
|
861
|
+
output: await options.settleOutput({
|
|
862
|
+
text: tick.text,
|
|
863
|
+
result: tick.result
|
|
864
|
+
}, record, { signal: ctx.signal })
|
|
865
|
+
};
|
|
866
|
+
if (tick.state === "failed") return {
|
|
867
|
+
state: "failed",
|
|
868
|
+
error: {
|
|
869
|
+
message: `detached turn ${ref.sessionId} failed: ${tick.error}`,
|
|
870
|
+
kind: "DetachedTurnFailedError"
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
ctx.report({
|
|
874
|
+
iteration: 0,
|
|
875
|
+
phase: detachedRunningPhase(tick.elapsedMs)
|
|
876
|
+
});
|
|
877
|
+
return { state: "running" };
|
|
878
|
+
}
|
|
879
|
+
};
|
|
836
880
|
}
|
|
837
|
-
|
|
838
|
-
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/mcp/delegates.ts
|
|
883
|
+
/**
|
|
884
|
+
* Build the sandbox-session coder delegate. It drives `runAgentRounds` against the project's
|
|
885
|
+
* sandbox client + coder profile; when `args.variants > 1` it switches to the multi-harness fanout
|
|
886
|
+
* topology.
|
|
887
|
+
*
|
|
888
|
+
* This is the SANDBOX-SESSION coder path: workers run the in-box harness via the
|
|
889
|
+
* `SandboxClient`'s `streamPrompt`, and single-variant turns can dispatch DETACHED
|
|
890
|
+
* (driveTurn ticks) so a durable queue resumes them across an MCP restart — a substrate
|
|
891
|
+
* the recursive worktree-CLI leaf does not yet have a journal-replay equivalent for.
|
|
892
|
+
*
|
|
893
|
+
* For NEW local-repo coding use `worktreeFanout` / `worktreeLoopRunner` (author an `AgentProfile`
|
|
894
|
+
* per harness → `createWorktreeCliExecutor` leaves → `gateOnDeliverable`). This delegate runs
|
|
895
|
+
* held-stream by default and only its OPTIONAL cross-restart resume (the `driveTurn` tick) is opt-in
|
|
896
|
+
* behind `MCP_ENABLE_DETACHED_RESUME`.
|
|
897
|
+
*
|
|
898
|
+
* @experimental
|
|
899
|
+
*/
|
|
839
900
|
function detachedSessionDelegate(options) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
901
|
+
const executor = resolveExecutor(options);
|
|
902
|
+
const sandboxClient = executor.client;
|
|
903
|
+
const fanoutHarnesses = options.fanoutHarnesses;
|
|
904
|
+
const maxConcurrency = options.maxConcurrency ?? 4;
|
|
905
|
+
const traceEmitter = options.traceEmitter;
|
|
906
|
+
return async (args, ctx) => {
|
|
907
|
+
const task = coderTaskFromArgs(args);
|
|
908
|
+
const variants = Math.max(1, Math.trunc(args.variants ?? 1));
|
|
909
|
+
const loopEmitter = composeLoopTraceEmitters(traceEmitter, ctx.traceEmitter);
|
|
910
|
+
ctx.report({
|
|
911
|
+
iteration: 0,
|
|
912
|
+
phase: "starting"
|
|
913
|
+
});
|
|
914
|
+
if (variants <= 1) {
|
|
915
|
+
const agentRunSpec = coderRunSpec({
|
|
916
|
+
...options.workerProfile ? { profile: options.workerProfile } : {},
|
|
917
|
+
...options.harness ? { harness: options.harness } : {},
|
|
918
|
+
...options.model ? { model: options.model } : {},
|
|
919
|
+
...options.systemPrompt ? { systemPrompt: options.systemPrompt } : {}
|
|
920
|
+
});
|
|
921
|
+
const output = coderOutputAdapter;
|
|
922
|
+
const validator = createCoderValidator(task);
|
|
923
|
+
if (ctx.detachedSessionRef !== void 0 && ctx.updateDetachedSessionRef) {
|
|
924
|
+
const { sessionId } = parseDetachedSessionRef(ctx.detachedSessionRef);
|
|
925
|
+
const rebind = ctx.updateDetachedSessionRef;
|
|
926
|
+
const chosen = await settleDetachedCoderTurn(await runDetachedTurn({
|
|
927
|
+
client: sandboxClient,
|
|
928
|
+
spec: agentRunSpec,
|
|
929
|
+
prompt: agentRunSpec.taskToPrompt(task),
|
|
930
|
+
sessionId,
|
|
931
|
+
bindSandbox: (sandboxId) => rebind(formatDetachedSessionRef({
|
|
932
|
+
sandboxId,
|
|
933
|
+
sessionId
|
|
934
|
+
})),
|
|
935
|
+
signal: ctx.signal,
|
|
936
|
+
report: ctx.report,
|
|
937
|
+
...loopEmitter ? { traceEmitter: loopEmitter } : {},
|
|
938
|
+
...executor.placement === "fleet" ? { placement: "fleet" } : {},
|
|
939
|
+
...options.detachedTickIntervalMs !== void 0 ? { tickIntervalMs: options.detachedTickIntervalMs } : {},
|
|
940
|
+
...options.detachedWallCapMs !== void 0 ? { wallCapMs: options.detachedWallCapMs } : {}
|
|
941
|
+
}), {
|
|
942
|
+
task,
|
|
943
|
+
sessionId,
|
|
944
|
+
signal: ctx.signal,
|
|
945
|
+
...options.harness ? { harness: options.harness } : {},
|
|
946
|
+
...options.model ? { model: options.model } : {},
|
|
947
|
+
...options.reviewer ? { reviewer: options.reviewer } : {}
|
|
948
|
+
});
|
|
949
|
+
ctx.report({
|
|
950
|
+
iteration: 1,
|
|
951
|
+
phase: "completed"
|
|
952
|
+
});
|
|
953
|
+
return chosen;
|
|
954
|
+
}
|
|
955
|
+
const chosen = await pickCoderWinner({
|
|
956
|
+
iterations: (await runAgentRounds({
|
|
957
|
+
driver: singleShotDriver,
|
|
958
|
+
agentRun: agentRunSpec,
|
|
959
|
+
output,
|
|
960
|
+
validator,
|
|
961
|
+
task,
|
|
962
|
+
ctx: {
|
|
963
|
+
sandboxClient,
|
|
964
|
+
signal: ctx.signal,
|
|
965
|
+
...loopEmitter ? { traceEmitter: loopEmitter } : {}
|
|
966
|
+
},
|
|
967
|
+
maxIterations: 1,
|
|
968
|
+
maxConcurrency
|
|
969
|
+
})).iterations,
|
|
970
|
+
reviewer: options.reviewer,
|
|
971
|
+
selection: options.winnerSelection ?? "highest-score",
|
|
972
|
+
task,
|
|
973
|
+
signal: ctx.signal
|
|
974
|
+
});
|
|
975
|
+
if (!chosen) throw new Error(noWinnerMessage(options.reviewer));
|
|
976
|
+
ctx.report({
|
|
977
|
+
iteration: 1,
|
|
978
|
+
phase: "completed"
|
|
979
|
+
});
|
|
980
|
+
return chosen;
|
|
981
|
+
}
|
|
982
|
+
const fanout = multiHarnessCoderFanout({
|
|
983
|
+
...options.workerProfile ? { profile: options.workerProfile } : {},
|
|
984
|
+
...fanoutHarnesses && fanoutHarnesses.length > 0 ? { harnesses: fanoutHarnesses.slice(0, variants) } : {},
|
|
985
|
+
...options.fanoutModels ? { models: options.fanoutModels.slice(0, variants) } : {}
|
|
986
|
+
});
|
|
987
|
+
const agentRuns = fanout.agentRuns.slice(0, variants);
|
|
988
|
+
const chosen = await pickCoderWinner({
|
|
989
|
+
iterations: (await runAgentRounds({
|
|
990
|
+
driver: fanout.driver,
|
|
991
|
+
agentRuns,
|
|
992
|
+
output: fanout.output,
|
|
993
|
+
validator: fanout.validator,
|
|
994
|
+
task,
|
|
995
|
+
ctx: {
|
|
996
|
+
sandboxClient,
|
|
997
|
+
signal: ctx.signal,
|
|
998
|
+
...loopEmitter ? { traceEmitter: loopEmitter } : {}
|
|
999
|
+
},
|
|
1000
|
+
maxIterations: variants,
|
|
1001
|
+
maxConcurrency: Math.min(maxConcurrency, variants)
|
|
1002
|
+
})).iterations,
|
|
1003
|
+
reviewer: options.reviewer,
|
|
1004
|
+
selection: options.winnerSelection ?? "highest-score",
|
|
1005
|
+
task,
|
|
1006
|
+
signal: ctx.signal
|
|
1007
|
+
});
|
|
1008
|
+
if (!chosen) throw new Error(noWinnerMessage(options.reviewer));
|
|
1009
|
+
ctx.report({
|
|
1010
|
+
iteration: agentRuns.length,
|
|
1011
|
+
phase: "completed"
|
|
1012
|
+
});
|
|
1013
|
+
return chosen;
|
|
1014
|
+
};
|
|
942
1015
|
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Pick the winning coder candidate from a finished loop's iterations:
|
|
1018
|
+
* 1. keep only mechanically-VALID candidates (the validator already gated
|
|
1019
|
+
* tests/typecheck/forbidden/diff/no-op/secrets),
|
|
1020
|
+
* 2. if a `reviewer` is wired, keep only those it APPROVES,
|
|
1021
|
+
* 3. select among survivors via the shared `selectValidWinner` (base strategies) or, for the
|
|
1022
|
+
* reviewer-only `highest-readiness`, a readiness sort (the one strategy the generic selector
|
|
1023
|
+
* does not express — a documented capability of this sandbox-session path).
|
|
1024
|
+
* Returns `undefined` when nothing survives — the delegate fails loud.
|
|
1025
|
+
*/
|
|
943
1026
|
async function pickCoderWinner(args) {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1027
|
+
const eligible = [];
|
|
1028
|
+
for (const iter of args.iterations) {
|
|
1029
|
+
if (iter.output === void 0 || iter.error || iter.verdict?.valid !== true) continue;
|
|
1030
|
+
const readiness = iter.verdict.score ?? 0;
|
|
1031
|
+
if (args.reviewer) {
|
|
1032
|
+
const review = await args.reviewer(iter.output, args.task, { signal: args.signal });
|
|
1033
|
+
if (!review.approved) continue;
|
|
1034
|
+
eligible.push({
|
|
1035
|
+
iter,
|
|
1036
|
+
readiness: review.readiness
|
|
1037
|
+
});
|
|
1038
|
+
} else eligible.push({
|
|
1039
|
+
iter,
|
|
1040
|
+
readiness
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
if (eligible.length === 0) return void 0;
|
|
1044
|
+
if (args.selection === "highest-readiness") return [...eligible].sort((a, b) => b.readiness - a.readiness || a.iter.index - b.iter.index)[0].iter.output;
|
|
1045
|
+
const wrapped = eligible.map(({ iter }) => ({
|
|
1046
|
+
...iter,
|
|
1047
|
+
output: {
|
|
1048
|
+
kind: "done",
|
|
1049
|
+
deliverable: iter.output
|
|
1050
|
+
}
|
|
1051
|
+
}));
|
|
1052
|
+
const out = selectValidWinner({
|
|
1053
|
+
strategy: baseStrategy(args.selection),
|
|
1054
|
+
sizeOf: (o) => o.diffStats.insertions + o.diffStats.deletions
|
|
1055
|
+
})(wrapped)?.output;
|
|
1056
|
+
if (out?.kind !== "done") return void 0;
|
|
1057
|
+
return out.deliverable;
|
|
974
1058
|
}
|
|
1059
|
+
/** Map the detached-session selection enum onto the shared `WinnerStrategy`. `first-approved`
|
|
1060
|
+
* reduces to `first-valid` over the already-approved set; `smallest-diff` to `smallest-artifact`. */
|
|
975
1061
|
function baseStrategy(selection) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
default:
|
|
982
|
-
return "highest-score";
|
|
983
|
-
}
|
|
1062
|
+
switch (selection) {
|
|
1063
|
+
case "smallest-diff": return "smallest-artifact";
|
|
1064
|
+
case "first-approved": return "first-valid";
|
|
1065
|
+
default: return "highest-score";
|
|
1066
|
+
}
|
|
984
1067
|
}
|
|
985
1068
|
function noWinnerMessage(reviewer) {
|
|
986
|
-
|
|
1069
|
+
return reviewer ? "coder delegate: no candidate passed validation + review" : "coder delegate: no candidate passed validation";
|
|
987
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Canonical `DelegateCodeArgs` → `CoderTask` mapping — the single source for
|
|
1073
|
+
* the delegate's live dispatch AND the resume driver's settle/message
|
|
1074
|
+
* rebuilding, so a resumed record reproduces exactly the task the original
|
|
1075
|
+
* process dispatched.
|
|
1076
|
+
*
|
|
1077
|
+
* @experimental
|
|
1078
|
+
*/
|
|
988
1079
|
function coderTaskFromArgs(args) {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1080
|
+
return {
|
|
1081
|
+
goal: buildCoderGoal(args),
|
|
1082
|
+
repoRoot: args.repoRoot,
|
|
1083
|
+
testCmd: args.config?.testCmd,
|
|
1084
|
+
typecheckCmd: args.config?.typecheckCmd,
|
|
1085
|
+
forbiddenPaths: args.config?.forbiddenPaths,
|
|
1086
|
+
maxDiffLines: args.config?.maxDiffLines
|
|
1087
|
+
};
|
|
997
1088
|
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Settle a completed detached coder turn through the same gate the streaming
|
|
1091
|
+
* path applies: parse the terminal payload with the coder output adapter,
|
|
1092
|
+
* run the mechanical validator (tests/typecheck/forbidden/diff/no-op/secrets),
|
|
1093
|
+
* then the optional reviewer. Throws when nothing survives — a resumed or
|
|
1094
|
+
* detached run must not return an unvalidated patch.
|
|
1095
|
+
*
|
|
1096
|
+
* SCOPE NOTE (detached/resume): the detached `driveTurn`-tick + cross-restart resume path is
|
|
1097
|
+
* bound to the `runAgentRounds` + sandbox-session substrate. The recursive `Scope`/worktree-CLI leaf has
|
|
1098
|
+
* journal→replay but no driveTurn-over-a-detached-sandbox-session equivalent yet, so resume is NOT
|
|
1099
|
+
* advertised on the generic `worktreeFanout` path. This helper (with `coderTaskFromArgs` and
|
|
1100
|
+
* `createDetachedTurnResumeDriver`) stays as the resume seam `bin.ts` wires for in-flight records.
|
|
1101
|
+
*
|
|
1102
|
+
* @experimental
|
|
1103
|
+
*/
|
|
998
1104
|
async function settleDetachedCoderTurn(turn, options) {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1105
|
+
const parsed = coderOutputAdapter.parse(detachedTurnEvents(options.sessionId, turn));
|
|
1106
|
+
if ((await createCoderValidator(options.task).validate(parsed, {
|
|
1107
|
+
iteration: 0,
|
|
1108
|
+
signal: options.signal
|
|
1109
|
+
})).valid !== true) throw new Error(noWinnerMessage(options.reviewer));
|
|
1110
|
+
if (options.reviewer) {
|
|
1111
|
+
if (!(await options.reviewer(parsed, options.task, { signal: options.signal })).approved) throw new Error(noWinnerMessage(options.reviewer));
|
|
1112
|
+
}
|
|
1113
|
+
return parsed;
|
|
1008
1114
|
}
|
|
1009
1115
|
function buildCoderGoal(args) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1116
|
+
if (!args.contextHint) return args.goal;
|
|
1117
|
+
return [
|
|
1118
|
+
args.goal,
|
|
1119
|
+
"",
|
|
1120
|
+
"## Context",
|
|
1121
|
+
args.contextHint
|
|
1122
|
+
].join("\n");
|
|
1012
1123
|
}
|
|
1013
1124
|
function resolveExecutor(options) {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (options.sandboxClient) {
|
|
1019
|
-
return createSiblingSandboxExecutor({ client: options.sandboxClient });
|
|
1020
|
-
}
|
|
1021
|
-
throw new Error("detachedSessionDelegate: `executor` or `sandboxClient` is required");
|
|
1125
|
+
if (options.executor && options.sandboxClient) throw new Error("detachedSessionDelegate: pass exactly one of `executor` or `sandboxClient`");
|
|
1126
|
+
if (options.executor) return options.executor;
|
|
1127
|
+
if (options.sandboxClient) return createSiblingSandboxExecutor({ client: options.sandboxClient });
|
|
1128
|
+
throw new Error("detachedSessionDelegate: `executor` or `sandboxClient` is required");
|
|
1022
1129
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1130
|
+
/**
|
|
1131
|
+
* Single-shot driver — plan one task on iteration 0, stop after one
|
|
1132
|
+
* iteration. Used by the coder delegate when `variants <= 1`. Keeps the
|
|
1133
|
+
* runAgentRounds kernel-level accounting (timing, cost, trace emission) while
|
|
1134
|
+
* skipping fanout/refine topology overhead.
|
|
1135
|
+
*/
|
|
1136
|
+
const singleShotDriver = {
|
|
1137
|
+
name: "mcp-single-shot",
|
|
1138
|
+
async plan(task, history) {
|
|
1139
|
+
return history.length === 0 ? [task] : [];
|
|
1140
|
+
},
|
|
1141
|
+
decide(history) {
|
|
1142
|
+
return history.length > 0 ? "pick-winner" : "fail";
|
|
1143
|
+
}
|
|
1031
1144
|
};
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1145
|
+
//#endregion
|
|
1146
|
+
//#region src/mcp/tools/checks.ts
|
|
1147
|
+
/**
|
|
1148
|
+
*
|
|
1149
|
+
* The trace-analyst KIND directory — the operator's lenses, as composable DATA.
|
|
1150
|
+
*
|
|
1151
|
+
* An analyst is not one question. A kind is ONE lens (completeness, correctness, policy, efficiency,
|
|
1152
|
+
* tool-use, …); each emits `AnalystFinding`s tagged by its `area`. The driver `list_analysts` to see
|
|
1153
|
+
* the menu, `run_analyst(kind, worker)` to apply a lens, and `define_analyst` to author a new one —
|
|
1154
|
+
* so at test time you compose the exact lenses a domain needs (maximum specificity), not one generic
|
|
1155
|
+
* reviewer. The kinds are data, the runner is generic, and the finding shape + firewall are reused
|
|
1156
|
+
* from agent-eval / the keystone — never re-derived.
|
|
1157
|
+
*
|
|
1158
|
+
* A kind here is a lightweight lens (`Check`); it is a deliberate SUBSET of agent-eval's full
|
|
1159
|
+
* `TraceAnalystKindSpec`, so a kind that needs the heavy agentic actor (sub-agent recursion, tools,
|
|
1160
|
+
* goldens) upgrades to `createTraceAnalystKind` without changing this directory's surface.
|
|
1161
|
+
*
|
|
1162
|
+
* @experimental
|
|
1163
|
+
*/
|
|
1164
|
+
const ANALYST_SEVERITIES = [
|
|
1165
|
+
"critical",
|
|
1166
|
+
"high",
|
|
1167
|
+
"medium",
|
|
1168
|
+
"low",
|
|
1169
|
+
"info"
|
|
1170
|
+
];
|
|
1171
|
+
const FINDING_SCHEMA_PROMPT = [
|
|
1172
|
+
"Each finding is a JSON object with these fields:",
|
|
1173
|
+
"- severity: one of \"critical\" | \"high\" | \"medium\" | \"low\" | \"info\"",
|
|
1174
|
+
"- claim: one-sentence statement",
|
|
1175
|
+
"- evidence_uri: REQUIRED, never blank — exactly one of \"span://<trace>/<span>\", \"artifact://<path>\",",
|
|
1176
|
+
" or \"metric://<name>\"; ALWAYS cite a real id from the trace. No citable id ⇒ omit the finding.",
|
|
1177
|
+
"- evidence_excerpt?: a short quote from the cited evidence",
|
|
1178
|
+
"- confidence: number 0..1",
|
|
1179
|
+
"- rationale?: one sentence of reasoning",
|
|
1180
|
+
"- recommended_action?: a concrete imperative (\"Add ...\", \"Replace ...\", \"Stop ...\")",
|
|
1181
|
+
"Emit an empty array when there is nothing to report. Never fabricate evidence."
|
|
1049
1182
|
].join("\n");
|
|
1183
|
+
/** Validate one raw finding row (the lightweight equivalent of agent-eval's `parseRawFinding`):
|
|
1184
|
+
* require a claim + a real trace evidence_uri; drop anything else. Returns null to discard. */
|
|
1050
1185
|
function validateRawFinding(row) {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
...typeof r.subject === "string" ? { subject: r.subject } : {}
|
|
1066
|
-
};
|
|
1186
|
+
if (!row || typeof row !== "object") return null;
|
|
1187
|
+
const r = row;
|
|
1188
|
+
if (typeof r.claim !== "string" || r.claim.length === 0) return null;
|
|
1189
|
+
if (typeof r.evidence_uri !== "string" || !/^(span|artifact|metric):\/\//.test(r.evidence_uri)) return null;
|
|
1190
|
+
return {
|
|
1191
|
+
severity: ANALYST_SEVERITIES.includes(r.severity) ? r.severity : "medium",
|
|
1192
|
+
claim: r.claim,
|
|
1193
|
+
evidence_uri: r.evidence_uri,
|
|
1194
|
+
confidence: typeof r.confidence === "number" ? r.confidence : .5,
|
|
1195
|
+
...typeof r.evidence_excerpt === "string" ? { evidence_excerpt: r.evidence_excerpt } : {},
|
|
1196
|
+
...typeof r.rationale === "string" ? { rationale: r.rationale } : {},
|
|
1197
|
+
...typeof r.recommended_action === "string" ? { recommended_action: r.recommended_action } : {},
|
|
1198
|
+
...typeof r.subject === "string" ? { subject: r.subject } : {}
|
|
1199
|
+
};
|
|
1067
1200
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1201
|
+
/** The built-in lens directory. Domain-blind (about any agent trace); compose at test time. */
|
|
1202
|
+
const defaultChecks = {
|
|
1203
|
+
completeness: {
|
|
1204
|
+
id: "completeness",
|
|
1205
|
+
description: "Required work the trace does not yet show done or verified.",
|
|
1206
|
+
area: "failure-mode",
|
|
1207
|
+
version: "1",
|
|
1208
|
+
lookFor: "every change the task requires that the trace does NOT yet show completed AND verified by a tool result. One finding per missing/unverified requirement."
|
|
1209
|
+
},
|
|
1210
|
+
correctness: {
|
|
1211
|
+
id: "correctness",
|
|
1212
|
+
description: "Tool calls that produced wrong, erroring, or contradicted results.",
|
|
1213
|
+
area: "correctness",
|
|
1214
|
+
version: "1",
|
|
1215
|
+
lookFor: "tool calls whose RESULT shows an error, a wrong value, or contradicts what the task required (e.g. set the wrong field, value did not take, an error was ignored)."
|
|
1216
|
+
},
|
|
1217
|
+
policy: {
|
|
1218
|
+
id: "policy",
|
|
1219
|
+
description: "Actions that violate a stated policy, constraint, or allow-list.",
|
|
1220
|
+
area: "safety",
|
|
1221
|
+
version: "1",
|
|
1222
|
+
lookFor: "actions in the trace that violate a policy/constraint stated in the task or system prompt (forbidden tool, missing approval, out-of-scope mutation, skipped precondition)."
|
|
1223
|
+
},
|
|
1224
|
+
efficiency: {
|
|
1225
|
+
id: "efficiency",
|
|
1226
|
+
description: "Wasted, redundant, or looping work.",
|
|
1227
|
+
area: "cost",
|
|
1228
|
+
version: "1",
|
|
1229
|
+
lookFor: "redundant or wasted actions — repeated identical calls, a stalled line retried the same way, work that produced no progress toward the goal."
|
|
1230
|
+
},
|
|
1231
|
+
"tool-use": {
|
|
1232
|
+
id: "tool-use",
|
|
1233
|
+
description: "Malformed or misused tool calls.",
|
|
1234
|
+
area: "tool-use",
|
|
1235
|
+
version: "1",
|
|
1236
|
+
lookFor: "tool calls with malformed/invalid arguments, the wrong tool for the intent, or a tool used against its contract — judged from the call + its result."
|
|
1237
|
+
}
|
|
1104
1238
|
};
|
|
1239
|
+
/** Lift validated raw rows into `AnalystFinding`s (agent-eval `makeFinding` stamps `finding_id`/
|
|
1240
|
+
* `produced_at`), then enforce the trace-derived firewall (selector ≠ judge). Pure — no LLM. */
|
|
1105
1241
|
function liftFindings(kind, rows, producedAt) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
assertTraceDerivedFindings(findings);
|
|
1127
|
-
return findings;
|
|
1242
|
+
const findings = [];
|
|
1243
|
+
for (const row of rows) {
|
|
1244
|
+
const raw = validateRawFinding(row);
|
|
1245
|
+
if (!raw) continue;
|
|
1246
|
+
findings.push(makeFinding({
|
|
1247
|
+
analyst_id: kind.id,
|
|
1248
|
+
area: kind.area,
|
|
1249
|
+
severity: raw.severity,
|
|
1250
|
+
claim: raw.claim,
|
|
1251
|
+
confidence: raw.confidence,
|
|
1252
|
+
produced_at: producedAt,
|
|
1253
|
+
evidence_refs: evidenceRefs(raw.evidence_uri, raw.evidence_excerpt),
|
|
1254
|
+
...raw.rationale ? { rationale: raw.rationale } : {},
|
|
1255
|
+
...raw.recommended_action ? { recommended_action: raw.recommended_action } : {},
|
|
1256
|
+
...raw.subject ? { subject: raw.subject } : {},
|
|
1257
|
+
metadata: { kind_version: kind.version }
|
|
1258
|
+
}));
|
|
1259
|
+
}
|
|
1260
|
+
assertTraceDerivedFindings(findings);
|
|
1261
|
+
return findings;
|
|
1128
1262
|
}
|
|
1263
|
+
/** Map a raw `evidence_uri` (span:// | artifact:// | metric://<name>) to a typed `EvidenceRef`. A
|
|
1264
|
+
* metric ref carries the bare NAME (the firewall checks the metric name for judge/verdict/score —
|
|
1265
|
+
* so a finding that cites a judge metric is rejected as not trace-derived). */
|
|
1129
1266
|
function evidenceRefs(uri, excerpt) {
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1267
|
+
const scheme = uri.split("://", 1)[0];
|
|
1268
|
+
if (scheme === "metric") return [{
|
|
1269
|
+
kind: "metric",
|
|
1270
|
+
uri: uri.replace(/^metric:\/\//, ""),
|
|
1271
|
+
...excerpt ? { excerpt } : {}
|
|
1272
|
+
}];
|
|
1273
|
+
return [{
|
|
1274
|
+
kind: scheme === "span" ? "span" : "artifact",
|
|
1275
|
+
uri,
|
|
1276
|
+
...excerpt ? { excerpt } : {}
|
|
1277
|
+
}];
|
|
1137
1278
|
}
|
|
1279
|
+
/** Render a worker's trace (tool calls + results) into the text an analyst lens reads. Generic over
|
|
1280
|
+
* the trace shape: a `{ messages }` conversation, a bare message array, else stringified. */
|
|
1138
1281
|
function renderTrace(trace) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1282
|
+
const messages = Array.isArray(trace) ? trace : trace && typeof trace === "object" && Array.isArray(trace.messages) ? trace.messages : void 0;
|
|
1283
|
+
if (!messages) return JSON.stringify(trace ?? {}).slice(0, 8e3);
|
|
1284
|
+
return messages.map((m) => {
|
|
1285
|
+
const r = m;
|
|
1286
|
+
if (r.role === "tool") return `RESULT ${String(r.content).slice(0, 300)}`;
|
|
1287
|
+
const calls = r.tool_calls?.map((c) => `${c.function?.name}(${c.function?.arguments})`).join(", ");
|
|
1288
|
+
return calls ? `CALL ${calls}` : `SAY ${String(r.content ?? "").slice(0, 200)}`;
|
|
1289
|
+
}).join("\n").slice(0, 8e3);
|
|
1147
1290
|
}
|
|
1291
|
+
/** Run ONE lens over a trace → findings. Generic over any kind: prompt = the lens + the agent-eval
|
|
1292
|
+
* finding schema; the model's JSON array is parsed (`parseRawFinding`), lifted, and firewalled. */
|
|
1148
1293
|
async function runCheck(kind, trace, opts, producedAt) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
let rows = [];
|
|
1162
|
-
try {
|
|
1163
|
-
const parsed = JSON.parse((match?.[1] ?? content).trim());
|
|
1164
|
-
rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.findings) ? parsed.findings : [];
|
|
1165
|
-
} catch {
|
|
1166
|
-
rows = [];
|
|
1167
|
-
}
|
|
1168
|
-
return liftFindings(kind, rows, producedAt);
|
|
1294
|
+
const sys = `You are a trace analyst applying ONE lens: look for ${kind.lookFor}\n\n${FINDING_SCHEMA_PROMPT}\n\nReturn ONLY a fenced \`\`\`json array of finding objects (possibly empty).`;
|
|
1295
|
+
const user = `WORKER TRACE:\n${renderTrace(trace)}\n\nApply your lens and emit the findings array.`;
|
|
1296
|
+
const content = await (opts.chat ?? defaultChat(opts))(sys, user);
|
|
1297
|
+
const match = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1298
|
+
let rows = [];
|
|
1299
|
+
try {
|
|
1300
|
+
const parsed = JSON.parse((match?.[1] ?? content).trim());
|
|
1301
|
+
rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.findings) ? parsed.findings : [];
|
|
1302
|
+
} catch {
|
|
1303
|
+
rows = [];
|
|
1304
|
+
}
|
|
1305
|
+
return liftFindings(kind, rows, producedAt);
|
|
1169
1306
|
}
|
|
1170
1307
|
function defaultChat(opts) {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1308
|
+
return async (system, user) => {
|
|
1309
|
+
const res = await fetch(`${opts.routerBaseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
1310
|
+
method: "POST",
|
|
1311
|
+
headers: {
|
|
1312
|
+
"content-type": "application/json",
|
|
1313
|
+
authorization: `Bearer ${opts.routerKey}`
|
|
1314
|
+
},
|
|
1315
|
+
body: JSON.stringify({
|
|
1316
|
+
model: opts.model,
|
|
1317
|
+
messages: [{
|
|
1318
|
+
role: "system",
|
|
1319
|
+
content: system
|
|
1320
|
+
}, {
|
|
1321
|
+
role: "user",
|
|
1322
|
+
content: user
|
|
1323
|
+
}],
|
|
1324
|
+
temperature: .3
|
|
1325
|
+
})
|
|
1326
|
+
});
|
|
1327
|
+
if (!res.ok) throw new Error(`analyst router ${res.status}`);
|
|
1328
|
+
return (await res.json()).choices?.[0]?.message?.content ?? "";
|
|
1329
|
+
};
|
|
1188
1330
|
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Build a `run_analyst` runner over a kind directory.
|
|
1333
|
+
* Returns findings, or a typed error for an unknown kind. `producedAt` is
|
|
1334
|
+
* passed in because replay-safe paths must not read `Date.now`.
|
|
1335
|
+
*/
|
|
1189
1336
|
function makeCheckRunner(kinds, opts) {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
};
|
|
1196
|
-
return runCheck(kind, trace, opts, producedAt);
|
|
1197
|
-
};
|
|
1337
|
+
return async (kindId, trace, producedAt) => {
|
|
1338
|
+
const kind = kinds[kindId];
|
|
1339
|
+
if (!kind) return { error: `unknown analyst kind ${JSON.stringify(kindId)} (have: ${Object.keys(kinds).join(", ")})` };
|
|
1340
|
+
return runCheck(kind, trace, opts, producedAt);
|
|
1341
|
+
};
|
|
1198
1342
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
DELEGATE_FEEDBACK_DESCRIPTION,
|
|
1203
|
-
DELEGATE_FEEDBACK_INPUT_SCHEMA,
|
|
1204
|
-
DELEGATE_FEEDBACK_TOOL_NAME,
|
|
1205
|
-
DELEGATE_INPUT_SCHEMA,
|
|
1206
|
-
DELEGATE_TOOL_NAME,
|
|
1207
|
-
DELEGATE_UI_AUDIT_DESCRIPTION,
|
|
1208
|
-
DELEGATE_UI_AUDIT_INPUT_SCHEMA,
|
|
1209
|
-
DELEGATE_UI_AUDIT_TOOL_NAME,
|
|
1210
|
-
DELEGATION_HISTORY_DESCRIPTION,
|
|
1211
|
-
DELEGATION_HISTORY_INPUT_SCHEMA,
|
|
1212
|
-
DELEGATION_HISTORY_TOOL_NAME,
|
|
1213
|
-
DELEGATION_STATUS_DESCRIPTION,
|
|
1214
|
-
DELEGATION_STATUS_INPUT_SCHEMA,
|
|
1215
|
-
DELEGATION_STATUS_TOOL_NAME,
|
|
1216
|
-
DELEGATION_TRACE_MAX_BYTES,
|
|
1217
|
-
DELEGATION_TRACE_MAX_SPANS,
|
|
1218
|
-
DelegationPersistenceError,
|
|
1219
|
-
DelegationStateCorruptError,
|
|
1220
|
-
DelegationTaskQueue,
|
|
1221
|
-
FileDelegationStore,
|
|
1222
|
-
InMemoryDelegationStore,
|
|
1223
|
-
InMemoryFeedbackStore,
|
|
1224
|
-
MEMORY_FILE_ENV,
|
|
1225
|
-
MEMORY_ITEMS_ENV,
|
|
1226
|
-
MEMORY_LOG_ENV,
|
|
1227
|
-
MEMORY_NAME_ENV,
|
|
1228
|
-
buildDelegationTraceSpans,
|
|
1229
|
-
capDelegationTrace,
|
|
1230
|
-
captureWorktreeDiff,
|
|
1231
|
-
coderTaskFromArgs,
|
|
1232
|
-
composeLoopTraceEmitters,
|
|
1233
|
-
createCoordinationTools,
|
|
1234
|
-
createDelegateFeedbackHandler,
|
|
1235
|
-
createDelegateHandler,
|
|
1236
|
-
createDelegateUiAuditHandler,
|
|
1237
|
-
createDelegationHistoryHandler,
|
|
1238
|
-
createDelegationStatusHandler,
|
|
1239
|
-
createDelegationTraceCollector,
|
|
1240
|
-
createDetachedTurnResumeDriver,
|
|
1241
|
-
createFleetWorkspaceExecutor,
|
|
1242
|
-
createInProcessExecutor,
|
|
1243
|
-
createInProcessTransport,
|
|
1244
|
-
createKbGate,
|
|
1245
|
-
createMcpServer,
|
|
1246
|
-
createMemoryToolServer,
|
|
1247
|
-
createPropagatingTraceEmitter,
|
|
1248
|
-
createSiblingSandboxExecutor,
|
|
1249
|
-
createStdioToolServer,
|
|
1250
|
-
createWorktree,
|
|
1251
|
-
defaultChecks,
|
|
1252
|
-
detachedSessionDelegate,
|
|
1253
|
-
detachedTurnEvents,
|
|
1254
|
-
detectExecutor,
|
|
1255
|
-
eventToSnapshot,
|
|
1256
|
-
formatDetachedSessionRef,
|
|
1257
|
-
hashIdempotencyInput,
|
|
1258
|
-
liftFindings,
|
|
1259
|
-
makeCheckRunner,
|
|
1260
|
-
mcpToolsForRuntimeMcp,
|
|
1261
|
-
mcpToolsForRuntimeMcpSubset,
|
|
1262
|
-
parseCodexTokenUsage,
|
|
1263
|
-
parseDetachedSessionRef,
|
|
1264
|
-
parseMemoryItems,
|
|
1265
|
-
readMemoryItemsFile,
|
|
1266
|
-
readTraceContextFromEnv,
|
|
1267
|
-
removeWorktree,
|
|
1268
|
-
renderTrace,
|
|
1269
|
-
resolveMemoryFromEnv,
|
|
1270
|
-
runCheck,
|
|
1271
|
-
runDetachedTurn,
|
|
1272
|
-
runLocalHarness,
|
|
1273
|
-
settleDetachedCoderTurn,
|
|
1274
|
-
traceContextToEnv,
|
|
1275
|
-
validateDelegateArgs,
|
|
1276
|
-
validateDelegateFeedbackArgs,
|
|
1277
|
-
validateDelegateUiAuditArgs,
|
|
1278
|
-
validateDelegationHistoryArgs,
|
|
1279
|
-
validateDelegationStatusArgs
|
|
1280
|
-
};
|
|
1343
|
+
//#endregion
|
|
1344
|
+
export { CodexExecutionDiagnosticError, DEFAULT_AWAIT_EVENT_TIMEOUT_MS, DELEGATE_DESCRIPTION, DELEGATE_FEEDBACK_DESCRIPTION, DELEGATE_FEEDBACK_INPUT_SCHEMA, DELEGATE_FEEDBACK_TOOL_NAME, DELEGATE_INPUT_SCHEMA, DELEGATE_TOOL_NAME, DELEGATE_UI_AUDIT_DESCRIPTION, DELEGATE_UI_AUDIT_INPUT_SCHEMA, DELEGATE_UI_AUDIT_TOOL_NAME, DELEGATION_HISTORY_DESCRIPTION, DELEGATION_HISTORY_INPUT_SCHEMA, DELEGATION_HISTORY_TOOL_NAME, DELEGATION_STATUS_DESCRIPTION, DELEGATION_STATUS_INPUT_SCHEMA, DELEGATION_STATUS_TOOL_NAME, DELEGATION_TRACE_MAX_BYTES, DELEGATION_TRACE_MAX_SPANS, DelegationPersistenceError, DelegationStateCorruptError, DelegationTaskQueue, FileDelegationStore, InMemoryDelegationStore, InMemoryFeedbackStore, MEMORY_FILE_ENV, MEMORY_ITEMS_ENV, MEMORY_LOG_ENV, MEMORY_NAME_ENV, buildDelegationTraceSpans, capDelegationTrace, captureWorktreeDiff, coderTaskFromArgs, composeLoopTraceEmitters, createCoordinationTools, createDelegateFeedbackHandler, createDelegateHandler, createDelegateUiAuditHandler, createDelegationHistoryHandler, createDelegationStatusHandler, createDelegationTraceCollector, createDetachedTurnResumeDriver, createFleetWorkspaceExecutor, createInProcessExecutor, createInProcessTransport, createKbGate, createMcpServer, createMemoryToolServer, createPropagatingTraceEmitter, createSiblingSandboxExecutor, createStdioToolServer, createWorktree, defaultChecks, detachedSessionDelegate, detachedTurnEvents, detectExecutor, eventToSnapshot, formatDetachedSessionRef, hashIdempotencyInput, liftFindings, makeCheckRunner, mcpToolsForRuntimeMcp, mcpToolsForRuntimeMcpSubset, parseCodexTokenUsage, parseDetachedSessionRef, parseMemoryItems, readMemoryItemsFile, readTraceContextFromEnv, removeWorktree, renderTrace, resolveMemoryFromEnv, runCheck, runDetachedTurn, runLocalHarness, settleDetachedCoderTurn, traceContextToEnv, validateDelegateArgs, validateDelegateFeedbackArgs, validateDelegateUiAuditArgs, validateDelegationHistoryArgs, validateDelegationStatusArgs };
|
|
1345
|
+
|
|
1281
1346
|
//# sourceMappingURL=index.js.map
|