@kuralle-agents/core 0.12.0 → 0.13.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 (89) hide show
  1. package/README.md +85 -0
  2. package/dist/capabilities/LivePromptAssembler.js +1 -0
  3. package/dist/flow/collectDigression.d.ts +5 -2
  4. package/dist/flow/collectDigression.js +48 -10
  5. package/dist/flow/collectUntilComplete.js +9 -1
  6. package/dist/flow/extraction.d.ts +1 -0
  7. package/dist/flow/extraction.js +4 -0
  8. package/dist/flow/runFlow.js +63 -5
  9. package/dist/index.d.ts +6 -1
  10. package/dist/index.js +4 -0
  11. package/dist/prompts/PromptBuilder.js +7 -0
  12. package/dist/prompts/types.d.ts +2 -0
  13. package/dist/runtime/InMemoryRetrievalCache.d.ts +25 -0
  14. package/dist/runtime/InMemoryRetrievalCache.js +59 -0
  15. package/dist/runtime/KnowledgeProvider.js +6 -1
  16. package/dist/runtime/Runtime.d.ts +44 -0
  17. package/dist/runtime/Runtime.js +196 -13
  18. package/dist/runtime/TokenAccumulator.d.ts +7 -0
  19. package/dist/runtime/TokenAccumulator.js +7 -0
  20. package/dist/runtime/TraceRecorder.d.ts +33 -0
  21. package/dist/runtime/TraceRecorder.js +290 -0
  22. package/dist/runtime/channels/TextDriver.js +45 -22
  23. package/dist/runtime/channels/executeModelTool.d.ts +8 -1
  24. package/dist/runtime/channels/executeModelTool.js +70 -1
  25. package/dist/runtime/channels/inputBuffer.d.ts +1 -0
  26. package/dist/runtime/channels/inputBuffer.js +9 -0
  27. package/dist/runtime/closeRun.js +11 -8
  28. package/dist/runtime/compaction.d.ts +2 -0
  29. package/dist/runtime/compaction.js +3 -2
  30. package/dist/runtime/ctx.js +110 -57
  31. package/dist/runtime/durable/RunStore.d.ts +16 -0
  32. package/dist/runtime/durable/RunStore.js +6 -0
  33. package/dist/runtime/durable/SessionRunStore.d.ts +7 -2
  34. package/dist/runtime/durable/SessionRunStore.js +136 -34
  35. package/dist/runtime/durable/idempotency.d.ts +1 -0
  36. package/dist/runtime/durable/idempotency.js +3 -0
  37. package/dist/runtime/durable/replay.js +7 -2
  38. package/dist/runtime/durable/types.d.ts +11 -0
  39. package/dist/runtime/goals.d.ts +18 -0
  40. package/dist/runtime/goals.js +158 -0
  41. package/dist/runtime/grounding/knowledge.js +1 -1
  42. package/dist/runtime/handoffContinuation.d.ts +20 -0
  43. package/dist/runtime/handoffContinuation.js +30 -0
  44. package/dist/runtime/handoffOscillation.d.ts +6 -0
  45. package/dist/runtime/handoffOscillation.js +17 -0
  46. package/dist/runtime/hostLoop.js +11 -0
  47. package/dist/runtime/index.d.ts +3 -1
  48. package/dist/runtime/index.js +1 -0
  49. package/dist/runtime/openRun.d.ts +2 -0
  50. package/dist/runtime/openRun.js +46 -17
  51. package/dist/runtime/policies/limits.d.ts +1 -0
  52. package/dist/runtime/policies/limits.js +3 -0
  53. package/dist/runtime/select.d.ts +5 -1
  54. package/dist/runtime/select.js +30 -1
  55. package/dist/runtime/turnTokenUsage.d.ts +28 -0
  56. package/dist/runtime/turnTokenUsage.js +70 -0
  57. package/dist/session/SessionStore.d.ts +6 -0
  58. package/dist/session/SessionStore.js +12 -1
  59. package/dist/session/stores/MemoryStore.d.ts +1 -1
  60. package/dist/session/stores/MemoryStore.js +16 -2
  61. package/dist/session/testing.d.ts +6 -1
  62. package/dist/session/testing.js +34 -0
  63. package/dist/session/utils.d.ts +3 -0
  64. package/dist/session/utils.js +23 -0
  65. package/dist/tools/effect/ToolExecutor.js +4 -1
  66. package/dist/tools/effect/defineTool.d.ts +2 -0
  67. package/dist/tools/effect/defineTool.js +2 -0
  68. package/dist/tracing/MemoryTraceStore.d.ts +16 -0
  69. package/dist/tracing/MemoryTraceStore.js +56 -0
  70. package/dist/tracing/OtelTraceSink.d.ts +127 -0
  71. package/dist/tracing/OtelTraceSink.js +101 -0
  72. package/dist/tracing/TraceStore.d.ts +19 -0
  73. package/dist/tracing/TraceStore.js +32 -0
  74. package/dist/tracing/index.d.ts +3 -0
  75. package/dist/tracing/index.js +3 -0
  76. package/dist/tracing/testing.d.ts +3 -0
  77. package/dist/tracing/testing.js +45 -0
  78. package/dist/types/agentConfig.d.ts +2 -1
  79. package/dist/types/channel.d.ts +14 -1
  80. package/dist/types/effectTool.d.ts +4 -0
  81. package/dist/types/index.d.ts +1 -0
  82. package/dist/types/index.js +1 -0
  83. package/dist/types/run-context.d.ts +14 -1
  84. package/dist/types/session.d.ts +10 -0
  85. package/dist/types/stream.d.ts +8 -0
  86. package/dist/types/trace.d.ts +50 -0
  87. package/dist/types/trace.js +1 -0
  88. package/guides/EXAMPLE_VERIFICATION.md +4 -4
  89. package/package.json +13 -4
