@tangle-network/agent-app 0.60.198 → 0.60.199

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 (41) hide show
  1. package/dist/chat-routes/index.js +6 -4
  2. package/dist/chat-routes/index.js.map +1 -1
  3. package/dist/chat-routes/turn-routes.d.ts +28 -0
  4. package/dist/{chunk-XJEIGVMQ.js → chunk-2OWT3TNV.js} +2 -2
  5. package/dist/{chunk-XP77DJEF.js → chunk-6XXKFYIM.js} +72 -30
  6. package/dist/chunk-6XXKFYIM.js.map +1 -0
  7. package/dist/chunk-7XQIY2LU.js +151 -0
  8. package/dist/chunk-7XQIY2LU.js.map +1 -0
  9. package/dist/chunk-BKPJFES3.js +229 -0
  10. package/dist/chunk-BKPJFES3.js.map +1 -0
  11. package/dist/{chunk-SUIH5B55.js → chunk-RJJRRR5Z.js} +184 -16
  12. package/dist/chunk-RJJRRR5Z.js.map +1 -0
  13. package/dist/{chunk-FPOOBGRG.js → chunk-T4BLISYH.js} +40 -5
  14. package/dist/chunk-T4BLISYH.js.map +1 -0
  15. package/dist/public-consultation/index.d.ts +1 -1
  16. package/dist/public-consultation/index.js +2 -1
  17. package/dist/public-consultation/index.js.map +1 -1
  18. package/dist/runtime/index.js +4 -2
  19. package/dist/runtime/index.js.map +1 -1
  20. package/dist/runtime/router-turn.d.ts +14 -1
  21. package/dist/runtime/router-turn.js +3 -1
  22. package/dist/sandbox/index.d.ts +19 -0
  23. package/dist/sandbox/index.js +2 -1
  24. package/dist/sandbox/prompt-trace.d.ts +20 -0
  25. package/dist/sandbox/sandbox-run-tool.d.ts +8 -0
  26. package/dist/trace/index.d.ts +0 -1
  27. package/dist/trace/index.js +0 -240
  28. package/dist/trace/index.js.map +1 -1
  29. package/dist/turn-trace/index.d.ts +11 -0
  30. package/dist/turn-trace/index.js +121 -0
  31. package/dist/turn-trace/index.js.map +1 -0
  32. package/dist/turn-trace/otel.d.ts +36 -0
  33. package/dist/turn-trace/recorder.d.ts +122 -0
  34. package/dist/turn-trace/server-timing.d.ts +15 -0
  35. package/dist/turn-trace/traced-fetch.d.ts +31 -0
  36. package/package.json +6 -1
  37. package/dist/chunk-FPOOBGRG.js.map +0 -1
  38. package/dist/chunk-SUIH5B55.js.map +0 -1
  39. package/dist/chunk-XP77DJEF.js.map +0 -1
  40. package/dist/trace/stage-timing.d.ts +0 -61
  41. /package/dist/{chunk-XJEIGVMQ.js.map → chunk-2OWT3TNV.js.map} +0 -0
@@ -55,6 +55,7 @@ import { type InteractionAnswerRoute, type InteractionAnswerRouteOptions } from
55
55
  import { type PersistedChatMessageForTurn, type TurnEventStore } from '../stream/index';
56
56
  import type { ModelFailoverAttempt } from '../model-resolution/failover';
57
57
  import { type ModelPolicy } from '../policy-filters/index';
58
+ import { type TurnSpanAttrs, type TurnTrace, type TurnTraceSink, type TurnTraceSummary } from '../turn-trace/recorder';
58
59
  import { type TurnRoutePhase } from './turn-progress';
59
60
  import { type ChatTurnPartInput, type ChatTurnRequestPayload } from './wire';
60
61
  /** Usage receipt persisted onto the assistant message (the flattened
@@ -261,6 +262,14 @@ export interface ChatTurnProduceArgs<TContext> {
261
262
  intentLine?: Promise<string | null>;
262
263
  /** Server-authenticated limits for this turn. Never copied from the body. */
263
264
  executionLimits?: ChatTurnExecutionLimits;
