@adonis-agora/agent 0.60.1 → 0.61.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 (198) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/configure.d.ts.map +1 -1
  3. package/dist/configure.js +2 -0
  4. package/dist/configure.js.map +1 -1
  5. package/dist/providers/agent_provider.d.ts.map +1 -1
  6. package/dist/providers/agent_provider.js +119 -2
  7. package/dist/providers/agent_provider.js.map +1 -1
  8. package/dist/src/action-proposal-approval.d.ts +26 -0
  9. package/dist/src/action-proposal-approval.d.ts.map +1 -0
  10. package/dist/src/action-proposal-approval.js +28 -0
  11. package/dist/src/action-proposal-approval.js.map +1 -0
  12. package/dist/src/action-proposal-discovery.d.ts +9 -0
  13. package/dist/src/action-proposal-discovery.d.ts.map +1 -0
  14. package/dist/src/action-proposal-discovery.js +20 -0
  15. package/dist/src/action-proposal-discovery.js.map +1 -0
  16. package/dist/src/action-proposal-executor.d.ts +37 -0
  17. package/dist/src/action-proposal-executor.d.ts.map +1 -0
  18. package/dist/src/action-proposal-executor.js +70 -0
  19. package/dist/src/action-proposal-executor.js.map +1 -0
  20. package/dist/src/action-proposal-outcome.d.ts +12 -0
  21. package/dist/src/action-proposal-outcome.d.ts.map +1 -0
  22. package/dist/src/action-proposal-outcome.js +86 -0
  23. package/dist/src/action-proposal-outcome.js.map +1 -0
  24. package/dist/src/action-proposal-preparation.d.ts +20 -0
  25. package/dist/src/action-proposal-preparation.d.ts.map +1 -0
  26. package/dist/src/action-proposal-preparation.js +27 -0
  27. package/dist/src/action-proposal-preparation.js.map +1 -0
  28. package/dist/src/action-proposal-receipt.d.ts +12 -0
  29. package/dist/src/action-proposal-receipt.d.ts.map +1 -0
  30. package/dist/src/action-proposal-receipt.js +4 -0
  31. package/dist/src/action-proposal-receipt.js.map +1 -0
  32. package/dist/src/action-proposal-runtime.d.ts +9 -0
  33. package/dist/src/action-proposal-runtime.d.ts.map +1 -0
  34. package/dist/src/action-proposal-runtime.js +34 -0
  35. package/dist/src/action-proposal-runtime.js.map +1 -0
  36. package/dist/src/action-proposal-service.d.ts +43 -0
  37. package/dist/src/action-proposal-service.d.ts.map +1 -0
  38. package/dist/src/action-proposal-service.js +132 -0
  39. package/dist/src/action-proposal-service.js.map +1 -0
  40. package/dist/src/action-proposal-text.d.ts +33 -0
  41. package/dist/src/action-proposal-text.d.ts.map +1 -0
  42. package/dist/src/action-proposal-text.js +51 -0
  43. package/dist/src/action-proposal-text.js.map +1 -0
  44. package/dist/src/action-proposal-transitions.d.ts +20 -0
  45. package/dist/src/action-proposal-transitions.d.ts.map +1 -0
  46. package/dist/src/action-proposal-transitions.js +311 -0
  47. package/dist/src/action-proposal-transitions.js.map +1 -0
  48. package/dist/src/action-proposal-worker.d.ts +31 -0
  49. package/dist/src/action-proposal-worker.d.ts.map +1 -0
  50. package/dist/src/action-proposal-worker.js +133 -0
  51. package/dist/src/action-proposal-worker.js.map +1 -0
  52. package/dist/src/ag-ui/adapter.d.ts.map +1 -1
  53. package/dist/src/ag-ui/adapter.js +33 -0
  54. package/dist/src/ag-ui/adapter.js.map +1 -1
  55. package/dist/src/ag-ui/frames.d.ts.map +1 -1
  56. package/dist/src/ag-ui/frames.js +1 -0
  57. package/dist/src/ag-ui/frames.js.map +1 -1
  58. package/dist/src/ag-ui/proposal-decision.d.ts +9 -0
  59. package/dist/src/ag-ui/proposal-decision.d.ts.map +1 -0
  60. package/dist/src/ag-ui/proposal-decision.js +23 -0
  61. package/dist/src/ag-ui/proposal-decision.js.map +1 -0
  62. package/dist/src/agent-deps-factory.d.ts +5 -0
  63. package/dist/src/agent-deps-factory.d.ts.map +1 -1
  64. package/dist/src/agent-deps-factory.js +9 -0
  65. package/dist/src/agent-deps-factory.js.map +1 -1
  66. package/dist/src/agent-deps.d.ts +4 -0
  67. package/dist/src/agent-deps.d.ts.map +1 -1
  68. package/dist/src/agent-deps.js.map +1 -1
  69. package/dist/src/agent-loop.d.ts +4 -0
  70. package/dist/src/agent-loop.d.ts.map +1 -1
  71. package/dist/src/agent-loop.js +372 -62
  72. package/dist/src/agent-loop.js.map +1 -1
  73. package/dist/src/agent-service.d.ts +42 -1
  74. package/dist/src/agent-service.d.ts.map +1 -1
  75. package/dist/src/agent-service.js +49 -1
  76. package/dist/src/agent-service.js.map +1 -1
  77. package/dist/src/ai-tool-ref.d.ts +2 -1
  78. package/dist/src/ai-tool-ref.d.ts.map +1 -1
  79. package/dist/src/ai-tool-ref.js +22 -2
  80. package/dist/src/ai-tool-ref.js.map +1 -1
  81. package/dist/src/chat-queue-service.d.ts.map +1 -1
  82. package/dist/src/chat-queue-service.js +2 -0
  83. package/dist/src/chat-queue-service.js.map +1 -1
  84. package/dist/src/client/chat-client.d.ts +12 -0
  85. package/dist/src/client/chat-client.d.ts.map +1 -1
  86. package/dist/src/client/chat-client.js +69 -1
  87. package/dist/src/client/chat-client.js.map +1 -1
  88. package/dist/src/client/sse.d.ts +15 -0
  89. package/dist/src/client/sse.d.ts.map +1 -1
  90. package/dist/src/client/sse.js +45 -0
  91. package/dist/src/client/sse.js.map +1 -1
  92. package/dist/src/define_config.d.ts +9 -0
  93. package/dist/src/define_config.d.ts.map +1 -1
  94. package/dist/src/define_config.js.map +1 -1
  95. package/dist/src/genui/factory.d.ts +8 -1
  96. package/dist/src/genui/factory.d.ts.map +1 -1
  97. package/dist/src/genui/factory.js +3 -1
  98. package/dist/src/genui/factory.js.map +1 -1
  99. package/dist/src/genui/index.d.ts.map +1 -1
  100. package/dist/src/genui/index.js +1 -0
  101. package/dist/src/genui/index.js.map +1 -1
  102. package/dist/src/index.d.ts +18 -2
  103. package/dist/src/index.d.ts.map +1 -1
  104. package/dist/src/index.js +18 -2
  105. package/dist/src/index.js.map +1 -1
  106. package/dist/src/negotiated-tool-ui.d.ts +10 -0
  107. package/dist/src/negotiated-tool-ui.d.ts.map +1 -0
  108. package/dist/src/negotiated-tool-ui.js +53 -0
  109. package/dist/src/negotiated-tool-ui.js.map +1 -0
  110. package/dist/src/spi/action-proposal-outcome-store.d.ts +43 -0
  111. package/dist/src/spi/action-proposal-outcome-store.d.ts.map +1 -0
  112. package/dist/src/spi/action-proposal-outcome-store.js +2 -0
  113. package/dist/src/spi/action-proposal-outcome-store.js.map +1 -0
  114. package/dist/src/spi/action-proposal-store.d.ts +152 -0
  115. package/dist/src/spi/action-proposal-store.d.ts.map +1 -0
  116. package/dist/src/spi/action-proposal-store.js +2 -0
  117. package/dist/src/spi/action-proposal-store.js.map +1 -0
  118. package/dist/src/spi/action-proposal-worker-store.d.ts +17 -0
  119. package/dist/src/spi/action-proposal-worker-store.d.ts.map +1 -0
  120. package/dist/src/spi/action-proposal-worker-store.js +2 -0
  121. package/dist/src/spi/action-proposal-worker-store.js.map +1 -0
  122. package/dist/src/spi/agent-store.d.ts +5 -0
  123. package/dist/src/spi/agent-store.d.ts.map +1 -1
  124. package/dist/src/spi/agent-store.js.map +1 -1
  125. package/dist/src/spi/approval-policy.d.ts +3 -0
  126. package/dist/src/spi/approval-policy.d.ts.map +1 -1
  127. package/dist/src/spi/approval-policy.js +8 -1
  128. package/dist/src/spi/approval-policy.js.map +1 -1
  129. package/dist/src/spi/background-actor-resolver.d.ts +9 -0
  130. package/dist/src/spi/background-actor-resolver.d.ts.map +1 -0
  131. package/dist/src/spi/background-actor-resolver.js +2 -0
  132. package/dist/src/spi/background-actor-resolver.js.map +1 -0
  133. package/dist/src/spi/chat-queue.d.ts +3 -0
  134. package/dist/src/spi/chat-queue.d.ts.map +1 -1
  135. package/dist/src/spi/chat-queue.js.map +1 -1
  136. package/dist/src/spi/token-stream-sink.d.ts +9 -0
  137. package/dist/src/spi/token-stream-sink.d.ts.map +1 -1
  138. package/dist/src/spi/token-stream-sink.js.map +1 -1
  139. package/dist/src/spi/tool.d.ts +22 -0
  140. package/dist/src/spi/tool.d.ts.map +1 -1
  141. package/dist/src/sse.d.ts.map +1 -1
  142. package/dist/src/sse.js +6 -0
  143. package/dist/src/sse.js.map +1 -1
  144. package/dist/src/stores/lucid-action-proposal-store.d.ts +55 -0
  145. package/dist/src/stores/lucid-action-proposal-store.d.ts.map +1 -0
  146. package/dist/src/stores/lucid-action-proposal-store.js +465 -0
  147. package/dist/src/stores/lucid-action-proposal-store.js.map +1 -0
  148. package/dist/src/stores/lucid-schema.d.ts +8 -6
  149. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  150. package/dist/src/stores/lucid-schema.js +59 -6
  151. package/dist/src/stores/lucid-schema.js.map +1 -1
  152. package/dist/src/stores/lucid.d.ts +4 -2
  153. package/dist/src/stores/lucid.d.ts.map +1 -1
  154. package/dist/src/stores/lucid.js +43 -14
  155. package/dist/src/stores/lucid.js.map +1 -1
  156. package/dist/src/stores/sqlite-transactions.d.ts +8 -0
  157. package/dist/src/stores/sqlite-transactions.d.ts.map +1 -0
  158. package/dist/src/stores/sqlite-transactions.js +24 -0
  159. package/dist/src/stores/sqlite-transactions.js.map +1 -0
  160. package/dist/src/stream-events.d.ts +4 -0
  161. package/dist/src/stream-events.d.ts.map +1 -1
  162. package/dist/src/testing/in-memory-action-proposal-store.d.ts +32 -0
  163. package/dist/src/testing/in-memory-action-proposal-store.d.ts.map +1 -0
  164. package/dist/src/testing/in-memory-action-proposal-store.js +159 -0
  165. package/dist/src/testing/in-memory-action-proposal-store.js.map +1 -0
  166. package/dist/src/testing/in-memory-store.d.ts +9 -1
  167. package/dist/src/testing/in-memory-store.d.ts.map +1 -1
  168. package/dist/src/testing/in-memory-store.js +79 -1
  169. package/dist/src/testing/in-memory-store.js.map +1 -1
  170. package/dist/src/testing/index.d.ts +1 -0
  171. package/dist/src/testing/index.d.ts.map +1 -1
  172. package/dist/src/testing/index.js +1 -0
  173. package/dist/src/testing/index.js.map +1 -1
  174. package/dist/src/tool-discovery.d.ts.map +1 -1
  175. package/dist/src/tool-discovery.js +7 -0
  176. package/dist/src/tool-discovery.js.map +1 -1
  177. package/dist/src/tool-registry.d.ts +25 -1
  178. package/dist/src/tool-registry.d.ts.map +1 -1
  179. package/dist/src/tool-registry.js +73 -3
  180. package/dist/src/tool-registry.js.map +1 -1
  181. package/dist/src/tool-retry.d.ts +0 -11
  182. package/dist/src/tool-retry.d.ts.map +1 -1
  183. package/dist/src/tool-retry.js +5 -1
  184. package/dist/src/tool-retry.js.map +1 -1
  185. package/dist/src/tool-ui.d.ts +4 -1
  186. package/dist/src/tool-ui.d.ts.map +1 -1
  187. package/dist/src/tool-ui.js +32 -4
  188. package/dist/src/tool-ui.js.map +1 -1
  189. package/dist/src/types.d.ts +28 -2
  190. package/dist/src/types.d.ts.map +1 -1
  191. package/dist/src/ui-capabilities.d.ts +9 -0
  192. package/dist/src/ui-capabilities.d.ts.map +1 -0
  193. package/dist/src/ui-capabilities.js +25 -0
  194. package/dist/src/ui-capabilities.js.map +1 -0
  195. package/dist/stubs/database/migrations/add_action_proposal_discovery.stub +24 -0
  196. package/dist/stubs/database/migrations/add_action_proposal_runtime.stub +19 -0
  197. package/dist/stubs/database/migrations/create_agent_tables.stub +3 -3
  198. package/package.json +1 -1
