@tangle-network/agent-runtime 0.48.0 → 0.50.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 (51) hide show
  1. package/README.md +79 -15
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/analyst-loop.d.ts +1 -1
  5. package/dist/{chunk-656G2XCL.js → chunk-BKAIVNFA.js} +3 -3
  6. package/dist/{chunk-IW2LMLK6.js → chunk-CM2IK7VS.js} +913 -152
  7. package/dist/chunk-CM2IK7VS.js.map +1 -0
  8. package/dist/{chunk-VR4JIC5H.js → chunk-ML4IXGTV.js} +2 -2
  9. package/dist/{chunk-TJS7S3HJ.js → chunk-NDM5VXZW.js} +19 -8
  10. package/dist/chunk-NDM5VXZW.js.map +1 -0
  11. package/dist/chunk-OM3YNZIW.js +978 -0
  12. package/dist/chunk-OM3YNZIW.js.map +1 -0
  13. package/dist/{chunk-JNPK46YH.js → chunk-RHW75JW5.js} +498 -350
  14. package/dist/chunk-RHW75JW5.js.map +1 -0
  15. package/dist/{coder-CVZNGbyg.d.ts → coder-_YCf3BAK.d.ts} +2 -2
  16. package/dist/{driver-DYU2sgHr.d.ts → driver-DLI1io57.d.ts} +1 -1
  17. package/dist/index.d.ts +34 -9
  18. package/dist/index.js +117 -27
  19. package/dist/index.js.map +1 -1
  20. package/dist/kb-gate-CHAyt4aI.d.ts +1571 -0
  21. package/dist/{loop-runner-bin-DEm4roYF.d.ts → loop-runner-bin-DFUNgpeK.d.ts} +4 -4
  22. package/dist/loop-runner-bin.d.ts +5 -5
  23. package/dist/loop-runner-bin.js +3 -3
  24. package/dist/loops.d.ts +6 -6
  25. package/dist/loops.js +17 -1
  26. package/dist/mcp/bin.js +206 -29
  27. package/dist/mcp/bin.js.map +1 -1
  28. package/dist/mcp/index.d.ts +41 -177
  29. package/dist/mcp/index.js +40 -6
  30. package/dist/mcp/index.js.map +1 -1
  31. package/dist/openai-tools-D4HLDWgw.d.ts +45 -0
  32. package/dist/platform.js +2 -2
  33. package/dist/platform.js.map +1 -1
  34. package/dist/profiles.d.ts +2 -2
  35. package/dist/{run-loop-DvD4aGiE.d.ts → run-loop-BIineL1T.d.ts} +1 -1
  36. package/dist/runtime.d.ts +403 -24
  37. package/dist/runtime.js +17 -1
  38. package/dist/{types-BpDfCPUp.d.ts → types-5MGt5KTY.d.ts} +1 -1
  39. package/dist/{types-nBMuollC.d.ts → types-BEQsBhOE.d.ts} +1 -1
  40. package/dist/workflow.d.ts +2 -2
  41. package/dist/workflow.js +1 -1
  42. package/package.json +6 -5
  43. package/dist/chunk-IW2LMLK6.js.map +0 -1
  44. package/dist/chunk-JNPK46YH.js.map +0 -1
  45. package/dist/chunk-LX66I3SC.js +0 -218
  46. package/dist/chunk-LX66I3SC.js.map +0 -1
  47. package/dist/chunk-TJS7S3HJ.js.map +0 -1
  48. package/dist/kb-gate-51BlLlVM.d.ts +0 -529
  49. package/dist/otel-export-EzfsVUhh.d.ts +0 -191
  50. /package/dist/{chunk-656G2XCL.js.map → chunk-BKAIVNFA.js.map} +0 -0
  51. /package/dist/{chunk-VR4JIC5H.js.map → chunk-ML4IXGTV.js.map} +0 -0
@@ -1,23 +1,236 @@
1
1
  import {
2
- NotFoundError
2
+ capDelegationTrace,
3
+ createDelegationTraceCollector,
4
+ formatDetachedSessionRef,
5
+ generateDelegationSpanId
6
+ } from "./chunk-OM3YNZIW.js";
7
+ import {
8
+ AgentEvalError,
9
+ NotFoundError,
10
+ ValidationError
3
11
  } from "./chunk-GSUO5QS6.js";
4
12
 