265
+ /**
266
+ * The turn's trace (`@tangle-network/agent-app/turn-trace`), bound to this turn's execution id. Pass it to the
267
+ * Router engine (`trace`), `streamSandboxPrompt` and `ensureWorkspaceSandbox` so the turn's model calls, tool
268
+ * calls and box claim are recorded under it; its `traceId` is what those calls send as `x-tangle-trace-id`.
269
+ * The route records admission and persistence itself. Optional for source compatibility with synthetic
270
+ * producers; every `createChatTurnRoutes` invocation supplies it.
271
+ */
272
+ trace?: TurnTrace;
264
273
  /** The durable `role:'user'` row this turn is anchored to — the row the
265
274
  * factory just inserted, or the one retry-dedup REUSED. Products anchor
266
275
  * optimistic-bubble swaps, retry targeting, and stop-polling to it, and it
@@ -367,6 +376,8 @@ interface ChatTurnLifecycleBase<TContext> {
367
376
  /** When the turn's first visible word streamed, measured from `onTurnStart`. Absent when no word streamed. */
368
377
  interface ChatTurnLifecycleTiming {
369
378
  firstTextMs?: number;
379
+ /** Where the turn's wall time went: its share inside named spans and the time per kind of span. */
380
+ trace?: TurnTraceSummary;
370
381
  }
371
382
  /** Define lifecycle start event with context and timestamp for a chat turn */
372
383
  export interface ChatTurnLifecycleStart<TContext> extends ChatTurnLifecycleBase<TContext> {
@@ -453,6 +464,16 @@ export interface ChatTurnCompleteInput<TContext> {
453
464
  * same id as `DetachedTurnResult.messageId`. */
454
465
  assistantMessageId: string | null;
455
466
  }
467
+ /** Where a finished turn's trace goes, and the host's labels for its root span. Metadata only: never content. */
468
+ export interface ChatTurnTraceOptions<TContext> {
469
+ sink?: TurnTraceSink;
470
+ /** Labels for the root span, e.g. the workspace id. Called once per exported turn. */
471
+ labels?(info: {
472
+ identity: ChatTurnIdentity;
473
+ executionId: string;
474
+ context: TContext;
475
+ }): TurnSpanAttrs;
476
+ }
456
477
  /** Define options to configure chat turn routes including authorization, storage, and event buffering */
457
478
  export interface CreateChatTurnRoutesOptions<TContext = void> {
458
479
  /** Names the product in `deriveExecutionId` so retries land on the same
@@ -606,6 +627,13 @@ export interface CreateChatTurnRoutesOptions<TContext = void> {
606
627
  }, context: TContext): void | Promise<void>;
607
628
  /** Trace flush handed to `waitUntil` (OTLP export). */
608
629
  traceFlush?(context: TContext): Promise<void>;
630
+ /**
631
+ * Where each finished turn's trace goes (`createIntelligenceTurnTraceSink`). The route always records the
632
+ * turn's trace (`ChatTurnProduceArgs.trace`) and reports its summary to `lifecycle`; this sends the spans on.
633
+ * The sink runs under `waitUntil` after the lifecycle hooks, and its failure never fails a turn. A turn handed
634
+ * to a durable owner (`handoffCompletion`) is exported by that owner, not here.
635
+ */
636
+ turnTrace?: ChatTurnTraceOptions<TContext>;
609
637
  /** Compose the interaction-answer endpoints (`/interactions`). Omit when the
610
638
  * product has no sidecar ask channel. */
611
639
  interactions?: InteractionAnswerRouteOptions;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runRouterTurn
3
- } from "./chunk-FPOOBGRG.js";
3
+ } from "./chunk-T4BLISYH.js";
4
4
 
5
5
  // src/runtime/protected-model.ts
