@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
@@ -0,0 +1,290 @@
1
+ export class TraceRecorder {
2
+ trace;
3
+ root;
4
+ currentFlow;
5
+ currentNode;
6
+ openTools = [];
7
+ toolCallIds = new Map();
8
+ emitted = new Set();
9
+ onSpan;
10
+ constructor(options = {}) {
11
+ const startedAt = Date.now();
12
+ const sessionId = options.sessionId ?? '';
13
+ const traceId = crypto.randomUUID().replaceAll('-', '');
14
+ this.onSpan = options.onSpan;
15
+ this.root = {
16
+ traceId,
17
+ spanId: newSpanId(),
18
+ name: 'turn',
19
+ kind: 'turn',
20
+ startTime: startedAt,
21
+ status: 'ok',
22
+ attributes: {
23
+ sessionId,
24
+ ...(options.input !== undefined ? { input: toJsonValue(options.input) } : {}),
25
+ },
26
+ };
27
+ this.trace = {
28
+ traceId,
29
+ sessionId,
30
+ spans: [this.root],
31
+ answer: '',
32
+ usedTool: false,
33
+ toolCalls: [],
34
+ toolResults: [],
35
+ startedAt,
36
+ };
37
+ }
38
+ record(part) {
39
+ try {
40
+ const at = Date.now();
41
+ switch (part.type) {
42
+ case 'text-delta':
43
+ this.trace.answer += part.delta;
44
+ break;
45
+ case 'flow-enter':
46
+ this.closeNode(at);
47
+ this.closeFlow(at);
48
+ this.currentFlow = this.openSpan({
49
+ name: `flow:${part.flow}`,
50
+ kind: 'flow',
51
+ parentSpanId: this.root.spanId,
52
+ at,
53
+ attributes: { activeFlow: part.flow },
54
+ });
55
+ break;
56
+ case 'flow-end':
57
+ this.closeNode(at);
58
+ this.closeFlow(at);
59
+ break;
60
+ case 'node-enter':
61
+ this.closeNode(at);
62
+ this.currentNode = this.openSpan({
63
+ name: `node:${part.nodeName}`,
64
+ kind: 'node',
65
+ parentSpanId: this.currentFlow?.spanId ?? this.root.spanId,
66
+ at,
67
+ attributes: {
68
+ ...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
69
+ nodeId: part.nodeName,
70
+ },
71
+ });
72
+ break;
73
+ case 'node-exit':
74
+ this.closeNode(at);
75
+ break;
76
+ case 'tool-call': {
77
+ const span = this.openSpan({
78
+ name: `tool:${part.toolName}`,
79
+ kind: 'tool',
80
+ parentSpanId: this.currentNode?.spanId ?? this.root.spanId,
81
+ at,
82
+ attributes: {
83
+ ...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
84
+ ...(this.currentNode ? { nodeId: nodeName(this.currentNode) } : {}),
85
+ toolName: part.toolName,
86
+ input: toJsonValue(part.args),
87
+ },
88
+ });
89
+ this.toolCallIds.set(span, part.toolCallId);
90
+ this.openTools.push(span);
91
+ this.trace.usedTool = true;
92
+ this.trace.toolCalls.push({ name: part.toolName, args: toJsonValue(part.args) });
93
+ break;
94
+ }
95
+ case 'tool-result': {
96
+ this.trace.usedTool = true;
97
+ const result = toJsonValue(part.result);
98
+ this.trace.toolResults.push({ name: part.toolName, result });
99
+ const span = this.takeToolSpan(part.toolName, part.toolCallId) ??
100
+ this.openSpan({
101
+ name: `tool:${part.toolName}`,
102
+ kind: 'tool',
103
+ parentSpanId: this.currentNode?.spanId ?? this.root.spanId,
104
+ at,
105
+ attributes: {
106
+ ...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
107
+ ...(this.currentNode ? { nodeId: nodeName(this.currentNode) } : {}),
108
+ toolName: part.toolName,
109
+ },
110
+ });
111
+ span.attributes.output = result;
112
+ span.endTime = at;
113
+ this.emitSpan(span);
114
+ break;
115
+ }
116
+ case 'handoff': {
117
+ const span = this.openSpan({
118
+ name: `handoff:${part.targetAgent}`,
119
+ kind: 'handoff',
120
+ parentSpanId: this.root.spanId,
121
+ at,
122
+ attributes: { handoffTo: part.targetAgent },
123
+ });
124
+ span.endTime = at;
125
+ this.emitSpan(span);
126
+ break;
127
+ }
128
+ case 'error': {
129
+ const span = this.openTools.at(-1) ?? this.currentNode ?? this.currentFlow ?? this.root;
130
+ span.status = 'error';
131
+ span.attributes.error = part.error;
132
+ this.root.status = 'error';
133
+ this.root.attributes.error = part.error;
134
+ break;
135
+ }
136
+ case 'done':
137
+ this.setSessionId(part.sessionId);
138
+ this.root.attributes.output = this.trace.answer;
139
+ if (part.usage) {
140
+ if (typeof part.usage.inputTokens === 'number')
141
+ this.root.attributes.tokensIn = part.usage.inputTokens;
142
+ if (typeof part.usage.outputTokens === 'number')
143
+ this.root.attributes.tokensOut = part.usage.outputTokens;
144
+ if (typeof part.usage.contextTokens === 'number')
145
+ this.root.attributes.contextTokens = part.usage.contextTokens;
146
+ }
147
+ this.close(at);
148
+ break;
149
+ }
150
+ }
151
+ catch {
152
+ // Tracing is observational: malformed telemetry must never affect the run.
153
+ }
154
+ }
155
+ finish(result) {
156
+ try {
157
+ if (!this.trace.answer) {
158
+ this.trace.answer = result.text;
159
+ }
160
+ this.root.attributes.output = this.trace.answer;
161
+ this.close(Date.now());
162
+ }
163
+ catch {
164
+ // Returning a partial trace is preferable to changing run behavior.
165
+ }
166
+ return this.trace;
167
+ }
168
+ openSpan(args) {
169
+ const span = {
170
+ traceId: this.trace.traceId,
171
+ spanId: newSpanId(),
172
+ parentSpanId: args.parentSpanId,
173
+ name: args.name,
174
+ kind: args.kind,
175
+ startTime: args.at,
176
+ status: 'ok',
177
+ attributes: { sessionId: this.trace.sessionId, ...args.attributes },
178
+ };
179
+ this.trace.spans.push(span);
180
+ return span;
181
+ }
182
+ takeToolSpan(toolName, toolCallId) {
183
+ const index = this.openTools.findIndex((span) => {
184
+ const spanCallId = this.toolCallIds.get(span);
185
+ return toolCallId ? spanCallId === toolCallId : span.attributes.toolName === toolName;
186
+ });
187
+ if (index < 0)
188
+ return undefined;
189
+ const span = this.openTools.splice(index, 1)[0];
190
+ if (span)
191
+ this.toolCallIds.delete(span);
192
+ return span;
193
+ }
194
+ closeTools(at) {
195
+ for (const span of this.openTools.splice(0)) {
196
+ span.endTime ??= at;
197
+ this.toolCallIds.delete(span);
198
+ this.emitSpan(span);
199
+ }
200
+ }
201
+ closeNode(at) {
202
+ this.closeTools(at);
203
+ if (this.currentNode) {
204
+ this.currentNode.endTime ??= at;
205
+ this.emitSpan(this.currentNode);
206
+ this.currentNode = undefined;
207
+ }
208
+ }
209
+ closeFlow(at) {
210
+ if (this.currentFlow) {
211
+ this.currentFlow.endTime ??= at;
212
+ this.emitSpan(this.currentFlow);
213
+ this.currentFlow = undefined;
214
+ }
215
+ }
216
+ setSessionId(sessionId) {
217
+ this.trace.sessionId = sessionId;
218
+ for (const span of this.trace.spans) {
219
+ span.attributes.sessionId = sessionId;
220
+ }
221
+ }
222
+ close(at) {
223
+ this.closeNode(at);
224
+ this.closeFlow(at);
225
+ this.trace.endedAt ??= at;
226
+ this.root.endTime ??= at;
227
+ this.emitSpan(this.root);
228
+ }
229
+ emitSpan(span) {
230
+ if (this.emitted.has(span.spanId) || span.endTime === undefined)
231
+ return;
232
+ this.emitted.add(span.spanId);
233
+ try {
234
+ this.onSpan?.(structuredClone(span));
235
+ }
236
+ catch {
237
+ // A tracing callback is never allowed to change run behavior.
238
+ }
239
+ }
240
+ }
241
+ export async function runOnce(runtime, opts) {
242
+ const handle = runtime.run(opts);
243
+ const recorder = new TraceRecorder({ sessionId: opts.sessionId, input: opts.input });
244
+ for await (const part of handle.events) {
245
+ recorder.record(part);
246
+ }
247
+ return recorder.finish(await handle);
248
+ }
249
+ function activeFlowName(span) {
250
+ return span.attributes.activeFlow ?? span.name.slice('flow:'.length);
251
+ }
252
+ function nodeName(span) {
253
+ return span.attributes.nodeId ?? span.name.slice('node:'.length);
254
+ }
255
+ function newSpanId() {
256
+ return crypto.randomUUID().replaceAll('-', '').slice(0, 16);
257
+ }
258
+ function toJsonValue(value, seen = new WeakSet()) {
259
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
260
+ return value;
261
+ if (typeof value === 'number')
262
+ return Number.isFinite(value) ? value : String(value);
263
+ if (typeof value === 'bigint')
264
+ return value.toString();
265
+ if (value === undefined)
266
+ return null;
267
+ if (typeof value === 'function' || typeof value === 'symbol')
268
+ return String(value);
269
+ if (typeof value !== 'object')
270
+ return String(value);
271
+ if (seen.has(value))
272
+ return '[Circular]';
273
+ seen.add(value);
274
+ if (value instanceof Date)
275
+ return value.toISOString();
276
+ if (value instanceof Error)
277
+ return { name: value.name, message: value.message };
278
+ if (Array.isArray(value))
279
+ return value.map((entry) => toJsonValue(entry, seen));
280
+ const output = {};
281
+ for (const [key, entry] of Object.entries(value)) {
282
+ try {
283
+ output[key] = toJsonValue(entry, seen);
284
+ }
285
+ catch {
286
+ output[key] = '[Unserializable]';
287
+ }
288
+ }
289
+ return output;
290
+ }
@@ -1,7 +1,7 @@
1
1
  import { streamText } from 'ai';