@@ -1,9 +1,14 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolveActionProposalApproval } from './action-proposal-approval.js';
3
+ import { prepareActionProposal } from './action-proposal-preparation.js';
4
+ import { actionProposalReceipt } from './action-proposal-receipt.js';
1
5
  import { danglingToolCallIds, settleDanglingToolCalls } from './dangling-tool-calls.js';
2
6
  import { detachedDelivered, detachedStarted, } from './delegation.js';
3
7
  import { publishAgentDelegated, publishAgentMessage, publishAgentRetrieved, publishAgentRunFinished, publishAgentRunStarted, publishAgentToolCall, publishAgentToolRetry, spannedAgent, } from './diagnostics.js';
4
8
  import { ASK_TOOL_NAME, askInputSchema, askToolDefinition, DEFAULT_INTAKE_PREAMBLE, isHumanDecision, normalizeElicitationReply, settleElicitation, } from './elicitation.js';
5
9
  import { buildMemoryBlock, offerMemories, REMEMBER_TOOL_NAME, rememberInputSchema, rememberToolDefinition, writeMemory, } from './memory.js';
6
- import { intersectAllowLists, personaFromTurn } from './personas.js';
10
+ import { createNegotiatedUiCollector } from './negotiated-tool-ui.js';
11
+ import { personaFromTurn } from './personas.js';
7
12
  import { createFrameBuffer, createIncrementalGate, gateTail, releaseGatedFrames, resolveGateLookback, resolveOutputGateMode, runInputProcessors, runOutputProcessors, } from './processors.js';
