@agentick/core 0.6.0 → 0.8.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 (126) hide show
  1. package/dist/.tsbuildinfo.build +1 -1
  2. package/dist/agentick-instance.d.ts.map +1 -1
  3. package/dist/agentick-instance.js +28 -0
  4. package/dist/agentick-instance.js.map +1 -1
  5. package/dist/app/inbox-storage.d.ts +24 -0
  6. package/dist/app/inbox-storage.d.ts.map +1 -0
  7. package/dist/app/inbox-storage.js +99 -0
  8. package/dist/app/inbox-storage.js.map +1 -0
  9. package/dist/app/session.d.ts +24 -3
  10. package/dist/app/session.d.ts.map +1 -1
  11. package/dist/app/session.js +285 -27
  12. package/dist/app/session.js.map +1 -1
  13. package/dist/app/types.d.ts +99 -0
  14. package/dist/app/types.d.ts.map +1 -1
  15. package/dist/app.d.ts +2 -1
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/app.js +1 -0
  18. package/dist/app.js.map +1 -1
  19. package/dist/com/object-model.d.ts +9 -3
  20. package/dist/com/object-model.d.ts.map +1 -1
  21. package/dist/com/object-model.js +52 -24
  22. package/dist/com/object-model.js.map +1 -1
  23. package/dist/compiler/collector.d.ts.map +1 -1
  24. package/dist/compiler/collector.js +453 -15
  25. package/dist/compiler/collector.js.map +1 -1
  26. package/dist/compiler/fiber-compiler.d.ts.map +1 -1
  27. package/dist/compiler/fiber-compiler.js.map +1 -1
  28. package/dist/engine/engine-events.d.ts +1 -0
  29. package/dist/engine/engine-events.d.ts.map +1 -1
  30. package/dist/engine/engine-events.js +1 -0
  31. package/dist/engine/engine-events.js.map +1 -1
  32. package/dist/engine/tool-confirmation-coordinator.d.ts +1 -1
  33. package/dist/engine/tool-confirmation-coordinator.d.ts.map +1 -1
  34. package/dist/engine/tool-confirmation-coordinator.js +2 -1
  35. package/dist/engine/tool-confirmation-coordinator.js.map +1 -1
  36. package/dist/engine/tool-executor.d.ts +15 -1
  37. package/dist/engine/tool-executor.d.ts.map +1 -1
  38. package/dist/engine/tool-executor.js +64 -6
  39. package/dist/engine/tool-executor.js.map +1 -1
  40. package/dist/hooks/expandable.d.ts +37 -0
  41. package/dist/hooks/expandable.d.ts.map +1 -0
  42. package/dist/hooks/expandable.js +40 -0
  43. package/dist/hooks/expandable.js.map +1 -0
  44. package/dist/hooks/gate.d.ts +27 -0
  45. package/dist/hooks/gate.d.ts.map +1 -0
  46. package/dist/hooks/gate.js +59 -0
  47. package/dist/hooks/gate.js.map +1 -0
  48. package/dist/hooks/index.d.ts +2 -0
  49. package/dist/hooks/index.d.ts.map +1 -1
  50. package/dist/hooks/index.js +4 -0
  51. package/dist/hooks/index.js.map +1 -1
  52. package/dist/hooks/knob.d.ts +2 -0
  53. package/dist/hooks/knob.d.ts.map +1 -1
  54. package/dist/hooks/knob.js +5 -0
  55. package/dist/hooks/knob.js.map +1 -1
  56. package/dist/hooks/knobs-component.d.ts +2 -0
  57. package/dist/hooks/knobs-component.d.ts.map +1 -1
  58. package/dist/hooks/knobs-component.js +88 -36
  59. package/dist/hooks/knobs-component.js.map +1 -1
  60. package/dist/hooks/runtime-context.d.ts +1 -0
  61. package/dist/hooks/runtime-context.d.ts.map +1 -1
  62. package/dist/hooks/runtime-context.js.map +1 -1
  63. package/dist/index.d.ts +2 -2
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +7 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/jsx/components/auto-summary.d.ts +31 -0
  68. package/dist/jsx/components/auto-summary.d.ts.map +1 -0
  69. package/dist/jsx/components/auto-summary.js +83 -0
  70. package/dist/jsx/components/auto-summary.js.map +1 -0
  71. package/dist/jsx/components/collapsed.d.ts +22 -0
  72. package/dist/jsx/components/collapsed.d.ts.map +1 -0
  73. package/dist/jsx/components/collapsed.js +21 -0
  74. package/dist/jsx/components/collapsed.js.map +1 -0
  75. package/dist/jsx/components/content.d.ts +26 -13
  76. package/dist/jsx/components/content.d.ts.map +1 -1
  77. package/dist/jsx/components/content.js +61 -22
  78. package/dist/jsx/components/content.js.map +1 -1
  79. package/dist/jsx/components/index.d.ts +2 -0
  80. package/dist/jsx/components/index.d.ts.map +1 -1
  81. package/dist/jsx/components/index.js +2 -0
  82. package/dist/jsx/components/index.js.map +1 -1
  83. package/dist/jsx/components/messages.d.ts.map +1 -1
  84. package/dist/jsx/components/messages.js +4 -8
  85. package/dist/jsx/components/messages.js.map +1 -1
  86. package/dist/jsx/components/primitives.d.ts +34 -2
  87. package/dist/jsx/components/primitives.d.ts.map +1 -1
  88. package/dist/jsx/components/primitives.js +79 -21
  89. package/dist/jsx/components/primitives.js.map +1 -1
  90. package/dist/jsx/components/semantic.d.ts.map +1 -1
  91. package/dist/jsx/components/semantic.js +10 -12
  92. package/dist/jsx/components/semantic.js.map +1 -1
  93. package/dist/jsx/components/timeline.d.ts.map +1 -1
  94. package/dist/jsx/components/timeline.js +9 -17
  95. package/dist/jsx/components/timeline.js.map +1 -1
  96. package/dist/local-transport.d.ts.map +1 -1
  97. package/dist/local-transport.js +4 -0
  98. package/dist/local-transport.js.map +1 -1
  99. package/dist/model/adapter.d.ts +1 -1
  100. package/dist/model/adapter.d.ts.map +1 -1
  101. package/dist/model/adapter.js +7 -4
  102. package/dist/model/adapter.js.map +1 -1
  103. package/dist/model/model.d.ts +2 -0
  104. package/dist/model/model.d.ts.map +1 -1
  105. package/dist/renderers/base.d.ts +9 -0
  106. package/dist/renderers/base.d.ts.map +1 -1
  107. package/dist/renderers/base.js +31 -0
  108. package/dist/renderers/base.js.map +1 -1
  109. package/dist/renderers/markdown.d.ts.map +1 -1
  110. package/dist/renderers/markdown.js +30 -0
  111. package/dist/renderers/markdown.js.map +1 -1
  112. package/dist/renderers/xml.d.ts.map +1 -1
  113. package/dist/renderers/xml.js +20 -0
  114. package/dist/renderers/xml.js.map +1 -1
  115. package/dist/testing/mock-app.d.ts.map +1 -1
  116. package/dist/testing/mock-app.js +15 -0
  117. package/dist/testing/mock-app.js.map +1 -1
  118. package/dist/tool/tool.d.ts +10 -0
  119. package/dist/tool/tool.d.ts.map +1 -1
  120. package/dist/tool/tool.js +2 -0
  121. package/dist/tool/tool.js.map +1 -1
  122. package/dist/utils/classify-error.d.ts +1 -1
  123. package/dist/utils/classify-error.d.ts.map +1 -1
  124. package/dist/utils/classify-error.js +8 -0
  125. package/dist/utils/classify-error.js.map +1 -1
  126. package/package.json +3 -3
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { EventEmitter } from "node:events";
16
16
  import { randomUUID } from "node:crypto";