13
+ // src/mcp/delegation-store.ts
14
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
15
+ import { dirname } from "path";
16
+ var DelegationStateCorruptError = class extends AgentEvalError {
17
+ constructor(message, options) {
18
+ super("validation", message, options);
19
+ }
20
+ };
21
+ var DelegationPersistenceError = class extends AgentEvalError {
22
+ constructor(message, options) {
23
+ super("config", message, options);
24
+ }
25
+ };
26
+ var InMemoryDelegationStore = class {
27
+ records = /* @__PURE__ */ new Map();
28
+ async loadAll() {
29
+ return [...this.records.values()].map(cloneRecord);
30
+ }
31
+ async upsert(record) {
32
+ this.records.set(record.taskId, cloneRecord(record));
33
+ }
34
+ async lookupIdempotencyKey(key) {
35
+ for (const record of this.records.values()) {
36
+ if (record.idempotencyKey === key) return record.taskId;
37
+ }
38
+ return void 0;
39
+ }
40
+ async remove(taskIds) {
41
+ for (const taskId of taskIds) this.records.delete(taskId);
42
+ }
43
+ };
44
+ var STATE_FORMAT_VERSION = 1;
45
+ var FileDelegationStore = class {
46
+ filePath;
47
+ recoverCorrupt;
48
+ records = /* @__PURE__ */ new Map();
49
+ loaded = false;
50
+ writeTail = Promise.resolve();
51
+ tmpSeq = 0;
52
+ constructor(options) {
53
+ this.filePath = options.filePath;
54
+ this.recoverCorrupt = options.recoverCorrupt ?? false;
55
+ }
56
+ async loadAll() {
57
+ let raw;
58
+ try {
59
+ raw = await readFile(this.filePath, "utf8");
60
+ } catch (err) {
61
+ if (err.code === "ENOENT") {
62
+ this.loaded = true;
63
+ return [];
64
+ }
65
+ throw new DelegationPersistenceError(
66
+ `FileDelegationStore: failed to read ${this.filePath}: ${errorMessage(err)}`,
67
+ { cause: err }
68
+ );
69
+ }
70
+ let state;
71
+ try {
72
+ state = parsePersistedState(raw);
73
+ } catch (err) {
74
+ if (!this.recoverCorrupt) {
75
+ throw new DelegationStateCorruptError(
76
+ `FileDelegationStore: state file ${this.filePath} is corrupt (${errorMessage(err)}). Repair or archive the file, or opt into automatic recovery (recoverCorrupt / AGENT_RUNTIME_DELEGATION_STATE_RECOVER=1) to archive it and start empty.`,
77
+ { cause: err }
78
+ );
79
+ }
80
+ const archivePath = `${this.filePath}.corrupt-${Date.now()}`;
81
+ await rename(this.filePath, archivePath);
82
+ this.loaded = true;
83
+ return [];
84
+ }
85
+ this.records.clear();
86
+ for (const record of state.records) this.records.set(record.taskId, record);
87
+ this.loaded = true;
88
+ return [...this.records.values()].map(cloneRecord);
89
+ }
90
+ async upsert(record) {
91
+ this.assertLoaded("upsert");
92
+ this.records.set(record.taskId, cloneRecord(record));
93
+ await this.enqueueWrite();
94
+ }
95
+ async lookupIdempotencyKey(key) {
96
+ this.assertLoaded("lookupIdempotencyKey");
97
+ for (const record of this.records.values()) {
98
+ if (record.idempotencyKey === key) return record.taskId;
99
+ }
100
+ return void 0;
101
+ }
102
+ async remove(taskIds) {
103
+ this.assertLoaded("remove");
104
+ let changed = false;
105
+ for (const taskId of taskIds) {
106
+ if (this.records.delete(taskId)) changed = true;
107
+ }
108
+ if (changed) await this.enqueueWrite();
109
+ }
110
+ assertLoaded(op) {
111
+ if (this.loaded) return;
112
+ throw new DelegationPersistenceError(
113
+ `FileDelegationStore: ${op} called before loadAll() \u2014 the on-disk state has not been read yet`
114
+ );
115
+ }
116
+ enqueueWrite() {
117
+ const write = this.writeTail.then(() => this.writeSnapshot());
118
+ this.writeTail = write.catch(() => {
119
+ });
120
+ return write;
121
+ }
122
+ async writeSnapshot() {
123
+ const state = {
124
+ version: STATE_FORMAT_VERSION,
125
+ records: [...this.records.values()]
126
+ };
127
+ const payload = `${JSON.stringify(state)}
128
+ `;
129
+ this.tmpSeq += 1;
130
+ const tmpPath = `${this.filePath}.tmp-${process.pid}-${this.tmpSeq}`;
131
+ try {
132
+ await mkdir(dirname(this.filePath), { recursive: true });
133
+ await writeFile(tmpPath, payload, "utf8");
134
+ await rename(tmpPath, this.filePath);
135
+ } catch (err) {
136
+ throw new DelegationPersistenceError(
137
+ `FileDelegationStore: failed to write ${this.filePath}: ${errorMessage(err)}`,
138
+ { cause: err }
139
+ );
140
+ }
141
+ }
142
+ };
143
+ function parsePersistedState(raw) {
144
+ const parsed = JSON.parse(raw);
145
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
146
+ throw new Error("top-level value is not an object");
147
+ }
148
+ const state = parsed;
149
+ if (state.version !== STATE_FORMAT_VERSION) {
150
+ throw new Error(`unsupported state version ${JSON.stringify(state.version)}`);
151
+ }
152
+ if (!Array.isArray(state.records)) {
153
+ throw new Error("`records` is not an array");
154
+ }
155
+ for (const record of state.records) {
156
+ if (record === null || typeof record !== "object") {
157
+ throw new Error("a record entry is not an object");
158
+ }
159
+ const candidate = record;
160
+ if (typeof candidate.taskId !== "string" || typeof candidate.status !== "string") {
161
+ throw new Error("a record entry is missing `taskId`/`status`");
162
+ }
163
+ }
164
+ return { version: STATE_FORMAT_VERSION, records: state.records };
165
+ }
166
+ function cloneRecord(record) {
167
+ return structuredClone(record);
168
+ }
169
+ function errorMessage(err) {
170
+ return err instanceof Error ? err.message : String(err);
171
+ }
172
+
5
173
  // src/mcp/task-queue.ts