8
13
  import { isReplayIntegrityError } from './replay-integrity.js';
9
14
  import { buildSkillsBlock, loadSkill, offerSkills, SKILL_TOOL_NAME, skillInputSchema, skillToolDefinition, } from './skills.js';
@@ -12,9 +17,9 @@ import { withSelectedModel } from './spi/model-catalog.js';
12
17
  import { estimateCost, resolveModelPrice, } from './spi/pricing-store.js';
13
18
  import { OutputRejectedError, } from './spi/processors.js';
14
19
  import { DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION, repairInstruction, StructuredOutputError, validateStructured, } from './structured-output.js';
15
- import { ToolForbiddenError, ToolInputInvalidError } from './tool-registry.js';
20
+ import { ToolForbiddenError, ToolInputInvalidError, ToolPreflightDeniedError, } from './tool-registry.js';
16
21
  import { invokeWithTransientRetry } from './tool-retry.js';
17
- import { createNoopEmitUi, createUiCollector, mergeUi, unwrapToolStepOutput, wrapToolStepOutput, } from './tool-ui.js';
22
+ import { createNoopEmitUi, escapeUnsafeToolUiText, mergeUi, unwrapToolStepOutput, wrapToolStepOutput, } from './tool-ui.js';
18
23
  import { observeTurnFrames, withTurnFrames } from './turn-frames.js';
19
24
  /** Renders retrieved passages as a numbered, citable context block appended to the system prompt. */
20
25
  function buildContextBlock(passages) {
@@ -93,6 +98,7 @@ function promptContext(input, persona) {
93
98
  actor: input.actor,
94
99
  ...(persona !== undefined ? { persona: personaFromTurn(persona) } : {}),
95
100
  ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
101
+ ...(input.uiCapabilities !== undefined ? { uiCapabilities: input.uiCapabilities } : {}),
96
102
  };
97
103
  }
