@agent-native/core 0.137.6 → 0.137.7

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.
@@ -211,6 +211,7 @@ function cloneContentParts(content) {
211
211
  args: { ...part.args },
212
212
  ...(part.mcpApp ? { mcpApp: { ...part.mcpApp } } : {}),
213
213
  ...(part.chatUI ? { chatUI: { ...part.chatUI } } : {}),
214
+ ...(part.approval ? { approval: { ...part.approval } } : {}),
214
215
  });
215
216
  }
216
217
  export function settleInterruptedAssistantToolCallsInRepo(repo) {
@@ -1,6 +1,8 @@
1
1
  import React from "react";
2
2
  import type { ContentPart } from "../sse-event-processor.js";
3
3
  import { type RunErrorInfo } from "./run-recovery.js";
4
+ import { toolCallHasPendingApproval } from "./tool-call-display.js";
5
+ export { toolCallHasPendingApproval };
4
6
  export declare function displayableUserMessageText(text: string): string;
5
7
  export declare function isHiddenUserMessage(message: unknown): boolean;
6
8
  interface FormattedMessageTimestamp {
@@ -98,6 +100,10 @@ export declare function isCollapsibleAssistantWorkPart(part: {
98
100
  toolName?: string;
99
101
  chatUI?: unknown;
100
102
  mcpApp?: unknown;
103
+ approval?: {
104
+ approvalKey?: string;
105
+ dismissed?: boolean;
106
+ };
101
107
  }): boolean;
102
108
  export declare function getAssistantToolSummaryInfo(parts: readonly {
103
109
  type?: string;
@@ -106,6 +112,10 @@ export declare function getAssistantToolSummaryInfo(parts: readonly {
106
112
  args?: Record<string, unknown>;
107
113
  chatUI?: unknown;
108
114
  mcpApp?: unknown;
115
+ approval?: {
116
+ approvalKey?: string;
117
+ dismissed?: boolean;
118
+ };
109
119
  }[]): {
110
120
  startIndex: number;
111
121
  hiddenToolCount: number;
@@ -126,5 +136,4 @@ export declare function RunningActivityStatus({ label }: {
126
136
  export declare function ThinkingIndicator({ label, }?: {
127
137
  label?: string;
128
138
  }): React.JSX.Element;
129
- export {};
130
139
  //# sourceMappingURL=message-components.d.ts.map
@@ -19,7 +19,8 @@ import { cn } from "../utils.js";
19
19
  import { MarkdownText, renderMarkdownToClipboardHtml, } from "./markdown-renderer.js";
20
20
  import { getAssistantRunDurationMs } from "./repo-helpers.js";
21
21
  import { getRunErrorMetadata, runErrorHeadline, runErrorKey, } from "./run-recovery.js";
22
- import { ToolCallFallback, ToolActivityPresentation, FilesChangedSummary, ASSISTANT_VISIBLE_TOOL_CALL_LIMIT, ChatRunningContext, ChatRunDurationContext, formatWorkedDuration, RanToolsSummary, ReasoningCell, WorkedForSummary, } from "./tool-call-display.js";
22
+ import { ToolCallFallback, ToolActivityPresentation, FilesChangedSummary, ASSISTANT_VISIBLE_TOOL_CALL_LIMIT, ChatRunningContext, ChatRunDurationContext, formatWorkedDuration, RanToolsSummary, ReasoningCell, WorkedForSummary, toolCallHasPendingApproval, } from "./tool-call-display.js";
23
+ export { toolCallHasPendingApproval };
23
24
  // ─── Pending selection context key ───────────────────────────────────────────
24
25
  // Mirrored from AssistantChat to avoid a cross-import on a private constant.
25
26
  const PENDING_SELECTION_KEY = "pending-selection-context";
@@ -506,7 +507,9 @@ export function assistantMessageHasCompletedCustomUi(content) {
506
507
  }
507
508
  hasCompletedTool = true;
508
509
  lastCompletedToolIsCustomUi =
509
- record.chatUI !== undefined || record.mcpApp !== undefined;
510
+ record.chatUI !== undefined ||
511
+ record.mcpApp !== undefined ||
512
+ toolCallHasPendingApproval(record);
510
513
  }
511
514
  return hasCompletedTool && lastCompletedToolIsCustomUi;
512
515
  }
@@ -518,7 +521,9 @@ export function assistantMessageHasCustomUi(content) {
518
521
  return false;
519
522
  const record = part;
520
523
  return (record.type === "tool-call" &&
521
- (record.chatUI !== undefined || record.mcpApp !== undefined));
524
+ (record.chatUI !== undefined ||
525
+ record.mcpApp !== undefined ||
526
+ toolCallHasPendingApproval(record)));
522
527
  });
523
528
  }
524
529
  // Only the last assistant message may shimmer as "the currently running
@@ -628,7 +633,11 @@ export function isCollapsibleAssistantWorkPart(part) {
628
633
  return (part.type === "tool-call" &&
629
634
  !ALWAYS_VISIBLE_ASSISTANT_TOOLS.has(part.toolName ?? "") &&
630
635
  part.chatUI === undefined &&
631
- part.mcpApp === undefined);
636
+ part.mcpApp === undefined &&
637
+ // Keep the Approve/Deny affordance outside "Worked for…" — needsApproval
638
+ // tools finish with a result string, so without this they collapse and the
639
+ // human gate disappears from the viewport.
640
+ !toolCallHasPendingApproval(part));
632
641
  }
633
642
  export function getAssistantToolSummaryInfo(parts) {
634
643
  const toolCallIndices = parts.reduce((indices, part, index) => {
@@ -622,7 +622,10 @@ function mapAgentNativeEvent(raw, input) {
622
622
  type: "approval-request",
623
623
  ...base,
624
624
  approvalId: ev.approvalKey ?? ev.id ?? createRuntimeId("approval"),
625
- toolCallId: ev.id,
625
+ // `approval_required` carries the model-side call id as `toolCallId`,
626
+ // not `id`. Without this the request falls back to matching by tool
627
+ // name, which picks the wrong call when two are pending at once.
628
+ toolCallId: ev.toolCallId ?? ev.id,
626
629
  toolName: ev.tool,
627
630
  message: ev.label ?? "Approve this tool call?",
628
631
  input: ev.input,
@@ -849,18 +852,26 @@ function applyRuntimeEventToContent(event, projection) {
849
852
  return { content: [...content] };
850
853
  }
851
854
  if (typed.type === "approval-request") {
852
- const part = [...content]
853
- .reverse()
854
- .find((candidate) => candidate.type === "tool-call" &&
855
- (candidate.toolCallId === typed.toolCallId ||
856
- candidate.toolName === typed.toolName));
857
- if (part) {
855
+ const reversed = [...content].reverse();
856
+ const isToolCall = (candidate) => candidate.type === "tool-call";
857
+ // Match on the exact call id whenever the server supplied one. Falling back
858
+ // to "newest call with this name" would hand this call's approvalKey to a
859
+ // different parallel call of the same action, so name matching is reserved
860
+ // for events that carry no id at all.
861
+ const part = typed.toolCallId
862
+ ? reversed.find((candidate) => isToolCall(candidate) && candidate.toolCallId === typed.toolCallId)
863
+ : reversed.find((candidate) => isToolCall(candidate) && candidate.toolName === typed.toolName);
864
+ if (part && part.type === "tool-call") {
858
865
  part.approval = { approvalKey: typed.approvalId };
859
866
  }
860
- else {
867
+ else if (!typed.toolCallId) {
868
+ // Only runtimes that never announced the call (no id) get a synthesized
869
+ // card. An id that matches nothing means the call was never observed or
870
+ // is already resolved, and inventing an Approve/Deny card for it would
871
+ // gate something the user cannot see.
861
872
  content.push({
862
873
  type: "tool-call",
863
- toolCallId: typed.toolCallId ?? typed.approvalId,
874
+ toolCallId: typed.approvalId,
864
875
  toolName: typed.toolName ?? "approval",
865
876
  argsText: typed.input ? JSON.stringify(typed.input) : "",
866
877
  args: toContentPartInput(typed.input),
@@ -30,6 +30,13 @@ export type ApprovalContextValue = {
30
30
  onAlwaysAllow?: (approvalKey: string) => void;
31
31
  };
32
32
  export declare const ApprovalContext: React.Context<ApprovalContextValue | null>;
33
+ /** Pending human-in-the-loop gate still waiting for Approve/Deny. */
34
+ export declare function toolCallHasPendingApproval(part: {
35
+ approval?: {
36
+ approvalKey?: string;
37
+ dismissed?: boolean;
38
+ } | null;
39
+ }): boolean;
33
40
  export declare const TOOL_LONG_RUNNING_HINT_DELAY_MS = 45000;
34
41
  export declare function ToolActivityPresentation({ toolName, isRunning, isActiveTail, suppressLongRunningHint, children, }: {
35
42
  toolName: string;
@@ -20,6 +20,13 @@ export const ChatRunningContext = React.createContext(false);
20
20
  export const ChatRunDurationContext = React.createContext(null);
21
21
  export const ASSISTANT_VISIBLE_TOOL_CALL_LIMIT = 3;
22
22
  export const ApprovalContext = React.createContext(null);
23
+ /** Pending human-in-the-loop gate still waiting for Approve/Deny. */
24
+ export function toolCallHasPendingApproval(part) {
25
+ const approval = part.approval;
26
+ return (typeof approval?.approvalKey === "string" &&
27
+ approval.approvalKey.length > 0 &&
28
+ approval.dismissed !== true);
29
+ }
23
30
  export const TOOL_LONG_RUNNING_HINT_DELAY_MS = 45_000;
24
31
  export function ToolActivityPresentation({ toolName, isRunning, isActiveTail, suppressLongRunningHint = false, children, }) {
25
32
  const [showLongRunningHint, setShowLongRunningHint] = useState(false);
@@ -514,7 +521,8 @@ function isReconnectSummarizablePart(part) {
514
521
  (part.type === "tool-call" &&
515
522
  part.toolName !== "connect-builder" &&
516
523
  part.chatUI === undefined &&
517
- part.mcpApp === undefined));
524
+ part.mcpApp === undefined &&
525
+ !toolCallHasPendingApproval(part)));
518
526
  }
519
527
  function isReconnectToolSummaryPart(content, index, startIndex) {
520
528
  if (startIndex < 0 || index >= startIndex)
@@ -68,6 +68,8 @@ export interface SSEEvent {
68
68
  /** Stable key the client echoes back in `approvedToolCalls` to approve a
69
69
  * paused `needsApproval` tool call. Present on `approval_required` events. */
70
70
  approvalKey?: string;
71
+ /** Model-side tool-call id for `approval_required` (mirrors AgentChatEvent). */
72
+ toolCallId?: string;
71
73
  error?: string;
72
74
  seq?: number;
73
75
  agent?: string;
@@ -201,6 +201,39 @@ function findPendingToolCallIndexById(content, toolCallId) {
201
201
  }
202
202
  return -1;
203
203
  }
204
+ /**
205
+ * Locate the tool call an `approval_required` event refers to.
206
+ *
207
+ * Stricter than `findPendingToolCallIndex`: when the server supplies a call id
208
+ * we never fall back to "newest pending call with this name". A paused
209
+ * `tool_done` resolves the call right after the gate fires, so a replayed or
210
+ * reordered approval would otherwise miss on id and silently attach this call's
211
+ * approvalKey to a *different* in-flight call of the same action — putting the
212
+ * wrong key behind a visible Approve button.
213
+ *
214
+ * The only tolerated fallback mirrors `findPendingActivityToolCallIndex`: a
215
+ * single unambiguous reader-local (`tc_N`) placeholder, which is how a call
216
+ * looks when an older server omitted the id on `tool_start`.
217
+ */
218
+ function findApprovalToolCallIndex(content, toolName, toolCallId) {
219
+ if (!toolCallId) {
220
+ return findPendingToolCallIndex(content, toolName);
221
+ }
222
+ const exactIndex = findPendingToolCallIndexById(content, toolCallId);
223
+ if (exactIndex >= 0)
224
+ return exactIndex;
225
+ const readerLocalCandidates = [];
226
+ for (let i = 0; i < content.length; i += 1) {
227
+ const part = content[i];
228
+ if (part.type === "tool-call" &&
229
+ part.toolName === toolName &&
230
+ part.result === undefined &&
231
+ /^tc_\d+$/.test(part.toolCallId)) {
232
+ readerLocalCandidates.push(i);
233
+ }
234
+ }
235
+ return readerLocalCandidates.length === 1 ? readerLocalCandidates[0] : -1;
236
+ }
204
237
  function findOldestPendingActivityToolCallIndex(content, toolName) {
205
238
  for (let i = 0; i < content.length; i += 1) {
206
239
  const part = content[i];
@@ -656,6 +689,8 @@ function coalesceJournalRecoveredTool(content, completedIndex) {
656
689
  prior.mcpApp = current.mcpApp;
657
690
  if (current.chatUI)
658
691
  prior.chatUI = current.chatUI;
692
+ if (current.approval)
693
+ prior.approval = { ...current.approval };
659
694
  }
660
695
  content.splice(completedIndex, 1);
661
696
  return true;
@@ -1044,7 +1079,10 @@ export function processEvent(ev, content, toolCallCounter, tabId, state) {
1044
1079
  const approvalTool = ev.tool ?? "unknown";
1045
1080
  const approvalKey = ev.approvalKey;
1046
1081
  if (approvalKey) {
1047
- const idx = findPendingToolCallIndex(content, approvalTool, ev.id);
1082
+ // `toolCallId` is the model-side id in the `approval_required` contract;
1083
+ // `id` is only carried by older frames. Same precedence as the runtime
1084
+ // path so both processors resolve an event to the same call.
1085
+ const idx = findApprovalToolCallIndex(content, approvalTool, ev.toolCallId ?? ev.id);
1048
1086
  if (idx >= 0) {
1049
1087
  const part = content[idx];
1050
1088
  if (part.type === "tool-call") {
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- ok?: undefined;
17
16
  error?: undefined;
17
+ ok?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- ok?: undefined;
22
21
  error?: undefined;
22
+ ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -28,7 +28,7 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- ok: boolean;
32
31
  error?: undefined;
32
+ ok: boolean;
33
33
  }>>;
34
34
  //# sourceMappingURL=routes.d.ts.map
@@ -15,7 +15,7 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- ok: boolean;
19
18
  error?: undefined;
19
+ ok: boolean;
20
20
  }>>;
21
21
  //# sourceMappingURL=routes.d.ts.map
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
37
37
  //# sourceMappingURL=agent-engine-api-key-route.d.ts.map
@@ -26,9 +26,9 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
+ error?: undefined;
29
30
  token: string;
30
31
  expiresAt: string;
31
32
  ttlSeconds: number;
32
- error?: undefined;
33
33
  }>>;
34
34
  //# sourceMappingURL=realtime-token.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.137.6",
3
+ "version": "0.137.7",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {