@tangle-network/agent-runtime 0.53.0 → 0.54.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.
Files changed (71) hide show
  1. package/README.md +27 -16
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +5 -3
  4. package/dist/agent.js.map +1 -1
  5. package/dist/analyst-loop.d.ts +1 -1
  6. package/dist/analyst-loop.js +2 -2
  7. package/dist/{chunk-K5M3SHEU.js → chunk-2BDXWZUC.js} +5 -34
  8. package/dist/chunk-2BDXWZUC.js.map +1 -0
  9. package/dist/chunk-A73RADPB.js +281 -0
  10. package/dist/chunk-A73RADPB.js.map +1 -0
  11. package/dist/chunk-FRBWCJLP.js +396 -0
  12. package/dist/chunk-FRBWCJLP.js.map +1 -0
  13. package/dist/chunk-GRAGM4MC.js +72 -0
  14. package/dist/chunk-GRAGM4MC.js.map +1 -0
  15. package/dist/{chunk-K6WP7PYW.js → chunk-JTH2FPCK.js} +3231 -424
  16. package/dist/chunk-JTH2FPCK.js.map +1 -0
  17. package/dist/{chunk-P4QNEXFC.js → chunk-KJH62YEK.js} +2 -2
  18. package/dist/{chunk-HNUXAZIJ.js → chunk-P5OKDSLB.js} +2 -2
  19. package/dist/{chunk-GSUO5QS6.js → chunk-VLF5RHEQ.js} +2 -5
  20. package/dist/{chunk-GSUO5QS6.js.map → chunk-VLF5RHEQ.js.map} +1 -1
  21. package/dist/{chunk-JFIYKDXF.js → chunk-XRYEZPR6.js} +2 -1
  22. package/dist/chunk-XRYEZPR6.js.map +1 -0
  23. package/dist/{coder-LKm3Mczw.d.ts → coder-CEkUFv8h.d.ts} +1 -1
  24. package/dist/{types-CNDJCL_0.d.ts → coordination-BMzskrUR.d.ts} +268 -2
  25. package/dist/{kb-gate-CKfykcYQ.d.ts → delegates-BPLIl8EC.d.ts} +491 -566
  26. package/dist/index.d.ts +7 -7
  27. package/dist/index.js +23 -80
  28. package/dist/index.js.map +1 -1
  29. package/dist/intelligence.d.ts +2 -2
  30. package/dist/kb-gate-CuzMYGYM.d.ts +76 -0
  31. package/dist/{loop-runner-bin-D4Ir7b00.d.ts → loop-runner-bin-BGpVVyXp.d.ts} +6 -27
  32. package/dist/loop-runner-bin.d.ts +5 -5
  33. package/dist/loop-runner-bin.js +6 -5
  34. package/dist/loops.d.ts +8 -6
  35. package/dist/loops.js +32 -8
  36. package/dist/mcp/bin.js +94 -30
  37. package/dist/mcp/bin.js.map +1 -1
  38. package/dist/mcp/index.d.ts +9 -272
  39. package/dist/mcp/index.js +45 -365
  40. package/dist/mcp/index.js.map +1 -1
  41. package/dist/{openai-tools-CKfR3EMh.d.ts → openai-tools-CoeLQ7Uo.d.ts} +1 -1
  42. package/dist/profiles.d.ts +2 -2
  43. package/dist/profiles.js +4 -4
  44. package/dist/{router-client-B0Qi1NiN.d.ts → router-client-30Y_pca8.d.ts} +1 -1
  45. package/dist/{run-loop-DgVhucoR.d.ts → run-loop-DluzfJ2h.d.ts} +1 -1
  46. package/dist/runtime.d.ts +424 -23
  47. package/dist/runtime.js +32 -8
  48. package/dist/topology.d.ts +54 -1
  49. package/dist/topology.js +222 -0
  50. package/dist/topology.js.map +1 -1
  51. package/dist/{types-CklkW4Eh.d.ts → types-C8rNlxfV.d.ts} +4 -4
  52. package/dist/workflow.d.ts +2 -2
  53. package/dist/workflow.js +4 -2
  54. package/dist/workflow.js.map +1 -1
  55. package/package.json +5 -5
  56. package/skills/agent-runtime-adoption/SKILL.md +54 -46
  57. package/skills/build-with-agent-runtime/SKILL.md +12 -8
  58. package/skills/loop-writer/SKILL.md +9 -1
  59. package/skills/supervise/SKILL.md +24 -0
  60. package/dist/chunk-5M2WDWBI.js +0 -908
  61. package/dist/chunk-5M2WDWBI.js.map +0 -1
  62. package/dist/chunk-AYRQZRDV.js +0 -623
  63. package/dist/chunk-AYRQZRDV.js.map +0 -1
  64. package/dist/chunk-FO4DCM7R.js +0 -1356
  65. package/dist/chunk-FO4DCM7R.js.map +0 -1
  66. package/dist/chunk-JFIYKDXF.js.map +0 -1
  67. package/dist/chunk-K5M3SHEU.js.map +0 -1
  68. package/dist/chunk-K6WP7PYW.js.map +0 -1
  69. package/dist/driver-B2RKkVJW.d.ts +0 -221
  70. /package/dist/{chunk-P4QNEXFC.js.map → chunk-KJH62YEK.js.map} +0 -0
  71. /package/dist/{chunk-HNUXAZIJ.js.map → chunk-P5OKDSLB.js.map} +0 -0