98
104
  /**
@@ -313,6 +319,7 @@ function threadForTurn(recorded) {
313
319
  const messages = settleDanglingToolCalls((recorded?.messages ?? []).map(toModelMessage));
314
320
  return {
315
321
  messages,
322
+ actionApprovalMode: recorded?.actionApprovalMode ?? 'blocking',
316
323
  title: recorded?.title ?? null,
317
324
  hasAssistantMessage: recorded?.hasAssistantMessage ?? messages.some((message) => message.role === 'assistant'),
318
325
  };
@@ -622,6 +629,7 @@ function elicitationContext(deps, input, hooks) {
622
629
  ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
623
630
  ...personaContext(input),
624
631
  ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
632
+ ...(input.uiCapabilities !== undefined ? { uiCapabilities: input.uiCapabilities } : {}),
625
633
  ...(deps.host !== undefined ? { host: deps.host } : {}),
626
634
  };
627
635
  }
@@ -653,6 +661,20 @@ const MESSAGE_TOOL_RESULTS_PATCH = 'agent:message-tool-results';
653
661
  async function claimToolCall(turn, call) {
654
662
  const { deps, input, hooks, messageId, writer } = turn;
655
663
  const persona = personaOf(input);
664
+ const ctx = {
665
+ actor: input.actor,
666
+ threadId: input.threadId,
667
+ runId: hooks.runId,
668
+ requestId: hooks.runId,
669
+ // Replaced by the call's own collector inside its `tool:` step (see invokeClaimedTool); a
670
+ // kind that never reaches a handler keeps this no-op.
671
+ emitUi: createNoopEmitUi(call.id),
672
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
673
+ ...(persona !== undefined ? { persona } : {}),
674
+ ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
675
+ ...(input.uiCapabilities !== undefined ? { uiCapabilities: input.uiCapabilities } : {}),
676
+ ...(deps.host !== undefined ? { host: deps.host } : {}),
677
+ };
656
678
  const persisted = (await hooks.step(`persist:toolcall:${call.id}`, async () => {
657
679
  // The kind the llm checkpoint stamped WINS: it came from the process that offered the tool,
658
680
  // whereas this one is only the process claiming the call — and the two disagree exactly when
@@ -682,15 +704,138 @@ async function claimToolCall(turn, call) {
682
704
  // set can both change while a run is parked, and the branch below (park or run) has to be the
683
705
  // one the journal recorded. A run claimed before this existed reads `approval` back as
684
706
  // undefined and waits on the requester, exactly as it did.
685
- const approval = kind === 'action' ? await claimApproval(turn, call) : undefined;
686
- const parks = kind === 'ask' || approval?.mode === 'ask';
707
+ let actionApproval = call.actionApproval;
708
+ if (kind === 'action' &&
709
+ turn.actionApprovalMode === 'independent' &&
710
+ actionApproval === undefined)
711
+ actionApproval = await resolveActionProposalApproval({
712
+ actor: input.actor,
713
+ thread: {
714
+ threadId: input.threadId,
715
+ runId: hooks.runId,
716
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
717
+ },
718
+ tool: {
719
+ name: call.name,
720
+ kind: 'action',
721
+ ...(deps.registry.spec(call.name) !== undefined
722
+ ? { spec: deps.registry.spec(call.name) }
723
+ : {}),
724
+ },
725
+ store: deps.store,
726
+ ...(deps.approvalPolicy !== undefined ? { policy: deps.approvalPolicy } : {}),
727
+ });
728
+ let preflight = call.preflight;
729
+ let prepared = call.prepared;
730
+ if (kind === 'action' && preflight === undefined && deps.registry.has(call.name)) {
731
+ try {
732
+ const allowedTools = intersectAllow(persona?.allowedTools, deps.toolAllowList);
733
+ if (turn.actionApprovalMode === 'independent' && actionApproval?.mode === 'ask') {
734
+ prepared = await prepareActionProposal(deps.registry, call.name, call.input, { ...ctx, toolCallId: call.id, idempotencyKey: `${hooks.runId}:${call.id}` }, deps.rolesPolicy, allowedTools === undefined ? {} : { allowedTools });
735
+ preflight = prepared.preflight;
736
+ }
737
+ else
738
+ preflight = await deps.registry.prepare(call.name, call.input, {
739
+ ...ctx,
740
+ toolCallId: call.id,
741
+ idempotencyKey: `${hooks.runId}:${call.id}`,
742
+ }, deps.rolesPolicy, allowedTools === undefined ? {} : { allowedTools });
743
+ }
744
+ catch (error) {
745
+ if (isReplayIntegrityError(error) || hooks.isControlFlowError?.(error) === true)
746
+ throw error;
747
+ preflight = {
748
+ status: 'failed',
749
+ error: error instanceof Error ? error.message : String(error),
750
+ };
751
+ }
752
+ }
753
+ const early = preflight !== undefined && preflight.status !== 'ready';
754
+ let confirmation = preflight?.status === 'ready' ? preflight.confirmation : undefined;
755
+ const approval = kind === 'action' && !early
756
+ ? (actionApproval ?? (await claimApproval(turn, call)))
757
+ : undefined;
758
+ let proposalId;
759
+ if (turn.actionApprovalMode === 'independent' && approval?.mode === 'ask') {
760
+ if (!prepared)
761
+ throw new Error('Independent action lacks trusted prepared input');
762
+ const store = deps.store;
763
+ if (typeof store.createActionProposal !== 'function')
764
+ throw new Error('Independent proposal store unavailable');
765
+ proposalId = `proposal-${createHash('sha256')
766
+ .update(JSON.stringify([hooks.runId, call.id]))
767
+ .digest('hex')}`;
768
+ const display = confirmation ??
769
+ deps.registry.spec(call.name)?.presentation?.confirm ?? {
770
+ title: `Allow ${call.name}?`,
771
+ verb: 'Approve',
772
+ };
773
+ const fill = (value) => value.replace(/\{([^}]+)\}/g, (_match, path) => {
774
+ let current = prepared.input;
775
+ for (const part of path.split('.'))
776
+ current =
777
+ current !== null && typeof current === 'object'
778
+ ? current[part]
779
+ : undefined;
780
+ return current === undefined ? '' : String(current);
781
+ });
782
+ const create = prepared.replacementKey === undefined
783
+ ? store.createActionProposal.bind(store)
784
+ : store.createReplacingActionProposal.bind(store);
785
+ const created = await create({
786
+ ...(prepared.replacementKey !== undefined
787
+ ? { replacementKey: prepared.replacementKey }
788
+ : {}),
789
+ id: proposalId,
790
+ tenantRef: input.actor.tenantRef ?? null,
791
+ actorRef: input.actor.id,
792
+ threadId: input.threadId,
793
+ originRunId: hooks.runId,
794
+ originMessageId: messageId,
795
+ originToolCallId: call.id,
796
+ toolName: call.name,
797
+ input: prepared.input,
798
+ preparationInput: prepared.preparationInput,
799
+ executionContext: {
800
+ requestId: hooks.runId,
801
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
802
+ ...(persona !== undefined ? { persona: persona.id } : {}),
803
+ ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
804
+ ...(input.uiCapabilities !== undefined ? { uiCapabilities: input.uiCapabilities } : {}),
805
+ },
806
+ confirmation: confirmation ?? {
807
+ title: fill(display.title),
808
+ verb: fill(display.verb),
809
+ ...(display.detail !== undefined ? { detail: fill(display.detail) } : {}),
810
+ },
811
+ approver: approval.approver,
812
+ expiresAt: approval.expiresAt === undefined ? null : Date.parse(approval.expiresAt),
813
+ idempotencyKey: `${hooks.runId}:${call.id}`,
814
+ });
815
+ if (created.status === 'conflict')
816
+ throw new Error('Action proposal replay payload conflict');
817
+ confirmation = created.proposal?.confirmation ?? confirmation;
818
+ }
819
+ const parks = kind === 'ask' || (approval?.mode === 'ask' && proposalId === undefined);
687
820
  await deps.store.recordToolCall({
688
821
  toolCallId: call.id,
689
822
  messageId,
690
823
  toolName: call.name,
691
824
  toolType: awaitsHuman ? 'action' : 'read',
692
825
  input: call.input,
693
- status: parks ? 'pending_approval' : 'auto_executed',
826
+ ...(proposalId !== undefined ? { proposalId } : {}),
827
+ status: proposalId !== undefined
828
+ ? 'proposed'
829
+ : early
830
+ ? preflight?.status === 'denied'
831
+ ? 'rejected'
832
+ : preflight?.status === 'failed'
833
+ ? 'failed'
834
+ : 'executed'
835
+ : parks
836
+ ? 'pending_approval'
837
+ : 'auto_executed',
838
+ ...(confirmation !== undefined ? { confirmation } : {}),
694
839
  runId: hooks.runId,
695
840
  ...(approval !== undefined && approval.mode !== 'auto'
696
841
  ? { approver: approval.approver }
@@ -699,6 +844,19 @@ async function claimToolCall(turn, call) {
699
844
  ? { expiresAt: approval.expiresAt }
700
845
  : {}),
701
846
  });
847
+ if (early) {
848
+ await deps.store.updateToolCall({
849
+ toolCallId: call.id,
850
+ status: preflight?.status === 'denied'
851
+ ? 'rejected'
852
+ : preflight?.status === 'failed'
853
+ ? 'failed'
854
+ : 'executed',
855
+ ...(preflight?.status === 'completed' ? { output: preflight.output } : {}),
856
+ ...(preflight?.status === 'denied' ? { error: preflight.reason } : {}),
857
+ ...(preflight?.status === 'failed' ? { error: preflight.error } : {}),
858
+ });
859
+ }
702
860
  // Written from INSIDE this checkpoint, so the frame is streamed once and a replay — which
703
861
  // returns the memoized result without re-running the body — never re-posts a form for a
704
862
  // decision already made. That is also why it spends no position of its own.
@@ -708,11 +866,15 @@ async function claimToolCall(turn, call) {
708
866
  if (approval?.mode === 'ask') {
709
867
  await writer.write({
710
868
  t: 'approval',
869
+ ...(proposalId !== undefined
870
+ ? { target: { kind: 'proposal', proposalId } }
871
+ : {}),
711
872
  runId: hooks.runId,
712
873
  id: call.id,
713
874
  toolName: call.name,
714
875
  input: call.input,
715
876
  approver: approval.approver,
877
+ ...(confirmation !== undefined ? { confirmation } : {}),
716
878
  ...(approval.expiresAt !== undefined ? { expiresAt: approval.expiresAt } : {}),
717
879
  });
718
880
  }
@@ -721,6 +883,8 @@ async function claimToolCall(turn, call) {
721
883
  const terminal = deps.registry.spec(call.name)?.terminal === true;
722
884
  return {
723
885
  kind,
886
+ ...(proposalId !== undefined ? { proposalId } : {}),
887
+ ...(preflight !== undefined ? { preflight } : {}),
724
888
  ...(approval !== undefined ? { approval } : {}),
725
889
  ...(terminal ? { terminal: true } : {}),
726
890
  };
@@ -732,24 +896,14 @@ async function claimToolCall(turn, call) {
732
896
  return {
733
897
  call,
734
898
  toolType,
899
+ ...(persisted?.preflight !== undefined ? { preflight: persisted.preflight } : {}),
735
900
  ...(persisted?.terminal === true ? { terminal: true } : {}),
901
+ ...(persisted?.proposalId !== undefined ? { proposalId: persisted.proposalId } : {}),
736
902
  ...(persisted?.delegation !== undefined ? { delegation: persisted.delegation } : {}),
737
903
  ...(toolType === 'action' && persisted?.approval !== undefined
738
904
  ? { approval: persisted.approval }
739
905
  : {}),
740
- ctx: {
741
- actor: input.actor,
742
- threadId: input.threadId,
743
- runId: hooks.runId,
744
- requestId: hooks.runId,
745
- // Replaced by the call's own collector inside its `tool:` step (see invokeClaimedTool); a
746
- // kind that never reaches a handler keeps this no-op.
747
- emitUi: createNoopEmitUi(call.id),
748
- ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
749
- ...(persona !== undefined ? { persona } : {}),
750
- ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
751
- ...(deps.host !== undefined ? { host: deps.host } : {}),
752
- },
906
+ ctx,
753
907
  };
754
908
  }
755
909
  /**
@@ -814,17 +968,19 @@ function spanToolType(kind) {
814
968
  async function invokeClaimedTool(turn, claimed) {
815
969
  const { deps, hooks } = turn;
816
970
  const { call, ctx, toolType } = claimed;
817
- // Under a persona with an allow-list, the same narrowing the offer had — agent ∩ persona — is what
818
- // `invoke` holds the call to. Without one, nothing new is checked.
819
- const personaAllow = personaOf(turn.input)?.allowedTools;
820
- const invokeOptions = personaAllow === undefined
821
- ? {}
822
- : { allowedTools: intersectAllowLists(deps.toolAllowList, personaAllow) ?? [] };
971
+ // Execution retains every narrowing the offer and preparation had, including an agent-only list.
972
+ const allowedTools = intersectAllow(personaOf(turn.input)?.allowedTools, deps.toolAllowList);
973
+ const invokeOptions = allowedTools === undefined ? {} : { allowedTools };
823
974
  try {
824
975
  const raw = await hooks.step(`tool:${call.id}`, async () => {
825
976
  // Pushed components stream as they happen and ride this step's RESULT into the journal (see
826
977
  // `wrapToolStepOutput`), so a replay neither re-streams nor re-persists them.
827
- const ui = createUiCollector(call.id, turn.writer);
978
+ const ui = createNegotiatedUiCollector(call.id, {
979
+ actor: ctx.actor,
980
+ threadId: ctx.threadId,
981
+ ...(ctx.agentName !== undefined ? { agentName: ctx.agentName } : {}),
982
+ ...(ctx.uiCapabilities !== undefined ? { uiCapabilities: ctx.uiCapabilities } : {}),
983
+ }, deps.resolveUiCatalog, turn.writer);
828
984
  const toolCtx = {
829
985
  ...ctx,
830
986
  // The same for every execution of THIS call (a replay after a crash, a transient retry).
@@ -838,31 +994,50 @@ async function invokeClaimedTool(turn, claimed) {
838
994
  : { value: data });
839
995
  },
840
996
  };
841
- const output = await spannedAgent('tool.execution', hooks.runId, {
842
- runId: hooks.runId,
843
- toolCallId: call.id,
844
- toolName: call.name,
845
- toolType: spanToolType(toolType),
846
- }, () => invokeWithTransientRetry(() => {
847
- ui.restart();
848
- return deps.registry.invoke(call.name, call.input, toolCtx, deps.rolesPolicy, invokeOptions);
849
- }, deps.toolTransientRetry ?? {}, {
850
- ...(hooks.isControlFlowError !== undefined
851
- ? { isControlFlowError: hooks.isControlFlowError }
852
- : {}),
853
- onRetry: (attempt, retryError) => {
854
- publishAgentToolRetry({
855
- runId: hooks.runId,
856
- toolName: call.name,
857
- toolCallId: call.id,
858
- attempt,
859
- message: retryError instanceof Error ? retryError.message : String(retryError),
860
- });
861
- },
862
- }), () => ({}));
863
- return wrapToolStepOutput(output, ui.components());
997
+ let output;
998
+ try {
999
+ output = await spannedAgent('tool.execution', hooks.runId, {
1000
+ runId: hooks.runId,
1001
+ toolCallId: call.id,
1002
+ toolName: call.name,
1003
+ toolType: spanToolType(toolType),
1004
+ }, () => invokeWithTransientRetry(() => {
1005
+ ui.restart();
1006
+ return deps.registry.invoke(call.name, call.input, toolCtx, deps.rolesPolicy, invokeOptions);
1007
+ }, deps.toolTransientRetry ?? {}, {
1008
+ ...(hooks.isControlFlowError !== undefined
1009
+ ? { isControlFlowError: hooks.isControlFlowError }
1010
+ : {}),
1011
+ onRetry: (attempt, retryError) => {
1012
+ publishAgentToolRetry({
1013
+ runId: hooks.runId,
1014
+ toolName: call.name,
1015
+ toolCallId: call.id,
1016
+ attempt,
1017
+ message: retryError instanceof Error ? retryError.message : String(retryError),
1018
+ });
1019
+ },
1020
+ }), () => ({}));
1021
+ }
1022
+ catch (error) {
1023
+ // A domain refusal is a settled result, so a durable replay must not re-run the check.
1024
+ if (!(error instanceof ToolPreflightDeniedError))
1025
+ throw error;
1026
+ return { '@@adonis-agent/tool-preflight-denied': 1, reason: error.reason };
1027
+ }
1028
+ return wrapToolStepOutput(output, ui.components(), ui.text());
864
1029
  });
865
- const { output, ui } = unwrapToolStepOutput(raw);
1030
+ if (typeof raw === 'object' &&
1031
+ raw !== null &&
1032
+ '@@adonis-agent/tool-preflight-denied' in raw &&
1033
+ raw['@@adonis-agent/tool-preflight-denied'] === 1 &&
1034
+ 'reason' in raw &&
1035
+ typeof raw.reason === 'string') {
1036
+ return { status: 'failed', error: raw.reason, denied: true };
1037
+ }
1038
+ const { output, ui, text } = unwrapToolStepOutput(raw);
1039
+ if (text)
1040
+ turn.toolText.set(call.id, text);
866
1041
  if (ui.length > 0) {
867
1042
  turn.toolUi.set(call.id, ui);
868
1043
  }
@@ -875,7 +1050,11 @@ async function invokeClaimedTool(turn, claimed) {
875
1050
  if (isReplayIntegrityError(error) || hooks.isControlFlowError?.(error) === true) {
876
1051
  throw error;
877
1052
  }
878
- return { status: 'failed', error: error instanceof Error ? error.message : String(error) };
1053
+ return {
1054
+ status: 'failed',
1055
+ error: error instanceof Error ? error.message : String(error),
1056
+ ...(error instanceof ToolPreflightDeniedError ? { denied: true } : {}),
1057
+ };
879
1058
  }
880
1059
  }
881
1060
  /** Persist one settled invocation and shape the result the model is fed. */
@@ -922,7 +1101,13 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef = turn.input
922
1101
  await settle();
923
1102
  });
924
1103
  publishAgentToolCall({ runId: hooks.runId, toolName: call.name, toolType, status: 'failed' });
925
- return { id: call.id, name: call.name, output: null, error: outcome.error };
1104
+ return {
1105
+ id: call.id,
1106
+ name: call.name,
1107
+ output: outcome.denied ? { reason: outcome.error } : null,
1108
+ error: outcome.error,
1109
+ ...(outcome.denied ? { denied: true } : {}),
1110
+ };
926
1111
  }
927
1112
  await hooks.step(`persist:toolexec:${call.id}`, async () => {
928
1113
  await deps.store.updateToolCall({
@@ -986,14 +1171,95 @@ async function delegateToolCall(turn, claimed) {
986
1171
  * what puts the kind in the journal — so the branch a call takes is a fact about the turn rather
987
1172
  * than about whichever process happened to replay it (see {@link claimToolCall}).
988
1173
  *
989
- * Only unstamped calls are touched: a kind already on a call was settled upstream, and a process
990
- * further down never second-guesses it.
1174
+ * Provider-supplied preparation is discarded. The trusted result rides this same model checkpoint
1175
+ * so a process that first claims the call without its handler still follows the offering process's verdict.
991
1176
  */
992
- function stampToolKinds(result, deps) {
993
- return {
994
- ...result,
995
- toolCalls: result.toolCalls.map((call) => call.kind === undefined ? { ...call, kind: declaredKind(deps, call.name) } : call),
996
- };
1177
+ async function stampToolKinds(result, deps, input, hooks) {
1178
+ const persona = personaOf(input);
1179
+ const allowedTools = intersectAllow(persona?.allowedTools, deps.toolAllowList);
1180
+ const toolCalls = [];
1181
+ for (const incoming of result.toolCalls) {
1182
+ // The provider cannot supply trusted preparation. Resolve it only where the tool was offered.
1183
+ const { preflight: _untrusted, prepared: _untrustedPrepared, actionApproval: _untrustedApproval, ...call } = incoming;
1184
+ const kind = declaredKind(deps, call.name);
1185
+ let preflight;
1186
+ let prepared;
1187
+ let actionApproval;
1188
+ if (kind === 'action') {
1189
+ try {
1190
+ if (deps.actionApprovalMode === 'independent')
1191
+ actionApproval = await resolveActionProposalApproval({
1192
+ actor: input.actor,
1193
+ thread: {
1194
+ threadId: input.threadId,
1195
+ runId: hooks.runId,
1196
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
1197
+ },
1198
+ tool: {
1199
+ name: call.name,
1200
+ kind: 'action',
1201
+ ...(deps.registry.spec(call.name) !== undefined
1202
+ ? { spec: deps.registry.spec(call.name) }
1203
+ : {}),
1204
+ },
1205
+ store: deps.store,
1206
+ ...(deps.approvalPolicy !== undefined ? { policy: deps.approvalPolicy } : {}),
1207
+ });
1208
+ if (deps.actionApprovalMode === 'independent' && actionApproval?.mode === 'ask') {
1209
+ prepared = await prepareActionProposal(deps.registry, call.name, call.input, {
1210
+ actor: input.actor,
1211
+ threadId: input.threadId,
1212
+ runId: hooks.runId,
1213
+ requestId: hooks.runId,
1214
+ toolCallId: call.id,
1215
+ idempotencyKey: `${hooks.runId}:${call.id}`,
1216
+ emitUi: createNoopEmitUi(call.id),
1217
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
1218
+ ...(persona !== undefined ? { persona } : {}),
1219
+ ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
1220
+ ...(input.uiCapabilities !== undefined
1221
+ ? { uiCapabilities: input.uiCapabilities }
1222
+ : {}),
1223
+ ...(deps.host !== undefined ? { host: deps.host } : {}),
1224
+ }, deps.rolesPolicy, allowedTools === undefined ? {} : { allowedTools });
1225
+ preflight = prepared.preflight;
1226
+ }
1227
+ else
1228
+ preflight = await deps.registry.prepare(call.name, call.input, {
1229
+ actor: input.actor,
1230
+ threadId: input.threadId,
1231
+ runId: hooks.runId,
1232
+ requestId: hooks.runId,
1233
+ toolCallId: call.id,
1234
+ idempotencyKey: `${hooks.runId}:${call.id}`,
1235
+ emitUi: createNoopEmitUi(call.id),
1236
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
1237
+ ...(persona !== undefined ? { persona } : {}),
1238
+ ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
1239
+ ...(input.uiCapabilities !== undefined
1240
+ ? { uiCapabilities: input.uiCapabilities }
1241
+ : {}),
1242
+ ...(deps.host !== undefined ? { host: deps.host } : {}),
1243
+ }, deps.rolesPolicy, allowedTools === undefined ? {} : { allowedTools });
1244
+ }
1245
+ catch (error) {
1246
+ if (isReplayIntegrityError(error) || hooks.isControlFlowError?.(error) === true)
1247
+ throw error;
1248
+ preflight = {
1249
+ status: 'failed',
1250
+ error: error instanceof Error ? error.message : String(error),
1251
+ };
1252
+ }
1253
+ }
1254
+ toolCalls.push({
1255
+ ...call,
1256
+ ...(actionApproval !== undefined ? { actionApproval } : {}),
1257
+ kind,
1258
+ ...(prepared !== undefined ? { prepared } : {}),
1259
+ ...(preflight !== undefined ? { preflight } : {}),
1260
+ });
1261
+ }
1262
+ return { ...result, toolCalls };
997
1263
  }
998
1264
  /**
999
1265
  * A call's declared kind, resolved from this process's own configuration and registry.
@@ -1077,6 +1343,7 @@ function skillContext(input) {
1077
1343
  threadId: input.threadId,
1078
1344
  ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
1079
1345
  ...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
1346
+ ...(input.uiCapabilities !== undefined ? { uiCapabilities: input.uiCapabilities } : {}),
1080
1347
  };
1081
1348
  }
1082
1349
  /**
@@ -1200,6 +1467,26 @@ function invokeClaimed(turn, claimed) {
1200
1467
  async function runClaimedToolCall(turn, claimed) {
1201
1468
  const { deps, hooks } = turn;
1202
1469
  const { call, toolType, ctx } = claimed;
1470
+ if (claimed.proposalId !== undefined) {
1471
+ turn.halt.independentProposal = true;
1472
+ return { id: call.id, name: call.name, output: actionProposalReceipt(claimed.proposalId) };
1473
+ }
1474
+ if (claimed.preflight !== undefined && claimed.preflight.status !== 'ready') {
1475
+ const result = claimed.preflight;
1476
+ if (result.status === 'completed') {
1477
+ if (claimed.terminal === true)
1478
+ turn.halt.terminal = true;
1479
+ return { id: call.id, name: call.name, output: result.output };
1480
+ }
1481
+ const reason = result.status === 'denied' ? result.reason : result.error;
1482
+ return {
1483
+ id: call.id,
1484
+ name: call.name,
1485
+ output: result.status === 'denied' ? { reason } : null,
1486
+ error: reason,
1487
+ ...(result.status === 'denied' ? { denied: true } : {}),
1488
+ };
1489
+ }
1203
1490
  if (toolType === 'agent') {
1204
1491
  return delegateToolCall(turn, claimed);
1205
1492
  }
@@ -1704,7 +1991,9 @@ export async function runAgentLoop(boundDeps, requested, hooks) {
1704
1991
  ...(input.attachments !== undefined ? { attachments: input.attachments } : {}),
1705
1992
  }));
1706
1993
  }
1707
- const thread = threadForTurn(await hooks.step('load:thread', () => readThreadForTurn(deps, input.threadId, turnMessageLimit(deps.historyWindow))));
1994
+ const thread = threadForTurn(await hooks.step('load:thread', () => readThreadForTurn(deps, input.threadId, turnMessageLimit(deps.historyWindow)).then((loaded) => loaded === null
1995
+ ? { messages: [], actionApprovalMode: deps.actionApprovalMode ?? 'blocking' }
1996
+ : { ...loaded, actionApprovalMode: deps.actionApprovalMode ?? 'blocking' })));
1708
1997
  // With no window configured this whole block is skipped, so an existing deployment sends the
1709
1998
  // messages `load:thread` returned and records byte-identical checkpoints.
1710
1999
  let modelMessages = thread.messages;
@@ -1884,6 +2173,7 @@ export async function runAgentLoop(boundDeps, requested, hooks) {
1884
2173
  const tools = withBuiltInTools({
1885
2174
  tools: await deps.registry.definitionsFor(input.actor, deps.rolesPolicy, intersectAllow(persona?.allowedTools, deps.toolAllowList), {
1886
2175
  threadId: input.threadId,
2176
+ ...(input.uiCapabilities !== undefined ? { uiCapabilities: input.uiCapabilities } : {}),
1887
2177
  ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
1888
2178
  }),
1889
2179
  ask: deps.ask,
@@ -1945,7 +2235,7 @@ export async function runAgentLoop(boundDeps, requested, hooks) {
1945
2235
  outputTokens: result.usage.outputTokens,
1946
2236
  textLength: result.text.length,
1947
2237
  toolCalls: result.toolCalls.length,
1948
- })), deps));
2238
+ })), { ...deps, actionApprovalMode: thread.actionApprovalMode ?? 'blocking' }, input, hooks));
1949
2239
  // The gate runs before ANYTHING downstream: before the held frames reach the subscriber, before
1950
2240
  // the assistant message is persisted, before this text becomes the next step's context.
1951
2241
  let rejection;
@@ -2171,7 +2461,9 @@ export async function runAgentLoop(boundDeps, requested, hooks) {
2171
2461
  messageId: assistant.id,
2172
2462
  writer,
2173
2463
  toolUi: new Map(),
2464
+ toolText: new Map(),
2174
2465
  halt: { terminal: false },
2466
+ actionApprovalMode: thread.actionApprovalMode ?? 'blocking',
2175
2467
  ...(skillOffer !== undefined ? { skills: skillOffer } : {}),
2176
2468
  ...(memoryDigest !== undefined ? { memory: memoryDigest } : {}),
2177
2469
  };
@@ -2231,6 +2523,16 @@ export async function runAgentLoop(boundDeps, requested, hooks) {
2231
2523
  await deps.store.setMessageToolResults(assistant.id, [...results, ...syntheticResults]);
2232
2524
  // What the tools pushed joins what the model turn streamed, on the same message, in call
2233
2525
  // order. Every list here was read off a checkpoint above, so a replay writes the same value.
2526
+ const fallbackText = escapeUnsafeToolUiText(turn.toolCalls
2527
+ .map((call) => turnCalls.toolText.get(call.id))
2528
+ .filter(Boolean)
2529
+ .join('\n'));
2530
+ if (fallbackText)
2531
+ await deps.store.appendMessage({
2532
+ threadId: input.threadId,
2533
+ role: 'assistant',
2534
+ content: fallbackText,
2535
+ });
2234
2536
  const toolUi = turn.toolCalls.flatMap((call) => turnCalls.toolUi.get(call.id) ?? []);
2235
2537
  if (toolUi.length > 0) {
2236
2538
  await deps.store.setMessageUi?.(assistant.id, mergeUi(turn.ui, toolUi));
@@ -2244,9 +2546,17 @@ export async function runAgentLoop(boundDeps, requested, hooks) {
2244
2546
  });
2245
2547
  }
2246
2548
  modelMessages.push({ role: 'user', content: '', toolResults: results });
2549
+ const toolText = escapeUnsafeToolUiText(turn.toolCalls
2550
+ .map((call) => turnCalls.toolText.get(call.id))
2551
+ .filter(Boolean)
2552
+ .join('\n'));
2553
+ if (toolText) {
2554
+ modelMessages.push({ role: 'assistant', content: toolText });
2555
+ lastText += `\n${toolText}`;
2556
+ }
2247
2557
  // A `terminal` tool succeeded: its effect is the answer, so no model call narrates it. Read off
2248
2558
  // the calls' `persist:toolcall` checkpoints and their outcomes, so a replay ends here too.
2249
- if (turnCalls.halt.terminal) {
2559
+ if (turnCalls.halt.terminal || turnCalls.halt.independentProposal) {
2250
2560
  break;
2251
2561
  }
2252
2562
  }