@rowan-agent/agent 0.10.1 → 0.11.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 (3) hide show
  1. package/dist/index.d.ts +135 -99
  2. package/dist/index.js +611 -171
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -518,15 +518,32 @@ async function executeRuntimeToolCall(input) {
518
518
  let decision;
519
519
  if (input.beforeToolCall) {
520
520
  await input.observe?.({ type: "approval_requested", tool, args });
521
- decision = await input.beforeToolCall({ tool, args });
522
- await input.observe?.({
523
- type: "approval_result",
521
+ decision = await input.beforeToolCall({
524
522
  tool,
525
523
  args,
526
- decision: decision ?? { allow: true }
524
+ toolCallId: input.toolCall.id
527
525
  });
526
+ if (decision && "allow" in decision) {
527
+ await input.observe?.({
528
+ type: "approval_result",
529
+ tool,
530
+ args,
531
+ decision
532
+ });
533
+ }
534
+ }
535
+ if (decision && "interaction" in decision) {
536
+ const result = toolResult({
537
+ context: input.toolContext,
538
+ toolName: tool.name,
539
+ ok: false,
540
+ content: null,
541
+ error: `Tool interaction required: ${decision.interaction.prompt}`
542
+ });
543
+ await input.observe?.({ type: "tool_blocked", tool, reason: decision.interaction.prompt });
544
+ return result;
528
545
  }
529
- if (decision && !decision.allow) {
546
+ if (decision && "allow" in decision && !decision.allow) {
530
547
  const result = toolResult({
531
548
  context: input.toolContext,
532
549
  toolName: tool.name,
@@ -660,7 +677,18 @@ function createReadTool(context) {
660
677
  error: invalidLimit
661
678
  });
662
679
  }
663
- const fileStat = await stat(resolved.absolutePath);
680
+ let fileStat;
681
+ try {
682
+ fileStat = await stat(resolved.absolutePath);
683
+ } catch (error) {
684
+ return toolResult({
685
+ context: toolContext,
686
+ toolName: "read",
687
+ ok: false,
688
+ content: null,
689
+ error: error instanceof Error ? error.message : String(error)
690
+ });
691
+ }
664
692
  if (!fileStat.isFile()) {
665
693
  return toolResult({
666
694
  context: toolContext,
@@ -670,7 +698,18 @@ function createReadTool(context) {
670
698
  error: `Not a file: ${resolved.relativePath}`
671
699
  });
672
700
  }
673
- const bytes = await readFile2(resolved.absolutePath);
701
+ let bytes;
702
+ try {
703
+ bytes = await readFile2(resolved.absolutePath);
704
+ } catch (error) {
705
+ return toolResult({
706
+ context: toolContext,
707
+ toolName: "read",
708
+ ok: false,
709
+ content: null,
710
+ error: error instanceof Error ? error.message : String(error)
711
+ });
712
+ }
674
713
  const source = new TextDecoder().decode(bytes);
675
714
  const sourceLines = source.split(/\r\n|\n|\r/);
676
715
  const requestedOffset = parsed.offset ?? 1;
@@ -1363,6 +1402,8 @@ function createPhaseInteractionDriver(state, phase, signal) {
1363
1402
  kind: input.kind,
1364
1403
  prompt: input.prompt,
1365
1404
  ...input.payload === void 0 ? {} : { payload: input.payload },
1405
+ ...input.origin === void 0 ? {} : { origin: input.origin },
1406
+ ...input.toolCallId === void 0 ? {} : { toolCallId: input.toolCallId },
1366
1407
  createdAt: createTimestamp(),
1367
1408
  status: "pending"
1368
1409
  };
@@ -1377,6 +1418,13 @@ function createPhaseInteractionDriver(state, phase, signal) {
1377
1418
  answers() {
1378
1419
  return new Map(answers);
1379
1420
  },
1421
+ checkpoint() {
1422
+ return checkpoint;
1423
+ },
1424
+ clearCheckpoint() {
1425
+ checkpoint = void 0;
1426
+ sync();
1427
+ },
1380
1428
  suspend(input = {}) {
1381
1429
  assertActive();
1382
1430
  const pending = this.pending();
@@ -2488,34 +2536,48 @@ async function executePhaseWithModel(ctx) {
2488
2536
  ...ctx.context,
2489
2537
  messages: ctx.messageManager.visible()
2490
2538
  };
2491
- const collected = await ctx.execution.invokeModel(roundContext);
2492
- output = {
2493
- message: collected.text,
2494
- phase: ctx.phase.name,
2495
- // A route decision sharing a model response with an ordinary Tool is
2496
- // rejected for this round. Keep ordinary calls visible so they can run,
2497
- // but do not let the route escape the error/re-decision boundary.
2498
- toolCalls: collected.toolCalls.some((candidate) => candidate.name === PhaseRouteTool) && collected.toolCalls.some((candidate) => candidate.name !== PhaseRouteTool) ? collected.toolCalls.filter((candidate) => candidate.name !== PhaseRouteTool) : collected.toolCalls
2499
- };
2500
- for (const toolCall of collected.toolCalls) {
2501
- if (toolCall.name !== PhaseRouteTool) continue;
2502
- const messageId = ctx.messageManager.start(
2503
- "tool",
2504
- createRouteToolResultContent(
2505
- toolCall,
2506
- collected.toolCalls.some((candidate) => candidate.name !== PhaseRouteTool) ? "Route must be called separately after ordinary tools finish; this route decision was ignored." : void 0
2507
- ),
2508
- {
2509
- phase: ctx.phase.name
2510
- }
2539
+ let executableToolCalls;
2540
+ const checkpoint = ctx.execution.interaction.checkpoint();
2541
+ if (checkpoint && typeof checkpoint === "object" && checkpoint.kind === "tool_call_suspension") {
2542
+ const toolSuspension = checkpoint;
2543
+ executableToolCalls = toolSuspension.toolCalls.filter(
2544
+ (toolCall) => executableToolNames.has(toolCall.name)
2511
2545
  );
2512
- await ctx.messageManager.end(messageId);
2513
- }
2514
- const executableToolCalls = collected.toolCalls.filter(
2515
- (toolCall) => executableToolNames.has(toolCall.name)
2516
- );
2517
- if (executableToolCalls.length === 0) {
2518
- return output;
2546
+ output = {
2547
+ message: "",
2548
+ phase: ctx.phase.name,
2549
+ toolCalls: [...toolSuspension.toolCalls]
2550
+ };
2551
+ } else {
2552
+ const collected = await ctx.execution.invokeModel(roundContext);
2553
+ output = {
2554
+ message: collected.text,
2555
+ phase: ctx.phase.name,
2556
+ // A route decision sharing a model response with an ordinary Tool is
2557
+ // rejected for this round. Keep ordinary calls visible so they can run,
2558
+ // but do not let the route escape the error/re-decision boundary.
2559
+ toolCalls: collected.toolCalls.some((candidate) => candidate.name === PhaseRouteTool) && collected.toolCalls.some((candidate) => candidate.name !== PhaseRouteTool) ? collected.toolCalls.filter((candidate) => candidate.name !== PhaseRouteTool) : collected.toolCalls
2560
+ };
2561
+ for (const toolCall of collected.toolCalls) {
2562
+ if (toolCall.name !== PhaseRouteTool) continue;
2563
+ const messageId = ctx.messageManager.start(
2564
+ "tool",
2565
+ createRouteToolResultContent(
2566
+ toolCall,
2567
+ collected.toolCalls.some((candidate) => candidate.name !== PhaseRouteTool) ? "Route must be called separately after ordinary tools finish; this route decision was ignored." : void 0
2568
+ ),
2569
+ {
2570
+ phase: ctx.phase.name
2571
+ }
2572
+ );
2573
+ await ctx.messageManager.end(messageId);
2574
+ }
2575
+ executableToolCalls = collected.toolCalls.filter(
2576
+ (toolCall) => executableToolNames.has(toolCall.name)
2577
+ );
2578
+ if (executableToolCalls.length === 0) {
2579
+ return output;
2580
+ }
2519
2581
  }
2520
2582
  const results = await ctx.execution.executeTools(roundContext, executableToolCalls);
2521
2583
  for (const result of results) {
@@ -2991,7 +3053,8 @@ async function executeToolCall(input) {
2991
3053
  if (input.config.runtime?.tools) {
2992
3054
  result = await input.config.runtime.tools({
2993
3055
  config: input.config,
2994
- toolCall: input.toolCall
3056
+ toolCall: input.toolCall,
3057
+ driver: input.driver
2995
3058
  });
2996
3059
  } else {
2997
3060
  const toolContext = {
@@ -3017,7 +3080,8 @@ async function executeToolCalls(input) {
3017
3080
  if (input.config.runtime?.toolsBatch) {
3018
3081
  const results2 = await input.config.runtime.toolsBatch({
3019
3082
  config: input.config,
3020
- toolCalls: input.toolCalls
3083
+ toolCalls: input.toolCalls,
3084
+ driver: input.driver
3021
3085
  });
3022
3086
  if (results2.length !== input.toolCalls.length) throw new Error("Runtime returned an invalid Tool batch result");
3023
3087
  return results2.map((result, index) => ({
@@ -3115,7 +3179,8 @@ function createPhaseExecution(config, state, phase, messageManager, toolExecutio
3115
3179
  }
3116
3180
  },
3117
3181
  tools,
3118
- toolCall
3182
+ toolCall,
3183
+ driver: interaction
3119
3184
  });
3120
3185
  await toolExecutionManager.end(result.toolCallId, result.toolName, result, !result.ok);
3121
3186
  return result;
@@ -3137,7 +3202,8 @@ function createPhaseExecution(config, state, phase, messageManager, toolExecutio
3137
3202
  }
3138
3203
  },
3139
3204
  tools,
3140
- toolCalls
3205
+ toolCalls,
3206
+ driver: interaction
3141
3207
  });
3142
3208
  for (const result of results) {
3143
3209
  await toolExecutionManager.end(result.toolCallId, result.toolName, result, !result.ok);
@@ -4222,12 +4288,30 @@ function assembleRegisteredExtensions(snapshot, runner, options = {}) {
4222
4288
  beforePrompt: (phaseId, input) => runner.emitBeforePrompt(phaseId, input),
4223
4289
  beforeToolCall: async (input) => {
4224
4290
  const tool = projectTool(input.tool, input.context.agentId, input.context.runId);
4225
- const decision = await runner.emitBeforeToolCall(tool, input.args);
4291
+ const decision = await runner.emitBeforeToolCall(tool, input.args, {
4292
+ runId: input.context.runId,
4293
+ agentId: input.context.agentId,
4294
+ toolCallId: input.context.toolCallId,
4295
+ metadata: input.context.runMetadata,
4296
+ answer: input.answer
4297
+ });
4298
+ if (decision.interaction !== void 0) {
4299
+ return { interaction: decision.interaction };
4300
+ }
4226
4301
  return decision.allow ? { allow: true } : { allow: false, reason: decision.reason ?? "Extension hook rejected the Tool." };
4227
4302
  },
4228
4303
  afterToolCall: async (input) => {
4229
4304
  const tool = projectTool(input.tool, input.context.agentId, input.context.runId);
4230
- const result = await runner.emitAfterToolCall(tool, toLoopResult(input.result, input.context.toolCallId, input.tool.name));
4305
+ const result = await runner.emitAfterToolCall(
4306
+ tool,
4307
+ toLoopResult(input.result, input.context.toolCallId, input.tool.name),
4308
+ {
4309
+ runId: input.context.runId,
4310
+ agentId: input.context.agentId,
4311
+ toolCallId: input.context.toolCallId,
4312
+ metadata: input.context.runMetadata
4313
+ }
4314
+ );
4231
4315
  return fromLoopResult(result);
4232
4316
  },
4233
4317
  setContext: (context2) => {
@@ -4873,19 +4957,28 @@ var ExtensionRunner = class {
4873
4957
  });
4874
4958
  return result?.input ?? input;
4875
4959
  }
4876
- async emitBeforeToolCall(tool, args) {
4960
+ async emitBeforeToolCall(tool, args, context) {
4877
4961
  const result = await this.emitHook("before_tool_call", {
4878
4962
  type: "before_tool_call",
4879
4963
  tool,
4880
- args
4964
+ args,
4965
+ ...context?.runId !== void 0 ? { runId: context.runId } : {},
4966
+ ...context?.agentId !== void 0 ? { agentId: context.agentId } : {},
4967
+ ...context?.toolCallId !== void 0 ? { toolCallId: context.toolCallId } : {},
4968
+ ...context?.metadata !== void 0 ? { metadata: context.metadata } : {},
4969
+ ...context?.answer !== void 0 ? { answer: context.answer } : {}
4881
4970
  });
4882
4971
  return result ?? { allow: true };
4883
4972
  }
4884
- async emitAfterToolCall(tool, result) {
4973
+ async emitAfterToolCall(tool, result, context) {
4885
4974
  const hookResult = await this.emitHook("after_tool_call", {
4886
4975
  type: "after_tool_call",
4887
4976
  tool,
4888
- result
4977
+ result,
4978
+ ...context?.runId !== void 0 ? { runId: context.runId } : {},
4979
+ ...context?.agentId !== void 0 ? { agentId: context.agentId } : {},
4980
+ ...context?.toolCallId !== void 0 ? { toolCallId: context.toolCallId } : {},
4981
+ ...context?.metadata !== void 0 ? { metadata: context.metadata } : {}
4889
4982
  });
4890
4983
  return hookResult?.result ?? result;
4891
4984
  }
@@ -6235,7 +6328,7 @@ var AgentRuntime = class _AgentRuntime {
6235
6328
  },
6236
6329
  onContext: assembly.setContext,
6237
6330
  runtime: {
6238
- tools: ({ toolCall }) => {
6331
+ tools: ({ toolCall, driver }) => {
6239
6332
  const task = toolQueue.then(async () => {
6240
6333
  const execution = await this.executeToolBatch({
6241
6334
  run,
@@ -6244,7 +6337,11 @@ var AgentRuntime = class _AgentRuntime {
6244
6337
  expectedRevision: executionRevision,
6245
6338
  toolConfig: executionTools,
6246
6339
  toolCalls: [toolCall],
6247
- signal: controller.signal
6340
+ signal: controller.signal,
6341
+ driver,
6342
+ onRevision: (rev) => {
6343
+ executionRevision = rev;
6344
+ }
6248
6345
  });
6249
6346
  executionRevision = execution.revision;
6250
6347
  return execution.results[0];
@@ -6252,7 +6349,7 @@ var AgentRuntime = class _AgentRuntime {
6252
6349
  toolQueue = task.then(() => void 0, () => void 0);
6253
6350
  return task;
6254
6351
  },
6255
- toolsBatch: ({ toolCalls }) => {
6352
+ toolsBatch: ({ toolCalls, driver }) => {
6256
6353
  const task = toolQueue.then(async () => {
6257
6354
  const execution = await this.executeToolBatch({
6258
6355
  run,
@@ -6261,7 +6358,11 @@ var AgentRuntime = class _AgentRuntime {
6261
6358
  expectedRevision: executionRevision,
6262
6359
  toolConfig: executionTools,
6263
6360
  toolCalls,
6264
- signal: controller.signal
6361
+ signal: controller.signal,
6362
+ driver,
6363
+ onRevision: (rev) => {
6364
+ executionRevision = rev;
6365
+ }
6265
6366
  });
6266
6367
  executionRevision = execution.revision;
6267
6368
  return execution.results;
@@ -6303,19 +6404,20 @@ var AgentRuntime = class _AgentRuntime {
6303
6404
  return;
6304
6405
  }
6305
6406
  if (result.type === "input_required") {
6306
- const output = latestAssistant(
6407
+ const isToolCallInteraction = result.interactions?.some((i) => i.origin === "tool_call");
6408
+ const output = result.interactions && result.interactions.length > 0 ? void 0 : latestAssistant(
6307
6409
  run,
6308
6410
  result.messages.slice(modelMessages.length),
6309
6411
  modelMessages.length
6310
6412
  );
6311
- const prompt = output ?? promptMessage(run, result.request.prompt, result.messages.length);
6413
+ const prompt = isToolCallInteraction ? void 0 : output ?? promptMessage(run, result.request.prompt, result.messages.length);
6312
6414
  await this.owned.commitInputRequired({
6313
6415
  runId: run.id,
6314
6416
  execution: claim.execution,
6315
6417
  expectedRevision: executionRevision,
6316
6418
  requestId: createId("input"),
6317
6419
  phase: result.request.phase,
6318
- prompt,
6420
+ ...prompt ? { prompt } : {},
6319
6421
  checkpoint: result.checkpoint,
6320
6422
  interactions: result.interactions,
6321
6423
  interactionAnswers: claim.run.interactionAnswers
@@ -6387,109 +6489,421 @@ var AgentRuntime = class _AgentRuntime {
6387
6489
  return { result: batch.results[0], revision: batch.revision };
6388
6490
  }
6389
6491
  async executeToolBatch(input) {
6390
- const reserved = await this.owned.reserveToolCalls({
6391
- runId: input.run.id,
6392
- execution: input.execution,
6393
- expectedRevision: input.expectedRevision,
6394
- requestMessageId: createId("msg"),
6395
- calls: input.toolCalls.map((toolCall) => ({
6396
- providerToolCallId: toolCall.id,
6397
- name: toolCall.name,
6398
- args: toJsonValue(toolCall.args)
6399
- }))
6400
- });
6401
- let revision = reserved.run.revision;
6402
- const results = [];
6403
- for (let index = 0; index < input.toolCalls.length; index += 1) {
6404
- const toolCall = input.toolCalls[index];
6405
- const tool = reserved.toolCalls[index];
6406
- const execution = await this.executeReservedTool({
6407
- ...input,
6408
- toolCall,
6409
- tool,
6410
- expectedRevision: revision
6411
- });
6412
- revision = execution.revision;
6413
- results.push(execution.result);
6414
- }
6415
- return { results, revision };
6416
- }
6417
- async executeReservedTool(input) {
6418
- const durableTool = input.toolConfig.tools.find((candidate) => candidate.name === input.toolCall.name);
6419
- const providerToolCallId = input.toolCall.id;
6420
- const toolCallId = input.tool.id;
6492
+ const checkpoint = input.driver?.checkpoint();
6493
+ const isSuspensionResume = checkpoint && typeof checkpoint === "object" && checkpoint.kind === "tool_call_suspension";
6494
+ const suspension = isSuspensionResume ? checkpoint : void 0;
6421
6495
  let revision = input.expectedRevision;
6422
- const protocolFailure = (error) => ({ toolCallId: providerToolCallId, toolName: input.toolCall.name, ok: false, content: null, error });
6423
- if (!durableTool) {
6424
- const failed = await this.owned.commitToolResult({ runId: input.run.id, execution: input.execution, expectedRevision: revision, toolCallId, state: "failed", result: { ok: false, content: null, error: `Tool ${input.toolCall.name} is not available.` } });
6425
- return { result: protocolFailure(`Tool ${input.toolCall.name} is not available.`), revision: failed.run.revision };
6426
- }
6427
- let progressActive = false;
6428
- const context = {
6429
- agentId: input.run.agentId,
6430
- runId: input.run.id,
6431
- ...input.agentMetadata === void 0 ? {} : { agentMetadata: input.agentMetadata },
6432
- ...input.run.metadata === void 0 ? {} : { runMetadata: input.run.metadata },
6433
- toolCallId,
6434
- reportProgress: (progress) => {
6435
- const active = this.executions.get(input.run.id);
6436
- if (!progressActive || this.closed || input.signal.aborted || active?.executionId !== input.execution.executionId || !isJsonValue(progress)) return;
6437
- let copy;
6496
+ const setRevision = (newRevision) => {
6497
+ revision = newRevision;
6498
+ input.onRevision?.(revision);
6499
+ };
6500
+ if (!suspension && input.toolCalls.length > 1) {
6501
+ let hasInteraction = false;
6502
+ const preDecisions = [];
6503
+ const toolCallIds = input.toolCalls.map(() => createId("tool"));
6504
+ for (let i = 0; i < input.toolCalls.length; i++) {
6505
+ const toolCall = input.toolCalls[i];
6506
+ const durableTool = input.toolConfig.tools.find((c) => c.name === toolCall.name);
6507
+ if (!durableTool || !input.toolConfig.beforeToolCall) {
6508
+ preDecisions.push({ allow: true });
6509
+ continue;
6510
+ }
6511
+ const toolCallId = toolCallIds[i];
6512
+ const context = {
6513
+ agentId: input.run.agentId,
6514
+ runId: input.run.id,
6515
+ ...input.agentMetadata === void 0 ? {} : { agentMetadata: input.agentMetadata },
6516
+ ...input.run.metadata === void 0 ? {} : { runMetadata: input.run.metadata },
6517
+ toolCallId,
6518
+ reportProgress: () => {
6519
+ }
6520
+ };
6438
6521
  try {
6439
- if (JSON.stringify(progress).length > 64 * 1024) return;
6440
- copy = structuredClone(progress);
6522
+ const decision = await input.toolConfig.beforeToolCall({
6523
+ tool: durableTool,
6524
+ args: toJsonValue(toolCall.args),
6525
+ context,
6526
+ signal: input.signal
6527
+ });
6528
+ if ("interaction" in decision && decision.interaction !== void 0) {
6529
+ hasInteraction = true;
6530
+ break;
6531
+ }
6532
+ preDecisions.push(decision);
6441
6533
  } catch {
6442
- return;
6534
+ preDecisions.push({ allow: true });
6443
6535
  }
6444
- this.transientEvents.publish({
6445
- kind: "tool_progress",
6446
- durability: "transient",
6536
+ }
6537
+ if (!hasInteraction) {
6538
+ const reserved = await this.owned.reserveToolCalls({
6447
6539
  runId: input.run.id,
6448
- executionId: input.execution.executionId,
6449
- toolCallId,
6450
- progress: copy
6540
+ execution: input.execution,
6541
+ expectedRevision: revision,
6542
+ requestMessageId: createId("msg"),
6543
+ calls: input.toolCalls.map((tc, i) => ({
6544
+ toolCallId: toolCallIds[i],
6545
+ providerToolCallId: tc.id,
6546
+ name: tc.name,
6547
+ args: toJsonValue(tc.args)
6548
+ }))
6451
6549
  });
6452
- }
6453
- };
6454
- try {
6455
- if (input.toolConfig.beforeToolCall) {
6456
- const decision = await input.toolConfig.beforeToolCall({ tool: durableTool, args: toJsonValue(input.toolCall.args), context, signal: input.signal });
6457
- if (!decision.allow) {
6458
- const failed = await this.owned.commitToolResult({ runId: input.run.id, execution: input.execution, expectedRevision: revision, toolCallId, state: "failed", result: { ok: false, content: null, error: decision.reason } });
6459
- return { result: protocolFailure(decision.reason), revision: failed.run.revision };
6550
+ setRevision(reserved.run.revision);
6551
+ const results2 = [];
6552
+ for (let i = 0; i < input.toolCalls.length; i++) {
6553
+ const toolCall = input.toolCalls[i];
6554
+ const durableTool = input.toolConfig.tools.find((c) => c.name === toolCall.name);
6555
+ const providerToolCallId = toolCall.id;
6556
+ const toolCallId = toolCallIds[i];
6557
+ const protocolFailure = (error) => ({
6558
+ toolCallId: providerToolCallId,
6559
+ toolName: toolCall.name,
6560
+ ok: false,
6561
+ content: null,
6562
+ error
6563
+ });
6564
+ if (!durableTool) {
6565
+ const failed = await this.owned.commitToolResult({
6566
+ runId: input.run.id,
6567
+ execution: input.execution,
6568
+ expectedRevision: revision,
6569
+ toolCallId,
6570
+ state: "failed",
6571
+ result: { ok: false, content: null, error: `Tool ${toolCall.name} is not available.` }
6572
+ });
6573
+ setRevision(failed.run.revision);
6574
+ results2.push(protocolFailure(`Tool ${toolCall.name} is not available.`));
6575
+ continue;
6576
+ }
6577
+ const decision = preDecisions[i] ?? { allow: true };
6578
+ if ("allow" in decision && !decision.allow) {
6579
+ const failed = await this.owned.commitToolResult({
6580
+ runId: input.run.id,
6581
+ execution: input.execution,
6582
+ expectedRevision: revision,
6583
+ toolCallId,
6584
+ state: "failed",
6585
+ result: { ok: false, content: null, error: decision.reason }
6586
+ });
6587
+ setRevision(failed.run.revision);
6588
+ results2.push(protocolFailure(decision.reason));
6589
+ continue;
6590
+ }
6591
+ let progressActive = false;
6592
+ const context = {
6593
+ agentId: input.run.agentId,
6594
+ runId: input.run.id,
6595
+ ...input.agentMetadata === void 0 ? {} : { agentMetadata: input.agentMetadata },
6596
+ ...input.run.metadata === void 0 ? {} : { runMetadata: input.run.metadata },
6597
+ toolCallId,
6598
+ reportProgress: (progress) => {
6599
+ const active = this.executions.get(input.run.id);
6600
+ if (!progressActive || this.closed || input.signal.aborted || active?.executionId !== input.execution.executionId || !isJsonValue(progress)) return;
6601
+ let copy;
6602
+ try {
6603
+ if (JSON.stringify(progress).length > 64 * 1024) return;
6604
+ copy = structuredClone(progress);
6605
+ } catch {
6606
+ return;
6607
+ }
6608
+ this.transientEvents.publish({
6609
+ kind: "tool_progress",
6610
+ durability: "transient",
6611
+ runId: input.run.id,
6612
+ executionId: input.execution.executionId,
6613
+ toolCallId,
6614
+ progress: copy
6615
+ });
6616
+ }
6617
+ };
6618
+ const started = await this.owned.startToolCall({
6619
+ runId: input.run.id,
6620
+ execution: input.execution,
6621
+ expectedRevision: revision,
6622
+ toolCallId
6623
+ });
6624
+ setRevision(started.run.revision);
6625
+ progressActive = true;
6626
+ try {
6627
+ let result = await durableTool.execute(toJsonValue(toolCall.args), context, input.signal);
6628
+ assertToolExecutionResult(result);
6629
+ if (input.toolConfig.afterToolCall) {
6630
+ result = await input.toolConfig.afterToolCall({
6631
+ tool: durableTool,
6632
+ result,
6633
+ context,
6634
+ signal: input.signal
6635
+ });
6636
+ }
6637
+ assertToolExecutionResult(result);
6638
+ result = await spillLargeToolResult(
6639
+ result,
6640
+ this.archiveDirFor(input.run.agentId),
6641
+ toolCall.name
6642
+ );
6643
+ const committed = await this.owned.commitToolResult({
6644
+ runId: input.run.id,
6645
+ execution: input.execution,
6646
+ expectedRevision: revision,
6647
+ toolCallId,
6648
+ state: result.ok ? "completed" : "failed",
6649
+ result
6650
+ });
6651
+ setRevision(committed.run.revision);
6652
+ results2.push({
6653
+ toolCallId: providerToolCallId,
6654
+ toolName: toolCall.name,
6655
+ ...result
6656
+ });
6657
+ } catch (error) {
6658
+ const reason = error instanceof Error ? error.message : "Tool execution outcome is indeterminate.";
6659
+ const indeterminate = await this.owned.commitToolResult({
6660
+ runId: input.run.id,
6661
+ execution: input.execution,
6662
+ expectedRevision: revision,
6663
+ toolCallId,
6664
+ state: "indeterminate",
6665
+ reason,
6666
+ result: { ok: false, content: null, error: reason }
6667
+ });
6668
+ setRevision(indeterminate.run.revision);
6669
+ results2.push(protocolFailure(reason));
6670
+ } finally {
6671
+ progressActive = false;
6672
+ this.transientEvents.clearTool(input.run.id, toolCallId);
6673
+ }
6460
6674
  }
6675
+ return { results: results2, revision };
6461
6676
  }
6462
- } catch (error) {
6463
- const reason = error instanceof Error ? error.message : "Tool policy rejected the call.";
6464
- const failed = await this.owned.commitToolResult({ runId: input.run.id, execution: input.execution, expectedRevision: revision, toolCallId, state: "failed", result: { ok: false, content: null, error: reason } });
6465
- return { result: protocolFailure(reason), revision: failed.run.revision };
6466
6677
  }
6467
- const started = await this.owned.startToolCall({ runId: input.run.id, execution: input.execution, expectedRevision: revision, toolCallId });
6468
- revision = started.run.revision;
6469
- progressActive = true;
6470
- try {
6471
- let result = await durableTool.execute(toJsonValue(input.toolCall.args), context, input.signal);
6472
- assertToolExecutionResult(result);
6473
- if (input.toolConfig.afterToolCall) result = await input.toolConfig.afterToolCall({ tool: durableTool, result, context, signal: input.signal });
6474
- assertToolExecutionResult(result);
6475
- result = await spillLargeToolResult(
6476
- result,
6477
- this.archiveDirFor(input.run.agentId),
6478
- input.toolCall.name
6479
- );
6480
- const committed = await this.owned.commitToolResult({ runId: input.run.id, execution: input.execution, expectedRevision: revision, toolCallId, state: result.ok ? "completed" : "failed", result });
6481
- return {
6482
- result: { toolCallId: providerToolCallId, toolName: input.toolCall.name, ...result },
6483
- revision: committed.run.revision
6678
+ const results = suspension ? [...suspension.completedResults] : [];
6679
+ const startIndex = suspension ? suspension.pendingIndex : 0;
6680
+ for (let index = startIndex; index < input.toolCalls.length; index += 1) {
6681
+ const toolCall = input.toolCalls[index];
6682
+ const durableTool = input.toolConfig.tools.find((candidate) => candidate.name === toolCall.name);
6683
+ const providerToolCallId = toolCall.id;
6684
+ const toolCallId = suspension && index === suspension.pendingIndex && suspension.toolCallId ? suspension.toolCallId : createId("tool");
6685
+ const protocolFailure = (error) => ({
6686
+ toolCallId: providerToolCallId,
6687
+ toolName: toolCall.name,
6688
+ ok: false,
6689
+ content: null,
6690
+ error
6691
+ });
6692
+ if (!durableTool) {
6693
+ const reserved2 = await this.owned.reserveToolCalls({
6694
+ runId: input.run.id,
6695
+ execution: input.execution,
6696
+ expectedRevision: revision,
6697
+ requestMessageId: createId("msg"),
6698
+ calls: [{
6699
+ toolCallId,
6700
+ providerToolCallId,
6701
+ name: toolCall.name,
6702
+ args: toJsonValue(toolCall.args)
6703
+ }]
6704
+ });
6705
+ setRevision(reserved2.run.revision);
6706
+ const failed = await this.owned.commitToolResult({
6707
+ runId: input.run.id,
6708
+ execution: input.execution,
6709
+ expectedRevision: revision,
6710
+ toolCallId,
6711
+ state: "failed",
6712
+ result: { ok: false, content: null, error: `Tool ${toolCall.name} is not available.` }
6713
+ });
6714
+ setRevision(failed.run.revision);
6715
+ results.push(protocolFailure(`Tool ${toolCall.name} is not available.`));
6716
+ continue;
6717
+ }
6718
+ let progressActive = false;
6719
+ const context = {
6720
+ agentId: input.run.agentId,
6721
+ runId: input.run.id,
6722
+ ...input.agentMetadata === void 0 ? {} : { agentMetadata: input.agentMetadata },
6723
+ ...input.run.metadata === void 0 ? {} : { runMetadata: input.run.metadata },
6724
+ toolCallId,
6725
+ reportProgress: (progress) => {
6726
+ const active = this.executions.get(input.run.id);
6727
+ if (!progressActive || this.closed || input.signal.aborted || active?.executionId !== input.execution.executionId || !isJsonValue(progress)) return;
6728
+ let copy;
6729
+ try {
6730
+ if (JSON.stringify(progress).length > 64 * 1024) return;
6731
+ copy = structuredClone(progress);
6732
+ } catch {
6733
+ return;
6734
+ }
6735
+ this.transientEvents.publish({
6736
+ kind: "tool_progress",
6737
+ durability: "transient",
6738
+ runId: input.run.id,
6739
+ executionId: input.execution.executionId,
6740
+ toolCallId,
6741
+ progress: copy
6742
+ });
6743
+ }
6484
6744
  };
6485
- } catch (error) {
6486
- const reason = error instanceof Error ? error.message : "Tool execution outcome is indeterminate.";
6487
- const indeterminate = await this.owned.commitToolResult({ runId: input.run.id, execution: input.execution, expectedRevision: revision, toolCallId, state: "indeterminate", reason, result: { ok: false, content: null, error: reason } });
6488
- return { result: protocolFailure(reason), revision: indeterminate.run.revision };
6489
- } finally {
6490
- progressActive = false;
6491
- this.transientEvents.clearTool(input.run.id, toolCallId);
6745
+ const answer = suspension && index === suspension.pendingIndex && suspension.interactionId ? input.driver?.answers().get(suspension.interactionId) : void 0;
6746
+ try {
6747
+ if (input.toolConfig.beforeToolCall) {
6748
+ const decision = await input.toolConfig.beforeToolCall({
6749
+ tool: durableTool,
6750
+ args: toJsonValue(toolCall.args),
6751
+ context,
6752
+ signal: input.signal,
6753
+ answer
6754
+ });
6755
+ if ("interaction" in decision && decision.interaction !== void 0) {
6756
+ if (!input.driver) {
6757
+ throw new Error("Tool call requested interaction but no interaction driver is active.");
6758
+ }
6759
+ const interactionRequest = input.driver.request({
6760
+ id: decision.interaction.id,
6761
+ kind: decision.interaction.kind,
6762
+ prompt: decision.interaction.prompt,
6763
+ payload: decision.interaction.payload,
6764
+ origin: "tool_call",
6765
+ toolCallId
6766
+ });
6767
+ input.driver.suspend({
6768
+ checkpoint: {
6769
+ kind: "tool_call_suspension",
6770
+ toolCalls: input.toolCalls,
6771
+ pendingIndex: index,
6772
+ toolCallId,
6773
+ interactionId: interactionRequest.id,
6774
+ completedResults: results
6775
+ }
6776
+ });
6777
+ }
6778
+ if ("allow" in decision && !decision.allow) {
6779
+ const reserved2 = await this.owned.reserveToolCalls({
6780
+ runId: input.run.id,
6781
+ execution: input.execution,
6782
+ expectedRevision: revision,
6783
+ requestMessageId: createId("msg"),
6784
+ calls: [{
6785
+ toolCallId,
6786
+ providerToolCallId,
6787
+ name: toolCall.name,
6788
+ args: toJsonValue(toolCall.args)
6789
+ }]
6790
+ });
6791
+ setRevision(reserved2.run.revision);
6792
+ const failed = await this.owned.commitToolResult({
6793
+ runId: input.run.id,
6794
+ execution: input.execution,
6795
+ expectedRevision: revision,
6796
+ toolCallId,
6797
+ state: "failed",
6798
+ result: { ok: false, content: null, error: decision.reason }
6799
+ });
6800
+ setRevision(failed.run.revision);
6801
+ results.push(protocolFailure(decision.reason));
6802
+ continue;
6803
+ }
6804
+ }
6805
+ } catch (error) {
6806
+ if (error instanceof PhaseInteractionBoundary || error instanceof PhaseInteractionCancelledError) {
6807
+ throw error;
6808
+ }
6809
+ const reason = error instanceof Error ? error.message : "Tool policy rejected the call.";
6810
+ const reserved2 = await this.owned.reserveToolCalls({
6811
+ runId: input.run.id,
6812
+ execution: input.execution,
6813
+ expectedRevision: revision,
6814
+ requestMessageId: createId("msg"),
6815
+ calls: [{
6816
+ toolCallId,
6817
+ providerToolCallId,
6818
+ name: toolCall.name,
6819
+ args: toJsonValue(toolCall.args)
6820
+ }]
6821
+ });
6822
+ setRevision(reserved2.run.revision);
6823
+ const failed = await this.owned.commitToolResult({
6824
+ runId: input.run.id,
6825
+ execution: input.execution,
6826
+ expectedRevision: revision,
6827
+ toolCallId,
6828
+ state: "failed",
6829
+ result: { ok: false, content: null, error: reason }
6830
+ });
6831
+ setRevision(failed.run.revision);
6832
+ results.push(protocolFailure(reason));
6833
+ continue;
6834
+ }
6835
+ const reserved = await this.owned.reserveToolCalls({
6836
+ runId: input.run.id,
6837
+ execution: input.execution,
6838
+ expectedRevision: revision,
6839
+ requestMessageId: createId("msg"),
6840
+ calls: [{
6841
+ toolCallId,
6842
+ providerToolCallId,
6843
+ name: toolCall.name,
6844
+ args: toJsonValue(toolCall.args)
6845
+ }]
6846
+ });
6847
+ setRevision(reserved.run.revision);
6848
+ const started = await this.owned.startToolCall({
6849
+ runId: input.run.id,
6850
+ execution: input.execution,
6851
+ expectedRevision: revision,
6852
+ toolCallId
6853
+ });
6854
+ setRevision(started.run.revision);
6855
+ progressActive = true;
6856
+ try {
6857
+ let result = await durableTool.execute(toJsonValue(toolCall.args), context, input.signal);
6858
+ assertToolExecutionResult(result);
6859
+ if (input.toolConfig.afterToolCall) {
6860
+ result = await input.toolConfig.afterToolCall({
6861
+ tool: durableTool,
6862
+ result,
6863
+ context,
6864
+ signal: input.signal
6865
+ });
6866
+ }
6867
+ assertToolExecutionResult(result);
6868
+ result = await spillLargeToolResult(
6869
+ result,
6870
+ this.archiveDirFor(input.run.agentId),
6871
+ toolCall.name
6872
+ );
6873
+ const committed = await this.owned.commitToolResult({
6874
+ runId: input.run.id,
6875
+ execution: input.execution,
6876
+ expectedRevision: revision,
6877
+ toolCallId,
6878
+ state: result.ok ? "completed" : "failed",
6879
+ result
6880
+ });
6881
+ setRevision(committed.run.revision);
6882
+ results.push({
6883
+ toolCallId: providerToolCallId,
6884
+ toolName: toolCall.name,
6885
+ ...result
6886
+ });
6887
+ } catch (error) {
6888
+ const reason = error instanceof Error ? error.message : "Tool execution outcome is indeterminate.";
6889
+ const indeterminate = await this.owned.commitToolResult({
6890
+ runId: input.run.id,
6891
+ execution: input.execution,
6892
+ expectedRevision: revision,
6893
+ toolCallId,
6894
+ state: "indeterminate",
6895
+ reason,
6896
+ result: { ok: false, content: null, error: reason }
6897
+ });
6898
+ setRevision(indeterminate.run.revision);
6899
+ results.push(protocolFailure(reason));
6900
+ } finally {
6901
+ progressActive = false;
6902
+ this.transientEvents.clearTool(input.run.id, toolCallId);
6903
+ }
6492
6904
  }
6905
+ input.driver?.clearCheckpoint();
6906
+ return { results, revision };
6493
6907
  }
6494
6908
  async requireAgent(agentId) {
6495
6909
  const agent = (await this.owned.listAgents()).find((candidate) => candidate.id === agentId);
@@ -6668,7 +7082,7 @@ function boundaryFromSnapshot(snapshot) {
6668
7082
  type: "input_required",
6669
7083
  requestId: snapshot.request.id,
6670
7084
  phase: snapshot.request.phase,
6671
- prompt: snapshot.request.prompt,
7085
+ ...snapshot.request.prompt ? { prompt: snapshot.request.prompt } : {},
6672
7086
  interactions: snapshot.interactions,
6673
7087
  answers: snapshot.answers
6674
7088
  };
@@ -7243,24 +7657,28 @@ var InMemoryStore = class _InMemoryStore {
7243
7657
  const requestId = input.requestId ?? createId("input");
7244
7658
  const operationKey = `input_required:${requestId}`;
7245
7659
  const interactions = input.interactions ?? [];
7246
- const operationPayload = canonicalJson([input.runId, input.expectedRevision, input.phase, input.prompt.id, input.checkpoint, interactions, input.interactionAnswers ?? null]);
7660
+ const operationPayload = canonicalJson([input.runId, input.expectedRevision, input.phase, input.prompt ? input.prompt.id : null, input.checkpoint, interactions, input.interactionAnswers ?? null]);
7247
7661
  const replay = this.replayOperation(operationKey, operationPayload);
7248
7662
  if (replay) return clone(replay);
7249
7663
  const run = this.requireRun(input.runId);
7250
7664
  this.assertExecution(run, input.execution, input.expectedRevision);
7251
7665
  this.assertState(run, ["running"]);
7252
7666
  this.assertNoOpenTools(run);
7253
- const prompt = {
7254
- ...clone(input.prompt),
7255
- sequenceWithinRun: this.nextMessageSequence(run.id)
7256
- };
7667
+ const isToolCallInteraction = interactions.length > 0 && interactions.some((interaction) => interaction.origin === "tool_call");
7668
+ let prompt;
7669
+ if (input.prompt && !isToolCallInteraction) {
7670
+ prompt = {
7671
+ ...clone(input.prompt),
7672
+ sequenceWithinRun: this.nextMessageSequence(run.id)
7673
+ };
7674
+ this.messages.set(prompt.id, prompt);
7675
+ }
7257
7676
  const request = {
7258
7677
  id: requestId,
7259
7678
  phase: input.phase,
7260
- messageId: prompt.id,
7679
+ ...prompt ? { messageId: prompt.id } : {},
7261
7680
  createdAt: createTimestamp()
7262
7681
  };
7263
- this.messages.set(prompt.id, prompt);
7264
7682
  run.state = "input_required";
7265
7683
  run.checkpoint = clone(input.checkpoint);
7266
7684
  run.openInputRequest = request;
@@ -7275,9 +7693,16 @@ var InMemoryStore = class _InMemoryStore {
7275
7693
  delete run.execution;
7276
7694
  run.revision += 1;
7277
7695
  run.updatedAt = createTimestamp();
7278
- this.appendMessage(run, prompt);
7696
+ if (prompt) {
7697
+ this.appendMessage(run, prompt);
7698
+ }
7279
7699
  this.appendTransition(run, "running", "input_required", request, prompt, void 0, void 0, void 0, interactions, run.interactionAnswers ?? {});
7280
- const result = { run: clone(run), prompt: clone(prompt), request: clone(request), interactions: clone(interactions) };
7700
+ const result = {
7701
+ run: clone(run),
7702
+ ...prompt ? { prompt: clone(prompt) } : {},
7703
+ request: clone(request),
7704
+ interactions: clone(interactions)
7705
+ };
7281
7706
  this.writeOperationReceipt(operationKey, operationPayload, result);
7282
7707
  return result;
7283
7708
  }
@@ -7330,28 +7755,33 @@ var InMemoryStore = class _InMemoryStore {
7330
7755
  this.assertRevision(run, input.expectedRevision);
7331
7756
  this.assertState(run, ["input_required"]);
7332
7757
  const openInteractions = run.openInteractions ?? [];
7333
- if (!openInteractions.some((interaction) => interaction.id === input.interactionId)) {
7758
+ const targetInteraction = openInteractions.find((interaction) => interaction.id === input.interactionId);
7759
+ if (!targetInteraction) {
7334
7760
  throw new RuntimeError("input_request_conflict", { runId: run.id, requestId: input.interactionId, reason: "not_found" });
7335
7761
  }
7336
- const message = {
7337
- id: createId("msg"),
7338
- agentId: run.agentId,
7339
- runId: run.id,
7340
- role: "user",
7341
- content: typeof input.input === "string" ? input.input : JSON.stringify(input.input),
7342
- metadata: { kind: "phase_interaction", interactionId: input.interactionId },
7343
- sequenceWithinRun: this.nextMessageSequence(run.id),
7344
- createdAt: createTimestamp()
7345
- };
7346
- this.messages.set(message.id, message);
7762
+ const isToolCall = targetInteraction.origin === "tool_call";
7763
+ const now = createTimestamp();
7764
+ if (!isToolCall) {
7765
+ const message = {
7766
+ id: createId("msg"),
7767
+ agentId: run.agentId,
7768
+ runId: run.id,
7769
+ role: "user",
7770
+ content: typeof input.input === "string" ? input.input : JSON.stringify(input.input),
7771
+ metadata: { kind: "phase_interaction", interactionId: input.interactionId },
7772
+ sequenceWithinRun: this.nextMessageSequence(run.id),
7773
+ createdAt: now
7774
+ };
7775
+ this.messages.set(message.id, message);
7776
+ this.appendMessage(run, message);
7777
+ }
7347
7778
  run.openInteractions = openInteractions.filter((interaction) => interaction.id !== input.interactionId);
7348
7779
  run.interactionAnswers = {
7349
7780
  ...run.interactionAnswers ?? {},
7350
7781
  [input.interactionId]: clone(input.input)
7351
7782
  };
7352
7783
  run.revision += 1;
7353
- run.updatedAt = message.createdAt;
7354
- this.appendMessage(run, message);
7784
+ run.updatedAt = now;
7355
7785
  if (run.openInteractions.length === 0) {
7356
7786
  delete run.openInteractions;
7357
7787
  delete run.openInputRequest;
@@ -7652,12 +8082,16 @@ var InMemoryStore = class _InMemoryStore {
7652
8082
  return { ...base, state: run.state };
7653
8083
  case "input_required": {
7654
8084
  const request = run.openInputRequest;
7655
- const prompt = request && this.messages.get(request.messageId);
7656
- if (!prompt || prompt.role !== "assistant") throw new Error(`Run ${run.id} has an invalid input prompt.`);
8085
+ const prompt = request?.messageId ? this.messages.get(request.messageId) : void 0;
8086
+ if (request?.messageId && (!prompt || prompt.role !== "assistant")) throw new Error(`Run ${run.id} has an invalid input prompt.`);
7657
8087
  return {
7658
8088
  ...base,
7659
8089
  state: "input_required",
7660
- request: { id: request.id, phase: request.phase, prompt: clone(prompt) },
8090
+ request: {
8091
+ id: request?.id ?? "",
8092
+ phase: request?.phase ?? "",
8093
+ ...prompt ? { prompt: clone(prompt) } : {}
8094
+ },
7661
8095
  interactions: clone(run.openInteractions ?? []),
7662
8096
  answers: clone(run.interactionAnswers ?? {})
7663
8097
  };
@@ -7979,7 +8413,13 @@ ${record.summary}`,
7979
8413
  kind: "run_state_changed",
7980
8414
  from,
7981
8415
  to,
7982
- ...to === "input_required" && request && prompt ? { request: { id: request.id, phase: request.phase, prompt } } : {},
8416
+ ...to === "input_required" && request ? {
8417
+ request: {
8418
+ id: request.id,
8419
+ phase: request.phase,
8420
+ ...prompt ? { prompt } : {}
8421
+ }
8422
+ } : {},
7983
8423
  ...to === "input_required" ? { interactions: clone(interactions ?? []), answers: clone(answers ?? {}) } : {},
7984
8424
  ...to === "completed" && outcome ? { outcome } : {},
7985
8425
  ...to === "failed" && failure ? { failure } : {},