17
- import { Context, createProcedure, Channel, EventBuffer, ExecutionHandleBrand, Logger, } from "@agentick/kernel";
17
+ import { Context, createProcedure, Channel, EventBuffer, ExecutionHandleBrand, Logger, parseSchema, } from "@agentick/kernel";
18
18
  import { FiberCompiler, StructureRenderer, ReconciliationScheduler, } from "../compiler";
19
19
  import { COM } from "../com/object-model";
20
20
  import { MarkdownRenderer } from "../renderers/index";
@@ -71,6 +71,7 @@ export class SessionImpl extends EventEmitter {
71
71
  _tick = 1;
72
72
  _isAborted = false;
73
73
  _currentExecutionId = null;
74
+ _currentToolCallId = null;
74
75
  // Compilation infrastructure (no intermediate layer)
75
76
  compiler = null;
76
77
  ctx = null;
@@ -117,8 +118,14 @@ export class SessionImpl extends EventEmitter {
117
118
  sessionOptions;
118
119
  // Last props for hot-update support
119
120
  _lastProps = null;
121
+ _mountPromise = null;
120
122
  // Captured context from session creation
121
123
  _capturedContext;
124
+ // Inbox (durable external message delivery)
125
+ _inboxStorage = null;
126
+ _inboxUnsubscribe = null;
127
+ _draining = false;
128
+ _drainRequested = false;
122
129
  // Channels for pub/sub communication
123
130
  _channels = new Map();
124
131
  // Current execution handle (for concurrent send idempotency)
@@ -238,6 +245,7 @@ export class SessionImpl extends EventEmitter {
238
245
  send;
239
246
  render;
240
247
  spawn;
248
+ dispatch;
241
249
  initProcedures() {
242
250
  // Queue procedure - queues messages and notifies components
243
251
  this.queue = createProcedure({
@@ -401,8 +409,15 @@ export class SessionImpl extends EventEmitter {
401
409
  inheritDefaults: this.appOptions.inheritDefaults,
402
410
  runner: spawnOptions?.runner ?? this.appOptions.runner,
403
411
  };
412
+ // Child abort signal: merge session-level (parent dies → child dies)
413
+ // and execution-level (parent execution aborted → child aborted).
414
+ // Session signal always present; execution signal only during active tick.
415
+ const abortSignals = [this.sessionAbortController.signal];
416
+ if (this.executionAbortController) {
417
+ abortSignals.push(this.executionAbortController.signal);
418
+ }
404
419
  const childOptions = {
405
- signal: this.executionAbortController?.signal,
420
+ signal: AbortSignal.any(abortSignals),
406
421
  devTools: this.sessionOptions.devTools ?? this.appOptions.devTools,
407
422
  };
408
423
  const child = new SessionImpl(Component, childAppOptions, childOptions);
@@ -414,15 +429,108 @@ export class SessionImpl extends EventEmitter {
414
429
  ...resolvedInput,
415
430
  props: mergedProps,
416
431
  });
417
- // 4. Cleanup on completion
418
- handle.result
432
+ // 4. Spawn lifecycle events & child event forwarding
433
+ const spawnId = randomUUID();
434
+ this.emitEvent({
435
+ type: "spawn_start",
436
+ spawnId,
437
+ parentExecutionId: this._currentExecutionId ?? "",
438
+ childExecutionId: handle.sessionId,
439
+ componentName: Component.displayName || Component.name,
440
+ label: spawnOptions?.label,
441
+ originCallId: this._currentToolCallId ?? undefined,
442
+ });
443
+ // Forward child events to parent's buffer, tagged with spawnPath.
444
+ // Recursive spawns work automatically: child events already have
445
+ // spawnPath from grandchildren, we prepend our spawnId.
446
+ const childCallIds = new Set();
447
+ // Forward child events, then emit spawn_end after forwarding completes.
448
+ // This guarantees spawn_end comes AFTER all forwarded child events —
449
+ // no interleaving of spawn_end with trailing content deltas.
450
+ const forwardingPromise = (async () => {
451
+ for await (const event of handle) {
452
+ if (event.type === "tool_confirmation_required") {
453
+ childCallIds.add(event.callId);
454
+ }
455
+ this.emitEvent({
456
+ ...event,
457
+ spawnPath: [spawnId, ...(event.spawnPath ?? [])],
458
+ });
459
+ }
460
+ })();
461
+ // Prevent unhandled rejection if parent aborts mid-forward
462
+ forwardingPromise.catch(() => { });
463
+ // Route parent confirmation responses to child's channel
464
+ const parentConfirmChannel = this.channel("tool_confirmation");
465
+ const unsubConfirm = parentConfirmChannel.subscribe((event) => {
466
+ if (event.type === "response" && event.id && childCallIds.has(event.id)) {
467
+ child.channel("tool_confirmation").publish(event);
468
+ childCallIds.delete(event.id);
469
+ }
470
+ });
471
+ // spawn_end after all child events are forwarded (not racing with them).
472
+ // We await forwardingPromise to ensure ordering, then use handle.result
473
+ // for the final output/error.
474
+ forwardingPromise
475
+ .then(() => handle.result)
476
+ .then((result) => {
477
+ this.emitEvent({
478
+ type: "spawn_end",
479
+ spawnId,
480
+ parentExecutionId: this._currentExecutionId ?? "",
481
+ childExecutionId: handle.sessionId,
482
+ output: result.response,
483
+ usage: result.usage,
484
+ });
485
+ })
486
+ .catch((error) => {
487
+ this.emitEvent({
488
+ type: "spawn_end",
489
+ spawnId,
490
+ parentExecutionId: this._currentExecutionId ?? "",
491
+ childExecutionId: handle.sessionId,
492
+ output: error?.message ?? "spawn failed",
493
+ isError: true,
494
+ });
495
+ });
496
+ // 5. Cleanup after spawn_end is emitted
497
+ forwardingPromise
498
+ .then(() => handle.result)
419
499
  .finally(async () => {
500
+ unsubConfirm();
501
+ childCallIds.clear();
420
502
  this._children = this._children.filter((c) => c !== child);
421
503
  await child.close();
422
504
  })
423
505
  .catch(() => { });
424
506
  return handle;
425
507
  });
508
+ // Dispatch procedure - dispatches tools by name/alias from the user side
509
+ this.dispatch = createProcedure({
510
+ name: "session:dispatch",
511
+ metadata: { operation: "dispatch" },
512
+ handleFactory: false,
513
+ executionBoundary: false,
514
+ }, async (name, input) => {
515
+ if (this.isTerminal)
516
+ throw new Error(this.terminalError);
517
+ await this.mount();
518
+ const tool = this.ctx.getTool(name) ?? this.ctx.getToolByAlias(name);
519
+ if (!tool)
520
+ throw new Error(`Unknown command: ${name}`);
521
+ if (!tool.run)
522
+ throw new Error(`Command "${name}" has no handler`);
523
+ // Validate input against tool's schema
524
+ const validatedInput = tool.metadata.input
525
+ ? await parseSchema(tool.metadata.input, input)
526
+ : input;
527
+ const result = await tool.run.exec(validatedInput).result;
528
+ if (Array.isArray(result))
529
+ return result;
530
+ if (typeof result === "string")
531
+ return [{ type: "text", text: result }];
532
+ throw new Error(`Unexpected tool result type: ${typeof result}. Expected ContentBlock[] or string.`);
533
+ });
426
534
  }
427
535
  // ════════════════════════════════════════════════════════════════════════
428
536
  // SessionExecutionHandle Creation
@@ -690,6 +798,40 @@ export class SessionImpl extends EventEmitter {
690
798
  clearAbort() {
691
799
  this._isAborted = false;
692
800
  }
801
+ async mount() {
802
+ // Order matters: _mountPromise must be checked FIRST.
803
+ // _doMount() sets this.compiler synchronously (via ensureCompilationInfrastructure)
804
+ // before tool registration completes. A concurrent caller seeing compiler=truthy
805
+ // would skip the mount and fail tool lookup.
806
+ if (this._mountPromise)
807
+ return this._mountPromise;
808
+ if (this.compiler)
809
+ return; // Mounted via render() or previous mount()
810
+ this._mountPromise = this._doMount();
811
+ try {
812
+ await this._mountPromise;
813
+ }
814
+ catch (e) {
815
+ this._mountPromise = null; // Allow retry on failure
816
+ throw e;
817
+ }
818
+ }
819
+ async _doMount() {
820
+ const rootElement = jsx(this.Component, this._lastProps ?? {});
821
+ await this.ensureCompilationInfrastructure(rootElement);
822
+ // Single compile pass — no notifyTickStart (no catch-up), no compileUntilStable (no afterCompile).
823
+ // This only renders the tree to collect tools. No tick lifecycle hooks fire.
824
+ const tickState = {
825
+ tick: 0,
826
+ queuedMessages: [],
827
+ timeline: [],
828
+ stop: () => { },
829
+ };
830
+ const compiled = await this.compiler.compile(rootElement, tickState);
831
+ // Register collected tools so they're available for dispatch
832
+ const mergedTools = this.mergeTools(this.appOptions.tools ?? [], this.sessionOptions.tools ?? [], [], compiled.tools);
833
+ await Promise.all(mergedTools.map((tool) => this.ctx.addTool(tool)));
834
+ }
693
835
  startExecutionAbort(signal) {
694
836
  this.executionAbortCleanup.forEach((cleanup) => cleanup());
695
837
  this.executionAbortCleanup = [];
@@ -778,16 +920,22 @@ export class SessionImpl extends EventEmitter {
778
920
  devToolsEnabled: sessionCtx.devToolsEnabled ?? this.sessionOptions.devTools ?? false,
779
921
  });
780
922
  // Invoke lifecycle callbacks
923
+ // Forwarded child events (with spawnPath) only trigger onEvent, not
924
+ // lifecycle-specific callbacks like onTickStart/onTickEnd — those
925
+ // semantics belong to the parent's own tick lifecycle.
781
926
  const cb = this.callbacks;
927
+ const isForwarded = enrichedEvent.spawnPath?.length > 0;
782
928
  try {
783
929
  cb.onEvent?.(enrichedEvent);
784
- // Call specific callbacks based on event type
785
- const eventType = enrichedEvent.type;
786
- if (eventType === "tick_start") {
787
- cb.onTickStart?.(enrichedEvent.tick, enrichedEvent.executionId);
788
- }
789
- else if (eventType === "tick_end") {
790
- cb.onTickEnd?.(enrichedEvent.tick, enrichedEvent.usage);
930
+ if (!isForwarded) {
931
+ // Call specific callbacks based on event type
932
+ const eventType = enrichedEvent.type;
933
+ if (eventType === "tick_start") {
934
+ cb.onTickStart?.(enrichedEvent.tick, enrichedEvent.executionId);
935
+ }
936
+ else if (eventType === "tick_end") {
937
+ cb.onTickEnd?.(enrichedEvent.tick, enrichedEvent.usage);
938
+ }
791
939
  }
792
940
  }
793
941
  catch {
@@ -829,6 +977,78 @@ export class SessionImpl extends EventEmitter {
829
977
  setPersistCallback(callback) {
830
978
  this._persistCallback = callback;
831
979
  }
980
+ /**
981
+ * Connect this session to an inbox storage backend.
982
+ * Subscribes to notifications and drains any pre-existing pending messages.
983
+ * @internal
984
+ */
985
+ setInboxStorage(storage) {
986
+ this._inboxStorage = storage;
987
+ this._inboxUnsubscribe = storage.subscribe(this.id, () => {
988
+ this.drainInbox().catch((err) => {
989
+ this.log.warn({ error: err }, "Inbox drain failed");
990
+ });
991
+ });
992
+ // Drain pre-existing pending messages
993
+ this.drainInbox().catch((err) => {
994
+ this.log.warn({ error: err }, "Initial inbox drain failed");
995
+ });
996
+ }
997
+ /**
998
+ * Process all pending inbox messages. Called by App.processInbox().
999
+ * @internal
1000
+ */
1001
+ async processInboxMessages() {
1002
+ await this.drainInbox();
1003
+ }
1004
+ async drainInbox() {
1005
+ const storage = this._inboxStorage;
1006
+ if (this.isTerminal || !storage)
1007
+ return;
1008
+ if (this._draining) {
1009
+ // Signal that new messages arrived; current drain will re-check on exit
1010
+ this._drainRequested = true;
1011
+ return;
1012
+ }
1013
+ this._draining = true;
1014
+ try {
1015
+ do {
1016
+ this._drainRequested = false;
1017
+ const messages = await storage.pending(this.id);
1018
+ for (const msg of messages) {
1019
+ if (this.isTerminal)
1020
+ return;
1021
+ try {
1022
+ if (msg.type === "message") {
1023
+ const handle = await this.send({ messages: [msg.payload] });
1024
+ await handle.result;
1025
+ }
1026
+ else {
1027
+ await this.dispatch.exec(msg.payload.tool, msg.payload.input).result;
1028
+ }
1029
+ if (this.isTerminal)
1030
+ return;
1031
+ await storage.markDone(this.id, msg.id);
1032
+ }
1033
+ catch (err) {
1034
+ this.log.warn({ error: err, messageId: msg.id }, "Inbox message processing failed");
1035
+ break; // Preserve FIFO — stop on first failure
1036
+ }
1037
+ }
1038
+ } while (this._drainRequested && !this.isTerminal);
1039
+ }
1040
+ finally {
1041
+ this._draining = false;
1042
+ // Do NOT clear _drainRequested here — a notification may have arrived
1043
+ // between the while-check and finally. If so, re-enter.
1044
+ if (this._drainRequested && !this.isTerminal) {
1045
+ this._drainRequested = false;
1046
+ this.drainInbox().catch((err) => {
1047
+ this.log.warn({ error: err }, "Inbox re-drain failed");
1048
+ });
1049
+ }
1050
+ }
1051
+ }
832
1052
  /**
833
1053
  * Set a snapshot to be applied/resolved when compilation infrastructure is created.
834
1054
  * @internal
@@ -1222,6 +1442,10 @@ export class SessionImpl extends EventEmitter {
1222
1442
  if (this.isTerminal)
1223
1443
  return;
1224
1444
  this._status = "closed";
1445
+ // Unsubscribe from inbox before anything else
1446
+ this._inboxUnsubscribe?.();
1447
+ this._inboxUnsubscribe = null;
1448
+ this._inboxStorage = null;
1225
1449
  // Notify execution runner of destroy
1226
1450
  if (this._runnerInitialized && this.appOptions.runner?.onDestroy) {
1227
1451
  try {
@@ -1474,12 +1698,25 @@ export class SessionImpl extends EventEmitter {
1474
1698
  }
1475
1699
  // Stream model output if supported
1476
1700
  let modelOutput;
1701
+ let streamed = false;
1477
1702
  if (model.stream) {
1703
+ streamed = true;
1478
1704
  const streamIterable = await model.stream(modelInput);
1479
1705
  for await (const event of streamIterable) {
1480
1706
  if (signal.aborted) {
1481
1707
  throw new AbortError("Execution aborted", signal.reason);
1482
1708
  }
1709
+ // Enrich tool_call with displaySummary — the stream accumulator
1710
+ // doesn't have access to tool definitions, so we add it here.
1711
+ if (event.type === "tool_call" && "name" in event && compiled.tools) {
1712
+ const toolDef = compiled.tools.find((t) => t.metadata?.name === event.name);
1713
+ if (toolDef) {
1714
+ const summary = tryDisplaySummary(toolDef, event.input);
1715
+ if (summary) {
1716
+ event.summary = summary;
1717
+ }
1718
+ }
1719
+ }
1483
1720
  this.emitEvent(event);
1484
1721
  if (event.type === "message" && "message" in event) {
1485
1722
  const messageEvent = event;
@@ -1561,20 +1798,24 @@ export class SessionImpl extends EventEmitter {
1561
1798
  let toolStartTime;
1562
1799
  if (response.toolCalls?.length && this.ctx) {
1563
1800
  toolStartTime = Date.now();
1564
- for (const call of response.toolCalls) {
1565
- const toolCallTimestamp = timestamp();
1566
- const toolDef = compiled.tools?.find((t) => t.metadata?.name === call.name);
1567
- const summary = tryDisplaySummary(toolDef, call.input);
1568
- this.emitEvent({
1569
- type: "tool_call",
1570
- callId: call.id,
1571
- blockIndex: 0,
1572
- name: call.name,
1573
- input: call.input,
1574
- summary,
1575
- startedAt: toolCallTimestamp,
1576
- completedAt: toolCallTimestamp,
1577
- });
1801
+ // Only emit tool_call events for non-streaming path.
1802
+ // The streaming path already emitted these via stream accumulator.
1803
+ if (!streamed) {
1804
+ for (const call of response.toolCalls) {
1805
+ const toolCallTimestamp = timestamp();
1806
+ const toolDef = compiled.tools?.find((t) => t.metadata?.name === call.name);
1807
+ const summary = tryDisplaySummary(toolDef, call.input);
1808
+ this.emitEvent({
1809
+ type: "tool_call",
1810
+ callId: call.id,
1811
+ blockIndex: 0,
1812
+ name: call.name,
1813
+ input: call.input,
1814
+ summary,
1815
+ startedAt: toolCallTimestamp,
1816
+ completedAt: toolCallTimestamp,
1817
+ });
1818
+ }
1578
1819
  }
1579
1820
  toolResults = await this.executeTools(toolExecutor, response.toolCalls, compiled.tools, outputs, currentTick, timestamp);
1580
1821
  }
@@ -1807,7 +2048,7 @@ export class SessionImpl extends EventEmitter {
1807
2048
  this.scheduler.schedule(reason ?? "COM recompile request");
1808
2049
  });
1809
2050
  // Wire COM spawn delegate to session's spawn Procedure
1810
- this.ctx.setSpawnCallback((agent, input) => this.spawn(agent, input));
2051
+ this.ctx.setSpawnCallback((agent, input, options) => this.spawn(agent, input, options));
1811
2052
  this.structureRenderer = new StructureRenderer(this.ctx);
1812
2053
  this.structureRenderer.setDefaultRenderer(new MarkdownRenderer());
1813
2054
  // Tools are registered in compileTick() after merging all sources
@@ -2005,10 +2246,15 @@ export class SessionImpl extends EventEmitter {
2005
2246
  if (event.type === "response" && event.id) {
2006
2247
  const payload = event.payload;
2007
2248
  if (payload) {
2008
- coordinator.resolveConfirmation(event.id, payload.approved, payload.always ?? false);
2249
+ coordinator.resolveConfirmation(event.id, payload.approved, payload.always ?? false, payload.reason);
2009
2250
  }
2010
2251
  }
2011
2252
  });
2253
+ // Cancel pending confirmations when execution is aborted (e.g. Ctrl+C).
2254
+ // Without this, waitForConfirmation() hangs forever on abort.
2255
+ const abortSignal = this.executionAbortController?.signal;
2256
+ const onAbort = () => coordinator.cancelAll();
2257
+ abortSignal?.addEventListener("abort", onAbort);
2012
2258
  // Confirmation callbacks for stream event emission
2013
2259
  const confirmationCallbacks = {
2014
2260
  onConfirmationRequired: async (call, message, metadata) => {
@@ -2027,12 +2273,22 @@ export class SessionImpl extends EventEmitter {
2027
2273
  callId: call.id,
2028
2274
  confirmed: confirmation.confirmed,
2029
2275
  always: confirmation.always,
2276
+ reason: confirmation.reason,
2030
2277
  });
2031
2278
  },
2032
2279
  };
2033
2280
  try {
2034
2281
  for (const call of toolCalls) {
2282
+ if (abortSignal?.aborted)
2283
+ break;
2035
2284
  const startedAt = timestamp();
2285
+ this._currentToolCallId = call.id;
2286
+ // Signal tool execution beginning (fills the gap between tool_call and tool_result)
2287
+ this.emitEvent({
2288
+ type: "tool_result_start",
2289
+ callId: call.id,
2290
+ name: call.name,
2291
+ });
2036
2292
  // Check if OUTPUT tool
2037
2293
  const tool = executableTools.find((t) => t.metadata?.name === call.name);
2038
2294
  const isOutputTool = tool && tool.metadata?.type === "output";
@@ -2107,6 +2363,8 @@ export class SessionImpl extends EventEmitter {
2107
2363
  }
2108
2364
  }
2109
2365
  finally {
2366
+ this._currentToolCallId = null;
2367
+ abortSignal?.removeEventListener("abort", onAbort);
2110
2368
  unsubscribe();
2111
2369
  }
2112
2370
  return results;