6
6
  import {
@@ -219,4 +219,4 @@ export {
219
219
  createRouterProtectedModelPort,
220
220
  runProtectedRouterTurn
221
221
  };
222
- //# sourceMappingURL=chunk-XJEIGVMQ.js.map
222
+ //# sourceMappingURL=chunk-2OWT3TNV.js.map
@@ -34,6 +34,9 @@ import {
34
34
  import {
35
35
  checkModelPolicy
36
36
  } from "./chunk-XELWWXJJ.js";
37
+ import {
38
+ createTurnTrace
39
+ } from "./chunk-BKPJFES3.js";
37
40
 
38
41
  // src/chat-routes/draft-persistence.ts
39
42
  var CONTENT_EVENT_TYPES = /* @__PURE__ */ new Set([
@@ -378,12 +381,13 @@ function createChatTurnRoutes(options) {
378
381
  return Promise.race([generated, deadline]).finally(() => clearTimeout(timer));
379
382
  }
380
383
  async function turn(request, ctx) {
381
- const admitted = await admitTurn(request);
384
+ const admitted = await admitTurn(request, createTurnTrace());
382
385
  if (!admitted.ok) return admitted.response;
383
386
  if (request.headers.get(TURN_PROGRESS_HEADER) !== TURN_PROGRESS_FIRST) return runTurn(request, ctx, admitted);
384
387
  return progressFirstTurn(request, ctx, admitted);
385
388
  }
386
- async function admitTurn(request) {
389
+ async function admitTurn(request, trace) {
390
+ const parse = trace.start("admission.parse", "admission");
387
391
  const [rawBody, badBody] = await parseJsonObjectBody(request);
388
392
  if (badBody) return { ok: false, response: badBody };
389
393
  let parsed;
@@ -393,21 +397,23 @@ function createChatTurnRoutes(options) {
393
397
  if (err instanceof ChatTurnInputError) return { ok: false, response: errorResponse(err) };
394
398
  throw err;
395
399
  }
396
- const auth = await options.authorize({ request, intent: "turn", body: parsed.payload });
400
+ parse.end();
401
+ const auth = await trace.measure("admission.authorize", "admission", () => options.authorize({ request, intent: "turn", body: parsed.payload }));
397
402
  if (!auth.ok) return { ok: false, response: auth.response };
398
403
  const namedModel = typeof parsed.payload.model === "string" ? parsed.payload.model.trim() : "";
399
- if (options.modelPolicy && namedModel) {
400
- const decision = await checkModelPolicy(
404
+ const policy = options.modelPolicy;
405
+ if (policy && namedModel) {
406
+ const decision = await trace.measure("admission.model_policy", "admission", () => checkModelPolicy(
401
407
  namedModel,
402
- options.modelPolicy.catalog,
403
- options.modelPolicy.filter,
404
- options.modelPolicy.context
405
- );
408
+ policy.catalog,
409
+ policy.filter,
410
+ policy.context
411
+ ));
406
412
  if (!decision.allowed) {
407
413
  return { ok: false, response: Response.json({ code: "model_policy_denied", error: decision.reason }, { status: 403 }) };
408
414
  }
409
415
  }
410
- return { ok: true, parsed, auth };
416
+ return { ok: true, parsed, auth, trace };
411
417
  }
412
418
  function progressFirstTurn(request, ctx, admitted) {
413
419
  const receivedAt = Date.now();
@@ -490,7 +496,7 @@ function createChatTurnRoutes(options) {
490
496
  });
491
497
  }
492
498
  async function runTurn(request, ctx, admitted, progress, startedIntent) {
493
- const { parsed, auth } = admitted;
499
+ const { parsed, auth, trace } = admitted;
494
500
  const intentLine = startedIntent ?? startIntentLine(admitted);
495
501
  const { payload, content, fileParts, mentions, turnId } = parsed;
496
502
  const { tenantId, userId, context } = auth;
@@ -503,8 +509,9 @@ function createChatTurnRoutes(options) {
503
509
  log("[chat-routes] turnLock.warm failed", { error: err instanceof Error ? err.message : String(err) });
504
510
  }
505
511
  }
506
- if (options.prepareTurn) {
507
- const prepared = await options.prepareTurn({
512
+ const prepareTurn = options.prepareTurn;
513
+ if (prepareTurn) {
514
+ const prepared = await trace.measure("admission.prepare", "admission", () => prepareTurn({
508
515
  request,
509
516
  body: payload,
510
517
  tenantId,
@@ -512,7 +519,7 @@ function createChatTurnRoutes(options) {
512
519
  threadId: payload.threadId,
513
520
  context,
514
521
  progress: (phase, message) => progress?.(phase, message)
515
- });
522
+ }));
516
523
  if (!prepared.ok) return prepared.response;
517
524
  }
518
525
  const readHistory = async () => (await options.store.listMessages(payload.threadId)).map((m) => ({
@@ -521,6 +528,7 @@ function createChatTurnRoutes(options) {
521
528
  content: m.content,
522
529
  parts: m.parts ?? null
523
530
  }));
531
+ const historySpan = trace.start("admission.history", "admission");
524
532
  const existingMessages = await readHistory();
525
533
  const historyBeforeLock = options.turnLock ? JSON.stringify(existingMessages) : void 0;
526
534
  let hasRunningTurn = false;
@@ -534,6 +542,7 @@ function createChatTurnRoutes(options) {
534
542
  });
535
543
  }
536
544
  }
545
+ historySpan.end();
537
546
  const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId, hasRunningTurn });
538
547
  const identity = {
539
548
  tenantId,
@@ -548,6 +557,7 @@ function createChatTurnRoutes(options) {
548
557
  });
549
558
  const executionId = payload.retryAttemptId ? `${turnExecutionId}:retry:${payload.retryAttemptId}` : turnExecutionId;
550
559
  if (executionId.length > 256) return errorResponse(new ChatTurnInputError("Retry execution id exceeds 256 bytes"));
560
+ trace.bind(executionId);
551
561
  const turnStreamId = crypto.randomUUID();
552
562
  const prompt = fileParts.length === 0 ? content : content ? [{ type: "text", text: content }, ...fileParts] : [...fileParts];
553
563
  let draft;
@@ -581,6 +591,7 @@ function createChatTurnRoutes(options) {
581
591
  turnStreamId,
582
592
  priorMessages: chatTurn.priorMessages,
583
593
  handoffCompletion,
594
+ trace,
584
595
  ...intentLine ? { intentLine } : {},
585
596
  ...ctx?.executionLimits ? { executionLimits: ctx.executionLimits } : {}
586
597
  };
@@ -599,8 +610,9 @@ function createChatTurnRoutes(options) {
599
610
  });
600
611
  }
601
612
  };