package/README.md CHANGED
@@ -60,6 +60,91 @@ for await (const part of handle.events) { // events is a property, not
60
60
  await handle; // resolves to TurnResult once the stream is consumed
61
61
  ```
62
62
 
63
+ ## Single-run trace / `runOnce`
64
+
65
+ Use `runOnce` when an evaluator needs one complete, JSON-serializable turn instead
66
+ of a live stream. The trace includes the answer, tool roll-up, and nested spans.
67
+
68
+ ```ts
69
+ const trace = await runtime.runOnce({
70
+ sessionId: 'grounding-eval-42',
71
+ input: 'What was my last invoice total?',
72
+ });
73
+
74
+ const judgeContext = {
75
+ answer: trace.answer,
76
+ evidence: trace.toolResults.map(({ name, result }) => ({ name, result })),
77
+ };
78
+
79
+ const verdict = await groundingJudge(judgeContext);
80
+ console.log(verdict, trace.usedTool, trace.traceId);
81
+ ```
82
+
83
+ `runOnce` executes exactly one normal runtime turn and observes its existing event
84
+ stream. Runtime tracing also captures normal `run()` calls; trace persistence is
85
+ physically separate from the session store and durable effect journal.
86
+
87
+ ## Observability
88
+
89
+ Tracing is enabled by default with an in-process `MemoryTraceStore`. Supply a native
90
+ store for durable reads and add any number of export sinks independently of
91
+ `sessionStore`:
92
+
93
+ ```ts
94
+ import { createRuntime } from '@kuralle-agents/core';
95
+ import { RedisTraceStore } from '@kuralle-agents/redis-store';
96
+
97
+ const traceStore = new RedisTraceStore({
98
+ client,
99
+ traceTtlSeconds: 7 * 24 * 60 * 60,
100
+ });
101
+
102
+ const runtime = createRuntime({
103
+ agents: [agent],
104
+ defaultAgentId: 'support',
105
+ tracing: {
106
+ store: traceStore,
107
+ sampling: 0.1,
108
+ sinks: [externalSink],
109
+ redact: (span) => ({
110
+ ...span,
111
+ attributes: { ...span.attributes, input: undefined, output: undefined },
112
+ }),
113
+ },
114
+ });
115
+
116
+ const latest = (await runtime.listTraces('session-42'))[0];
117
+ const trace = latest ? await runtime.getTrace(latest.traceId) : null;
118
+ ```
119
+
120
+ Sink failures are swallowed and never change the run result. Redaction is off by
121
+ default for useful local debugging; use the hook above before persisting sensitive
122
+ tool inputs or outputs. Set `tracing.enabled: false` to disable capture.
123
+
124
+ ### OTLP and Langfuse
125
+
126
+ The built-in exporter uses HTTP/JSON over `fetch`, so it runs in Bun, Node.js, and
127
+ Cloudflare Workers without a Node OpenTelemetry SDK:
128
+
129
+ ```ts
130
+ import { langfuseSink, otelSink } from '@kuralle-agents/core';
131
+
132
+ const runtime = createRuntime({
133
+ agents: [agent],
134
+ defaultAgentId: 'support',
135
+ tracing: {
136
+ store: traceStore,
137
+ sinks: [
138
+ otelSink({ endpoint: 'https://collector.example.com', headers: { Authorization: 'Bearer token' } }),
139
+ langfuseSink({ publicKey: env.LANGFUSE_PUBLIC_KEY, secretKey: env.LANGFUSE_SECRET_KEY }),
140
+ ],
141
+ },
142
+ });
143
+ ```
144
+
145
+ Endpoints may include `/v1/traces`; otherwise the sink appends it. For self-hosted
146
+ Langfuse, pass its OTLP base URL as `endpoint`.
147
+
63
148
  ## Flows
64
149
 
65
150
  A flow is a node graph that enforces a multi-step procedure without embedding a 600-line SOP in a system prompt.
@@ -47,6 +47,7 @@ const INTERNAL_MEMORY_KEYS = [
47
47
  'flowStateByAgent',
48
48
  '__ariaAssistantText',
49
49
  '__ariaContextBudget',
50
+ '__goals',
50
51
  ];
51
52
  /**
52
53
  * Default LivePromptAssembler implementation.
@@ -1,6 +1,5 @@
1
1
  import type { AgentConfig } from '../types/agentConfig.js';
2
2
  import type { ChannelDriver } from '../types/channel.js';
3
- import type { CollectNode } from '../types/flow.js';
4
3
  import type { RunContext } from '../types/run-context.js';
5
4
  import type { RunState } from '../runtime/durable/types.js';
6
5
  import type { NormalizedTransition } from './normalizeTransition.js';
@@ -9,11 +8,15 @@ export interface FlowPark {
9
8
  flow: string;
10
9
  node: string;
11
10
  }
11
+ export declare function pushFlowPark(state: Record<string, unknown>, park: FlowPark): void;
12
+ export declare function popFlowPark(state: Record<string, unknown>): FlowPark | undefined;
12
13
  export declare function getFlowPark(state: Record<string, unknown>): FlowPark | undefined;
13
14
  export declare function looksLikeOffScriptQuestion(input: string): boolean;
14
15
  export interface CollectDigressionOptions {
15
16
  agent: AgentConfig;
16
- node: CollectNode;
17
+ node: {
18
+ id: string;
19
+ };
17
20
  activeFlowName: string;
18
21
  run: RunState;
19
22
  driver: ChannelDriver;
@@ -1,20 +1,57 @@
1
1
  import { resolveReplyNode } from './nodeBuilders.js';
2
2
  import { selectHostTarget } from '../runtime/select.js';
3
- const FLOW_PARK_KEY = '__flowPark';
4
- export function getFlowPark(state) {
5
- const raw = state[FLOW_PARK_KEY];
3
+ import { persistTurnUsageFromTurn } from '../runtime/turnTokenUsage.js';
4
+ const FLOW_PARK_STACK_KEY = '__flowParkStack';
5
+ const LEGACY_FLOW_PARK_KEY = '__flowPark';
6
+ const MAX_FLOW_PARK_DEPTH = 8;
7
+ function isFlowPark(raw) {
6
8
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
7
- return undefined;
9
+ return false;
8
10
  }
9
11
  const flow = raw.flow;
10
12
  const node = raw.node;
11
- if (typeof flow === 'string' && typeof node === 'string') {
12
- return { flow, node };
13
+ return typeof flow === 'string' && typeof node === 'string';
14
+ }
15
+ function getFlowParkStack(state) {
16
+ const raw = state[FLOW_PARK_STACK_KEY];
17
+ if (!Array.isArray(raw)) {
18
+ return undefined;
13
19
  }
14
- return undefined;
20
+ const stack = raw.filter(isFlowPark);
21
+ return stack.length > 0 ? stack : undefined;
22
+ }
23
+ export function pushFlowPark(state, park) {
24
+ const stack = getFlowParkStack(state) ?? [];
25
+ stack.push(park);
26
+ while (stack.length > MAX_FLOW_PARK_DEPTH) {
27
+ stack.shift();
28
+ }
29
+ state[FLOW_PARK_STACK_KEY] = stack;
15
30
  }
16
- function setFlowPark(state, park) {
17
- state[FLOW_PARK_KEY] = park;
31
+ export function popFlowPark(state) {
32
+ const stack = getFlowParkStack(state);
33
+ if (!stack) {
34
+ return undefined;
35
+ }
36
+ const park = stack.pop();
37
+ if (stack.length === 0) {
38
+ delete state[FLOW_PARK_STACK_KEY];
39
+ }
40
+ else {
41
+ state[FLOW_PARK_STACK_KEY] = stack;
42
+ }
43
+ return park;
44
+ }
45
+ export function getFlowPark(state) {
46
+ const stack = getFlowParkStack(state);
47
+ if (stack) {
48
+ return stack[stack.length - 1];
49
+ }
50
+ const legacy = state[LEGACY_FLOW_PARK_KEY];
51
+ if (isFlowPark(legacy)) {
52
+ return legacy;
53
+ }
54
+ return undefined;
18
55
  }
19
56
  function appendAssistantMessage(run, text) {
20
57
  if (!text.trim()) {
@@ -53,7 +90,7 @@ export async function runCollectDigression(options) {
53
90
  };
54
91
  }
55
92
  if (selection.kind === 'enterFlow') {
56
- setFlowPark(run.state, { flow: activeFlowName, node: node.id });
93
+ pushFlowPark(run.state, { flow: activeFlowName, node: node.id });
57
94
  return {
58
95
  kind: 'transition',
59
96
  transition: {
@@ -74,6 +111,7 @@ export async function runCollectDigression(options) {
74
111
  tools: ctx.globalTools,
75
112
  };
76
113
  const turn = await driver.runAgentTurn(resolveReplyNode(replyNode, run.state, { freeConversation: true }), ctx);
114
+ await persistTurnUsageFromTurn(ctx, turn);
77
115
  if (turn.text.trim()) {
78
116
  appendAssistantMessage(run, turn.text);
79
117
  }
@@ -1,4 +1,5 @@
1
1
  import { runCollectDigression } from './collectDigression.js';
2
+ import { persistTurnUsageFromTurn } from '../runtime/turnTokenUsage.js';
2
3
  import { hasPendingUserInput } from '../runtime/channels/inputBuffer.js';
3
4
  import { userInputToText } from '../runtime/userInput.js';
4
5
  import { resolveCollectExtractionNode } from './nodeBuilders.js';
@@ -16,8 +17,13 @@ function appendUserMessage(run, input) {
16
17
  run.messages = [...run.messages, message];
17
18
  }
18
19
  export async function collectUntilComplete(node, run, driver, ctx, options) {
20
+ // A satisfied collect defers completion for ONE pass when there is pending input,
21
+ // so a correction ("no, Tuesday") is re-extracted and overwrites before completing
22
+ // (G14). Bounded to a single pass so the loop cannot spin if a driver's awaitUser
23
+ // does not clear the pending buffer — completion no longer depends on that.
24
+ let pendingConsumed = false;
19
25
  for (;;) {
20
- if (schemaSatisfied(node, run.state)) {
26
+ if (schemaSatisfied(node, run.state) && (!hasPendingUserInput(ctx.session) || pendingConsumed)) {
21
27
  const data = projectCollectData(node, run.state);
22
28
  return normalizeTransition(await node.onComplete(data, run.state));
23
29
  }
@@ -32,6 +38,7 @@ export async function collectUntilComplete(node, run, driver, ctx, options) {
32
38
  if (hasPendingUserInput(ctx.session)) {
33
39
  const signal = await driver.awaitUser(ctx);
34
40
  appendUserMessage(run, signal.input);
41
+ pendingConsumed = true;
35
42
  }
36
43
  else if (ctx.turnInputConsumed) {
37
44
  // No fresh input to extract this turn: ask (deterministically) for the
@@ -59,6 +66,7 @@ export async function collectUntilComplete(node, run, driver, ctx, options) {
59
66
  const turn = await (driver.runExtraction
60
67
  ? driver.runExtraction(resolved, ctx)
61
68
  : driver.runAgentTurn(resolved, ctx));
69
+ await persistTurnUsageFromTurn(ctx, turn);
62
70
  mergeExtractionFromTurn(node, run, turn, ctx);
63
71
  const missingAfter = computeMissingFields(node, getCollectData(run.state, node.id));
64
72
  const advanced = missingAfter.length < missingBefore.length || schemaSatisfied(node, run.state);
@@ -1,6 +1,7 @@
1
1
  import type { CollectNode, FlowState } from '../types/flow.js';
2
2
  import type { Tool } from '../types/effectTool.js';
3
3
  import type { HarnessStreamPart } from '../types/stream.js';
4
+ export declare function resetCollect(state: FlowState, nodeId: string): void;
4
5
  export declare function getCollectData(state: FlowState, nodeId: string): Record<string, unknown>;
5
6
  export declare function incrementCollectTurns(state: FlowState, nodeId: string): number;
6
7
  export declare function computeMissingFields(node: CollectNode, data: Record<string, unknown>): string[];
@@ -6,6 +6,10 @@ function collectDataKey(nodeId) {
6
6
  function collectTurnsKey(nodeId) {
7
7
  return `__collectTurns_${nodeId}`;
8
8
  }
9
+ export function resetCollect(state, nodeId) {
10
+ delete state[collectDataKey(nodeId)];
11
+ delete state[collectTurnsKey(nodeId)];
12
+ }
9
13
  export function getCollectData(state, nodeId) {
10
14
  const raw = state[collectDataKey(nodeId)];
11
15
  if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
@@ -1,6 +1,6 @@
1
- import { getFlowPark } from './collectDigression.js';
1
+ import { popFlowPark, runCollectDigression } from './collectDigression.js';
2
2
  import { parseConfirmation } from './confirmParse.js';
3
- import { hasPendingUserInput } from '../runtime/channels/inputBuffer.js';
3
+ import { hasPendingUserInput, setPendingUserInput } from '../runtime/channels/inputBuffer.js';
4
4
  import { userInputToText } from '../runtime/userInput.js';
5
5
  import { collectUntilComplete } from './collectUntilComplete.js';
6
6
  import { isActionNode, isCollectNode, isDecideNode, isReplyNode, } from './nodeKinds.js';
@@ -10,6 +10,7 @@ import { resolveReplyNode } from './nodeBuilders.js';
10
10
  import { evaluateReplyControl } from './controlEvaluator.js';
11
11
  import { runNodeVerify, VerifyBlockedError } from './verify.js';
12
12
  import { loadRecordedSteps } from '../runtime/durable/replay.js';
13
+ import { persistTurnUsageFromTurn } from '../runtime/turnTokenUsage.js';
13
14
  import { SuspendError } from '../runtime/durable/RunStore.js';
14
15
  import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
15
16
  import { emitInteractiveOnNodeEnter } from './emitInteractive.js';
@@ -68,16 +69,24 @@ async function dispatchConfirmGate(node, run, driver, ctx) {
68
69
  return { kind: 'stay' };
69
70
  }
70
71
  let input = '';
72
+ let rawInput = '';
71
73
  if (hasPendingUserInput(ctx.session)) {
72
74
  const signal = await driver.awaitUser(ctx);
75
+ rawInput = signal.input;
73
76
  input = userInputToText(signal.input);
74
77
  appendUserMessage(run, signal.input);
75
78
  }
76
79
  else {
77
80
  input = latestUserText(run);
81
+ rawInput = input;
78
82
  }
79
- ctx.turnInputConsumed = true;
80
83
  const verdict = parseConfirmation(input);
84
+ if (verdict === 'decline') {
85
+ setPendingUserInput(ctx.session, rawInput);
86
+ }
87
+ else {
88
+ ctx.turnInputConsumed = true;
89
+ }
81
90
  const branch = verdict === 'affirm'
82
91
  ? gate.onConfirm
83
92
  : verdict === 'decline'
@@ -136,7 +145,40 @@ async function dispatchNode(node, run, driver, ctx, agent, flow) {
136
145
  return normalizeTransition(await node.decide(structured, run.state));
137
146
  }
138
147
  if (isReplyNode(node)) {
148
+ // Consume input here ONLY to feed the out-of-band digression check below, and
149
+ // only for input this turn genuinely fresh to THIS reply — gated on both:
150
+ // !turnInputConsumed: a prior node (e.g. a collect) already took the turn's
151
+ // input, so a terminal reply like `done` (next→end) must not re-digress on
152
+ // the leftover — otherwise it returns `stay`, the main loop's stay+pending
153
+ // branch re-dispatches, and completion hangs on a driver clearing the buffer.
154
+ // outOfBandControl: with OOB off there is no digression, so the reply must not
155
+ // swallow input here — it returns its transition (`next: () => 'stay'`) and the
156
+ // main loop's stay-branch owns awaitUser.
157
+ let freshUserInput = false;
158
+ if (hasPendingUserInput(ctx.session) && !ctx.turnInputConsumed && ctx.outOfBandControl) {
159
+ const signal = await driver.awaitUser(ctx);
160
+ appendUserMessage(run, signal.input);
161
+ ctx.turnInputConsumed = true;
162
+ freshUserInput = true;
163
+ }
164
+ if (freshUserInput && agent) {
165
+ const digression = await runCollectDigression({
166
+ agent,
167
+ node,
168
+ activeFlowName: flow.name,
169
+ run,
170
+ driver,
171
+ ctx,
172
+ });
173
+ if (digression.kind === 'transition') {
174
+ return digression.transition;
175
+ }
176
+ if (digression.kind === 'answeredThenResume') {
177
+ return { kind: 'stay' };
178
+ }
179
+ }
139
180
  const turn = await driver.runAgentTurn(resolveReplyNode(node, run.state), ctx);
181
+ await persistTurnUsageFromTurn(ctx, turn);
140
182
  if (ctx.outOfBandControl) {
141
183
  const decision = await evaluateReplyControl({
142
184
  node,
@@ -147,6 +189,23 @@ async function dispatchNode(node, run, driver, ctx, agent, flow) {
147
189
  if (decision.kind === 'redispatch') {
148
190
  const signal = await driver.awaitUser(ctx);
149
191
  appendUserMessage(run, signal.input);
192
+ ctx.turnInputConsumed = true;
193
+ if (agent) {
194
+ const digression = await runCollectDigression({
195
+ agent,
196
+ node,
197
+ activeFlowName: flow.name,
198
+ run,
199
+ driver,
200
+ ctx,
201
+ });
202
+ if (digression.kind === 'transition') {
203
+ return digression.transition;
204
+ }
205
+ if (digression.kind === 'answeredThenResume') {
206
+ return { kind: 'stay' };
207
+ }
208
+ }
150
209
  return dispatchNode(node, run, driver, ctx, agent, flow);
151
210
  }
152
211
  appendAssistantMessage(run, turn.text);
@@ -232,9 +291,8 @@ export async function runFlow(flow, run, driver, ctx, agent) {
232
291
  return runFlow(transition.flow, run, driver, ctx, agent);
233
292
  }
234
293
  if (transition.kind === 'end') {
235
- const park = getFlowPark(run.state);
294
+ const park = popFlowPark(run.state);
236
295
  if (park && agent) {
237
- delete run.state.__flowPark;
238
296
  const parkedFlow = agent.flows?.find((candidate) => candidate.name === park.flow);
239
297
  if (parkedFlow) {
240
298
  run.activeFlow = park.flow;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createFlowTransition, createFlowTransitionWithNode, isFlowTransition, createFlowUpdate, isFlowUpdate, } from './flows/index.js';
2
2
  export { SessionManager } from './session/SessionManager.js';
3
3
  export type { SessionStore } from './session/SessionStore.js';
4
+ export { StaleWriteError } from './session/SessionStore.js';
4
5
  export { MemoryStore } from './session/stores/MemoryStore.js';
5
6
  export { reviveSession } from './session/utils.js';
6
7
  export { DEFAULT_CHANNEL_POLICIES, applyChannelPolicy, getDefaultChannelPolicy, resolveChannelPolicy, } from './channels/index.js';
@@ -109,6 +110,10 @@ export type { ConfirmVerdict } from './flow/confirmParse.js';
109
110
  export type { Route } from './types/route.js';
110
111
  export type { TurnHandle } from './types/stream.js';
111
112
  export type { HarnessStreamPart } from './types/stream.js';
113
+ export type { AgentSpan, AgentTrace, SpanKind } from './types/trace.js';
114
+ export { MemoryTraceStore, isTraceStore, type MemoryTraceStoreOptions, type TraceListWindow, type TraceSink, type TraceStore, } from './tracing/index.js';
115
+ export { OtelTraceSink, langfuseSink, otelSink, toOtlpPayload, type LangfuseSinkOptions, type OtelTraceSinkOptions, } from './tracing/index.js';
116
+ export { TraceRecorder, runOnce, type TraceRecorderOptions } from './runtime/TraceRecorder.js';
112
117
  export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
113
118
  export type { KuralleMetadata, KuralleDataParts, KuralleUIMessage, } from './ai-sdk/uiMessageStream.js';
114
119
  export type { ChoiceOption, ResolvedSelection } from './types/selection.js';
@@ -119,7 +124,7 @@ export type { ChannelDriver, TextDriver } from './runtime/channels/index.js';
119
124
  export type { TurnResult } from './types/channel.js';
120
125
  export type { RunContext, ToolContext, ActionContext } from './types/run-context.js';
121
126
  export type { AnyTool } from './types/effectTool.js';
122
- export { createRuntime, Runtime, type HarnessConfig, type RunOptions, } from './runtime/Runtime.js';
127
+ export { createRuntime, Runtime, type HarnessConfig, type RunOptions, type TracingConfig, } from './runtime/Runtime.js';
123
128
  export type { RuntimeLike } from './runtime/RuntimeLike.js';
124
129
  export { userInputToText, mergeUserInputContents, hasMediaParts, transcribeAudioParts, type UserInputContent, } from './runtime/userInput.js';
125
130
  export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './runtime/channels/inputBuffer.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createFlowTransition, createFlowTransitionWithNode, isFlowTransition, createFlowUpdate, isFlowUpdate, } from './flows/index.js';
2
2
  export { SessionManager } from './session/SessionManager.js';
3
+ export { StaleWriteError } from './session/SessionStore.js';
3
4
  export { MemoryStore } from './session/stores/MemoryStore.js';
4
5
  export { reviveSession } from './session/utils.js';
5
6
  export { DEFAULT_CHANNEL_POLICIES, applyChannelPolicy, getDefaultChannelPolicy, resolveChannelPolicy, } from './channels/index.js';
@@ -57,6 +58,9 @@ export { createShellTool } from './tools/fs/createShellTool.js';
57
58
  export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, InlineSkillStore, } from './skills/index.js';
58
59
  export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
59
60
  export { parseConfirmation } from './flow/confirmParse.js';
61
+ export { MemoryTraceStore, isTraceStore, } from './tracing/index.js';
62
+ export { OtelTraceSink, langfuseSink, otelSink, toOtlpPayload, } from './tracing/index.js';
63
+ export { TraceRecorder, runOnce } from './runtime/TraceRecorder.js';
60
64
  export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
61
65
  export { DURABLE_RUNS_KEY } from './runtime/durable/types.js';
62
66
  export { createRuntime, Runtime, } from './runtime/Runtime.js';
@@ -235,6 +235,13 @@ export class PromptBuilder {
235
235
  priority: LAYER_PRIORITIES.CONVERSATION_SUMMARY,
236
236
  });
237
237
  }
238
+ if (memory.openGoalsPrompt) {
239
+ sections.push({
240
+ type: 'conversation_summary',
241
+ content: memory.openGoalsPrompt,
242
+ priority: LAYER_PRIORITIES.CONVERSATION_SUMMARY,
243
+ });
244
+ }
238
245
  return sections;
239
246
  }
240
247
  formatSection(section) {
@@ -49,6 +49,8 @@ export interface SessionMemory {
49
49
  label: string;
50
50
  content: string;
51
51
  }>;
52
+ /** Compact open-thread note projected from session.workingMemory.__goals (G5). */
53
+ openGoalsPrompt?: string;
52
54
  }
53
55
  export interface AgentDefinition {
54
56
  identity: string;
@@ -0,0 +1,25 @@
1
+ import type { KnowledgeRetrievalResult, RetrievalCacheAdapter } from '../types/voice.js';
2
+ /**
3
+ * Zero-config, in-process session retrieval cache (G6). Keyed by the query
4
+ * embedding: a lookup returns the cached results of the most-similar recent
5
+ * query (cosine ≥ threshold, within TTL). LRU-bounded. Needs only the embedder
6
+ * the KnowledgeProvider already requires — no external dependency. Apps that
7
+ * want a shared/vector-backed cache can inject their own `RetrievalCacheAdapter`
8
+ * via the KnowledgeProvider's `cacheFactory` instead.
9
+ */
10
+ export interface InMemoryRetrievalCacheOptions {
11
+ maxEntries?: number;
12
+ ttlMs?: number;
13
+ similarityThreshold?: number;
14
+ }
15
+ export declare class InMemoryRetrievalCache implements RetrievalCacheAdapter {
16
+ /** Ordered oldest → newest (LRU front, MRU back). */
17
+ private entries;
18
+ private readonly maxEntries;
19
+ private readonly ttlMs;
20
+ private readonly threshold;
21
+ constructor(options?: InMemoryRetrievalCacheOptions);
22
+ get size(): number;
23
+ lookup(queryEmbedding: readonly number[], topK?: number): KnowledgeRetrievalResult[];
24
+ populate(results: KnowledgeRetrievalResult[], queryEmbedding?: readonly number[]): void;
25
+ }
@@ -0,0 +1,59 @@
1
+ function cosineSimilarity(a, b) {
2
+ const n = Math.min(a.length, b.length);
3
+ if (n === 0)
4
+ return 0;
5
+ let dot = 0;
6
+ let na = 0;
7
+ let nb = 0;
8
+ for (let i = 0; i < n; i++) {
9
+ dot += a[i] * b[i];
10
+ na += a[i] * a[i];
11
+ nb += b[i] * b[i];
12
+ }
13
+ if (na === 0 || nb === 0)
14
+ return 0;
15
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
16
+ }
17
+ export class InMemoryRetrievalCache {
18
+ /** Ordered oldest → newest (LRU front, MRU back). */
19
+ entries = [];
20
+ maxEntries;
21
+ ttlMs;
22
+ threshold;
23
+ constructor(options = {}) {
24
+ this.maxEntries = options.maxEntries ?? 256;
25
+ this.ttlMs = options.ttlMs ?? 300_000;
26
+ this.threshold = options.similarityThreshold ?? 0.85;
27
+ }
28
+ get size() {
29
+ return this.entries.length;
30
+ }
31
+ lookup(queryEmbedding, topK) {
32
+ const now = Date.now();
33
+ let best;
34
+ let bestSim = this.threshold;
35
+ for (const entry of this.entries) {
36
+ if (now - entry.at > this.ttlMs)
37
+ continue;
38
+ const sim = cosineSimilarity(queryEmbedding, entry.embedding);
39
+ if (sim >= bestSim) {
40
+ bestSim = sim;
41
+ best = entry;
42
+ }
43
+ }
44
+ if (!best)
45
+ return [];
46
+ // Touch: move to MRU.
47
+ this.entries = this.entries.filter((e) => e !== best);
48
+ this.entries.push(best);
49
+ const results = best.results;
50
+ return topK === undefined ? results : results.slice(0, topK);
51
+ }
52
+ populate(results, queryEmbedding) {
53
+ if (!queryEmbedding || queryEmbedding.length === 0 || results.length === 0)
54
+ return;
55
+ this.entries.push({ embedding: queryEmbedding, results: [...results], at: Date.now() });
56
+ while (this.entries.length > this.maxEntries)
57
+ this.entries.shift();
58
+ }
59
+ }
@@ -12,6 +12,7 @@
12
12
  * The provider holds no mutable state itself — per-session cache state is
13
13
  * stored on RunContext.retrievalCache so it survives agent handoffs.
14
14
  */
15
+ import { InMemoryRetrievalCache } from './InMemoryRetrievalCache.js';
15
16
  export class KnowledgeProvider {
16
17
  config;
17
18
  retriever;
@@ -21,7 +22,11 @@ export class KnowledgeProvider {
21
22
  this.config = options.config;
22
23
  this.retriever = options.config.retriever;
23
24
  this.embedder = options.config.embedder;
24
- this.cacheFactory = options.cacheFactory;
25
+ // Default to a zero-config in-process cache when an embedder is available
26
+ // (the cache is keyed by query embedding). Apps can inject their own adapter.
27
+ this.cacheFactory =
28
+ options.cacheFactory ??
29
+ (this.embedder ? () => new InMemoryRetrievalCache(options.config.cache) : undefined);
25
30
  }
26
31
  /**
27
32
  * Create a new session-level cache instance. Called once per session
@@ -18,6 +18,19 @@ import type { PersistentMemoryStore } from '../memory/blocks/types.js';
18
18
  import { type CompactionConfig } from './compaction.js';
19
19
  import type { EscalationConfig } from '../escalation/types.js';
20
20
  import type { WakeOptions } from '../scheduler/index.js';
21
+ import type { HandoffInputFilter } from './handoffFilters.js';
22
+ import type { AgentSpan, AgentTrace } from '../types/trace.js';
23
+ import { type TraceSink, type TraceStore } from '../tracing/TraceStore.js';
24
+ export interface TracingConfig {
25
+ enabled?: boolean;
26
+ store?: TraceStore;
27
+ sinks?: TraceSink[];
28
+ redact?: (span: AgentSpan) => AgentSpan | null;
29
+ sampling?: number | ((context: {
30
+ sessionId: string;
31
+ input?: unknown;
32
+ }) => boolean);
33
+ }
21
34
  export interface HarnessConfig {
22
35
  agents: AgentConfig[];
23
36
  defaultAgentId: string;
@@ -56,6 +69,24 @@ export interface HarnessConfig {
56
69
  * invokes the handler. Resume with `runtime.resumeFromEscalation()`.
57
70
  */
58
71
  escalation?: EscalationConfig;
72
+ /** Default handoff input filter when a route does not define `filter`. */
73
+ handoffInputFilter?: HandoffInputFilter;
74
+ /**
75
+ * Silent handoff (default `true`). A transfer between agents reads as one
76
+ * continuous assistant: the transfer is a silent control tool call, and the
77
+ * target is given a continuation directive so it does not greet or
78
+ * re-introduce itself. Set `false` for an explicit visible transfer (the
79
+ * target follows its own instructions, e.g. "Bill here").
80
+ */
81
+ silentHandoff?: boolean;
82
+ /**
83
+ * Structured goal/thread tracking (G5). When enabled, a cheap control-model
84
+ * pass at turn end patches `session.workingMemory.__goals` and open threads
85
+ * are projected into the next turn's prompt. Default off — opt-in cost/latency.
86
+ */
87
+ trackGoals?: boolean;
88
+ /** Read-only observability, configured independently from durable session state. */
89
+ tracing?: TracingConfig;
59
90
  }
60
91
  export interface RunOptions {
61
92
  sessionId?: string;
@@ -75,6 +106,8 @@ export interface RunOptions {
75
106
  historyDelta?: ModelMessage[];
76
107
  driver?: ChannelDriver;
77
108
  signalDelivery?: SignalDelivery;
109
+ /** Stable key for this inbound user message; duplicate webhook retries are ignored (H2). */
110
+ idempotencyKey?: string;
78
111
  abortSignal?: AbortSignal;
79
112
  }
80
113
  export declare class Runtime {
@@ -87,10 +120,17 @@ export declare class Runtime {
87
120
  private readonly hooks?;
88
121
  private readonly activeTurnAborts;
89
122
  private readonly sessionMutex;
123
+ private readonly traceStore?;
124
+ private readonly traceSinks;
125
+ private readonly pendingTraceWrites;
90
126
  constructor(config: HarnessConfig);
91
127
  run(opts: RunOptions): TurnHandle;
128
+ runOnce(opts: RunOptions): Promise<AgentTrace>;
92
129
  stream(opts: RunOptions): TurnHandle;
93
130
  getSession(sessionId: string): Promise<Session | null>;
131
+ getTrace(traceId: string): Promise<AgentTrace | null>;
132
+ listTraces(sessionId: string): Promise<AgentTrace[]>;
133
+ getTraceStore(): TraceStore | undefined;
94
134
  getSessionStore(): SessionStore;
95
135
  deleteSession(sessionId: string): Promise<void>;
96
136
  abortSession(sessionId: string, reason?: string): void;
@@ -99,6 +139,10 @@ export declare class Runtime {
99
139
  reason?: string;
100
140
  markedBy?: ConversationOutcomeMarkedBy;
101
141
  }): Promise<void>;
142
+ private shouldTrace;
143
+ private writeSpan;
144
+ private settleTraceWrites;
145
+ private flushTraceSinks;
102
146
  /**
103
147
  * Compact `runState.messages` when over the configured trigger (or always,
104
148
  * when `force`). Persists both the run state and the session message mirror.