@agent-native/core 0.149.1 → 0.149.2
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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
- package/corpus/templates/analytics/app/global.css +1 -1
- package/corpus/templates/analytics/app/i18n-data.ts +37 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
- package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/assets/app/global.css +1 -1
- package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/brain/app/global.css +1 -1
- package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +274 -137
- package/corpus/templates/clips/desktop/src/styles.css +35 -0
- package/corpus/templates/content/app/global.css +1 -0
- package/corpus/templates/crm/app/global.css +1 -1
- package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -0
- package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
- package/corpus/templates/slides/AGENTS.md +11 -10
- package/corpus/templates/slides/actions/add-deck.ts +2 -0
- package/corpus/templates/slides/actions/add-slide.ts +15 -1
- package/corpus/templates/slides/actions/create-deck.ts +18 -6
- package/corpus/templates/slides/actions/get-deck.ts +29 -1
- package/corpus/templates/slides/actions/patch-deck.ts +122 -9
- package/corpus/templates/slides/actions/save-deck.ts +24 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +69 -60
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +3 -3
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
- package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
- package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
- package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
- package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -27
- package/corpus/templates/slides/app/pages/Index.tsx +240 -89
- package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
- package/corpus/templates/slides/shared/api.ts +2 -1
- package/corpus/templates/slides/shared/deck-title.ts +135 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
- package/dist/agent/engine/ai-sdk-engine.js +1 -1
- package/dist/agent/production-agent.d.ts +21 -1
- package/dist/agent/production-agent.js +129 -10
- package/dist/agent/tool-approval-store.d.ts +18 -0
- package/dist/agent/tool-approval-store.js +122 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/cli/code-agent-executor.js +11 -2
- package/dist/client/AgentPanel.js +31 -6
- package/dist/client/AssistantChat.d.ts +3 -2
- package/dist/client/AssistantChat.js +7 -20
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat-adapter.js +11 -0
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/chat/markdown-renderer.js +14 -2
- package/dist/client/chat/message-components.d.ts +6 -2
- package/dist/client/chat/message-components.js +25 -47
- package/dist/client/chat/tool-call-display.d.ts +3 -5
- package/dist/client/chat/tool-call-display.js +7 -212
- package/dist/client/chat-first/apps-rail.js +3 -3
- package/dist/client/chat-first/browser-pane.js +4 -1
- package/dist/client/chat-first/chat-history.d.ts +3 -1
- package/dist/client/chat-first/chat-history.js +2 -2
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +52 -41
- package/dist/client/chat-first/types.d.ts +3 -0
- package/dist/client/chat-first.d.ts +10 -0
- package/dist/client/chat-first.js +37 -2
- package/dist/client/components/ui/message-scroller.js +4 -1
- package/dist/client/i18n.js +3 -1
- package/dist/client/resources/McpConnectionSuggestion.js +9 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/use-chat-models.js +156 -80
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/webhook-handler.js +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/auth.js +262 -31
- package/dist/server/onboarding-html.js +56 -9
- package/dist/shared/reasoning-effort.d.ts +8 -8
- package/dist/shared/reasoning-effort.js +10 -10
- package/dist/shared/streaming-text-smoothing.js +41 -11
- package/dist/styles/agent-native.css +19 -32
- package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/docs/content/template-chat.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
|
@@ -137,7 +137,7 @@ function normalizeString(value: unknown, fallback: string): string {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function normalizeElementPath(value: unknown): number[] | undefined {
|
|
140
|
-
if (!Array.isArray(value)) return undefined;
|
|
140
|
+
if (!Array.isArray(value) || value.length === 0) return undefined;
|
|
141
141
|
const path = value.filter(
|
|
142
142
|
(part): part is number =>
|
|
143
143
|
typeof part === "number" &&
|
|
@@ -155,6 +155,7 @@ function normalizeSlideAnimation(
|
|
|
155
155
|
if (!isRecord(value)) return null;
|
|
156
156
|
|
|
157
157
|
const elementPath = normalizeElementPath(value.elementPath);
|
|
158
|
+
if (value.elementPath !== undefined && !elementPath) return null;
|
|
158
159
|
const rawElementIndex = value.elementIndex;
|
|
159
160
|
const hasElementIndex =
|
|
160
161
|
typeof rawElementIndex === "number" &&
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const GENERATED_TITLE_PLACEHOLDERS = new Set([
|
|
2
|
+
"deck",
|
|
3
|
+
"date",
|
|
4
|
+
"image slide title",
|
|
5
|
+
"presentation title",
|
|
6
|
+
"section",
|
|
7
|
+
"section title",
|
|
8
|
+
"slide title",
|
|
9
|
+
"untitled",
|
|
10
|
+
"untitled deck",
|
|
11
|
+
"your name",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generated deck ids should never become user-facing titles. Keep this
|
|
16
|
+
* deliberately narrow so normal titles with spaces and punctuation remain
|
|
17
|
+
* valid, while catching opaque mixed-case tokens such as H3sVsnns-TEVUOpz9w.
|
|
18
|
+
*/
|
|
19
|
+
const OPAQUE_DECK_TITLE_PATTERN =
|
|
20
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z0-9_-]{12,64}$/;
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_DECK_TITLE = "Untitled Deck";
|
|
23
|
+
|
|
24
|
+
export function isOpaqueDeckTitle(value: unknown): value is string {
|
|
25
|
+
return (
|
|
26
|
+
typeof value === "string" && OPAQUE_DECK_TITLE_PATTERN.test(value.trim())
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isGeneratedDeckTitle(value: unknown): value is string {
|
|
31
|
+
if (typeof value !== "string") return false;
|
|
32
|
+
const title = value.trim();
|
|
33
|
+
return (
|
|
34
|
+
GENERATED_TITLE_PLACEHOLDERS.has(title.toLowerCase()) ||
|
|
35
|
+
isOpaqueDeckTitle(title)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decodeHtmlEntities(value: string): string {
|
|
40
|
+
const decodeCodePoint = (raw: string, radix: number): string => {
|
|
41
|
+
const codePoint = Number.parseInt(raw, radix);
|
|
42
|
+
return Number.isNaN(codePoint) || codePoint > 0x10ffff
|
|
43
|
+
? ""
|
|
44
|
+
: String.fromCodePoint(codePoint);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return value
|
|
48
|
+
.replace(/ /gi, " ")
|
|
49
|
+
.replace(/&/gi, "&")
|
|
50
|
+
.replace(/</gi, "<")
|
|
51
|
+
.replace(/>/gi, ">")
|
|
52
|
+
.replace(/"/gi, '"')
|
|
53
|
+
.replace(/(?:'|')/gi, "'")
|
|
54
|
+
.replace(/&#x([0-9a-f]+);?/gi, (_, hex: string) => decodeCodePoint(hex, 16))
|
|
55
|
+
.replace(/&#(\d+);?/g, (_, digits: string) => decodeCodePoint(digits, 10));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function plainText(value: string): string {
|
|
59
|
+
return decodeHtmlEntities(
|
|
60
|
+
value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]+>/g, " "),
|
|
61
|
+
)
|
|
62
|
+
.replace(/\s+/g, " ")
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function usableCandidate(value: string): string | null {
|
|
67
|
+
const candidate = plainText(value).replace(/^[•●▪‣\-\s]+/, "");
|
|
68
|
+
if (!candidate || candidate.length > 140) return null;
|
|
69
|
+
if (GENERATED_TITLE_PLACEHOLDERS.has(candidate.toLowerCase())) return null;
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recover a deck title from the largest title-like text in its first slide.
|
|
75
|
+
* This is intentionally HTML-string based because the action runs on the
|
|
76
|
+
* server without a browser DOM.
|
|
77
|
+
*/
|
|
78
|
+
export function deriveDeckTitleFromSlideContent(
|
|
79
|
+
content: unknown,
|
|
80
|
+
): string | null {
|
|
81
|
+
if (typeof content !== "string" || !content.trim()) return null;
|
|
82
|
+
|
|
83
|
+
const candidates: Array<{ score: number; text: string }> = [];
|
|
84
|
+
const headingPattern = /<(h[1-6])\b[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
85
|
+
for (const match of content.matchAll(headingPattern)) {
|
|
86
|
+
const text = usableCandidate(match[2] ?? "");
|
|
87
|
+
if (text) {
|
|
88
|
+
const level = Number.parseInt(match[1].slice(1), 10);
|
|
89
|
+
candidates.push({ score: 1000 - level * 10, text });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const styledTextPattern =
|
|
94
|
+
/<([a-z][\w:-]*)\b[^>]*\bstyle\s*=\s*(["'])([\s\S]*?)\2[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
95
|
+
for (const match of content.matchAll(styledTextPattern)) {
|
|
96
|
+
const fontSize = Number.parseFloat(
|
|
97
|
+
match[3]?.match(/(?:^|;)\s*font-size\s*:\s*([\d.]+)px/i)?.[1] ?? "0",
|
|
98
|
+
);
|
|
99
|
+
if (fontSize < 28) continue;
|
|
100
|
+
const text = usableCandidate(match[4] ?? "");
|
|
101
|
+
if (text) candidates.push({ score: fontSize, text });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return candidates.sort((a, b) => b.score - a.score)[0]?.text ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Return a human-readable replacement only when the requested title is a
|
|
109
|
+
* generated placeholder or opaque id. A meaningful existing title wins when
|
|
110
|
+
* a stale full-payload save tries to replace it with a generated value.
|
|
111
|
+
*/
|
|
112
|
+
export function repairGeneratedDeckTitle(
|
|
113
|
+
requestedTitle: unknown,
|
|
114
|
+
firstSlideContent: unknown,
|
|
115
|
+
existingTitle?: unknown,
|
|
116
|
+
): string | null {
|
|
117
|
+
if (!isGeneratedDeckTitle(requestedTitle)) return null;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
deriveDeckTitleFromSlideContent(firstSlideContent) ??
|
|
121
|
+
(typeof existingTitle === "string" &&
|
|
122
|
+
existingTitle.trim() &&
|
|
123
|
+
!isGeneratedDeckTitle(existingTitle)
|
|
124
|
+
? existingTitle
|
|
125
|
+
: null)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function assertHumanReadableDeckTitle(title: string): void {
|
|
130
|
+
if (isOpaqueDeckTitle(title)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Deck title must be a concise, human-readable title; generated ids are not valid titles.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -89,7 +89,10 @@ export function TaskListRow({
|
|
|
89
89
|
onAnimationEnd={completion.handleRowAnimationEnd}
|
|
90
90
|
dataAttributes={{ "data-task-id": item.id }}
|
|
91
91
|
className={cn(
|
|
92
|
-
|
|
92
|
+
// Scoped to the phases that actually animate. Applied unconditionally
|
|
93
|
+
// it promoted a layer for every row in the list and never retired any
|
|
94
|
+
// of them, for an animation that only runs while a row completes.
|
|
95
|
+
completionPhase !== "idle" && "will-change-[opacity,transform]",
|
|
93
96
|
displayDone && completionPhase === "idle" && !selected && "opacity-60",
|
|
94
97
|
completionPhase === "completing" &&
|
|
95
98
|
hideAfterComplete &&
|
|
@@ -138,7 +138,7 @@ function googleThinkingBudget(effort) {
|
|
|
138
138
|
return -1;
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
|
-
* Map
|
|
141
|
+
* Map an effort level to Gemini 3.x thinkingLevel string.
|
|
142
142
|
* Gemini 3 models (gemini-3.*) reject thinkingBudget and require thinkingLevel
|
|
143
143
|
* with values 'low' | 'medium' | 'high'. Gemini 3.0 only supports 'low'/'high';
|
|
144
144
|
* Gemini 3.1+ adds 'medium'. We always emit 'medium' for medium effort since it
|
|
@@ -266,7 +266,7 @@ export interface ProductionAgentOptions {
|
|
|
266
266
|
model?: string;
|
|
267
267
|
/** App/template id used for org-scoped per-app model defaults. */
|
|
268
268
|
appId?: string;
|
|
269
|
-
/** Default
|
|
269
|
+
/** Default effort for requests that do not supply an override. */
|
|
270
270
|
reasoningEffort?: ReasoningEffort;
|
|
271
271
|
/** Provider-specific options passed through to the engine */
|
|
272
272
|
providerOptions?: EngineMessage extends never ? never : any;
|
|
@@ -632,8 +632,16 @@ export interface ExecuteAgentToolCallOptions {
|
|
|
632
632
|
threadId?: string;
|
|
633
633
|
turnId?: string;
|
|
634
634
|
approvedToolCalls?: string[];
|
|
635
|
+
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
|
|
636
|
+
consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
|
|
635
637
|
send?: (event: AgentChatEvent) => void;
|
|
636
638
|
}
|
|
639
|
+
export interface AgentApprovalBinding {
|
|
640
|
+
toolName: string;
|
|
641
|
+
input: unknown;
|
|
642
|
+
callId: string;
|
|
643
|
+
approvalKey: string;
|
|
644
|
+
}
|
|
637
645
|
/**
|
|
638
646
|
* Execute one externally-selected tool through the exact same guarded runtime
|
|
639
647
|
* as a normal model-selected tool call. Realtime voice and other duplex
|
|
@@ -658,6 +666,11 @@ export declare function buildFirstRequestPayloadDetail(input: {
|
|
|
658
666
|
availableToolCount: number;
|
|
659
667
|
}): string;
|
|
660
668
|
export declare function toolCallCacheKey(toolName: string, input: unknown): string;
|
|
669
|
+
export declare function findApprovedStructuredToolCall(history: AgentChatStructuredMessage[] | undefined, approvedToolCalls: readonly string[] | undefined): {
|
|
670
|
+
name: string;
|
|
671
|
+
input: unknown;
|
|
672
|
+
callId: string;
|
|
673
|
+
} | undefined;
|
|
661
674
|
export declare function repeatedSourceSweepGuardMessage(opts: {
|
|
662
675
|
toolName: string;
|
|
663
676
|
priorCalls: number;
|
|
@@ -769,6 +782,13 @@ export declare function runAgentLoop(opts: {
|
|
|
769
782
|
* `approval_required`. See `AgentChatRequest.approvedToolCalls`.
|
|
770
783
|
*/
|
|
771
784
|
approvedToolCalls?: string[];
|
|
785
|
+
/**
|
|
786
|
+
* Server-side approval persistence hooks. The HTTP transport supplies these
|
|
787
|
+
* so client-supplied history is never the authorization source; direct loop
|
|
788
|
+
* callers may omit them when they own the surrounding approval boundary.
|
|
789
|
+
*/
|
|
790
|
+
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
|
|
791
|
+
consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
|
|
772
792
|
/**
|
|
773
793
|
* In-loop processor seam (see `processors.ts`). Each processor can observe
|
|
774
794
|
* streamed chunks, observe model responses around tool execution, and
|
|
@@ -39,6 +39,7 @@ import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js
|
|
|
39
39
|
import { startRun, subscribeToRun, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnDurably, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, } from "./run-manager.js";
|
|
40
40
|
import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, insertRun, insertRunEvent, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn, RUN_DIAG_STAGE, UNCLAIMED_BACKGROUND_RUN_GRACE_MS, } from "./run-store.js";
|
|
41
41
|
import { buildCurrentTimeUserContext } from "./runtime-context.js";
|
|
42
|
+
import { consumeAgentToolApproval, createAgentToolApproval, } from "./tool-approval-store.js";
|
|
42
43
|
import { findCompletedJournalEntry, } from "./tool-call-journal.js";
|
|
43
44
|
import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, } from "./tool-error-redaction.js";
|
|
44
45
|
import { describeToolResultImages, extractAgentImagesFromActionResult, } from "./tool-result-images.js";
|
|
@@ -688,7 +689,7 @@ const VISIBLE_RETRY_THRESHOLD_MS = 10_000;
|
|
|
688
689
|
*/
|
|
689
690
|
const FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS = 25_000;
|
|
690
691
|
// Raised from 1 -> 2 now that each retry actually adapts (raises the token
|
|
691
|
-
// ceiling and steps
|
|
692
|
+
// ceiling and steps effort down a tier) instead of re-issuing the
|
|
692
693
|
// exact same doomed request twice.
|
|
693
694
|
const EMPTY_FINAL_RESPONSE_RETRY_LIMIT = 2;
|
|
694
695
|
const MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT = 6;
|
|
@@ -2120,6 +2121,8 @@ export async function executeAgentToolCall(options) {
|
|
|
2120
2121
|
threadId: options.threadId,
|
|
2121
2122
|
turnId: options.turnId,
|
|
2122
2123
|
approvedToolCalls: options.approvedToolCalls,
|
|
2124
|
+
onApprovalRequired: options.onApprovalRequired,
|
|
2125
|
+
consumeApproval: options.consumeApproval,
|
|
2123
2126
|
});
|
|
2124
2127
|
}
|
|
2125
2128
|
catch (error) {
|
|
@@ -2307,6 +2310,51 @@ function stableStringify(value) {
|
|
|
2307
2310
|
export function toolCallCacheKey(toolName, input) {
|
|
2308
2311
|
return `${toolName}:${stableStringify(normalizeToolCallInputForHistory(input))}`;
|
|
2309
2312
|
}
|
|
2313
|
+
export function findApprovedStructuredToolCall(history, approvedToolCalls) {
|
|
2314
|
+
if (!Array.isArray(history) || !approvedToolCalls?.length)
|
|
2315
|
+
return undefined;
|
|
2316
|
+
const approved = new Set(approvedToolCalls);
|
|
2317
|
+
const calls = new Map();
|
|
2318
|
+
let match;
|
|
2319
|
+
for (const message of history) {
|
|
2320
|
+
if (message.role === "assistant") {
|
|
2321
|
+
for (const part of message.content) {
|
|
2322
|
+
if (part.type !== "tool-call")
|
|
2323
|
+
continue;
|
|
2324
|
+
const callId = typeof part.id === "string"
|
|
2325
|
+
? part.id
|
|
2326
|
+
: typeof part.toolCallId === "string"
|
|
2327
|
+
? part.toolCallId
|
|
2328
|
+
: "";
|
|
2329
|
+
const name = typeof part.name === "string"
|
|
2330
|
+
? part.name
|
|
2331
|
+
: typeof part.toolName === "string"
|
|
2332
|
+
? part.toolName
|
|
2333
|
+
: "";
|
|
2334
|
+
if (!callId || !name)
|
|
2335
|
+
continue;
|
|
2336
|
+
calls.set(callId, { name, input: part.input ?? part.args ?? {} });
|
|
2337
|
+
}
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
for (const part of message.content) {
|
|
2341
|
+
if (part.type !== "tool-result")
|
|
2342
|
+
continue;
|
|
2343
|
+
const call = calls.get(part.toolCallId);
|
|
2344
|
+
if (!call || !approved.has(toolCallCacheKey(call.name, call.input))) {
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
const result = part.content.toLowerCase();
|
|
2348
|
+
if (!result.includes("did not execute") &&
|
|
2349
|
+
!result.includes("awaiting human approval") &&
|
|
2350
|
+
!result.includes("waiting for your approval")) {
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
match = { ...call, callId: part.toolCallId };
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return match;
|
|
2357
|
+
}
|
|
2310
2358
|
const INTERRUPTED_TOOL_LEDGER_POLL_MS = process.env.NODE_ENV === "test" ? 0 : 2_000;
|
|
2311
2359
|
async function waitForInterruptedToolLedgerEntry(opts) {
|
|
2312
2360
|
const pollMs = INTERRUPTED_TOOL_LEDGER_POLL_MS;
|
|
@@ -3965,7 +4013,16 @@ export async function runAgentLoop(opts) {
|
|
|
3965
4013
|
const approvalKey = toolCallCacheKey(toolCall.name, toolCall.input);
|
|
3966
4014
|
// Consume a grant on its first exact match. A second identical call in
|
|
3967
4015
|
// the same continuation must request its own human approval.
|
|
3968
|
-
const
|
|
4016
|
+
const approvalBinding = {
|
|
4017
|
+
toolName: toolCall.name,
|
|
4018
|
+
input: toolCall.input,
|
|
4019
|
+
callId: toolCall.id,
|
|
4020
|
+
approvalKey,
|
|
4021
|
+
};
|
|
4022
|
+
const requestedApproval = approvedToolCallKeys.delete(approvalKey);
|
|
4023
|
+
const wasApproved = requestedApproval && opts.consumeApproval
|
|
4024
|
+
? await opts.consumeApproval(approvalBinding)
|
|
4025
|
+
: requestedApproval;
|
|
3969
4026
|
if (actionEntry.needsApproval && !wasApproved) {
|
|
3970
4027
|
let mustApprove = false;
|
|
3971
4028
|
try {
|
|
@@ -3989,6 +4046,7 @@ export async function runAgentLoop(opts) {
|
|
|
3989
4046
|
mustApprove = true;
|
|
3990
4047
|
}
|
|
3991
4048
|
if (mustApprove) {
|
|
4049
|
+
await opts.onApprovalRequired?.(approvalBinding);
|
|
3992
4050
|
send({
|
|
3993
4051
|
type: "tool_start",
|
|
3994
4052
|
id: toolCall.id,
|
|
@@ -6433,6 +6491,12 @@ export function createProductionAgentHandler(options) {
|
|
|
6433
6491
|
...historyMessages,
|
|
6434
6492
|
{ role: "user", content: userContent },
|
|
6435
6493
|
];
|
|
6494
|
+
const requestedApprovedToolCalls = Array.isArray(body.approvedToolCalls) && body.approvedToolCalls.length > 0
|
|
6495
|
+
? body.approvedToolCalls
|
|
6496
|
+
.filter((key) => typeof key === "string")
|
|
6497
|
+
.slice(0, 200)
|
|
6498
|
+
: undefined;
|
|
6499
|
+
const exactApprovedToolCall = findApprovedStructuredToolCall(structuredHistory, requestedApprovedToolCalls);
|
|
6436
6500
|
const firstRequestPayloadDetail = buildFirstRequestPayloadDetail({
|
|
6437
6501
|
isFirstRequest: history.length === 0,
|
|
6438
6502
|
systemPrompt: requestSystemPrompt,
|
|
@@ -6486,6 +6550,38 @@ export function createProductionAgentHandler(options) {
|
|
|
6486
6550
|
: typeof requestTurnId === "string" && requestTurnId.trim()
|
|
6487
6551
|
? requestTurnId.trim()
|
|
6488
6552
|
: runId;
|
|
6553
|
+
const approvalStoreBinding = (binding) => {
|
|
6554
|
+
if (!ownerEmail) {
|
|
6555
|
+
throw new Error("Cannot create or consume an approval without an authenticated owner");
|
|
6556
|
+
}
|
|
6557
|
+
return {
|
|
6558
|
+
ownerEmail,
|
|
6559
|
+
orgId: getRequestOrgId() ?? null,
|
|
6560
|
+
threadId: effectiveThreadId,
|
|
6561
|
+
turnId: effectiveTurnId,
|
|
6562
|
+
toolName: binding.toolName,
|
|
6563
|
+
callId: binding.callId,
|
|
6564
|
+
approvalKey: binding.approvalKey,
|
|
6565
|
+
};
|
|
6566
|
+
};
|
|
6567
|
+
const approvalHooks = {
|
|
6568
|
+
onApprovalRequired: async (binding) => {
|
|
6569
|
+
await createAgentToolApproval(approvalStoreBinding(binding));
|
|
6570
|
+
},
|
|
6571
|
+
consumeApproval: async (binding) => {
|
|
6572
|
+
if (!ownerEmail)
|
|
6573
|
+
return false;
|
|
6574
|
+
return consumeAgentToolApproval(approvalStoreBinding(binding));
|
|
6575
|
+
},
|
|
6576
|
+
};
|
|
6577
|
+
const exactApprovedToolEntry = exactApprovedToolCall
|
|
6578
|
+
? requestActions[exactApprovedToolCall.name]
|
|
6579
|
+
: undefined;
|
|
6580
|
+
const approvedToolCallsForExecution = exactApprovedToolCall && exactApprovedToolEntry?.needsApproval
|
|
6581
|
+
? [
|
|
6582
|
+
toolCallCacheKey(exactApprovedToolCall.name, exactApprovedToolCall.input),
|
|
6583
|
+
]
|
|
6584
|
+
: undefined;
|
|
6489
6585
|
if (isBackgroundWorker &&
|
|
6490
6586
|
(await isTurnAborted(effectiveThreadId, effectiveTurnId))) {
|
|
6491
6587
|
await markRunAborted(runId, "user").catch(() => { });
|
|
@@ -7236,15 +7332,13 @@ export function createProductionAgentHandler(options) {
|
|
|
7236
7332
|
? { threadId: effectiveThreadId, turnId: effectiveTurnId }
|
|
7237
7333
|
: {}),
|
|
7238
7334
|
// Human-in-the-loop approval grants for this turn (sanitized — the
|
|
7239
|
-
// request is untrusted;
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
? {
|
|
7243
|
-
approvedToolCalls: body.approvedToolCalls
|
|
7244
|
-
.filter((k) => typeof k === "string")
|
|
7245
|
-
.slice(0, 200),
|
|
7246
|
-
}
|
|
7335
|
+
// request is untrusted; only the exact structured call is passed to
|
|
7336
|
+
// the loop, where the durable grant is consumed atomically.
|
|
7337
|
+
...(approvedToolCallsForExecution
|
|
7338
|
+
? { approvedToolCalls: approvedToolCallsForExecution }
|
|
7247
7339
|
: {}),
|
|
7340
|
+
onApprovalRequired: approvalHooks.onApprovalRequired,
|
|
7341
|
+
consumeApproval: approvalHooks.consumeApproval,
|
|
7248
7342
|
// A worker PROVEN to be running inside the real 15-min Netlify
|
|
7249
7343
|
// `-background` function (`runsInBackgroundFunction`) has minutes of
|
|
7250
7344
|
// budget left on THIS invocation. Let it catch a recoverable
|
|
@@ -7269,6 +7363,31 @@ export function createProductionAgentHandler(options) {
|
|
|
7269
7363
|
send({ type: "activity", label: "Contacting model" });
|
|
7270
7364
|
let instrumented = false;
|
|
7271
7365
|
try {
|
|
7366
|
+
if (exactApprovedToolCall &&
|
|
7367
|
+
approvedToolCallsForExecution &&
|
|
7368
|
+
requestActions[exactApprovedToolCall.name]) {
|
|
7369
|
+
send({
|
|
7370
|
+
type: "activity",
|
|
7371
|
+
label: `Running approved ${exactApprovedToolCall.name} action`,
|
|
7372
|
+
tool: exactApprovedToolCall.name,
|
|
7373
|
+
});
|
|
7374
|
+
await executeAgentToolCall({
|
|
7375
|
+
actions: requestActions,
|
|
7376
|
+
name: exactApprovedToolCall.name,
|
|
7377
|
+
input: exactApprovedToolCall.input,
|
|
7378
|
+
callId: exactApprovedToolCall.callId,
|
|
7379
|
+
signal: agentLoopOpts.signal,
|
|
7380
|
+
ownerEmail,
|
|
7381
|
+
orgId: getRequestOrgId() ?? null,
|
|
7382
|
+
threadId: effectiveThreadId,
|
|
7383
|
+
turnId: effectiveTurnId,
|
|
7384
|
+
approvedToolCalls: approvedToolCallsForExecution,
|
|
7385
|
+
onApprovalRequired: approvalHooks.onApprovalRequired,
|
|
7386
|
+
consumeApproval: approvalHooks.consumeApproval,
|
|
7387
|
+
send,
|
|
7388
|
+
});
|
|
7389
|
+
return;
|
|
7390
|
+
}
|
|
7272
7391
|
try {
|
|
7273
7392
|
const { getObservabilityConfig, instrumentAgentLoop } = await import("../observability/traces.js");
|
|
7274
7393
|
const obsConfig = await getObservabilityConfig();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface AgentToolApprovalBinding {
|
|
2
|
+
ownerEmail: string;
|
|
3
|
+
orgId?: string | null;
|
|
4
|
+
threadId?: string | null;
|
|
5
|
+
turnId?: string | null;
|
|
6
|
+
toolName: string;
|
|
7
|
+
callId: string;
|
|
8
|
+
approvalKey: string;
|
|
9
|
+
expiresAt?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function hashAgentToolApprovalKey(approvalKey: string): string;
|
|
12
|
+
export declare function createAgentToolApproval(binding: AgentToolApprovalBinding): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Atomically consume the server-created approval for one exact tool call.
|
|
15
|
+
* Client history and approval keys are only lookup input; the pending row is
|
|
16
|
+
* the authorization boundary and cannot be manufactured by the client.
|
|
17
|
+
*/
|
|
18
|
+
export declare function consumeAgentToolApproval(binding: AgentToolApprovalBinding): Promise<boolean>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { getDbExec, intType, isPostgres, retryOnDdlRace, } from "../db/client.js";
|
|
3
|
+
import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
|
|
4
|
+
const APPROVAL_TTL_MS = 15 * 60_000;
|
|
5
|
+
const APPROVAL_CLEANUP_AGE_MS = 24 * 60 * 60_000;
|
|
6
|
+
let initPromise;
|
|
7
|
+
async function ensureAgentToolApprovalTable() {
|
|
8
|
+
if (!initPromise) {
|
|
9
|
+
initPromise = (async () => {
|
|
10
|
+
const createSql = `CREATE TABLE IF NOT EXISTS agent_tool_approvals (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
owner_email TEXT NOT NULL,
|
|
13
|
+
org_id TEXT,
|
|
14
|
+
thread_id TEXT,
|
|
15
|
+
turn_id TEXT,
|
|
16
|
+
tool_name TEXT NOT NULL,
|
|
17
|
+
call_id TEXT NOT NULL,
|
|
18
|
+
approval_key_hash TEXT NOT NULL,
|
|
19
|
+
status TEXT NOT NULL,
|
|
20
|
+
expires_at ${intType()} NOT NULL,
|
|
21
|
+
consumed_at ${intType()},
|
|
22
|
+
created_at ${intType()} NOT NULL,
|
|
23
|
+
updated_at ${intType()} NOT NULL
|
|
24
|
+
)`;
|
|
25
|
+
const indexSql = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_binding
|
|
26
|
+
ON agent_tool_approvals(owner_email, org_id, thread_id, tool_name, call_id, status)`;
|
|
27
|
+
if (isPostgres()) {
|
|
28
|
+
await ensureTableExists("agent_tool_approvals", createSql);
|
|
29
|
+
await ensureIndexExists("idx_agent_tool_approvals_binding", indexSql);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const client = getDbExec();
|
|
33
|
+
await retryOnDdlRace(() => client.execute(createSql));
|
|
34
|
+
await retryOnDdlRace(() => client.execute(indexSql));
|
|
35
|
+
})().catch((error) => {
|
|
36
|
+
initPromise = undefined;
|
|
37
|
+
throw error;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return initPromise;
|
|
41
|
+
}
|
|
42
|
+
export function hashAgentToolApprovalKey(approvalKey) {
|
|
43
|
+
return createHash("sha256").update(approvalKey).digest("hex");
|
|
44
|
+
}
|
|
45
|
+
export async function createAgentToolApproval(binding) {
|
|
46
|
+
await ensureAgentToolApprovalTable();
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const expiresAt = binding.expiresAt ?? now + APPROVAL_TTL_MS;
|
|
49
|
+
await getDbExec().execute({
|
|
50
|
+
sql: `INSERT INTO agent_tool_approvals
|
|
51
|
+
(id, owner_email, org_id, thread_id, turn_id, tool_name, call_id,
|
|
52
|
+
approval_key_hash, status, expires_at, consumed_at, created_at, updated_at)
|
|
53
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, NULL, ?, ?)`,
|
|
54
|
+
args: [
|
|
55
|
+
`agent-tool-approval-${randomUUID()}`,
|
|
56
|
+
binding.ownerEmail,
|
|
57
|
+
binding.orgId ?? null,
|
|
58
|
+
binding.threadId ?? null,
|
|
59
|
+
binding.turnId ?? null,
|
|
60
|
+
binding.toolName,
|
|
61
|
+
binding.callId,
|
|
62
|
+
hashAgentToolApprovalKey(binding.approvalKey),
|
|
63
|
+
expiresAt,
|
|
64
|
+
now,
|
|
65
|
+
now,
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
// Cleanup is hygiene only. Expired rows remain unusable because consume uses
|
|
69
|
+
// an atomic status and expiry predicate, so cleanup failures must not affect
|
|
70
|
+
// the approval that was just recorded.
|
|
71
|
+
try {
|
|
72
|
+
await getDbExec().execute({
|
|
73
|
+
sql: `DELETE FROM agent_tool_approvals
|
|
74
|
+
WHERE expires_at < ? AND status <> 'pending'`,
|
|
75
|
+
args: [now - APPROVAL_CLEANUP_AGE_MS],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.warn("[agent] Could not clean expired tool approval records", error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Atomically consume the server-created approval for one exact tool call.
|
|
84
|
+
* Client history and approval keys are only lookup input; the pending row is
|
|
85
|
+
* the authorization boundary and cannot be manufactured by the client.
|
|
86
|
+
*/
|
|
87
|
+
export async function consumeAgentToolApproval(binding) {
|
|
88
|
+
await ensureAgentToolApprovalTable();
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const result = await getDbExec().execute({
|
|
91
|
+
sql: `UPDATE agent_tool_approvals
|
|
92
|
+
SET status = 'consumed', consumed_at = ?, updated_at = ?
|
|
93
|
+
WHERE id = (
|
|
94
|
+
SELECT id FROM agent_tool_approvals
|
|
95
|
+
WHERE owner_email = ?
|
|
96
|
+
AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
|
|
97
|
+
AND ((thread_id IS NULL AND CAST(? AS TEXT) IS NULL) OR thread_id = ?)
|
|
98
|
+
AND tool_name = ?
|
|
99
|
+
AND call_id = ?
|
|
100
|
+
AND approval_key_hash = ?
|
|
101
|
+
AND status = 'pending'
|
|
102
|
+
AND expires_at > ?
|
|
103
|
+
ORDER BY created_at DESC
|
|
104
|
+
LIMIT 1
|
|
105
|
+
)
|
|
106
|
+
AND status = 'pending'`,
|
|
107
|
+
args: [
|
|
108
|
+
now,
|
|
109
|
+
now,
|
|
110
|
+
binding.ownerEmail,
|
|
111
|
+
binding.orgId ?? null,
|
|
112
|
+
binding.orgId ?? null,
|
|
113
|
+
binding.threadId ?? null,
|
|
114
|
+
binding.threadId ?? null,
|
|
115
|
+
binding.toolName,
|
|
116
|
+
binding.callId,
|
|
117
|
+
hashAgentToolApprovalKey(binding.approvalKey),
|
|
118
|
+
now,
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
return result.rowsAffected === 1;
|
|
122
|
+
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -189,7 +189,7 @@ export interface AgentChatRequest {
|
|
|
189
189
|
model?: string;
|
|
190
190
|
/** Per-request engine override (sent alongside model for cross-provider switches). */
|
|
191
191
|
engine?: string;
|
|
192
|
-
/** Per-request
|
|
192
|
+
/** Per-request effort override (ephemeral, from the composer picker). */
|
|
193
193
|
effort?: ReasoningEffort;
|
|
194
194
|
/** Usage-tracking label for this call (e.g. "chat", "summarize"). Default: "chat". */
|
|
195
195
|
usageLabel?: string;
|
|
@@ -471,7 +471,14 @@ async function executeClaudeCliRun(options) {
|
|
|
471
471
|
});
|
|
472
472
|
try {
|
|
473
473
|
const result = await runClaudeCodeParticipant({
|
|
474
|
-
|
|
474
|
+
// Claude's driver uses acceptEdits, which is the closest available
|
|
475
|
+
// mapping for auto-edit and full-auto. Keep ask-before-edit on the
|
|
476
|
+
// read-only watchdog path because this non-interactive runner has no
|
|
477
|
+
// approval channel to honor an edit prompt safely.
|
|
478
|
+
role: options.permissionMode === "auto-edit" ||
|
|
479
|
+
options.permissionMode === "full-auto"
|
|
480
|
+
? "driver"
|
|
481
|
+
: "watchdog",
|
|
475
482
|
prompt: buildClaudeCliPrompt(options.run, options.prompt),
|
|
476
483
|
cwd,
|
|
477
484
|
model,
|
|
@@ -604,7 +611,9 @@ async function executeClaudeCliRun(options) {
|
|
|
604
611
|
}
|
|
605
612
|
function buildClaudeCliPrompt(run, prompt) {
|
|
606
613
|
const permissionMode = run.permissionMode ?? "full-auto";
|
|
607
|
-
const mode = permissionMode === "
|
|
614
|
+
const mode = permissionMode === "auto-edit" || permissionMode === "full-auto"
|
|
615
|
+
? "Auto"
|
|
616
|
+
: "Plan";
|
|
608
617
|
const modeInstruction = mode === "Plan"
|
|
609
618
|
? "Inspect and explain only. Do not edit files or run mutating commands."
|
|
610
619
|
: "Edit and verify as needed. Do not create, switch, reset, rebase, or stash git branches.";
|