602
- if (options.turnLock) {
603
- const acquired = await options.turnLock.acquire(produceArgs);
613
+ const turnLock = options.turnLock;
614
+ if (turnLock) {
615
+ const acquired = await trace.measure("admission.lock", "admission", () => turnLock.acquire(produceArgs));
604
616
  if (!acquired.acquired) return acquired.response;
605
617
  lockAcquired = true;
606
618
  lockHandle = acquired.handle;
@@ -619,12 +631,17 @@ function createChatTurnRoutes(options) {
619
631
  const fireTerminalLifecycle = async (failed, terminalError) => {
620
632
  if (lifecycleSettled) return;
621
633
  lifecycleSettled = true;
634
+ const failureCode = failed ? failureCodeOf(lastFailureData) ?? (terminalError instanceof Error ? terminalError.name : "turn_failed") : void 0;
635
+ const snapshot = trace.finish(failureCode === void 0 ? {} : { error: failureCode });
622
636
  const lifecycle = options.lifecycle;
623
- if (!lifecycle) return;
624
637
  const durationMs = Date.now() - turnStartedAtMs;
625
- const timing = firstTextAtMs === void 0 ? {} : { firstTextMs: firstTextAtMs - turnStartedAtMs };
638
+ const timing = {
639
+ ...firstTextAtMs === void 0 ? {} : { firstTextMs: firstTextAtMs - turnStartedAtMs },
640
+ trace: snapshot.summary
641
+ };
626
642
  try {
627
- if (failed) {
643
+ if (!lifecycle) {
644
+ } else if (failed) {
628
645
  await lifecycle.onTurnError?.({
629
646
  identity,
630
647
  executionId,
@@ -662,9 +679,28 @@ function createChatTurnRoutes(options) {
662
679
  error: err instanceof Error ? err.message : String(err)
663
680
  });
664
681
  }
682
+ const traceOptions = options.turnTrace;
683
+ if (traceOptions?.sink) {
684
+ try {
685
+ await traceOptions.sink(snapshot, {
686
+ "tangle.sessionId": identity.sessionId,
687
+ "tangle.userId": identity.userId,
688
+ "tangle.tenantId": identity.tenantId,
689
+ "tangle.turn.failed": failed,
690
+ ...gatedTurn ? { "tangle.turn.gated": true } : {},
691
+ ...failureCode ? { "tangle.turn.failure_code": failureCode } : {},
692
+ ...traceOptions.labels?.({ identity, executionId, context }) ?? {}
693
+ });
694
+ } catch (err) {
695
+ log("[chat-routes] turn trace sink failed", {
696
+ turnId: turnStreamId,
697
+ error: err instanceof Error ? err.message : String(err)
698
+ });
699
+ }
700
+ }
665
701
  };
666
702
  try {
667
- if (lockAcquired && JSON.stringify(await readHistory()) !== historyBeforeLock) {
703
+ if (lockAcquired && JSON.stringify(await trace.measure("admission.history_recheck", "admission", readHistory)) !== historyBeforeLock) {
668
704
  await releaseLock();
669
705
  return Response.json({
670
706
  code: "chat_history_changed",
@@ -675,17 +711,18 @@ function createChatTurnRoutes(options) {
675
711
  let userMessageId = chatTurn.reusedUserMessageId ?? null;
676
712
  if (insertUserMessage) {
677
713
  userMessageId = rowIdOf(
678
- await options.store.appendMessage({
714
+ await trace.measure("admission.persist_user", "admission", () => options.store.appendMessage({
679
715
  threadId: payload.threadId,
680
716
  role: "user",
681
717
  content,
682
718
  parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions)
683
- })
719
+ }))
684
720
  );
685
721
  }
686
722
  produceArgs = { ...produceArgs, userMessageId };
687
- if (options.contextGate) {
688
- const gate = await options.contextGate(produceArgs);
723
+ const contextGate = options.contextGate;
724
+ if (contextGate) {
725
+ const gate = await trace.measure("admission.gate", "admission", () => contextGate(produceArgs));
689
726
  if (gate.proceed === "handoff") {
690
727
  await releaseLock();
691
728
  return gate.response;
@@ -715,10 +752,12 @@ function createChatTurnRoutes(options) {
715
752
  return gate.response;
716
753
  }
717
754
  }
718
- if (options.beforeTurn) {
719
- const patch = await options.beforeTurn(produceArgs);
755
+ const beforeTurn = options.beforeTurn;
756
+ if (beforeTurn) {
757
+ const patch = await trace.measure("admission.before_turn", "admission", () => beforeTurn(produceArgs));
720
758
  if (patch) produceArgs = { ...produceArgs, ...patch };
721
759
  }
760
+ const streamSetup = trace.start("admission.stream_setup", "admission");
722
761
  tap = createBufferedTurnTap({
723
762
  store: options.turnStore,
724
763
  turnId: turnStreamId,
@@ -766,6 +805,7 @@ function createChatTurnRoutes(options) {
766
805
  });
767
806
  }
768
807
  }
808
+ streamSetup.end();
769
809
  const result = handleChatTurn({
770
810
  identity,
771
811
  waitUntil: ctx?.waitUntil,
@@ -775,7 +815,7 @@ function createChatTurnRoutes(options) {
775
815
  // defer it into the generator's first pull.
776
816
  produce: () => ({
777
817
  stream: (async function* () {
778
- producer = await options.produce(produceArgs);
818
+ producer = await trace.measure("admission.produce", "admission", () => options.produce(produceArgs));
779
819
  let source = producer.stream;
780
820
  if (ctx?.cancelOnDisconnect) {
781
821
  source = abortOnSignal(source, request.signal);
@@ -801,7 +841,7 @@ function createChatTurnRoutes(options) {
801
841
  if (options.onEvent) await options.onEvent(event, context);
802
842
  },
803
843
  ...options.transformFinalText ? { transformFinalText: options.transformFinalText } : {},
804
- persistAssistantMessage: async ({ finalText }) => {
844
+ persistAssistantMessage: ({ finalText }) => trace.measure("persist.assistant", "persist", async () => {
805
845
  if (completionHandoff) return;
806
846
  if (ctx?.cancelOnDisconnect && request.signal.aborted) {
807
847
  await draft?.discard();
@@ -849,7 +889,7 @@ function createChatTurnRoutes(options) {
849
889
  ...values
850
890
  })
851
891
  );
852
- },
892
+ }),
853
893
  ...options.onTurnComplete ? {
854
894
  // Wired into the engine's completion hook, which fires only when
855
895
  // the stream ended without throwing. A terminal error EVENT
@@ -901,6 +941,7 @@ function createChatTurnRoutes(options) {
901
941
  await tap.detach();
902
942
  return;
903
943
  }
944
+ const finalize = trace.start("persist.finalize", "persist");
904
945
  if (ctx?.cancelOnDisconnect && request.signal.aborted) {
905
946
  await draft?.discard();
906
947
  } else {
@@ -914,6 +955,7 @@ function createChatTurnRoutes(options) {
914
955
  error: err instanceof Error ? err.message : String(err)
915
956
  });
916
957
  }
958
+ finalize.end();
917
959
  await fireTerminalLifecycle(failed, drainError);
918
960
  await releaseLock();
919
961
  })();
@@ -1285,4 +1327,4 @@ export {
1285
1327
  createChatTurnRoutes,
1286
1328
  streamChatRouteAsSandboxEvents
1287
1329
  };
1288
- //# sourceMappingURL=chunk-XP77DJEF.js.map
1330
+ //# sourceMappingURL=chunk-6XXKFYIM.js.map