@@ -0,0 +1,396 @@
1
+ import {
2
+ createFleetWorkspaceExecutor,
3
+ createSiblingSandboxExecutor
4
+ } from "./chunk-A73RADPB.js";
5
+ import {
6
+ runLocalHarness
7
+ } from "./chunk-GLR25NG7.js";
8
+ import {
9
+ buildLoopOtelSpans,
10
+ createOtelExporter
11
+ } from "./chunk-G3RGMA7C.js";
12
+
13
+ // src/mcp/worktree.ts
14
+ import { spawn } from "child_process";
15
+ async function runGitAsync(args, cwd, runner) {
16
+ if (runner) return runner(args, { cwd });
17
+ return new Promise((resolve, reject) => {
18
+ const proc = spawn("git", args, { cwd, stdio: "pipe" });
19
+ let stdout = "";
20
+ let stderr = "";
21
+ proc.stdout?.on("data", (c) => {
22
+ stdout += String(c);
23
+ });
24
+ proc.stderr?.on("data", (c) => {
25
+ stderr += String(c);
26
+ });
27
+ proc.on("error", reject);
28
+ proc.on("close", (code) => resolve({ stdout, stderr, exitCode: code ?? -1 }));
29
+ });
30
+ }
31
+ function ensureGitOk(step, result) {
32
+ if (result.exitCode !== 0) {
33
+ throw new Error(
34
+ `worktree: git ${step} failed (exit ${result.exitCode}): ${result.stderr.slice(0, 400)}`
35
+ );
36
+ }
37
+ }
38
+ async function createWorktree(options) {
39
+ const variants = options.variantsDir ?? ".coder-variants";
40
+ const baseRef = options.baseRef ?? "HEAD";
41
+ const branch = `delegate/${options.runId}`;
42
+ const path = `${options.repoRoot.replace(/\/+$/, "")}/${variants}/${options.runId}`;
43
+ const headSha = await runGitAsync(["rev-parse", baseRef], options.repoRoot, options.runGit);
44
+ ensureGitOk(`rev-parse ${baseRef}`, headSha);
45
+ const add = await runGitAsync(
46
+ ["worktree", "add", "-b", branch, path, baseRef],
47
+ options.repoRoot,
48
+ options.runGit
49
+ );
50
+ ensureGitOk(`worktree add ${path}`, add);
51
+ return { path, baseSha: headSha.stdout.trim(), branch };
52
+ }
53
+ async function captureWorktreeDiff(options) {
54
+ const baseRef = options.baseRef ?? options.worktree.baseSha;
55
+ const patch = await runGitAsync(["diff", baseRef], options.worktree.path, options.runGit);
56
+ const shortstat = await runGitAsync(
57
+ ["diff", "--shortstat", baseRef],
58
+ options.worktree.path,
59
+ options.runGit
60
+ );
61
+ const stats = parseShortstat(shortstat.stdout);
62
+ return { patch: patch.stdout, stats };
63
+ }
64
+ function parseShortstat(text) {
65
+ const out = { filesChanged: 0, insertions: 0, deletions: 0 };
66
+ const filesMatch = text.match(/(\d+)\s+files?\s+changed/);
67
+ if (filesMatch?.[1]) out.filesChanged = Number(filesMatch[1]);
68
+ const insertMatch = text.match(/(\d+)\s+insertions?/);
69
+ if (insertMatch?.[1]) out.insertions = Number(insertMatch[1]);
70
+ const deleteMatch = text.match(/(\d+)\s+deletions?/);
71
+ if (deleteMatch?.[1]) out.deletions = Number(deleteMatch[1]);
72
+ return out;
73
+ }
74
+ async function removeWorktree(options) {
75
+ const force = options.force ?? true;
76
+ const args = ["worktree", "remove"];
77
+ if (force) args.push("--force");
78
+ args.push(options.worktree.path);
79
+ const result = await runGitAsync(args, options.repoRoot, options.runGit);
80
+ if (result.exitCode !== 0 && !/not a working tree/.test(result.stderr)) {
81
+ await runGitAsync(
82
+ ["branch", "-D", options.worktree.branch],
83
+ options.repoRoot,
84
+ options.runGit
85
+ ).catch(() => void 0);
86
+ }
87
+ await runGitAsync(
88
+ ["branch", "-D", options.worktree.branch],
89
+ options.repoRoot,
90
+ options.runGit
91
+ ).catch(() => void 0);
92
+ }
93
+
94
+ // src/mcp/in-process-executor.ts
95
+ import { randomUUID } from "crypto";
96
+ var DEFAULT_HARNESS_TIMEOUT_MS = 5 * 60 * 1e3;
97
+ var DEFAULT_POSTCHECK_TIMEOUT_MS = 2 * 60 * 1e3;
98
+ function createInProcessExecutor(options) {
99
+ const harnesses = options.harnesses && options.harnesses.length > 0 ? [...options.harnesses] : ["claude"];
100
+ const runHarness = options.runHarness ?? runLocalHarness;
101
+ const runPostCheck = options.runPostCheck ?? defaultRunPostCheck;
102
+ let callIndex = 0;
103
+ const client = {
104
+ async create(_opts) {
105
+ const runId = randomUUID();
106
+ const harness = harnesses[callIndex % harnesses.length];
107
+ callIndex += 1;
108
+ const virtual = {
109
+ // Synthesize the minimum SandboxInstance surface the kernel touches.
110
+ // We CAST through unknown because SandboxInstance is a `declare class`
111
+ // with private fields; we're producing a structural subtype that
112
+ // satisfies the kernel's narrow usage (`box.id`, `box.streamPrompt`).
113
+ id: `in-process-${runId}`,
114
+ __inProcess: { runId, harness },
115
+ // eslint-disable-next-line require-yield
116
+ async *streamPrompt(message, promptOpts) {
117
+ const taskPrompt = typeof message === "string" ? message : message.map(
118
+ (p) => typeof p === "object" && p && "text" in p ? String(p.text) : ""
119
+ ).join("\n");
120
+ let worktree;
121
+ try {
122
+ worktree = await createWorktree({
123
+ repoRoot: options.repoRoot,
124
+ runId,
125
+ runGit: options.runGit
126
+ });
127
+ this.__inProcess.worktree = worktree;
128
+ yield {
129
+ type: "in_process.harness.started",
130
+ data: {
131
+ runId,
132
+ harness,
133
+ worktreePath: worktree.path,
134
+ command: harness
135
+ }
136
+ };
137
+ const harnessResult = await runHarness({
138
+ harness,
139
+ cwd: worktree.path,
140
+ taskPrompt,
141
+ timeoutMs: options.harnessTimeoutMs ?? DEFAULT_HARNESS_TIMEOUT_MS,
142
+ signal: promptOpts?.signal
143
+ });
144
+ yield {
145
+ type: "in_process.harness.ended",
146
+ data: {
147
+ runId,
148
+ exitCode: harnessResult.exitCode,
149
+ durationMs: harnessResult.durationMs,
150
+ killedBySignal: harnessResult.killedBySignal,
151
+ timedOut: harnessResult.timedOut,
152
+ stdoutBytes: harnessResult.stdout.length,
153
+ stderrBytes: harnessResult.stderr.length
154
+ }
155
+ };
156
+ const diff = await captureWorktreeDiff({ worktree, runGit: options.runGit });
157
+ const testCheck = options.testCmd ? await runPostCheck(options.testCmd, worktree.path, promptOpts?.signal).catch(
158
+ (err) => ({
159
+ exitCode: -1,
160
+ stdout: "",
161
+ stderr: err instanceof Error ? err.message : String(err)
162
+ })
163
+ ) : { exitCode: 0, stdout: "", stderr: "" };
164
+ const typecheckCheck = options.typecheckCmd ? await runPostCheck(options.typecheckCmd, worktree.path, promptOpts?.signal).catch(
165
+ (err) => ({
166
+ exitCode: -1,
167
+ stdout: "",
168
+ stderr: err instanceof Error ? err.message : String(err)
169
+ })
170
+ ) : { exitCode: 0, stdout: "", stderr: "" };
171
+ const coderOutput = {
172
+ branch: worktree.branch,
173
+ patch: diff.patch,
174
+ testResult: {
175
+ passed: !options.testCmd || testCheck.exitCode === 0,
176
+ output: tail(testCheck.stderr || testCheck.stdout, 4e3)
177
+ },
178
+ typecheckResult: {
179
+ passed: !options.typecheckCmd || typecheckCheck.exitCode === 0,
180
+ output: tail(typecheckCheck.stderr || typecheckCheck.stdout, 4e3)
181
+ },
182
+ diffStats: diff.stats,
183
+ reviewerNotes: harnessResult.exitCode === 0 ? void 0 : `harness ${harness} exited ${harnessResult.exitCode}${harnessResult.timedOut ? " (timed out)" : ""}`
184
+ };
185
+ yield {
186
+ type: "result",
187
+ data: {
188
+ result: coderOutput,
189
+ source: "in-process-executor",
190
+ harness,
191
+ runId
192
+ }
193
+ };
194
+ } finally {
195
+ if (worktree) {
196
+ await removeWorktree({
197
+ worktree,
198
+ repoRoot: options.repoRoot,
199
+ runGit: options.runGit
200
+ }).catch(() => void 0);
201
+ }
202
+ }
203
+ }
204
+ };
205
+ return virtual;
206
+ },
207
+ describePlacement(box) {
208
+ const sandboxId = box.id;
209
+ const meta = box.__inProcess;
210
+ return {
211
+ kind: "sibling",
212
+ sandboxId,
213
+ worktreePath: meta?.worktree?.path,
214
+ harness: meta?.harness
215
+ };
216
+ }
217
+ };
218
+ return {
219
+ client,
220
+ placement: "in-process",
221
+ describe() {
222
+ return `in-process (repoRoot=${options.repoRoot}, harnesses=[${harnesses.join(",")}]${options.testCmd ? `, testCmd="${options.testCmd}"` : ""}${options.typecheckCmd ? `, typecheckCmd="${options.typecheckCmd}"` : ""})`;
223
+ }
224
+ };
225
+ }
226
+ async function defaultRunPostCheck(cmd, cwd, signal) {
227
+ const { spawn: spawn2 } = await import("child_process");
228
+ return new Promise((resolve, reject) => {
229
+ const child = spawn2("sh", ["-c", cmd], { cwd, stdio: "pipe" });
230
+ let stdout = "";
231
+ let stderr = "";
232
+ child.stdout?.on("data", (c) => {
233
+ stdout += String(c);
234
+ });
235
+ child.stderr?.on("data", (c) => {
236
+ stderr += String(c);
237
+ });
238
+ if (signal) {
239
+ const onAbort = () => {
240
+ if (!child.killed) child.kill("SIGTERM");
241
+ };
242
+ if (signal.aborted) onAbort();
243
+ else signal.addEventListener("abort", onAbort, { once: true });
244
+ }
245
+ const killTimer = setTimeout(() => {
246
+ if (!child.killed) child.kill("SIGTERM");
247
+ }, DEFAULT_POSTCHECK_TIMEOUT_MS);
248
+ if (typeof killTimer.unref === "function") {
249
+ ;
250
+ killTimer.unref();
251
+ }
252
+ child.on("error", (err) => {
253
+ clearTimeout(killTimer);
254
+ reject(err);
255
+ });
256
+ child.on("close", (code) => {
257
+ clearTimeout(killTimer);
258
+ resolve({ exitCode: code ?? -1, stdout, stderr });
259
+ });
260
+ });
261
+ }
262
+ function tail(text, max) {
263
+ if (text.length <= max) return text;
264
+ return text.slice(text.length - max);
265
+ }
266
+
267
+ // src/mcp/bin-helpers.ts
268
+ async function detectExecutor(args) {
269
+ const env = args.env ?? process.env;
270
+ if (env.AGENT_RUNTIME_IN_SANDBOX === "1") {
271
+ const repoRoot = env.AGENT_RUNTIME_REPO_ROOT?.trim();
272
+ if (!repoRoot) {
273
+ throw new Error(
274
+ "agent-runtime-mcp: AGENT_RUNTIME_IN_SANDBOX=1 requires AGENT_RUNTIME_REPO_ROOT to point at the workspace root"
275
+ );
276
+ }
277
+ return createInProcessExecutor({
278
+ repoRoot,
279
+ harnesses: parseHarnesses(env.AGENT_RUNTIME_LOCAL_HARNESSES),
280
+ testCmd: env.AGENT_RUNTIME_TEST_CMD?.trim() || void 0,
281
+ typecheckCmd: env.AGENT_RUNTIME_TYPECHECK_CMD?.trim() || void 0
282
+ });
283
+ }
284
+ const fleetId = parseFleetId(env.TANGLE_FLEET_ID);
285
+ if (!fleetId) {
286
+ return createSiblingSandboxExecutor({ client: args.sandboxClient });
287
+ }
288
+ const resolveFleet = args.resolveFleet ?? defaultResolveFleet;
289
+ const fleet = await resolveFleet(args.sandboxClient, fleetId);
290
+ const excludeMachineIds = parseList(env.TANGLE_FLEET_EXCLUDE_MACHINES);
291
+ return createFleetWorkspaceExecutor({
292
+ fleet,
293
+ excludeMachineIds
294
+ });
295
+ }
296
+ var KNOWN_HARNESSES = ["claude", "codex", "opencode"];
297
+ function parseHarnesses(raw) {
298
+ if (!raw) return void 0;
299
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
300
+ if (parts.length === 0) return void 0;
301
+ for (const part of parts) {
302
+ if (!KNOWN_HARNESSES.includes(part)) {
303
+ throw new Error(
304
+ `agent-runtime-mcp: AGENT_RUNTIME_LOCAL_HARNESSES contains unknown harness "${part}". Expected: ${KNOWN_HARNESSES.join(", ")}.`
305
+ );
306
+ }
307
+ }
308
+ return parts;
309
+ }
310
+ async function defaultResolveFleet(sandboxClient, fleetId) {
311
+ const fleets = sandboxClient.fleets;
312
+ if (!fleets || typeof fleets.get !== "function") {
313
+ throw new Error(
314
+ "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."
315
+ );
316
+ }
317
+ const raw = await fleets.get(fleetId);
318
+ if (!raw || typeof raw !== "object") {
319
+ throw new Error(`agent-runtime-mcp: fleets.get(${fleetId}) returned no handle`);
320
+ }
321
+ const handle = raw;
322
+ if (typeof handle.fleetId !== "string" || !Array.isArray(handle.ids)) {
323
+ throw new Error(
324
+ `agent-runtime-mcp: fleet handle for ${fleetId} is missing fleetId/ids \u2014 incompatible sandbox SDK shape`
325
+ );
326
+ }
327
+ if (typeof handle.sandbox !== "function") {
328
+ throw new Error(
329
+ `agent-runtime-mcp: fleet handle for ${fleetId} is missing sandbox(machineId) \u2014 incompatible sandbox SDK shape`
330
+ );
331
+ }
332
+ return handle;
333
+ }
334
+ function parseFleetId(raw) {
335
+ if (typeof raw !== "string") return void 0;
336
+ const trimmed = raw.trim();
337
+ return trimmed.length > 0 ? trimmed : void 0;
338
+ }
339
+ function parseList(raw) {
340
+ if (!raw) return void 0;
341
+ const list = raw.split(",").map((entry) => entry.trim()).filter(Boolean);
342
+ return list.length > 0 ? list : void 0;
343
+ }
344
+
345
+ // src/mcp/trace-propagation.ts
346
+ function readTraceContextFromEnv() {
347
+ const traceId = process.env.TRACE_ID || generateTraceId();
348
+ const parentSpanId = process.env.PARENT_SPAN_ID || void 0;
349
+ return { traceId, parentSpanId };
350
+ }
351
+ function createPropagatingTraceEmitter(ctx) {
352
+ const exporter = createOtelExporter();
353
+ const buffers = /* @__PURE__ */ new Map();
354
+ const emitter = {
355
+ emit(event) {
356
+ if (!exporter) return;
357
+ const buf = buffers.get(event.runId);
358
+ if (buf) buf.push(event);
359
+ else buffers.set(event.runId, [event]);
360
+ if (event.kind === "loop.ended") {
361
+ const events = buffers.get(event.runId) ?? [event];
362
+ buffers.delete(event.runId);
363
+ for (const span of buildLoopOtelSpans(events, ctx.traceId, ctx.parentSpanId)) {
364
+ exporter.exportSpan(span);
365
+ }
366
+ }
367
+ }
368
+ };
369
+ return { emitter, exporter, context: ctx };
370
+ }
371
+ function traceContextToEnv(ctx) {
372
+ const env = { TRACE_ID: ctx.traceId };
373
+ if (ctx.parentSpanId) env.PARENT_SPAN_ID = ctx.parentSpanId;
374
+ return env;
375
+ }
376
+ function generateTraceId() {
377
+ const bytes = new Uint8Array(16);
378
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
379
+ globalThis.crypto.getRandomValues(bytes);
380
+ } else {
381
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
382
+ }
383
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
384
+ }
385
+
386
+ export {
387
+ createWorktree,
388
+ captureWorktreeDiff,
389
+ removeWorktree,
390
+ createInProcessExecutor,
391
+ detectExecutor,
392
+ readTraceContextFromEnv,
393
+ createPropagatingTraceEmitter,
394
+ traceContextToEnv
395
+ };
396
+ //# sourceMappingURL=chunk-FRBWCJLP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp/worktree.ts","../src/mcp/in-process-executor.ts","../src/mcp/bin-helpers.ts","../src/mcp/trace-propagation.ts"],"sourcesContent":["/**\n * @experimental\n *\n * Git worktree helpers for the in-process delegation executor. Each\n * delegation runs in its own worktree so multiple parallel harness\n * subprocesses (claude / codex / opencode in a 3-way fanout) don't clobber\n * each other's edits on the shared workspace.\n *\n * Worktrees live under `<repoRoot>/.coder-variants/<runId>/`. After the\n * harness exits + the diff is captured, the worktree is removed.\n *\n * All operations spawn `git` via `child_process.spawn` synchronously\n * (via a `runGit` helper). Stays narrow on purpose: no working-tree\n * staging, no commits, no rebases.\n */\n\nimport { spawn } from 'node:child_process'\n\n/** @experimental */\nexport interface WorktreeHandle {\n /** Absolute path to the worktree directory. */\n path: string\n /** SHA the worktree was created at. */\n baseSha: string\n /** Branch name created for this worktree (typically `delegate/<runId>`). */\n branch: string\n}\n\n/** @experimental */\nexport interface CreateWorktreeOptions {\n /** Absolute path to the main git checkout. */\n repoRoot: string\n /** Unique id for the worktree path + branch. Use the delegation run id. */\n runId: string\n /** Parent directory the worktree lives under. Defaults to `.coder-variants`. */\n variantsDir?: string\n /** Override the base ref (default `HEAD`). */\n baseRef?: string\n /** Test seam — inject a custom git runner. */\n runGit?: GitRunner\n}\n\n/** @experimental */\nexport interface DiffOptions {\n /** Worktree to diff. */\n worktree: WorktreeHandle\n /** What to compare against. Default `worktree.baseSha`. */\n baseRef?: string\n /** Test seam. */\n runGit?: GitRunner\n}\n\n/** @experimental */\nexport interface DiffResult {\n patch: string\n stats: {\n filesChanged: number\n insertions: number\n deletions: number\n }\n}\n\n/** @experimental */\nexport interface RemoveWorktreeOptions {\n worktree: WorktreeHandle\n repoRoot: string\n /** Force removal even if dirty (default true; the loser of a fanout has uncommitted changes). */\n force?: boolean\n /** Test seam. */\n runGit?: GitRunner\n}\n\n/** Pluggable git runner (sync) — replaceable in tests. */\nexport type GitRunner = (\n args: ReadonlyArray<string>,\n opts: { cwd: string },\n) => { stdout: string; stderr: string; exitCode: number }\n\nasync function runGitAsync(\n args: ReadonlyArray<string>,\n cwd: string,\n runner?: GitRunner,\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n if (runner) return runner(args, { cwd })\n return new Promise((resolve, reject) => {\n const proc = spawn('git', args, { cwd, stdio: 'pipe' })\n let stdout = ''\n let stderr = ''\n proc.stdout?.on('data', (c) => {\n stdout += String(c)\n })\n proc.stderr?.on('data', (c) => {\n stderr += String(c)\n })\n proc.on('error', reject)\n proc.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? -1 }))\n })\n}\n\nfunction ensureGitOk(\n step: string,\n result: { stdout: string; stderr: string; exitCode: number },\n): void {\n if (result.exitCode !== 0) {\n throw new Error(\n `worktree: git ${step} failed (exit ${result.exitCode}): ${result.stderr.slice(0, 400)}`,\n )\n }\n}\n\n/** @experimental */\nexport async function createWorktree(options: CreateWorktreeOptions): Promise<WorktreeHandle> {\n const variants = options.variantsDir ?? '.coder-variants'\n const baseRef = options.baseRef ?? 'HEAD'\n const branch = `delegate/${options.runId}`\n const path = `${options.repoRoot.replace(/\\/+$/, '')}/${variants}/${options.runId}`\n\n const headSha = await runGitAsync(['rev-parse', baseRef], options.repoRoot, options.runGit)\n ensureGitOk(`rev-parse ${baseRef}`, headSha)\n\n const add = await runGitAsync(\n ['worktree', 'add', '-b', branch, path, baseRef],\n options.repoRoot,\n options.runGit,\n )\n ensureGitOk(`worktree add ${path}`, add)\n\n return { path, baseSha: headSha.stdout.trim(), branch }\n}\n\n/** @experimental */\nexport async function captureWorktreeDiff(options: DiffOptions): Promise<DiffResult> {\n const baseRef = options.baseRef ?? options.worktree.baseSha\n const patch = await runGitAsync(['diff', baseRef], options.worktree.path, options.runGit)\n // No `ensureGitOk` here — diff returns 0 even when there are no changes.\n\n // Stats: `git diff --shortstat` produces e.g. \" 3 files changed, 42 insertions(+), 10 deletions(-)\".\n const shortstat = await runGitAsync(\n ['diff', '--shortstat', baseRef],\n options.worktree.path,\n options.runGit,\n )\n const stats = parseShortstat(shortstat.stdout)\n return { patch: patch.stdout, stats }\n}\n\nfunction parseShortstat(text: string): DiffResult['stats'] {\n // `text` is the raw stdout of `git diff --shortstat`. Empty when no\n // changes. Parse defensively — the format is stable but we don't trust\n // it for type-safety.\n const out = { filesChanged: 0, insertions: 0, deletions: 0 }\n const filesMatch = text.match(/(\\d+)\\s+files?\\s+changed/)\n if (filesMatch?.[1]) out.filesChanged = Number(filesMatch[1])\n const insertMatch = text.match(/(\\d+)\\s+insertions?/)\n if (insertMatch?.[1]) out.insertions = Number(insertMatch[1])\n const deleteMatch = text.match(/(\\d+)\\s+deletions?/)\n if (deleteMatch?.[1]) out.deletions = Number(deleteMatch[1])\n return out\n}\n\n/** @experimental */\nexport async function removeWorktree(options: RemoveWorktreeOptions): Promise<void> {\n const force = options.force ?? true\n const args = ['worktree', 'remove']\n if (force) args.push('--force')\n args.push(options.worktree.path)\n const result = await runGitAsync(args, options.repoRoot, options.runGit)\n // Don't ensureGitOk — partial-removal scenarios are tolerable; the\n // worktree dir may already be gone (caller deleted it manually).\n if (result.exitCode !== 0 && !/not a working tree/.test(result.stderr)) {\n // Best-effort branch cleanup so the next run can reuse the runId.\n await runGitAsync(\n ['branch', '-D', options.worktree.branch],\n options.repoRoot,\n options.runGit,\n ).catch(() => undefined)\n }\n // Always attempt branch removal — the worktree-remove sometimes leaves\n // the branch behind even when the directory is gone.\n await runGitAsync(\n ['branch', '-D', options.worktree.branch],\n options.repoRoot,\n options.runGit,\n ).catch(() => undefined)\n}\n","/**\n * @experimental\n *\n * In-process delegation executor — when `agent-runtime-mcp` is running\n * inside a sandbox whose image carries the local coding-harness CLIs\n * (claude / codex / opencode), delegations spawn the harness AS A\n * SUBPROCESS against a git worktree on the SAME filesystem instead of\n * provisioning a sibling sandbox.\n *\n * Why: zero provisioning latency, worker diffs land in-place, multi-harness\n * fanout = N parallel subprocesses in N parallel worktrees.\n *\n * Selection:\n * - env `AGENT_RUNTIME_IN_SANDBOX=1` (set by the parent harness at MCP\n * server launch) → in-process executor\n * - env `TANGLE_FLEET_ID=...` → fleet executor (Phase 2.5)\n * - neither → sibling sandbox executor (default)\n *\n * Multi-harness rotation: pass `harnesses: ['claude', 'codex', 'opencode']`\n * to round-robin across calls. A `runLoop` + `FanoutVote(n: 3)` against this\n * executor produces three parallel iterations, each running a different\n * harness on its own worktree.\n *\n * Architecture:\n *\n * client.create() → returns a fake SandboxInstance whose streamPrompt:\n * 1. createWorktree() — git worktree add /workspace/.coder-variants/<id>\n * 2. runLocalHarness() — spawn claude/codex/opencode subprocess\n * 3. captureWorktreeDiff() — git diff HEAD → patch + stats\n * 4. run testCmd + typecheckCmd if specified (the executor doesn't\n * own these — caller wires via task-extractor callback)\n * 5. emit ONE SandboxEvent { type: 'result', data: { result: CoderOutput } }\n * 6. removeWorktree() in finally\n */\n\nimport { randomUUID } from 'node:crypto'\nimport type { CreateSandboxOptions, SandboxEvent, SandboxInstance } from '@tangle-network/sandbox'\nimport type { LoopSandboxPlacement, SandboxClient } from '../runtime'\nimport type { DelegationExecutor } from './executor'\nimport { type LocalHarness, runLocalHarness } from './local-harness'\nimport {\n captureWorktreeDiff,\n createWorktree,\n type GitRunner,\n removeWorktree,\n type WorktreeHandle,\n} from './worktree'\n\n/** @experimental */\nexport interface InProcessExecutorOptions {\n /**\n * Absolute path to the git repo (the workspace inside the sandbox). The\n * executor creates worktrees under `<repoRoot>/.coder-variants/`.\n */\n repoRoot: string\n /**\n * Harnesses to round-robin across calls. With one entry every delegation\n * uses that harness; with three you get fanout diversity for free.\n * Default `['claude']`.\n */\n harnesses?: ReadonlyArray<LocalHarness>\n /**\n * Optional per-delegation test command. Run with `cwd = worktree.path`\n * after the harness exits. The exit code populates\n * `CoderOutput.testResult.passed`.\n */\n testCmd?: string\n /**\n * Optional per-delegation typecheck command. Same shape as `testCmd`.\n */\n typecheckCmd?: string\n /** Wall-clock cap per harness subprocess (ms). Default 5min. */\n harnessTimeoutMs?: number\n /** Wall-clock cap per test/typecheck subprocess (ms). Default 2min. */\n postCheckTimeoutMs?: number\n /** Test seam — override the git runner used by the worktree helpers. */\n runGit?: GitRunner\n /**\n * Test seam — override the harness runner. Defaults to spawning the real\n * CLI via `runLocalHarness`. Tests inject a stub that returns a scripted\n * `LocalHarnessResult`.\n */\n runHarness?: typeof runLocalHarness\n /**\n * Test seam — override the post-check runner. Defaults to spawning the\n * configured `testCmd` / `typecheckCmd` via `child_process.spawn`.\n */\n runPostCheck?: (\n cmd: string,\n cwd: string,\n signal?: AbortSignal,\n ) => Promise<{ exitCode: number; stdout: string; stderr: string }>\n}\n\n/** @experimental */\nexport interface InProcessExecutorDescribePlacement extends LoopSandboxPlacement {\n /**\n * Worktree path in the parent sandbox's filesystem. Set so trace\n * consumers can correlate dispatch events with on-disk artifacts after\n * the worker exits.\n */\n worktreePath?: string\n /** Which harness handled this delegation. */\n harness?: LocalHarness\n}\n\ninterface VirtualSandbox extends SandboxInstance {\n __inProcess: {\n runId: string\n harness: LocalHarness\n worktree?: WorktreeHandle\n }\n}\n\nconst DEFAULT_HARNESS_TIMEOUT_MS = 5 * 60 * 1000\nconst DEFAULT_POSTCHECK_TIMEOUT_MS = 2 * 60 * 1000\n\n/**\n * Build an in-process executor.\n *\n * Returns a {@link DelegationExecutor} whose `client.create()` returns a\n * minimal \"virtual\" SandboxInstance — the kernel calls `streamPrompt(msg)`\n * on it, which runs the local harness on a worktree and emits one\n * `result` event whose `data.result` is a `CoderOutput`-shaped record.\n *\n * Pairs with `coderProfile`'s event parser (it walks the event list\n * back-to-front for the first `type === 'result'`).\n *\n * @experimental\n */\nexport function createInProcessExecutor(options: InProcessExecutorOptions): DelegationExecutor {\n const harnesses =\n options.harnesses && options.harnesses.length > 0\n ? [...options.harnesses]\n : (['claude'] as const)\n const runHarness = options.runHarness ?? runLocalHarness\n const runPostCheck = options.runPostCheck ?? defaultRunPostCheck\n\n let callIndex = 0\n\n const client: SandboxClient = {\n async create(_opts?: CreateSandboxOptions): Promise<SandboxInstance> {\n const runId = randomUUID()\n const harness = harnesses[callIndex % harnesses.length] as LocalHarness\n callIndex += 1\n\n const virtual: VirtualSandbox = {\n // Synthesize the minimum SandboxInstance surface the kernel touches.\n // We CAST through unknown because SandboxInstance is a `declare class`\n // with private fields; we're producing a structural subtype that\n // satisfies the kernel's narrow usage (`box.id`, `box.streamPrompt`).\n id: `in-process-${runId}`,\n __inProcess: { runId, harness },\n // eslint-disable-next-line require-yield\n async *streamPrompt(\n this: VirtualSandbox,\n message: string | unknown[],\n promptOpts?: { signal?: AbortSignal },\n ): AsyncGenerator<SandboxEvent> {\n const taskPrompt =\n typeof message === 'string'\n ? message\n : message\n .map((p) =>\n typeof p === 'object' && p && 'text' in p\n ? String((p as { text: unknown }).text)\n : '',\n )\n .join('\\n')\n\n let worktree: WorktreeHandle | undefined\n try {\n worktree = await createWorktree({\n repoRoot: options.repoRoot,\n runId,\n runGit: options.runGit,\n })\n this.__inProcess.worktree = worktree\n\n // Yield a dispatch-equivalent event so traces see the placement.\n yield {\n type: 'in_process.harness.started',\n data: {\n runId,\n harness,\n worktreePath: worktree.path,\n command: harness,\n },\n }\n\n const harnessResult = await runHarness({\n harness,\n cwd: worktree.path,\n taskPrompt,\n timeoutMs: options.harnessTimeoutMs ?? DEFAULT_HARNESS_TIMEOUT_MS,\n signal: promptOpts?.signal,\n })\n\n yield {\n type: 'in_process.harness.ended',\n data: {\n runId,\n exitCode: harnessResult.exitCode,\n durationMs: harnessResult.durationMs,\n killedBySignal: harnessResult.killedBySignal,\n timedOut: harnessResult.timedOut,\n stdoutBytes: harnessResult.stdout.length,\n stderrBytes: harnessResult.stderr.length,\n },\n }\n\n // Capture diff regardless of exit code — a failed run can still\n // leave a partial diff worth inspecting.\n const diff = await captureWorktreeDiff({ worktree, runGit: options.runGit })\n\n // Optional post-checks. Each runs in the WORKTREE so it sees the\n // harness's edits.\n const testCheck = options.testCmd\n ? await runPostCheck(options.testCmd, worktree.path, promptOpts?.signal).catch(\n (err) => ({\n exitCode: -1,\n stdout: '',\n stderr: err instanceof Error ? err.message : String(err),\n }),\n )\n : { exitCode: 0, stdout: '', stderr: '' }\n const typecheckCheck = options.typecheckCmd\n ? await runPostCheck(options.typecheckCmd, worktree.path, promptOpts?.signal).catch(\n (err) => ({\n exitCode: -1,\n stdout: '',\n stderr: err instanceof Error ? err.message : String(err),\n }),\n )\n : { exitCode: 0, stdout: '', stderr: '' }\n\n const coderOutput = {\n branch: worktree.branch,\n patch: diff.patch,\n testResult: {\n passed: !options.testCmd || testCheck.exitCode === 0,\n output: tail(testCheck.stderr || testCheck.stdout, 4000),\n },\n typecheckResult: {\n passed: !options.typecheckCmd || typecheckCheck.exitCode === 0,\n output: tail(typecheckCheck.stderr || typecheckCheck.stdout, 4000),\n },\n diffStats: diff.stats,\n reviewerNotes:\n harnessResult.exitCode === 0\n ? undefined\n : `harness ${harness} exited ${harnessResult.exitCode}${harnessResult.timedOut ? ' (timed out)' : ''}`,\n }\n\n // The terminal event the coderProfile parser looks for.\n yield {\n type: 'result',\n data: {\n result: coderOutput,\n source: 'in-process-executor',\n harness,\n runId,\n },\n }\n } finally {\n if (worktree) {\n await removeWorktree({\n worktree,\n repoRoot: options.repoRoot,\n runGit: options.runGit,\n }).catch(() => undefined)\n }\n }\n },\n } as unknown as VirtualSandbox\n\n return virtual\n },\n describePlacement(box: SandboxInstance): InProcessExecutorDescribePlacement {\n const sandboxId = (box as unknown as { id?: string }).id\n const meta = (box as VirtualSandbox).__inProcess\n return {\n kind: 'sibling',\n sandboxId,\n worktreePath: meta?.worktree?.path,\n harness: meta?.harness,\n }\n },\n }\n\n return {\n client,\n placement: 'in-process',\n describe(): string {\n return `in-process (repoRoot=${options.repoRoot}, harnesses=[${harnesses.join(',')}]${\n options.testCmd ? `, testCmd=\"${options.testCmd}\"` : ''\n }${options.typecheckCmd ? `, typecheckCmd=\"${options.typecheckCmd}\"` : ''})`\n },\n }\n}\n\nasync function defaultRunPostCheck(\n cmd: string,\n cwd: string,\n signal?: AbortSignal,\n): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n const { spawn } = await import('node:child_process')\n return new Promise((resolve, reject) => {\n // Run via sh -c so multi-word commands (\"pnpm test\") and shell features work.\n const child = spawn('sh', ['-c', cmd], { cwd, stdio: 'pipe' })\n let stdout = ''\n let stderr = ''\n child.stdout?.on('data', (c) => {\n stdout += String(c)\n })\n child.stderr?.on('data', (c) => {\n stderr += String(c)\n })\n if (signal) {\n const onAbort = () => {\n if (!child.killed) child.kill('SIGTERM')\n }\n if (signal.aborted) onAbort()\n else signal.addEventListener('abort', onAbort, { once: true })\n }\n const killTimer = setTimeout(() => {\n if (!child.killed) child.kill('SIGTERM')\n }, DEFAULT_POSTCHECK_TIMEOUT_MS)\n if (typeof (killTimer as { unref?: () => void }).unref === 'function') {\n ;(killTimer as { unref: () => void }).unref()\n }\n child.on('error', (err) => {\n clearTimeout(killTimer)\n reject(err)\n })\n child.on('close', (code) => {\n clearTimeout(killTimer)\n resolve({ exitCode: code ?? -1, stdout, stderr })\n })\n })\n}\n\nfunction tail(text: string, max: number): string {\n if (text.length <= max) return text\n return text.slice(text.length - max)\n}\n","/**\n * @experimental\n *\n * Helpers extracted from `bin.ts` so the env-detection + executor-selection\n * logic is unit-testable without spawning a subprocess. The bin imports from\n * here; tests import from here directly.\n */\n\nimport type { SandboxClient } from '../runtime'\nimport {\n createFleetWorkspaceExecutor,\n createSiblingSandboxExecutor,\n type DelegationExecutor,\n type FleetHandle,\n} from './executor'\nimport { createInProcessExecutor } from './in-process-executor'\nimport type { LocalHarness } from './local-harness'\n\n/** @experimental */\nexport interface DetectExecutorArgs {\n sandboxClient: SandboxClient\n /** Raw env (defaults to `process.env`). Pass an explicit map for tests. */\n env?: Record<string, string | undefined>\n /**\n * Override how a fleet handle is resolved from the client + fleet id. The\n * default reads `client.fleets.get(fleetId)` and validates the returned\n * shape against the structural `FleetHandle` contract.\n */\n resolveFleet?: (client: SandboxClient, fleetId: string) => Promise<FleetHandle>\n}\n\n/**\n * Pick the right executor for an MCP server invocation based on env vars.\n *\n * - `TANGLE_FLEET_ID` set → fleet-workspace placement; resolves the handle\n * via `sandboxClient.fleets.get(...)`.\n * - Otherwise → sibling-sandbox placement; each delegation creates a fresh\n * sandbox via `sandboxClient.create(...)`.\n *\n * Fails loud (throws) when fleet mode is requested but the SDK shape is\n * incompatible — the operator chose fleet semantics, silently degrading to\n * sibling mode would lie about workspace topology.\n *\n * @experimental\n */\nexport async function detectExecutor(args: DetectExecutorArgs): Promise<DelegationExecutor> {\n const env = args.env ?? process.env\n\n // In-process (Phase 2.8): parent harness sets AGENT_RUNTIME_IN_SANDBOX=1\n // and points us at the workspace root. Highest-priority — when this is\n // set, delegations spawn local harness CLIs on git worktrees in the\n // SAME filesystem instead of provisioning sibling sandboxes.\n if (env.AGENT_RUNTIME_IN_SANDBOX === '1') {\n const repoRoot = env.AGENT_RUNTIME_REPO_ROOT?.trim()\n if (!repoRoot) {\n throw new Error(\n 'agent-runtime-mcp: AGENT_RUNTIME_IN_SANDBOX=1 requires AGENT_RUNTIME_REPO_ROOT to point at the workspace root',\n )\n }\n return createInProcessExecutor({\n repoRoot,\n harnesses: parseHarnesses(env.AGENT_RUNTIME_LOCAL_HARNESSES),\n testCmd: env.AGENT_RUNTIME_TEST_CMD?.trim() || undefined,\n typecheckCmd: env.AGENT_RUNTIME_TYPECHECK_CMD?.trim() || undefined,\n })\n }\n\n const fleetId = parseFleetId(env.TANGLE_FLEET_ID)\n if (!fleetId) {\n return createSiblingSandboxExecutor({ client: args.sandboxClient })\n }\n const resolveFleet = args.resolveFleet ?? defaultResolveFleet\n const fleet = await resolveFleet(args.sandboxClient, fleetId)\n const excludeMachineIds = parseList(env.TANGLE_FLEET_EXCLUDE_MACHINES)\n return createFleetWorkspaceExecutor({\n fleet,\n excludeMachineIds,\n })\n}\n\nconst KNOWN_HARNESSES: ReadonlyArray<LocalHarness> = ['claude', 'codex', 'opencode']\n\nfunction parseHarnesses(raw: string | undefined): ReadonlyArray<LocalHarness> | undefined {\n if (!raw) return undefined\n const parts = raw\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (parts.length === 0) return undefined\n for (const part of parts) {\n if (!KNOWN_HARNESSES.includes(part as LocalHarness)) {\n throw new Error(\n `agent-runtime-mcp: AGENT_RUNTIME_LOCAL_HARNESSES contains unknown harness \"${part}\". Expected: ${KNOWN_HARNESSES.join(', ')}.`,\n )\n }\n }\n return parts as LocalHarness[]\n}\n\ninterface FleetsApi {\n get(fleetId: string): Promise<unknown>\n}\n\nasync function defaultResolveFleet(\n sandboxClient: SandboxClient,\n fleetId: string,\n): Promise<FleetHandle> {\n const fleets = (sandboxClient as unknown as { fleets?: FleetsApi }).fleets\n if (!fleets || typeof fleets.get !== 'function') {\n throw new Error(\n '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.',\n )\n }\n const raw = await fleets.get(fleetId)\n if (!raw || typeof raw !== 'object') {\n throw new Error(`agent-runtime-mcp: fleets.get(${fleetId}) returned no handle`)\n }\n const handle = raw as Partial<FleetHandle>\n if (typeof handle.fleetId !== 'string' || !Array.isArray(handle.ids)) {\n throw new Error(\n `agent-runtime-mcp: fleet handle for ${fleetId} is missing fleetId/ids — incompatible sandbox SDK shape`,\n )\n }\n if (typeof handle.sandbox !== 'function') {\n throw new Error(\n `agent-runtime-mcp: fleet handle for ${fleetId} is missing sandbox(machineId) — incompatible sandbox SDK shape`,\n )\n }\n return handle as FleetHandle\n}\n\nfunction parseFleetId(raw: string | undefined): string | undefined {\n if (typeof raw !== 'string') return undefined\n const trimmed = raw.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nfunction parseList(raw: string | undefined): string[] | undefined {\n if (!raw) return undefined\n const list = raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n return list.length > 0 ? list : undefined\n}\n","/**\n * @experimental\n *\n * Trace context propagation for MCP subprocess.\n *\n * When the MCP server is launched as a child process by a sandbox harness,\n * the parent passes trace context via environment variables:\n *\n * TRACE_ID=<current-run-trace-id>\n * PARENT_SPAN_ID=<span-that-dispatched-the-delegation>\n *\n * The MCP server reads these at startup and uses them as the root of its\n * internal trace tree. All spans emitted by `runLoop` invocations inside\n * the MCP are children of the parent's delegation span.\n *\n * When these env vars are absent, the MCP generates a fresh trace root —\n * the server operates standalone without trace joining.\n */\n\nimport type { OtelExporter } from '../otel-export'\nimport { buildLoopOtelSpans, createOtelExporter } from '../otel-export'\nimport type { LoopTraceEmitter, LoopTraceEvent } from '../runtime/types'\n\nexport interface TraceContext {\n /** Trace id inherited from the parent process, or a fresh one. */\n traceId: string\n /** Parent span id from the delegation that launched this MCP server. */\n parentSpanId?: string\n}\n\n/**\n * Read trace context from the process environment.\n * Returns a context with inherited ids or a freshly generated root.\n */\nexport function readTraceContextFromEnv(): TraceContext {\n const traceId = process.env.TRACE_ID || generateTraceId()\n const parentSpanId = process.env.PARENT_SPAN_ID || undefined\n return { traceId, parentSpanId }\n}\n\n/**\n * Create a LoopTraceEmitter that:\n * 1. Parents all spans under the inherited PARENT_SPAN_ID.\n * 2. Exports spans to OTEL when OTEL_EXPORTER_OTLP_ENDPOINT is set.\n *\n * Returns both the emitter and the optional exporter handle for shutdown.\n */\nexport function createPropagatingTraceEmitter(ctx: TraceContext): {\n emitter: LoopTraceEmitter\n exporter: OtelExporter | undefined\n context: TraceContext\n} {\n const exporter = createOtelExporter()\n\n // Buffer events per loop run, then emit the full nested span tree on\n // `loop.ended` so the topology hierarchy (loop → round → branch) reaches the\n // OTLP collector — not a flat list of zero-duration point spans. A run that\n // never reaches `loop.ended` (hard abort) drops its buffer; acceptable for\n // the short-lived MCP subprocess.\n const buffers = new Map<string, LoopTraceEvent[]>()\n\n const emitter: LoopTraceEmitter = {\n emit(event: LoopTraceEvent) {\n if (!exporter) return\n const buf = buffers.get(event.runId)\n if (buf) buf.push(event)\n else buffers.set(event.runId, [event])\n if (event.kind === 'loop.ended') {\n const events = buffers.get(event.runId) ?? [event]\n buffers.delete(event.runId)\n for (const span of buildLoopOtelSpans(events, ctx.traceId, ctx.parentSpanId)) {\n exporter.exportSpan(span)\n }\n }\n },\n }\n\n return { emitter, exporter, context: ctx }\n}\n\n/**\n * Build env vars to pass to a child MCP subprocess so it inherits the\n * current trace context.\n */\nexport function traceContextToEnv(ctx: TraceContext): Record<string, string> {\n const env: Record<string, string> = { TRACE_ID: ctx.traceId }\n if (ctx.parentSpanId) env.PARENT_SPAN_ID = ctx.parentSpanId\n return env\n}\n\nfunction generateTraceId(): string {\n const bytes = new Uint8Array(16)\n if (typeof globalThis.crypto?.getRandomValues === 'function') {\n globalThis.crypto.getRandomValues(bytes)\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256)\n }\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n"],"mappings":"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","names":["spawn"]}
@@ -0,0 +1,72 @@
1
+ // src/model-resolution.ts
2
+ var DEFAULT_ROUTER_BASE_URL = "https://router.tangle.tools";
3
+ function resolveRouterBaseUrl(env = {}) {
4
+ return (env.TANGLE_ROUTER_URL ?? env.TANGLE_ROUTER_BASE_URL ?? DEFAULT_ROUTER_BASE_URL).replace(/\/v1\/?$/, "").replace(/\/$/, "");
5
+ }
6
+ async function getModels(routerBaseUrl = DEFAULT_ROUTER_BASE_URL) {
7
+ const res = await fetch(`${routerBaseUrl}/v1/models`, {
8
+ headers: { Accept: "application/json" }
9
+ });
10
+ if (!res.ok) throw new Error(`router /v1/models ${res.status}`);
11
+ const body = await res.json();
12
+ return Array.isArray(body.data) ? body.data : [];
13
+ }
14
+ function cleanModelId(value) {
15
+ if (typeof value !== "string") return void 0;
16
+ const trimmed = value.trim();
17
+ return trimmed.length > 0 ? trimmed : void 0;
18
+ }
19
+ function resolveChatModel(candidates, fallback) {
20
+ for (const candidate of candidates) {
21
+ const model = cleanModelId(candidate.model);
22
+ if (model) return { source: candidate.source, model };
23
+ }
24
+ return fallback;
25
+ }
26
+ var WELL_FORMED_MODEL_ID = /^[A-Za-z0-9._/@:-]+$/;
27
+ function isWellFormedModelId(modelId) {
28
+ return modelId.length <= 200 && WELL_FORMED_MODEL_ID.test(modelId);
29
+ }
30
+ function catalogIdsForModel(model) {
31
+ const ids = /* @__PURE__ */ new Set();
32
+ const id = cleanModelId(model.id);
33
+ if (id) ids.add(id);
34
+ const provider = cleanModelId(model._provider) ?? cleanModelId(model.provider);
35
+ if (provider && id && !id.includes("/")) ids.add(`${provider}/${id}`);
36
+ return [...ids];
37
+ }
38
+ async function validateChatModelId(modelId, options = {}) {
39
+ const {
40
+ allowlist = [],
41
+ routerBaseUrl = DEFAULT_ROUTER_BASE_URL,
42
+ loadModels = getModels
43
+ } = options;
44
+ const cleaned = cleanModelId(modelId);
45
+ if (!cleaned) return { succeeded: false, error: "Model id must be a non-empty string." };
46
+ if (!isWellFormedModelId(cleaned)) {
47
+ return { succeeded: false, error: `Model id is malformed: ${cleaned}` };
48
+ }
49
+ if (allowlist.some((id) => cleanModelId(id) === cleaned)) {
50
+ return { succeeded: true, value: cleaned };
51
+ }
52
+ let catalog;
53
+ try {
54
+ catalog = await loadModels(routerBaseUrl);
55
+ } catch (err) {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ return { succeeded: false, error: `Could not validate model catalog: ${message}` };
58
+ }
59
+ const ids = new Set(catalog.flatMap(catalogIdsForModel));
60
+ if (!ids.has(cleaned)) return { succeeded: false, error: `Model is not available: ${cleaned}` };
61
+ return { succeeded: true, value: cleaned };
62
+ }
63
+
64
+ export {
65
+ DEFAULT_ROUTER_BASE_URL,
66
+ resolveRouterBaseUrl,
67
+ getModels,
68
+ cleanModelId,
69
+ resolveChatModel,
70
+ validateChatModelId
71
+ };
72
+ //# sourceMappingURL=chunk-GRAGM4MC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/model-resolution.ts"],"sourcesContent":["/**\n * @stable\n *\n * Chat-model resolution + catalog validation — the shared primitive every\n * product chat handler needs and was, until now, hand-rolling. Lifts the\n * router `/v1/models` fetch, the fail-closed id validation, and the\n * precedence resolver out of four near-identical per-repo copies.\n *\n * Policy-free by design: callers pass their own precedence order\n * (`resolveChatModel`) and their own known-good `allowlist`\n * (`validateChatModelId`), so each product keeps its resolution policy while\n * sharing the catalog fetch, the malformed-id guard, and the fail-closed\n * admission rule. No React, no `process.env` assumption — `env` is an\n * explicit narrow record so this runs unchanged in Node and in Workers.\n */\n\n/**\n * A model entry as returned by the Tangle Router `/v1/models` endpoint.\n * Intentionally minimal — only the fields resolution + validation read.\n */\nexport interface ModelInfo {\n id: string\n name?: string\n description?: string\n /** Provider slug, when the router exposes it (`provider` or `_provider`). */\n provider?: string\n _provider?: string\n architecture?: {\n modality?: string\n input_modalities?: string[]\n output_modalities?: string[]\n }\n}\n\n/** Env keys the router base URL is resolved from. */\nexport interface RouterEnv {\n TANGLE_ROUTER_URL?: string\n TANGLE_ROUTER_BASE_URL?: string\n}\n\nexport const DEFAULT_ROUTER_BASE_URL = 'https://router.tangle.tools'\n\n/** Resolve the router base URL from env, normalised — no trailing `/v1` or `/`. */\nexport function resolveRouterBaseUrl(env: RouterEnv = {}): string {\n return (env.TANGLE_ROUTER_URL ?? env.TANGLE_ROUTER_BASE_URL ?? DEFAULT_ROUTER_BASE_URL)\n .replace(/\\/v1\\/?$/, '')\n .replace(/\\/$/, '')\n}\n\n/**\n * Fetch the model catalog from the router's `/v1/models`. Throws on a non-2xx\n * response — callers decide whether to fail open (empty catalog) or closed.\n */\nexport async function getModels(\n routerBaseUrl: string = DEFAULT_ROUTER_BASE_URL,\n): Promise<ModelInfo[]> {\n const res = await fetch(`${routerBaseUrl}/v1/models`, {\n headers: { Accept: 'application/json' },\n })\n if (!res.ok) throw new Error(`router /v1/models ${res.status}`)\n const body = (await res.json()) as { data?: ModelInfo[] }\n return Array.isArray(body.data) ? body.data : []\n}\n\n/** Trim a candidate model id; `undefined` for non-strings and blanks. */\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport interface ChatModelCandidate {\n /** Stable label for telemetry — e.g. `request`, `workspace`, `env`. */\n source: string\n model: string | undefined\n}\n\nexport interface ResolvedChatModel {\n source: string\n model: string\n}\n\n/**\n * Resolve a chat model by precedence: the first candidate carrying a\n * non-blank model wins, else `fallback`. The caller owns the precedence\n * order, so each product keeps its own policy (request → workspace → env,\n * etc.) while the first-non-blank logic and the telemetry shape stay shared.\n */\nexport function resolveChatModel(\n candidates: ChatModelCandidate[],\n fallback: ResolvedChatModel,\n): ResolvedChatModel {\n for (const candidate of candidates) {\n const model = cleanModelId(candidate.model)\n if (model) return { source: candidate.source, model }\n }\n return fallback\n}\n\nexport type ChatModelValidation =\n | { succeeded: true; value: string }\n | { succeeded: false; error: string }\n\nconst WELL_FORMED_MODEL_ID = /^[A-Za-z0-9._/@:-]+$/\n\nfunction isWellFormedModelId(modelId: string): boolean {\n return modelId.length <= 200 && WELL_FORMED_MODEL_ID.test(modelId)\n}\n\n/**\n * Every id a catalog entry can be addressed by — its bare id, plus a\n * `provider/id` form when the router exposes a separate provider slug.\n */\nfunction catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n const id = cleanModelId(model.id)\n if (id) ids.add(id)\n const provider = cleanModelId(model._provider) ?? cleanModelId(model.provider)\n if (provider && id && !id.includes('/')) ids.add(`${provider}/${id}`)\n return [...ids]\n}\n\n/**\n * Validate a caller-supplied chat-model id. Rejects non-strings, malformed\n * ids, and ids absent from both the caller's `allowlist` and the live router\n * catalog. Fails closed: when the catalog cannot be fetched, an unverifiable\n * id is rejected rather than admitted — a bad model never reaches the agent.\n */\nexport async function validateChatModelId(\n modelId: unknown,\n options: {\n /**\n * Known-good ids that skip the catalog round trip — e.g. the product's\n * default model plus any env-configured ids.\n */\n allowlist?: string[]\n routerBaseUrl?: string\n /** Injectable catalog loader — overridden in tests. */\n loadModels?: (routerBaseUrl: string) => Promise<ModelInfo[]>\n } = {},\n): Promise<ChatModelValidation> {\n const {\n allowlist = [],\n routerBaseUrl = DEFAULT_ROUTER_BASE_URL,\n loadModels = getModels,\n } = options\n\n const cleaned = cleanModelId(modelId)\n if (!cleaned) return { succeeded: false, error: 'Model id must be a non-empty string.' }\n if (!isWellFormedModelId(cleaned)) {\n return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n }\n if (allowlist.some((id) => cleanModelId(id) === cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await loadModels(routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (!ids.has(cleaned)) return { succeeded: false, error: `Model is not available: ${cleaned}` }\n return { succeeded: true, value: cleaned }\n}\n"],"mappings":";AAwCO,IAAM,0BAA0B;AAGhC,SAAS,qBAAqB,MAAiB,CAAC,GAAW;AAChE,UAAQ,IAAI,qBAAqB,IAAI,0BAA0B,yBAC5D,QAAQ,YAAY,EAAE,EACtB,QAAQ,OAAO,EAAE;AACtB;AAMA,eAAsB,UACpB,gBAAwB,yBACF;AACtB,QAAM,MAAM,MAAM,MAAM,GAAG,aAAa,cAAc;AAAA,IACpD,SAAS,EAAE,QAAQ,mBAAmB;AAAA,EACxC,CAAC;AACD,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,qBAAqB,IAAI,MAAM,EAAE;AAC9D,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,SAAO,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC;AACjD;AAGO,SAAS,aAAa,OAAoC;AAC/D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAmBO,SAAS,iBACd,YACA,UACmB;AACnB,aAAW,aAAa,YAAY;AAClC,UAAM,QAAQ,aAAa,UAAU,KAAK;AAC1C,QAAI,MAAO,QAAO,EAAE,QAAQ,UAAU,QAAQ,MAAM;AAAA,EACtD;AACA,SAAO;AACT;AAMA,IAAM,uBAAuB;AAE7B,SAAS,oBAAoB,SAA0B;AACrD,SAAO,QAAQ,UAAU,OAAO,qBAAqB,KAAK,OAAO;AACnE;AAMA,SAAS,mBAAmB,OAA4B;AACtD,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,KAAK,aAAa,MAAM,EAAE;AAChC,MAAI,GAAI,KAAI,IAAI,EAAE;AAClB,QAAM,WAAW,aAAa,MAAM,SAAS,KAAK,aAAa,MAAM,QAAQ;AAC7E,MAAI,YAAY,MAAM,CAAC,GAAG,SAAS,GAAG,EAAG,KAAI,IAAI,GAAG,QAAQ,IAAI,EAAE,EAAE;AACpE,SAAO,CAAC,GAAG,GAAG;AAChB;AAQA,eAAsB,oBACpB,SACA,UASI,CAAC,GACyB;AAC9B,QAAM;AAAA,IACJ,YAAY,CAAC;AAAA,IACb,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf,IAAI;AAEJ,QAAM,UAAU,aAAa,OAAO;AACpC,MAAI,CAAC,QAAS,QAAO,EAAE,WAAW,OAAO,OAAO,uCAAuC;AACvF,MAAI,CAAC,oBAAoB,OAAO,GAAG;AACjC,WAAO,EAAE,WAAW,OAAO,OAAO,0BAA0B,OAAO,GAAG;AAAA,EACxE;AACA,MAAI,UAAU,KAAK,CAAC,OAAO,aAAa,EAAE,MAAM,OAAO,GAAG;AACxD,WAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAAA,EAC3C;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,WAAW,aAAa;AAAA,EAC1C,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO,EAAE,WAAW,OAAO,OAAO,qCAAqC,OAAO,GAAG;AAAA,EACnF;AAEA,QAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,kBAAkB,CAAC;AACvD,MAAI,CAAC,IAAI,IAAI,OAAO,EAAG,QAAO,EAAE,WAAW,OAAO,OAAO,2BAA2B,OAAO,GAAG;AAC9F,SAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAC3C;","names":[]}