2
2
  import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/nodeBuilders.js';
3
3
  import { buildToolSet } from '../../tools/effect/index.js';
4
- import { executeModelToolCall, toolResultMessage } from './executeModelTool.js';
4
+ import { dispatchModelToolCalls, toolResultMessage } from './executeModelTool.js';
5
5
  import { consumeAllPendingUserInput } from './inputBuffer.js';
6
6
  import { runSilentExtraction } from './extractionTurn.js';
7
7
  import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
@@ -44,6 +44,7 @@ export class TextDriver {
44
44
  const scope = resolveNodeGatherScope(replyNode, ctx.runState.state, ctx.runState.messages);
45
45
  const gather = await runGatherPhase(ctx, scope);
46
46
  const out = { text: '', toolResults: [] };
47
+ const toolMessages = [];
47
48
  const model = replyNode.model ?? ctx.model;
48
49
  const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
49
50
  const baseSystem = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state, ctx.skillPrompt, ctx.workingMemoryPrompt);
@@ -54,6 +55,7 @@ export class TextDriver {
54
55
  const toolCallsMade = [];
55
56
  const mode = resolveStreamMode(ctx, node);
56
57
  const turnId = crypto.randomUUID();
58
+ let turnUsage;
57
59
  const source = {
58
60
  async *[Symbol.asyncIterator]() {
59
61
  for (let step = 0; step < maxSteps; step += 1) {
@@ -79,23 +81,26 @@ export class TextDriver {
79
81
  }
80
82
  const finishReason = await result.finishReason;
81
83
  const response = await result.response;
82
- messages.push(...response.messages);
84
+ if (result.totalUsage) {
85
+ const stepUsage = await result.totalUsage;
86
+ if (stepUsage) {
87
+ turnUsage = addTurnUsage(turnUsage, stepUsage);
88
+ }
89
+ }
83
90
  if (finishReason !== 'tool-calls') {
91
+ messages.push(...response.messages);
84
92
  break;
85
93
  }
94
+ messages.push(...response.messages);
95
+ toolMessages.push(...response.messages);
86
96
  const toolCalls = await result.toolCalls;
87
- for (const call of toolCalls) {
88
- ctx.emit({
89
- type: 'tool-call',
90
- toolName: call.toolName,
91
- args: call.input,
92
- toolCallId: call.toolCallId,
93
- });
94
- const { result: toolResult, control, failed } = await executeModelToolCall(ctx, { toolName: call.toolName, input: call.input, toolCallId: call.toolCallId }, {
95
- ...ctx.globalTools,
96
- ...(ctx.workingMemoryTools ?? {}),
97
- ...node.localTools,
98
- });
97
+ const mergedTools = {
98
+ ...ctx.globalTools,
99
+ ...(ctx.workingMemoryTools ?? {}),
100
+ ...node.localTools,
101
+ };
102
+ await dispatchModelToolCalls(ctx, toolCalls, mergedTools, ({ call, outcome }) => {
103
+ const { result: toolResult, control, failed } = outcome;
99
104
  out.toolResults.push({
100
105
  name: call.toolName,
101
106
  args: call.input,
@@ -111,14 +116,10 @@ export class TextDriver {
111
116
  timestamp: Date.now(),
112
117
  });
113
118
  out.control ??= control;
114
- ctx.emit({
115
- type: 'tool-result',
116
- toolName: call.toolName,
117
- result: toolResult,
118
- toolCallId: call.toolCallId,
119
- });
120
- messages.push(toolResultMessage({ toolName: call.toolName, input: call.input, toolCallId: call.toolCallId }, toolResult));
121
- }
119
+ const resultMessage = toolResultMessage(call, toolResult);
120
+ messages.push(resultMessage);
121
+ toolMessages.push(resultMessage);
122
+ });
122
123
  }
123
124
  },
124
125
  };
@@ -153,6 +154,12 @@ export class TextDriver {
153
154
  out.text = spoken.text;
154
155
  out.control = spoken.control ?? out.control;
155
156
  out.confidence = spoken.confidence;
157
+ if (toolMessages.length > 0) {
158
+ out.toolMessages = toolMessages;
159
+ }
160
+ if (turnUsage && turnUsage.totalTokens > 0) {
161
+ out.usage = turnUsage;
162
+ }
156
163
  ctx.emit({ type: 'turn-end' });
157
164
  return out;
158
165
  }
@@ -210,4 +217,20 @@ export class TextDriver {
210
217
  return aiTools;
211
218
  }
212
219
  }
220
+ function addTurnUsage(current, usage) {
221
+ const inputTokens = usage.inputTokens ?? 0;
222
+ const outputTokens = usage.outputTokens ?? 0;
223
+ const totalTokens = usage.totalTokens ?? inputTokens + outputTokens;
224
+ const cacheReadTokens = usage.inputTokenDetails?.cacheReadTokens ?? 0;
225
+ if (!current) {
226
+ return { inputTokens, outputTokens, totalTokens, cacheReadTokens, contextTokens: inputTokens };
227
+ }
228
+ return {
229
+ inputTokens: current.inputTokens + inputTokens,
230
+ outputTokens: current.outputTokens + outputTokens,
231
+ totalTokens: current.totalTokens + totalTokens,
232
+ cacheReadTokens: (current.cacheReadTokens ?? 0) + cacheReadTokens,
233
+ contextTokens: inputTokens,
234
+ };
235
+ }
213
236
  export { buildNodePrompt };
@@ -12,7 +12,14 @@ export interface ModelToolCallOutcome {
12
12
  control?: TurnControl;
13
13
  failed: boolean;
14
14
  }
15
- export declare function executeModelToolCall(ctx: RunContext, call: ModelToolCall, localTools?: Record<string, AnyTool>): Promise<ModelToolCallOutcome>;
15
+ export declare function executeModelToolCall(ctx: RunContext, call: ModelToolCall, localTools?: Record<string, AnyTool>, durableOpts?: {
16
+ callsite?: string;
17
+ index?: number;
18
+ }): Promise<ModelToolCallOutcome>;
19
+ export declare function dispatchModelToolCalls(ctx: RunContext, toolCalls: ModelToolCall[], localTools: Record<string, AnyTool>, onEach: (args: {
20
+ call: ModelToolCall;
21
+ outcome: ModelToolCallOutcome;
22
+ }) => void): Promise<void>;
16
23
  export declare function toolResultMessage(call: ModelToolCall, result: unknown): {
17
24
  role: 'tool';
18
25
  content: [
@@ -1,10 +1,14 @@
1
1
  import { classifyControl } from '../../flow/classifyControl.js';
2
2
  import { toolErrorResult } from '../../tools/controlResults.js';
3
- export async function executeModelToolCall(ctx, call, localTools) {
3
+ import { idempotencyKey, logicalRunId } from '../durable/idempotency.js';
4
+ import { findStepByKey } from '../durable/replay.js';
5
+ export async function executeModelToolCall(ctx, call, localTools, durableOpts) {
4
6
  try {
5
7
  const localTool = localTools?.[call.toolName];
6
8
  const toolResult = await ctx.tool(call.toolName, call.input, {
7
9
  toolCallId: call.toolCallId,
10
+ callsite: durableOpts?.callsite,
11
+ index: durableOpts?.index,
8
12
  ...(localTool && {
9
13
  def: localTool,
10
14
  toolCtx: {
@@ -25,6 +29,71 @@ export async function executeModelToolCall(ctx, call, localTools) {
25
29
  return { result: toolErrorResult(error), failed: true };
26
30
  }
27
31
  }
32
+ function isParallelSafeTool(def) {
33
+ return def?.parallelSafe === true || def?.replay === false;
34
+ }
35
+ export async function dispatchModelToolCalls(ctx, toolCalls, localTools, onEach) {
36
+ const runOne = async (call, durableOpts) => {
37
+ ctx.emit({
38
+ type: 'tool-call',
39
+ toolName: call.toolName,
40
+ args: call.input,
41
+ toolCallId: call.toolCallId,
42
+ });
43
+ const outcome = await executeModelToolCall(ctx, call, localTools, durableOpts);
44
+ onEach({ call, outcome });
45
+ ctx.emit({
46
+ type: 'tool-result',
47
+ toolName: call.toolName,
48
+ result: outcome.result,
49
+ toolCallId: call.toolCallId,
50
+ });
51
+ };
52
+ const runParallel = async (parallel) => {
53
+ const callsites = ctx.reserveCallsites(parallel.length);
54
+ const steps = await ctx.runStore.getSteps(ctx.runState.runId);
55
+ const logicalId = logicalRunId(ctx.runState.runId, ctx.runState.runEpoch);
56
+ const unresolved = parallel.filter((call, i) => {
57
+ const def = localTools[call.toolName];
58
+ const key = def?.idempotencyKey != null
59
+ ? def.idempotencyKey(call.input)
60
+ : idempotencyKey(logicalId, callsites[i], { name: call.toolName, args: call.input });
61
+ const hit = findStepByKey(steps, key);
62
+ return !hit || hit.status === 'running';
63
+ });
64
+ let indices;
65
+ if (unresolved.length > 0 && ctx.runStore.reserveSteps) {
66
+ indices = await ctx.runStore.reserveSteps(ctx.runState.runId, unresolved.length);
67
+ }
68
+ let unresolvedCursor = 0;
69
+ const assignments = parallel.map((call, i) => {
70
+ const def = localTools[call.toolName];
71
+ const key = def?.idempotencyKey != null
72
+ ? def.idempotencyKey(call.input)
73
+ : idempotencyKey(logicalId, callsites[i], { name: call.toolName, args: call.input });
74
+ const hit = findStepByKey(steps, key);
75
+ const needsIndex = !hit || hit.status === 'running';
76
+ const index = needsIndex ? indices?.[unresolvedCursor++] : hit?.index;
77
+ return { call, callsite: callsites[i], index };
78
+ });
79
+ await Promise.all(assignments.map(({ call, callsite, index }) => runOne(call, { callsite, index })));
80
+ };
81
+ for (let cursor = 0; cursor < toolCalls.length;) {
82
+ const call = toolCalls[cursor];
83
+ if (!isParallelSafeTool(localTools[call.toolName])) {
84
+ await runOne(call);
85
+ cursor += 1;
86
+ continue;
87
+ }
88
+ const parallel = [];
89
+ while (cursor < toolCalls.length &&
90
+ isParallelSafeTool(localTools[toolCalls[cursor].toolName])) {
91
+ parallel.push(toolCalls[cursor]);
92
+ cursor += 1;
93
+ }
94
+ await runParallel(parallel);
95
+ }
96
+ }
28
97
  export function toolResultMessage(call, result) {
29
98
  return {
30
99
  role: 'tool',
@@ -7,3 +7,4 @@ export declare function consumePendingUserInput(session: Session): UserInputCont
7
7
  export declare function consumeAllPendingUserInput(session: Session): UserInputContent | undefined;
8
8
  export declare function peekPendingUserInput(session: Session): UserInputContent | undefined;
9
9
  export declare function hasPendingUserInput(session: Session): boolean;
10
+ export declare function syncPendingUserInput(source: Session, target: Session): void;
@@ -33,3 +33,12 @@ export function peekPendingUserInput(session) {
33
33
  export function hasPendingUserInput(session) {
34
34
  return queue(session).length > 0;
35
35
  }
36
+ export function syncPendingUserInput(source, target) {
37
+ const pending = queue(source);
38
+ if (pending.length > 0) {
39
+ target.workingMemory[PENDING_INPUT_KEY] = [...pending];
40
+ }
41
+ else {
42
+ delete target.workingMemory[PENDING_INPUT_KEY];
43
+ }
44
+ }
@@ -1,4 +1,6 @@
1
1
  import { isTerminalOutcome, markSessionOutcome } from './outcomeMarking.js';
2
+ import { mutateSessionWithRetry } from '../session/utils.js';
3
+ import { syncPendingUserInput } from './channels/inputBuffer.js';
2
4
  export async function closeRun(options) {
3
5
  const { session, runState, runStore, sessionStore, hooks, ctx } = options;
4
6
  runState.updatedAt = Date.now();
@@ -19,12 +21,13 @@ export async function closeRun(options) {
19
21
  outcome: outcomeRecord,
20
22
  });
21
23
  }
22
- const latest = (await sessionStore.get(session.id)) ?? session;
23
- latest.currentAgent = runState.activeAgentId;
24
- latest.activeAgentId = runState.activeAgentId;
25
- // Sync the session-level message mirror to the canonical run record — it
26
- // otherwise lacks assistant turns and keeps pre-guardrail (unredacted)
27
- // user input written at openRun.
28
- latest.messages = [...runState.messages];
29
- await sessionStore.save(latest);
24
+ await mutateSessionWithRetry(sessionStore, session.id, (latest) => {
25
+ latest.currentAgent = runState.activeAgentId;
26
+ latest.activeAgentId = runState.activeAgentId;
27
+ // Sync the session-level message mirror to the canonical run record — it
28
+ // otherwise lacks assistant turns and keeps pre-guardrail (unredacted)
29
+ // user input written at openRun.
30
+ latest.messages = [...runState.messages];
31
+ syncPendingUserInput(ctx.session, latest);
32
+ });
30
33
  }
@@ -42,6 +42,8 @@ export interface CompactMessagesOptions {
42
42
  /** Skip the threshold check (overflow recovery). */
43
43
  force?: boolean;
44
44
  abortSignal?: AbortSignal;
45
+ /** Real last-turn prompt tokens when available (replaces chars/4 for threshold). */
46
+ lastPromptTokens?: number;
45
47
  }
46
48
  /**
47
49
  * Pure compaction step: returns a new message list when compaction applied.
@@ -64,10 +64,11 @@ function truncate(text, max) {
64
64
  * pairs are never split across the summary boundary.
65
65
  */
66
66
  export async function compactMessages(options) {
67
- const { messages, model, config, force, abortSignal } = options;
67
+ const { messages, model, config, force, abortSignal, lastPromptTokens } = options;
68
68
  const triggerTokens = config.triggerTokens ?? DEFAULT_COMPACTION_TRIGGER_TOKENS;
69
69
  const keepRecent = config.keepRecentMessages ?? DEFAULT_COMPACTION_KEEP_RECENT;
70
- const beforeTokens = estimateMessagesTokens(messages);
70
+ const estimatedTokens = estimateMessagesTokens(messages);
71
+ const beforeTokens = lastPromptTokens ?? estimatedTokens;
71
72
  if (!force && beforeTokens < triggerTokens) {
72
73
  return { compacted: false, reason: 'under-threshold', beforeTokens };
73
74
  }