6
- var DelegationTaskQueue = class {
174
+ var DelegationTaskQueue = class _DelegationTaskQueue {
7
175
  records = /* @__PURE__ */ new Map();
8
176
  controllers = /* @__PURE__ */ new Map();
9
177
  byIdempotencyKey = /* @__PURE__ */ new Map();
10
178
  generateId;
11
179
  now;
180
+ store;
181
+ resumeDelegate;
182
+ maxTerminalRecords;
183
+ onPersistError;
184
+ traceContext;
185
+ persistTail = Promise.resolve();
186
+ persistFailure;
12
187
  constructor(options = {}) {
13
188
  this.generateId = options.generateId ?? randomTaskId;
14
189
  this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
190
+ this.store = options.store ?? new InMemoryDelegationStore();
191
+ this.resumeDelegate = options.resumeDelegate;
192
+ if (options.maxTerminalRecords !== void 0) {
193
+ if (!Number.isInteger(options.maxTerminalRecords) || options.maxTerminalRecords < 1) {
194
+ throw new ValidationError(
195
+ `DelegationTaskQueue: maxTerminalRecords must be a positive integer, got ${String(options.maxTerminalRecords)}`
196
+ );
197
+ }
198
+ }
199
+ this.maxTerminalRecords = options.maxTerminalRecords ?? Number.POSITIVE_INFINITY;
200
+ this.traceContext = options.traceContext;
201
+ this.onPersistError = options.onPersistError ?? ((error) => {
202
+ queueMicrotask(() => {
203
+ throw error;
204
+ });
205
+ });
206
+ }
207
+ /**
208
+ * Construct a queue from previously-persisted state. Loads every record
209
+ * from `options.store`, rebuilds the idempotency index (so a re-submitted
210
+ * identical task returns the prior taskId and its terminal state), then:
211
+ *
212
+ * - terminal records stay queryable via `status()` / `history()`
213
+ * - in-flight records with a `detachedSessionRef` re-attach through
214
+ * `options.resumeDelegate` and report `running`
215
+ * - other in-flight records settle as failed — their driver died with
216
+ * the previous process and the result is unrecoverable
217
+ *
218
+ * The retention cap applies to the loaded set as well.
219
+ */
220
+ static async restore(options = {}) {
221
+ const queue = new _DelegationTaskQueue(options);
222
+ const loaded = await queue.store.loadAll();
223
+ queue.rehydrate(loaded);
224
+ return queue;
15
225
  }
16
226
  /**
17
227
  * Kick off a delegation in the background. Returns immediately. The
18
- * `taskId` is queryable via `status` once this method returns.
228
+ * `taskId` is queryable via `status` once this method returns. Throws
229
+ * the recorded `DelegationPersistenceError` once the store has failed —
230
+ * the queue does not accept work it cannot journal.
19
231
  */
20
232
  submit(input) {
233
+ if (this.persistFailure) throw this.persistFailure;
21
234
  if (input.idempotencyKey) {
22
235
  const existing = this.byIdempotencyKey.get(input.idempotencyKey);
23
236
  if (existing && this.records.has(existing)) {
@@ -34,11 +247,17 @@ var DelegationTaskQueue = class {
34
247
  status: "pending",
35
248
  startedAt: this.now(),
36
249
  feedback: [],
37
- idempotencyKey: input.idempotencyKey
250
+ idempotencyKey: input.idempotencyKey,
251
+ detachedSessionRef: input.detachedSessionRef,
252
+ ...this.traceContext !== void 0 ? {
253
+ traceId: this.traceContext.traceId,
254
+ ...this.traceContext.parentSpanId !== void 0 ? { parentSpanId: this.traceContext.parentSpanId } : {}
255
+ } : {}
38
256
  };
39
257
  this.records.set(taskId, record);
40
258
  this.controllers.set(taskId, controller);
41
259
  if (input.idempotencyKey) this.byIdempotencyKey.set(input.idempotencyKey, taskId);
260
+ this.persist(record);
42
261
  queueMicrotask(() => {
43
262
  this.execute(taskId, input, controller);
44
263
  });
@@ -47,11 +266,13 @@ var DelegationTaskQueue = class {
47
266
  /**
48
267
  * Snapshot the current state of a delegation. Returns `undefined` for
49
268
  * unknown ids so callers can distinguish missing from terminal.
269
+ * `includeTrace` attaches the journaled loop-trace span tree — off by
270
+ * default so status polls stay light.
50
271
  */
51
- status(taskId) {
272
+ status(taskId, opts) {
52
273
  const record = this.records.get(taskId);
53
274
  if (!record) return void 0;
54
- return toStatusResult(record);
275
+ return toStatusResult(record, opts);
55
276
  }
56
277
  /**
57
278
  * Abort an in-flight delegation. Returns `false` if the task is unknown
@@ -69,6 +290,8 @@ var DelegationTaskQueue = class {
69
290
  record.status = "cancelled";
70
291
  record.completedAt = this.now();
71
292
  record.error = { message: "cancelled by caller", kind: "CancelledError" };
293
+ this.persist(record);
294
+ this.enforceRetention();
72
295
  return true;
73
296
  }
74
297
  /**
@@ -81,6 +304,7 @@ var DelegationTaskQueue = class {
81
304
  const record = this.records.get(taskId);
82
305
  if (!record) return false;
83
306
  record.feedback.push(snapshot);
307
+ this.persist(record);
84
308
  return true;
85
309
  }
86
310
  /**
@@ -100,6 +324,15 @@ var DelegationTaskQueue = class {
100
324
  out.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
101
325
  return out.slice(0, limit);
102
326
  }
327
+ /**
328
+ * Await every journal write issued so far. Rejects with the recorded
329
+ * `DelegationPersistenceError` when any of them failed. Call before
330
+ * handing the store's backing file to another process.
331
+ */
332
+ async flush() {
333
+ await this.persistTail;
334
+ if (this.persistFailure) throw this.persistFailure;
335
+ }
103
336
  /** Test-only — number of in-flight (non-terminal) records. */
104
337
  inflightCount() {
105
338
  let n = 0;
@@ -112,26 +345,215 @@ var DelegationTaskQueue = class {
112
345
  const record = this.records.get(taskId);
113
346
  if (!record) return;
114
347
  record.status = "running";
348
+ this.persist(record);
349
+ const traceCollector = createDelegationTraceCollector((spans) => {
350
+ if (isTerminal(currentStatus(record))) return;
351
+ this.appendTrace(record, spans);
352
+ this.persist(record);
353
+ });
115
354
  try {
116
355
  const output = await input.run({
117
356
  signal: controller.signal,
118
357
  report: (progress) => {
119
- if (record.status === "running") record.progress = progress;
358
+ if (record.status === "running") {
359
+ record.progress = progress;
360
+ this.persist(record);
361
+ }
362
+ },
363
+ traceEmitter: traceCollector.emitter,
364
+ ...record.detachedSessionRef !== void 0 ? { detachedSessionRef: record.detachedSessionRef } : {},
365
+ updateDetachedSessionRef: (ref) => {
366
+ if (typeof ref !== "string" || ref.length === 0) {
367
+ throw new ValidationError(
368
+ "DelegationTaskQueue: updateDetachedSessionRef requires a non-empty ref"
369
+ );
370
+ }
371
+ if (isTerminal(currentStatus(record))) return;
372
+ record.detachedSessionRef = ref;
373
+ this.persist(record);
120
374
  }
121
375
  });
376
+ traceCollector.settle();
122
377
  if (currentStatus(record) === "cancelled") return;
123
378
  record.status = "completed";
124
379
  record.completedAt = this.now();
125
380
  record.result = { profile: input.profile, output };
381
+ this.persist(record);
382
+ this.enforceRetention();
126
383
  } catch (err) {
384
+ traceCollector.settle();
127
385
  if (currentStatus(record) === "cancelled") return;
128
386
  record.status = "failed";
129
387
  record.completedAt = this.now();
130
388
  record.error = errorToShape(err);
389
+ this.persist(record);
390
+ this.enforceRetention();
131
391
  } finally {
132
392
  this.controllers.delete(taskId);
133
393
  }
134
394
  }
395
+ appendTrace(record, spans) {
396
+ if (spans.length === 0) return;
397
+ const { trace, truncated } = capDelegationTrace([...record.trace ?? [], ...spans]);
398
+ record.trace = trace;
399
+ if (truncated) record.traceTruncated = true;
400
+ }
401
+ rehydrate(loaded) {
402
+ const records = [...loaded].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
403
+ for (const record of records) {
404
+ this.records.set(record.taskId, record);
405
+ if (record.idempotencyKey) this.byIdempotencyKey.set(record.idempotencyKey, record.taskId);
406
+ }
407
+ for (const record of this.records.values()) {
408
+ if (isTerminal(record.status)) continue;
409
+ if (record.detachedSessionRef && this.resumeDelegate) {
410
+ record.status = "running";
411
+ this.persist(record);
412
+ this.startResume(record, record.detachedSessionRef, this.resumeDelegate);
413
+ continue;
414
+ }
415
+ record.status = "failed";
416
+ record.completedAt = this.now();
417
+ record.error = {
418
+ message: record.detachedSessionRef ? `delegation driver restarted while the task was in flight; detached session "${record.detachedSessionRef}" needs a resumeDelegate to be resumed` : "delegation driver restarted while the task was in flight; the run was not detached and cannot be resumed",
419
+ kind: "DriverRestartError"
420
+ };
421
+ this.persist(record);
422
+ }
423
+ this.enforceRetention();
424
+ }
425
+ startResume(record, detachedSessionRef, driver) {
426
+ const controller = new AbortController();
427
+ this.controllers.set(record.taskId, controller);
428
+ void this.driveResume(record, detachedSessionRef, driver, controller);
429
+ }
430
+ async driveResume(record, detachedSessionRef, driver, controller) {
431
+ const intervalMs = driver.intervalMs ?? 5e3;
432
+ const resumeStartMs = Date.parse(this.now());
433
+ const ctx = {
434
+ signal: controller.signal,
435
+ report: (progress) => {
436
+ if (currentStatus(record) !== "running") return;
437
+ record.progress = progress;
438
+ this.persist(record);
439
+ }
440
+ };
441
+ try {
442
+ while (!controller.signal.aborted && currentStatus(record) === "running") {
443
+ const tick = await driver.tick({ record: structuredClone(record), detachedSessionRef }, ctx);
444
+ if (currentStatus(record) === "cancelled") return;
445
+ if (tick.state === "completed") {
446
+ this.appendResumeSpan(record, detachedSessionRef, resumeStartMs);
447
+ record.status = "completed";
448
+ record.completedAt = this.now();
449
+ record.result = {
450
+ profile: record.profile,
451
+ output: tick.output
452
+ };
453
+ if (tick.costUsd !== void 0) record.costUsd = tick.costUsd;
454
+ this.persist(record);
455
+ this.enforceRetention();
456
+ return;
457
+ }
458
+ if (tick.state === "failed") {
459
+ this.appendResumeSpan(record, detachedSessionRef, resumeStartMs, tick.error.message);
460
+ record.status = "failed";
461
+ record.completedAt = this.now();
462
+ record.error = tick.error;
463
+ this.persist(record);
464
+ this.enforceRetention();
465
+ return;
466
+ }
467
+ await abortableDelay(intervalMs, controller.signal);
468
+ }
469
+ } catch (err) {
470
+ if (currentStatus(record) === "cancelled") return;
471
+ this.appendResumeSpan(record, detachedSessionRef, resumeStartMs, errorToShape(err).message);
472
+ record.status = "failed";
473
+ record.completedAt = this.now();
474
+ record.error = errorToShape(err);
475
+ this.persist(record);
476
+ this.enforceRetention();
477
+ } finally {
478
+ this.controllers.delete(record.taskId);
479
+ }
480
+ }
481
+ /**
482
+ * Journal the resumed segment of a detached run as one compact span. The
483
+ * resume driver re-attaches after a process restart, so the original
484
+ * process's loop events are gone — this span records the post-restart
485
+ * observation window (re-attach → terminal tick) under the
486
+ * `'detached-resume'` driver tag, keeping restored delegations observable
487
+ * in the journal alongside trace-carrying live runs.
488
+ */
489
+ appendResumeSpan(record, detachedSessionRef, startMs, error) {
490
+ this.appendTrace(record, [
491
+ {
492
+ spanId: generateDelegationSpanId(),
493
+ name: "loop",
494
+ kind: "loop",
495
+ startMs,
496
+ endMs: Date.parse(this.now()),
497
+ meta: {
498
+ "tangle.loop.driver": "detached-resume",
499
+ "tangle.loop.detached_session_ref": detachedSessionRef,
500
+ ...error !== void 0 ? { "tangle.loop.error": error } : {}
501
+ }
502
+ }
503
+ ]);
504
+ }
505
+ persist(record) {
506
+ if (this.persistFailure) return;
507
+ const snapshot = structuredClone(record);
508
+ this.persistTail = this.persistTail.then(async () => {
509
+ if (this.persistFailure) return;
510
+ try {
511
+ await this.store.upsert(snapshot);
512
+ } catch (err) {
513
+ this.failPersistence(err);
514
+ }
515
+ });
516
+ }
517
+ persistRemoval(taskIds) {
518
+ if (this.persistFailure || taskIds.length === 0) return;
519
+ this.persistTail = this.persistTail.then(async () => {
520
+ if (this.persistFailure) return;
521
+ try {
522
+ await this.store.remove(taskIds);
523
+ } catch (err) {
524
+ this.failPersistence(err);
525
+ }
526
+ });
527
+ }
528
+ failPersistence(cause) {
529
+ if (this.persistFailure) return;
530
+ const error = cause instanceof DelegationPersistenceError ? cause : new DelegationPersistenceError(
531
+ `DelegationTaskQueue: store write failed: ${cause instanceof Error ? cause.message : String(cause)}`,
532
+ { cause }
533
+ );
534
+ this.persistFailure = error;
535
+ this.onPersistError(error);
536
+ }
537
+ enforceRetention() {
538
+ if (!Number.isFinite(this.maxTerminalRecords)) return;
539
+ const terminal = [];
540
+ for (const record of this.records.values()) {
541
+ if (isTerminal(record.status)) terminal.push(record);
542
+ }
543
+ const excess = terminal.length - this.maxTerminalRecords;
544
+ if (excess <= 0) return;
545
+ terminal.sort(
546
+ (a, b) => (a.completedAt ?? a.startedAt).localeCompare(b.completedAt ?? b.startedAt)
547
+ );
548
+ const evicted = terminal.slice(0, excess);
549
+ for (const record of evicted) {
550
+ this.records.delete(record.taskId);
551
+ if (record.idempotencyKey && this.byIdempotencyKey.get(record.idempotencyKey) === record.taskId) {
552
+ this.byIdempotencyKey.delete(record.idempotencyKey);
553
+ }
554
+ }
555
+ this.persistRemoval(evicted.map((record) => record.taskId));
556
+ }
135
557
  };
136
558
  function isTerminal(status) {
137
559
  return status === "completed" || status === "failed" || status === "cancelled";
@@ -145,7 +567,24 @@ function clampLimit(raw) {
145
567
  if (n <= 0) return 50;
146
568
  return Math.min(n, 500);
147
569
  }
148
- function toStatusResult(record) {
570
+ function abortableDelay(ms, signal) {
571
+ return new Promise((resolve) => {
572
+ if (signal.aborted) {
573
+ resolve();
574
+ return;
575
+ }
576
+ const onAbort = () => {
577
+ clearTimeout(timer);
578
+ resolve();
579
+ };
580
+ const timer = setTimeout(() => {
581
+ signal.removeEventListener("abort", onAbort);
582
+ resolve();
583
+ }, ms);
584
+ signal.addEventListener("abort", onAbort, { once: true });
585
+ });
586
+ }
587
+ function toStatusResult(record, opts) {
149
588
  const out = {
150
589
  taskId: record.taskId,
151
590
  profile: record.profile,
@@ -157,6 +596,12 @@ function toStatusResult(record) {
157
596
  if (record.error) out.error = record.error;
158
597
  if (record.costUsd !== void 0) out.costUsd = record.costUsd;
159
598
  if (record.completedAt) out.completedAt = record.completedAt;
599
+ if (record.traceId !== void 0) out.traceId = record.traceId;
600
+ if (record.parentSpanId !== void 0) out.parentSpanId = record.parentSpanId;
601
+ if (opts?.includeTrace === true && record.trace && record.trace.length > 0) {
602
+ out.trace = record.trace.map((span) => ({ ...span }));
603
+ if (record.traceTruncated) out.traceTruncated = true;
604
+ }
160
605
  return out;
161
606
  }
162
607
  function toHistoryEntry(record) {
@@ -165,12 +610,14 @@ function toHistoryEntry(record) {
165
610
  profile: record.profile,
166
611
  args: record.args,
167
612
  status: record.status,
168
- startedAt: record.startedAt
613
+ startedAt: record.startedAt,
614
+ hasTrace: record.trace !== void 0 && record.trace.length > 0
169
615
  };
170
616
  if (record.namespace) entry.namespace = record.namespace;
171
617
  if (record.completedAt) entry.completedAt = record.completedAt;
172
618
  if (record.costUsd !== void 0) entry.costUsd = record.costUsd;
173
619
  if (record.feedback.length > 0) entry.feedback = [...record.feedback];
620
+ if (record.traceId !== void 0) entry.traceId = record.traceId;
174
621
  return entry;
175
622
  }
176
623
  function errorToShape(err) {
@@ -351,11 +798,17 @@ function createDelegateCodeHandler(options) {
351
798
  config: args.config,
352
799
  namespace: args.namespace
353
800
  });
801
+ const detached = options.detachedDispatch === true && (args.variants ?? 1) <= 1;
354
802
  const submitted = options.queue.submit({
355
803
  profile: "coder",
356
804
  args,
357
805
  namespace: args.namespace,
358
806
  idempotencyKey,
807
+ ...detached ? {
808
+ detachedSessionRef: formatDetachedSessionRef({
809
+ sessionId: `dlg-turn-coder-${idempotencyKey}`
810
+ })
811
+ } : {},
359
812
  run: async (ctx) => options.delegate(args, ctx)
360
813
  });
361
814
  return {
@@ -702,11 +1155,17 @@ function createDelegateResearchHandler(options) {
702
1155
  variants: args.variants ?? 1,
703
1156
  config: args.config
704
1157
  });
1158
+ const detached = options.detachedDispatch === true && (args.variants ?? 1) <= 1;
705
1159
  const submitted = options.queue.submit({
706
1160
  profile: "researcher",
707
1161
  args,
708
1162
  namespace: args.namespace,
709
1163
  idempotencyKey,
1164
+ ...detached ? {
1165
+ detachedSessionRef: formatDetachedSessionRef({
1166
+ sessionId: `dlg-turn-research-${idempotencyKey}`
1167
+ })
1168
+ } : {},
710
1169
  run: async (ctx) => options.delegate(args, ctx)
711
1170
  });
712
1171
  return {
@@ -735,6 +1194,9 @@ var DELEGATION_HISTORY_DESCRIPTION = [
735
1194
  'success rate of coder delegations on this repo?". Feed the results back',
736
1195
  "into your own routing and calibration.",
737
1196
  "",
1197
+ "Each entry carries `hasTrace` \u2014 when true, the full loop-trace span tree",
1198
+ "is retrievable via delegation_status { taskId, includeTrace: true }.",
1199
+ "",
738
1200
  'Filters: `namespace` (multi-tenant scope), `profile` ("coder" | "researcher"),',
739
1201
  "`since` (ISO date \u2014 only delegations started at-or-after). `limit` defaults",
740
1202
  "to 50, capped at 500."
@@ -805,13 +1267,21 @@ var DELEGATION_STATUS_DESCRIPTION = [
805
1267
  "patch, test/typecheck results, and diff stats. For a completed research",
806
1268
  "task, `result.output` is the items + citations + proposedWrites bundle.",
807
1269
  "",
1270
+ "Pass includeTrace: true to also receive the journaled loop-trace span",
1271
+ "tree (loop \u2192 round \u2192 iteration, with placement/cost/verdict metadata).",
1272
+ "Default false \u2014 keep routine polls light.",
1273
+ "",
808
1274
  "Throws NotFoundError when taskId is unknown \u2014 never silently returns",
809
1275
  "`pending` for a typo."
810
1276
  ].join("\n");
811
1277
  var DELEGATION_STATUS_INPUT_SCHEMA = {
812
1278
  type: "object",
813
1279
  properties: {
814
- taskId: { type: "string", description: "Returned by delegate_code / delegate_research." }
1280
+ taskId: { type: "string", description: "Returned by delegate_code / delegate_research." },
1281
+ includeTrace: {
1282
+ type: "boolean",
1283
+ description: "Also return the journaled loop-trace span tree for this delegation. Default false."
1284
+ }
815
1285
  },
816
1286
  required: ["taskId"],
817
1287
  additionalProperties: false
@@ -825,12 +1295,22 @@ function validateDelegationStatusArgs(raw) {
825
1295
  if (typeof taskId !== "string" || taskId.trim().length === 0) {
826
1296
  throw new TypeError("delegation_status: `taskId` must be a non-empty string");
827
1297
  }
828
- return { taskId: taskId.trim() };
1298
+ const out = { taskId: taskId.trim() };
1299
+ if (value.includeTrace !== void 0) {
1300
+ if (typeof value.includeTrace !== "boolean") {
1301
+ throw new TypeError("delegation_status: `includeTrace` must be a boolean");
1302
+ }
1303
+ out.includeTrace = value.includeTrace;
1304
+ }
1305
+ return out;
829
1306
  }
830
1307
  function createDelegationStatusHandler(options) {
831
1308
  return async (raw) => {
832
1309
  const args = validateDelegationStatusArgs(raw);
833
- const status = options.queue.status(args.taskId);
1310
+ const status = options.queue.status(
1311
+ args.taskId,
1312
+ args.includeTrace !== void 0 ? { includeTrace: args.includeTrace } : void 0
1313
+ );
834
1314
  if (!status) {
835
1315
  throw new NotFoundError(`delegation_status: unknown taskId "${args.taskId}"`);
836
1316
  }
@@ -838,338 +1318,11 @@ function createDelegationStatusHandler(options) {
838
1318
  };
839
1319
  }
840
1320
 
841
- // src/otel-export.ts
842
- var SCOPE = { name: "@tangle-network/agent-runtime", version: "0.33.0" };
843
- var GEN_AI = {
844
- operation: "gen_ai.operation.name",
845
- agentName: "gen_ai.agent.name",
846
- conversationId: "gen_ai.conversation.id",
847
- inputTokens: "gen_ai.usage.input_tokens",
848
- outputTokens: "gen_ai.usage.output_tokens"
849
- };
850
- function createOtelExporter(config) {
851
- const resolvedEndpoint = config?.endpoint ?? (typeof process !== "undefined" ? process.env.OTEL_EXPORTER_OTLP_ENDPOINT : void 0);
852
- if (!resolvedEndpoint) return void 0;
853
- const endpoint = resolvedEndpoint;
854
- const headers = config?.headers ?? parseHeadersFromEnv();
855
- const batchSize = config?.batchSize ?? 64;
856
- const flushIntervalMs = config?.flushIntervalMs ?? 5e3;
857
- const serviceName = config?.serviceName ?? "agent-runtime";
858
- const resourceAttrs = config?.resourceAttributes ?? {};
859
- const pending = [];
860
- let timer;
861
- let stopped = false;
862
- const exporter = {
863
- exportSpan(span) {
864
- if (stopped) return;
865
- pending.push(span);
866
- if (pending.length >= batchSize) {
867
- void doFlush();
868
- }
869
- },
870
- async flush() {
871
- await doFlush();
872
- },
873
- async shutdown() {
874
- stopped = true;
875
- if (timer !== void 0) {
876
- clearInterval(timer);
877
- timer = void 0;
878
- }
879
- await doFlush();
880
- }
881
- };
882
- timer = setInterval(() => {
883
- if (pending.length > 0) void doFlush();
884
- }, flushIntervalMs);
885
- if (typeof timer === "object" && "unref" in timer) {
886
- ;
887
- timer.unref();
888
- }
889
- async function doFlush() {
890
- if (pending.length === 0) return;
891
- const batch = pending.splice(0);
892
- const body = {
893
- resourceSpans: [
894
- {
895
- resource: {
896
- attributes: toAttributes({
897
- "service.name": serviceName,
898
- ...resourceAttrs
899
- })
900
- },
901
- scopeSpans: [{ scope: SCOPE, spans: batch }]
902
- }
903
- ]
904
- };
905
- const url = `${endpoint.replace(/\/+$/, "")}/v1/traces`;
906
- try {
907
- await fetch(url, {
908
- method: "POST",
909
- headers: { "content-type": "application/json", ...headers },
910
- body: JSON.stringify(body)
911
- });
912
- } catch {
913
- }
914
- }
915
- return exporter;
916
- }
917
- function loopEventToOtelSpan(event, traceId, parentSpanId) {
918
- const spanId = generateSpanId();
919
- const attrs = {
920
- "loop.event_kind": event.kind,
921
- "loop.run_id": event.runId
922
- };
923
- for (const [k, v] of Object.entries(event.payload)) {
924
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
925
- attrs[`loop.${k}`] = v;
926
- }
927
- }
928
- const ts = msToNs(event.timestamp);
929
- return {
930
- traceId: padTraceId(traceId),
931
- spanId,
932
- parentSpanId: parentSpanId ? padSpanId(parentSpanId) : void 0,
933
- name: event.kind,
934
- kind: 1,
935
- startTimeUnixNano: ts,
936
- endTimeUnixNano: ts,
937
- attributes: toAttributes(attrs),
938
- status: { code: 1 }
939
- };
940
- }
941
- function buildLoopOtelSpans(events, traceId, rootParentSpanId) {
942
- if (events.length === 0) return [];
943
- const tid = padTraceId(traceId);
944
- const out = [];
945
- const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
946
- const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
947
- const rec = (v) => v && typeof v === "object" ? v : {};
948
- const started = events.find((e) => e.kind === "loop.started");
949
- const ended = events.find((e) => e.kind === "loop.ended");
950
- const runId = events[0]?.runId ?? "";
951
- const rootStart = started?.timestamp ?? events[0].timestamp;
952
- const rootEnd = ended?.timestamp ?? events[events.length - 1].timestamp;
953
- const rootId = generateSpanId();
954
- const make = (spanId, parentSpanId, name, startMs, endMs, attrs, statusCode = 1) => ({
955
- traceId: tid,
956
- spanId,
957
- parentSpanId: parentSpanId ? padSpanId(parentSpanId) : void 0,
958
- name,
959
- kind: 1,
960
- startTimeUnixNano: msToNs(startMs),
961
- endTimeUnixNano: msToNs(endMs),
962
- attributes: toAttributes(attrs),
963
- status: { code: statusCode }
964
- });
965
- const sp = rec(started?.payload);
966
- const rootAttrs = {
967
- [GEN_AI.operation]: "invoke_workflow",
968
- [GEN_AI.conversationId]: runId,
969
- "tangle.loop.driver": str(sp.driver) ?? "driver"
970
- };
971
- if (Array.isArray(sp.agentRunNames) && sp.agentRunNames.length > 0) {
972
- rootAttrs["tangle.loop.agents"] = sp.agentRunNames.map(String).join(",");
973
- }
974
- if (ended) {
975
- const ep = rec(ended.payload);
976
- const win = num(ep.winnerIterationIndex);
977
- if (win !== void 0) rootAttrs["tangle.loop.winner.iteration_index"] = win;
978
- const cost = num(ep.totalCostUsd);
979
- if (cost !== void 0) rootAttrs["tangle.cost.usd"] = cost;
980
- const dur = num(ep.durationMs);
981
- if (dur !== void 0) rootAttrs["tangle.loop.duration_ms"] = dur;
982
- const iters = num(ep.iterations);
983
- if (iters !== void 0) rootAttrs["tangle.loop.iterations"] = iters;
984
- }
985
- out.push(make(rootId, rootParentSpanId, "loop", rootStart, rootEnd, rootAttrs));
986
- const iterStartTs = /* @__PURE__ */ new Map();
987
- const placementByIdx = /* @__PURE__ */ new Map();
988
- let currentRoundId;
989
- let pendingRound;
990
- const flushRound = (endMs) => {
991
- if (!pendingRound) return;
992
- out.push(
993
- make(pendingRound.id, rootId, "loop.round", pendingRound.start, endMs, pendingRound.attrs)
994
- );
995
- pendingRound = void 0;
996
- };
997
- for (const e of events) {
998
- const p = rec(e.payload);
999
- switch (e.kind) {
1000
- case "loop.plan": {
1001
- flushRound(e.timestamp);
1002
- const id = generateSpanId();
1003
- const roundIdx = num(p.roundIndex) ?? 0;
1004
- const attrs = {
1005
- [GEN_AI.operation]: "invoke_workflow",
1006
- "tangle.loop.round.index": roundIdx,
1007
- "tangle.loop.move.kind": str(p.moveKind) ?? "unknown",
1008
- "tangle.loop.move.round": roundIdx,
1009
- "tangle.loop.move.width": num(p.plannedCount) ?? 0
1010
- };
1011
- const r = str(p.rationale);
1012
- if (r) attrs["tangle.loop.move.rationale"] = r;
1013
- const parent = num(p.parentIndex);
1014
- if (parent !== void 0) attrs["tangle.loop.move.parent_index"] = parent;
1015
- if (Array.isArray(p.childIndices) && p.childIndices.length > 0) {
1016
- attrs["tangle.loop.move.child_indices"] = p.childIndices.map(String).join(",");
1017
- }
1018
- pendingRound = { id, start: e.timestamp, attrs };
1019
- currentRoundId = id;
1020
- break;
1021
- }
1022
- case "loop.iteration.started": {
1023
- const idx = num(p.iterationIndex);
1024
- if (idx !== void 0) iterStartTs.set(idx, e.timestamp);
1025
- break;
1026
- }
1027
- case "loop.iteration.dispatch": {
1028
- const idx = num(p.iterationIndex);
1029
- if (idx === void 0) break;
1030
- const place = {};
1031
- const kind = str(p.placement);
1032
- if (kind) place["tangle.loop.placement.kind"] = kind;
1033
- const sid = str(p.sandboxId);
1034
- if (sid) place["tangle.sandbox.id"] = sid;
1035
- const fid = str(p.fleetId);
1036
- if (fid) place["tangle.fleet.id"] = fid;
1037
- const mid = str(p.machineId);
1038
- if (mid) place["tangle.machine.id"] = mid;
1039
- placementByIdx.set(idx, place);
1040
- break;
1041
- }
1042
- case "loop.iteration.ended": {
1043
- const idx = num(p.iterationIndex) ?? 0;
1044
- const start = iterStartTs.get(idx) ?? e.timestamp;
1045
- const err = str(p.error);
1046
- const attrs = {
1047
- [GEN_AI.operation]: "invoke_agent",
1048
- "tangle.loop.iteration.index": idx
1049
- };
1050
- const agent = str(p.agentRunName);
1051
- if (agent) attrs[GEN_AI.agentName] = agent;
1052
- const tu = rec(p.tokenUsage);
1053
- const inTok = num(tu.input);
1054
- if (inTok !== void 0) attrs[GEN_AI.inputTokens] = inTok;
1055
- const outTok = num(tu.output);
1056
- if (outTok !== void 0) attrs[GEN_AI.outputTokens] = outTok;
1057
- const cost = num(p.costUsd);
1058
- if (cost !== void 0) attrs["tangle.cost.usd"] = cost;
1059
- const verdict = rec(p.verdict);
1060
- if (typeof verdict.valid === "boolean") attrs["tangle.loop.verdict.valid"] = verdict.valid;
1061
- const score = num(verdict.score);
1062
- if (score !== void 0) attrs["tangle.loop.verdict.score"] = score;
1063
- if (err) attrs["tangle.loop.error"] = err;
1064
- const gid = num(p.groupId);
1065
- if (gid !== void 0) attrs["tangle.loop.iteration.group_id"] = gid;
1066
- const par = num(p.parentIndex);
1067
- if (par !== void 0) attrs["tangle.loop.iteration.parent_index"] = par;
1068
- const dur = num(p.durationMs);
1069
- if (dur !== void 0) attrs["tangle.loop.iteration.duration_ms"] = dur;
1070
- const preview = str(p.outputPreview);
1071
- if (preview) attrs["tangle.loop.iteration.output_preview"] = preview;
1072
- Object.assign(attrs, placementByIdx.get(idx) ?? {});
1073
- out.push(
1074
- make(
1075
- generateSpanId(),
1076
- currentRoundId ?? rootId,
1077
- "loop.iteration",
1078
- start,
1079
- e.timestamp,
1080
- attrs,
1081
- err ? 2 : 1
1082
- )
1083
- );
1084
- break;
1085
- }
1086
- case "loop.decision": {
1087
- if (pendingRound) {
1088
- const dec = str(p.decision);
1089
- if (dec) pendingRound.attrs["tangle.loop.decision"] = dec;
1090
- flushRound(e.timestamp);
1091
- }
1092
- currentRoundId = void 0;
1093
- break;
1094
- }
1095
- }
1096
- }
1097
- flushRound(rootEnd);
1098
- return out;
1099
- }
1100
- function parseHeadersFromEnv() {
1101
- if (typeof process === "undefined") return {};
1102
- const raw = process.env.OTEL_EXPORTER_OTLP_HEADERS;
1103
- if (!raw) return {};
1104
- const out = {};
1105
- for (const pair of raw.split(",")) {
1106
- const eq = pair.indexOf("=");
1107
- if (eq < 0) continue;
1108
- const key = pair.slice(0, eq).trim();
1109
- const value = pair.slice(eq + 1).trim();
1110
- if (key) out[key] = value;
1111
- }
1112
- return out;
1113
- }
1114
- function toAttributes(record) {
1115
- return Object.entries(record).map(([key, value]) => ({
1116
- key,
1117
- value: typeof value === "number" ? Number.isInteger(value) ? { intValue: value.toString() } : { doubleValue: value } : typeof value === "boolean" ? { boolValue: value } : { stringValue: value }
1118
- }));
1119
- }
1120
- function msToNs(ms) {
1121
- return (BigInt(Math.floor(ms)) * 1000000n).toString();
1122
- }
1123
- function padSpanId(id) {
1124
- const cleaned = id.replace(/-/g, "");
1125
- return cleaned.slice(0, 16).padEnd(16, "0");
1126
- }
1127
- function padTraceId(id) {
1128
- const cleaned = id.replace(/-/g, "");
1129
- return cleaned.slice(0, 32).padEnd(32, "0");
1130
- }
1131
- function generateSpanId() {
1132
- const bytes = new Uint8Array(8);
1133
- if (typeof globalThis.crypto?.getRandomValues === "function") {
1134
- globalThis.crypto.getRandomValues(bytes);
1135
- } else {
1136
- for (let i = 0; i < 8; i++) bytes[i] = Math.floor(Math.random() * 256);
1137
- }
1138
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1139
- }
1140
- var INTELLIGENCE_WIRE_VERSION = "2026-05-26.v1";
1141
- var DEFAULT_INTELLIGENCE_BASE = "https://intelligence.tangle.tools";
1142
- async function exportEvalRuns(events, config) {
1143
- if (events.length === 0) return { ok: true, status: 0, accepted: 0, rejected: [] };
1144
- const apiKey = config?.apiKey ?? (typeof process !== "undefined" ? process.env.TANGLE_API_KEY : void 0);
1145
- if (!apiKey)
1146
- throw new Error("exportEvalRuns: apiKey required (pass config.apiKey or set TANGLE_API_KEY)");
1147
- const base = config?.base ?? (typeof process !== "undefined" ? process.env.INTELLIGENCE_BASE : void 0) ?? DEFAULT_INTELLIGENCE_BASE;
1148
- const url = `${base.replace(/\/+$/, "")}/v1/ingest/eval-runs`;
1149
- const res = await fetch(url, {
1150
- method: "POST",
1151
- headers: {
1152
- "content-type": "application/json",
1153
- authorization: `Bearer ${apiKey}`,
1154
- "X-Tangle-Wire-Version": INTELLIGENCE_WIRE_VERSION,
1155
- ...config?.idempotencyKey ? { "Idempotency-Key": config.idempotencyKey } : {}
1156
- },
1157
- body: JSON.stringify({ wireVersion: INTELLIGENCE_WIRE_VERSION, events })
1158
- });
1159
- let parsed = {};
1160
- try {
1161
- parsed = await res.json();
1162
- } catch {
1163
- }
1164
- return {
1165
- ok: res.ok,
1166
- status: res.status,
1167
- accepted: parsed.accepted ?? (res.ok ? events.length : 0),
1168
- rejected: parsed.rejected ?? []
1169
- };
1170
- }
1171
-
1172
1321
  export {
1322
+ DelegationStateCorruptError,
1323
+ DelegationPersistenceError,
1324
+ InMemoryDelegationStore,
1325
+ FileDelegationStore,
1173
1326
  DelegationTaskQueue,
1174
1327
  hashIdempotencyInput,
1175
1328
  DELEGATE_CODE_TOOL_NAME,
@@ -1198,11 +1351,6 @@ export {
1198
1351
  DELEGATION_STATUS_DESCRIPTION,
1199
1352
  DELEGATION_STATUS_INPUT_SCHEMA,
1200
1353
  validateDelegationStatusArgs,
1201
- createDelegationStatusHandler,
1202
- createOtelExporter,
1203
- loopEventToOtelSpan,
1204
- buildLoopOtelSpans,
1205
- INTELLIGENCE_WIRE_VERSION,
1206
- exportEvalRuns
1354
+ createDelegationStatusHandler
1207
1355
  };
1208
- //# sourceMappingURL=chunk-JNPK46YH.js.map
1356
+ //# sourceMappingURL=chunk-RHW75JW5.js.map