@agent-native/core 0.153.2 → 0.153.5
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.md +4 -0
- package/corpus/templates/analytics/actions/account-deep-dive.ts +1 -0
- package/corpus/templates/analytics/actions/bigquery-query.ts +1 -0
- package/corpus/templates/analytics/actions/bigquery.ts +1 -0
- package/corpus/templates/analytics/actions/content-calendar-schema.ts +1 -0
- package/corpus/templates/analytics/actions/content-calendar.ts +1 -0
- package/corpus/templates/analytics/actions/gcloud.ts +1 -0
- package/corpus/templates/analytics/actions/get-data-program.ts +1 -0
- package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
- package/corpus/templates/analytics/actions/get-monitor-stats.ts +1 -0
- package/corpus/templates/analytics/actions/get-monitor.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-events.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +1 -0
- package/corpus/templates/analytics/actions/gong-calls.ts +1 -0
- package/corpus/templates/analytics/actions/gong-native-insights.ts +1 -0
- package/corpus/templates/analytics/actions/grafana.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-deals.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-metrics.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-pipelines.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-records.ts +1 -0
- package/corpus/templates/analytics/actions/jira-search.ts +1 -0
- package/corpus/templates/analytics/actions/jira.ts +1 -0
- package/corpus/templates/analytics/actions/list-error-issues.ts +1 -0
- package/corpus/templates/analytics/actions/list-monitors.ts +1 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
- package/corpus/templates/analytics/actions/match-error-issues.ts +1 -0
- package/corpus/templates/analytics/actions/prometheus.ts +1 -0
- package/corpus/templates/analytics/actions/pylon-issues.ts +1 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -0
- package/corpus/templates/analytics/actions/query-dashboard-panel.ts +1 -0
- package/corpus/templates/analytics/actions/query-inbound-forms.ts +1 -0
- package/corpus/templates/analytics/actions/run-monitor-check.ts +1 -0
- package/corpus/templates/analytics/actions/sentry.ts +1 -0
- package/corpus/templates/analytics/actions/seo-blog-pages.ts +1 -0
- package/corpus/templates/analytics/actions/seo-page-keywords.ts +1 -0
- package/corpus/templates/analytics/actions/seo-top-keywords.ts +1 -0
- package/corpus/templates/analytics/actions/slack-messages.ts +1 -0
- package/corpus/templates/analytics/actions/stripe.ts +1 -0
- package/corpus/templates/analytics/actions/test-custom-api-connection.ts +1 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +38 -47
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +41 -34
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +9 -6
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +70 -10
- package/corpus/templates/forms/AGENTS.md +6 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/mail/AGENTS.md +5 -4
- package/corpus/templates/mail/actions/get-automation-settings.ts +17 -6
- package/corpus/templates/mail/actions/send-email.ts +17 -9
- package/corpus/templates/mail/actions/send-queued-drafts.ts +21 -11
- package/corpus/templates/mail/actions/update-automation-settings.ts +20 -6
- package/corpus/templates/mail/app/i18n/en-US.ts +5 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +68 -7
- package/corpus/templates/mail/server/lib/automation-settings.ts +27 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +3 -3
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +17 -4
- package/corpus/templates/slides/app/context/DeckContext.tsx +3 -2
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -0
- package/corpus/templates/slides/changelog/2026-08-12-arrow-key-navigation-in-the-slide-film-strip-now-scrolls-sli.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-12-duplicate-a-slide-from-the-film-strip-with-cmd-ctrl-c-then-c.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-12-presenting-a-deck-now-returns-you-to-the-slide-you-were-view.md +6 -0
- package/dist/action.d.ts +13 -0
- package/dist/action.js +3 -0
- package/dist/agent/engine/tool-call-journal-seed.d.ts +27 -10
- package/dist/agent/engine/tool-call-journal-seed.js +46 -32
- package/dist/agent/production-agent.d.ts +33 -0
- package/dist/agent/production-agent.js +172 -57
- package/dist/agent/run-loop-with-resume.d.ts +1 -1
- package/dist/agent/run-loop-with-resume.js +49 -35
- package/dist/client/agent-chat-adapter.js +204 -273
- package/dist/client/resources/mcp-integration-catalog.d.ts +7 -0
- package/dist/client/resources/mcp-integration-catalog.js +14 -1
- package/dist/client/settings/AgentsSection.js +46 -30
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +21 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/github-repo-files.js +1 -0
- package/dist/provider-api/actions/provider-api.js +1 -0
- package/dist/provider-api/actions/staged-datasets.js +1 -0
- package/dist/provider-api/corpus-jobs.js +1 -0
- package/dist/server/action-discovery.js +2 -0
- package/dist/server/agent-chat-plugin.js +1 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
|
@@ -2439,6 +2439,11 @@ async function waitForInterruptedToolLedgerEntry(opts) {
|
|
|
2439
2439
|
* Dropping the echoed input costs nothing: the fault is already named by the
|
|
2440
2440
|
* sentence before it, and two failures that differ ONLY in the arguments they
|
|
2441
2441
|
* echo are the same failure.
|
|
2442
|
+
*
|
|
2443
|
+
* Numbers go the same way, for the same reason. A guard that reports its own
|
|
2444
|
+
* running tally ("this turn already made 13 call(s)") hands the breaker a new
|
|
2445
|
+
* key on every single decline, so the decline that exists to stop a spiral is
|
|
2446
|
+
* itself uncountable.
|
|
2442
2447
|
*/
|
|
2443
2448
|
function normalizeToolErrorForBreaker(error) {
|
|
2444
2449
|
return (error
|
|
@@ -2446,6 +2451,7 @@ function normalizeToolErrorForBreaker(error) {
|
|
|
2446
2451
|
.replace(/Received:\s*[\s\S]*?\.\s(?=Expected:|The tool was not executed)/g, "")
|
|
2447
2452
|
// Bare JSON payloads some providers inline instead of a Received: span.
|
|
2448
2453
|
.replace(/\{[\s\S]{0,2000}?\}/g, "{}")
|
|
2454
|
+
.replace(/\d+/g, "#")
|
|
2449
2455
|
.replace(/\s+/g, " ")
|
|
2450
2456
|
.trim());
|
|
2451
2457
|
}
|
|
@@ -2455,6 +2461,50 @@ function rateLimitRecoveryHint(message) {
|
|
|
2455
2461
|
}
|
|
2456
2462
|
return "\n\nProvider rate-limit guidance: stop retrying this provider in this turn. Report the rate limit as a coverage gap, include any evidence already gathered, and ask the user to retry after the provider quota resets if full coverage is required.";
|
|
2457
2463
|
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Tool errors the model has no way to clear: the missing thing lives outside
|
|
2466
|
+
* the turn (a credential, a role grant, a connected account, a runtime, the
|
|
2467
|
+
* user's own approval). Every retry costs a full round-trip carrying the whole
|
|
2468
|
+
* transcript and lands on the identical error, so these stop on the FIRST
|
|
2469
|
+
* occurrence rather than after `MAX_SAME_ERROR_ACROSS_ARGUMENTS` of them.
|
|
2470
|
+
*
|
|
2471
|
+
* The distinction is the one `stopForBigQueryNotConfigured` draws in
|
|
2472
|
+
* templates/analytics: a *configuration* gap is permanent for this turn, a
|
|
2473
|
+
* *query* failure is not. Anything the model can act on — a stale read to
|
|
2474
|
+
* rebase, a bad argument, a full quota it can free, a wrong dataset name, a
|
|
2475
|
+
* statement timeout — must stay out of this list and keep its retries.
|
|
2476
|
+
* Precision over recall: a wrong match kills a turn that would have succeeded,
|
|
2477
|
+
* which is worse than one more retry.
|
|
2478
|
+
*/
|
|
2479
|
+
export function permanentPreconditionRemedy(message) {
|
|
2480
|
+
const trimmed = message.replace(/\s+/g, " ").trim();
|
|
2481
|
+
for (const pattern of PERMANENT_PRECONDITION_PATTERNS) {
|
|
2482
|
+
if (pattern.test(trimmed))
|
|
2483
|
+
return trimmed;
|
|
2484
|
+
}
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
const PERMANENT_PRECONDITION_PATTERNS = [
|
|
2488
|
+
// "Requires editor role on deck ZJshjrXhjx (have viewer)". The trailing space
|
|
2489
|
+
// is load-bearing: it keeps "must have required property" out.
|
|
2490
|
+
/\brequires? (?:an? |the )?[\w-]+ role\b/i,
|
|
2491
|
+
// "Gemini API key not configured. Save GEMINI_API_KEY in settings." Only the
|
|
2492
|
+
// not-configured/not-set wordings — "not found" is how providers report a
|
|
2493
|
+
// missing dataset or record, which the model can and should correct.
|
|
2494
|
+
/\b(?:api[ -]?keys?|access tokens?|credentials?|secrets?)\b[^.]{0,60}\bnot (?:configured|set|connected|available)\b/i,
|
|
2495
|
+
/\bsave [A-Z][A-Z0-9_]{3,} in (?:the )?settings\b/i,
|
|
2496
|
+
// "Connect Builder.io before indexing a design system from Figma or code."
|
|
2497
|
+
/\bconnect\b[^;]{1,40}?\b(?:before|first|in settings)\b/i,
|
|
2498
|
+
// "…is only available in local Plan runtime." Excludes "only available after
|
|
2499
|
+
// …", which names a condition that clears on its own.
|
|
2500
|
+
/\bonly available (?:in|on|from|to)\b/i,
|
|
2501
|
+
// "Plan mode blocked `update-extension`. Switch to Act mode …" — cleared by
|
|
2502
|
+
// the user approving the plan, never by the model trying again.
|
|
2503
|
+
/\bplan mode blocked\b/i,
|
|
2504
|
+
/\bno authenticated user\b/i,
|
|
2505
|
+
// "SSRF blocked: refusing to fetch private/internal address (…)"
|
|
2506
|
+
/\bssrf blocked\b/i,
|
|
2507
|
+
];
|
|
2458
2508
|
const SOURCE_SWEEP_TOOL_NAME = /\b(?:api|calls?|deals?|docs?|events?|issues?|messages?|metrics?|provider|query|records?|request|search|source|tickets?|transcripts?)\b/i;
|
|
2459
2509
|
const SOURCE_SWEEP_PROVIDER_TOKEN = /\b(?:amplitude|apollo|bigquery|commonroom|data-source|ga4|github|gong|grafana|hubspot|jira|mixpanel|notion|posthog|postgres|postgresql|pylon|sentry|slack|stripe)\b/i;
|
|
2460
2510
|
const SOURCE_SWEEP_EXCLUDED_TOOLS = new Set([
|
|
@@ -3095,29 +3145,50 @@ export async function runAgentLoop(opts) {
|
|
|
3095
3145
|
// calls/results are folded into `toolCallHistory` / `toolResultHistory` so
|
|
3096
3146
|
// final response guards see evidence from earlier chunks of this turn.
|
|
3097
3147
|
const consumedJournalKeys = new Set();
|
|
3098
|
-
const
|
|
3148
|
+
const journalRead = await loadPriorTurnToolCallJournal(opts.threadId, opts.turnId);
|
|
3149
|
+
const toolCallJournal = journalRead.status === "read" ? journalRead.toolCallJournal : null;
|
|
3150
|
+
const journaledPriorToolCalls = journalRead.status === "read" ? journalRead.priorToolCalls : [];
|
|
3151
|
+
const journaledPriorToolResults = journalRead.status === "read" ? journalRead.priorToolResults : [];
|
|
3099
3152
|
toolCallHistory.push(...journaledPriorToolCalls);
|
|
3100
3153
|
toolResultHistory.push(...journaledPriorToolResults);
|
|
3154
|
+
// Every per-turn ceiling below is seeded from that read, and the write-replay
|
|
3155
|
+
// hard block is enforced from it. On a continuation we cannot tell an
|
|
3156
|
+
// already-completed side effect from a fresh one without it, so a failed read
|
|
3157
|
+
// stops the turn instead of quietly resuming with a blank ledger.
|
|
3158
|
+
const unreadableJournalStop = journalRead.status === "unreadable" && isInternalContinuationTurn(messages)
|
|
3159
|
+
? {
|
|
3160
|
+
message: "I stopped because I could not read this turn's run ledger, so I could not tell which steps had already finished. " +
|
|
3161
|
+
"Retrying would risk repeating a step that already completed. Please send the request again.",
|
|
3162
|
+
errorCode: "tool_call_journal_unreadable",
|
|
3163
|
+
details: journalRead.error,
|
|
3164
|
+
}
|
|
3165
|
+
: null;
|
|
3101
3166
|
const readOnlyToolResultCache = seedReadOnlyToolResultsFromHistory(messages, actions, seedReadOnlyToolResultsFromJournal(toolCallJournal, actions));
|
|
3102
3167
|
const duplicateReadOnlyToolCalls = seedDuplicateReadOnlyToolCallsFromHistory(messages, actions);
|
|
3103
3168
|
const writeToolInterruptions = seedWriteToolInterruptionsFromHistory(messages, actions);
|
|
3169
|
+
// All three repetition ledgers are SEEDED from earlier chunks of this turn,
|
|
3170
|
+
// like `toolCallHistory` and `toolResultHistory` above. A fresh Map makes the
|
|
3171
|
+
// limit per-CHUNK rather than per-turn, and a turn may chain up to
|
|
3172
|
+
// MAX_RUN_LOOP_CONTINUATIONS (6) or MAX_BACKGROUND_RUN_LOOP_CONTINUATIONS
|
|
3173
|
+
// (20) of them — so the real ceiling becomes 20x the constant, and a spiral
|
|
3174
|
+
// resumes with a clean slate at every chunk boundary.
|
|
3104
3175
|
const repeatedToolErrors = new Map();
|
|
3105
|
-
|
|
3106
|
-
//
|
|
3107
|
-
// SEEDED from earlier chunks of this turn, like `toolCallHistory` and
|
|
3108
|
-
// `toolResultHistory` above. A fresh Map here would make the limit per-CHUNK
|
|
3109
|
-
// rather than per-turn, and a turn may chain up to MAX_RUN_LOOP_CONTINUATIONS
|
|
3110
|
-
// (6) or MAX_BACKGROUND_RUN_LOOP_CONTINUATIONS (20) of them — so the real
|
|
3111
|
-
// ceiling would be 36 or 120 identical failures, not 6, and a spiral would
|
|
3112
|
-
// resume with a clean slate at every chunk boundary.
|
|
3176
|
+
/** Keyed WITHOUT the arguments — see MAX_SAME_ERROR_ACROSS_ARGUMENTS. */
|
|
3113
3177
|
const repeatedToolErrorsAnyArgs = new Map();
|
|
3114
|
-
|
|
3178
|
+
const repeatedToolCalls = new Map();
|
|
3179
|
+
for (const prior of journaledPriorToolCalls) {
|
|
3180
|
+
const key = toolCallCacheKey(prior.name, prior.input);
|
|
3181
|
+
repeatedToolCalls.set(key, (repeatedToolCalls.get(key) ?? 0) + 1);
|
|
3182
|
+
}
|
|
3183
|
+
for (const prior of journaledPriorToolResults) {
|
|
3115
3184
|
if (!prior.isError)
|
|
3116
3185
|
continue;
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3186
|
+
const normalized = normalizeToolErrorForBreaker(prior.content);
|
|
3187
|
+
const anyArgsKey = `${prior.name}:${normalized}`;
|
|
3188
|
+
repeatedToolErrorsAnyArgs.set(anyArgsKey, (repeatedToolErrorsAnyArgs.get(anyArgsKey) ?? 0) + 1);
|
|
3189
|
+
const errorKey = `${toolCallCacheKey(prior.name, prior.input)}:${normalized}`;
|
|
3190
|
+
repeatedToolErrors.set(errorKey, (repeatedToolErrors.get(errorKey) ?? 0) + 1);
|
|
3119
3191
|
}
|
|
3120
|
-
const repeatedToolCalls = new Map();
|
|
3121
3192
|
let finalGuardRetries = 0;
|
|
3122
3193
|
let emptyFinalResponseRetries = 0;
|
|
3123
3194
|
let iterations = 0;
|
|
@@ -3125,6 +3196,31 @@ export async function runAgentLoop(opts) {
|
|
|
3125
3196
|
// trailing `done` would overwrite the boundary the continuation logic reads.
|
|
3126
3197
|
let endedAtLoopLimit = false;
|
|
3127
3198
|
let terminalActionStop = null;
|
|
3199
|
+
/**
|
|
3200
|
+
* A stop is either a hand-off to the user (the two `input_required` codes) or
|
|
3201
|
+
* a failed run. Failed runs MUST leave a terminal `error` event behind: the
|
|
3202
|
+
* run row's status, `error_code`, `listErroredRuns`, the outcome counters and
|
|
3203
|
+
* the client's retry affordance all key off that event, and a stop that only
|
|
3204
|
+
* streams text is recorded as `status='completed', terminal_reason='done'` —
|
|
3205
|
+
* a guard trip counted as a success.
|
|
3206
|
+
*/
|
|
3207
|
+
const sendTerminalActionStop = (stop) => {
|
|
3208
|
+
if (stop.errorCode === "needs-approval" ||
|
|
3209
|
+
stop.errorCode === "awaiting-user-input") {
|
|
3210
|
+
send({ type: "text", text: stop.message });
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
// Sent ONLY as an error: both the live client and the thread rebuild render
|
|
3214
|
+
// an error event's message as assistant text themselves, so also sending it
|
|
3215
|
+
// as `text` prints the same sentence twice.
|
|
3216
|
+
send({
|
|
3217
|
+
type: "error",
|
|
3218
|
+
error: stop.message,
|
|
3219
|
+
errorCode: stop.errorCode ?? "tool_failed",
|
|
3220
|
+
...(stop.details ? { details: stop.details } : {}),
|
|
3221
|
+
recoverable: false,
|
|
3222
|
+
});
|
|
3223
|
+
};
|
|
3128
3224
|
// Overridden (raised tokens, lowered effort) only after an empty-final-
|
|
3129
3225
|
// response retry below — kept separate from `opts.maxOutputTokens`/
|
|
3130
3226
|
// `opts.reasoningEffort` so the very first attempt is unaffected and later
|
|
@@ -3151,6 +3247,11 @@ export async function runAgentLoop(opts) {
|
|
|
3151
3247
|
while (true) {
|
|
3152
3248
|
if (signal.aborted)
|
|
3153
3249
|
break;
|
|
3250
|
+
if (unreadableJournalStop) {
|
|
3251
|
+
terminalActionStop = unreadableJournalStop;
|
|
3252
|
+
sendTerminalActionStop(unreadableJournalStop);
|
|
3253
|
+
break;
|
|
3254
|
+
}
|
|
3154
3255
|
const turnInputTokens = priorTurnInputTokens + usage.inputTokens;
|
|
3155
3256
|
if (turnInputTokens > maxRunInputTokens) {
|
|
3156
3257
|
// Terminal for the whole TURN, not a chunk boundary: emitting
|
|
@@ -3882,9 +3983,6 @@ export async function runAgentLoop(opts) {
|
|
|
3882
3983
|
// call run. The model cannot populate this — it comes from the request.
|
|
3883
3984
|
const approvedToolCallKeys = new Set(opts.approvedToolCalls ?? []);
|
|
3884
3985
|
const runToolCall = async (toolCall) => {
|
|
3885
|
-
// Counted before any journal/cache short-circuit: serving a repeat from
|
|
3886
|
-
// cache still means the model is asking the same question again.
|
|
3887
|
-
noteRepeatedToolCall(toolCall.name, toolCall.input);
|
|
3888
3986
|
const actionEntry = actions[toolCall.name];
|
|
3889
3987
|
const placeholderNormalization = actionEntry
|
|
3890
3988
|
? normalizeOptionalToolPlaceholders(actionEntry.tool.parameters, toolCall.input)
|
|
@@ -3898,6 +3996,11 @@ export async function runAgentLoop(opts) {
|
|
|
3898
3996
|
if (jsonStringCoercion.changed) {
|
|
3899
3997
|
toolCall = { ...toolCall, input: jsonStringCoercion.input };
|
|
3900
3998
|
}
|
|
3999
|
+
// Count after the same normalization that is persisted in the journal:
|
|
4000
|
+
// a model can send a JSON-encoded object/array, while the action and
|
|
4001
|
+
// ledger both see the coerced value. Serving a repeat from cache still
|
|
4002
|
+
// counts as asking the same question again.
|
|
4003
|
+
noteRepeatedToolCall(toolCall.name, toolCall.input);
|
|
3901
4004
|
const toolInputNormalized = placeholderNormalization.changed || jsonStringCoercion.changed;
|
|
3902
4005
|
const wireToolInput = JSON.stringify(toolCall.input ?? {});
|
|
3903
4006
|
const normalizedToolInput = normalizeToolCallInputForHistory(toolCall.input);
|
|
@@ -3931,6 +4034,20 @@ export async function runAgentLoop(opts) {
|
|
|
3931
4034
|
};
|
|
3932
4035
|
const finalizeToolErrorResult = (rawResult) => {
|
|
3933
4036
|
const sanitizedResult = sanitizeToolErrorText(rawResult);
|
|
4037
|
+
// Counting is the wrong instrument for a precondition the turn cannot
|
|
4038
|
+
// satisfy: six identical round-trips through a missing API key cost the
|
|
4039
|
+
// user minutes and end where the first one did. Classified first so the
|
|
4040
|
+
// remedy reaches them on attempt one.
|
|
4041
|
+
const permanentRemedy = permanentPreconditionRemedy(sanitizedResult);
|
|
4042
|
+
if (permanentRemedy) {
|
|
4043
|
+
requestedActionStop ??= {
|
|
4044
|
+
message: `I stopped because ${toolCall.name} cannot run until this is fixed: ${permanentRemedy} Retrying would not have changed it, and anything completed before this is saved.`,
|
|
4045
|
+
errorCode: "permanent_precondition",
|
|
4046
|
+
details: sanitizedResult,
|
|
4047
|
+
};
|
|
4048
|
+
return (`Stopped: ${toolCall.name} cannot run until a setup step outside this turn is fixed. ` +
|
|
4049
|
+
`Do not retry it with different arguments. ${sanitizedResult}`);
|
|
4050
|
+
}
|
|
3934
4051
|
const errorKey = `${toolCallCacheKey(toolCall.name, toolCall.input)}:${normalizeToolErrorForBreaker(sanitizedResult)}`;
|
|
3935
4052
|
const count = (repeatedToolErrors.get(errorKey) ?? 0) + 1;
|
|
3936
4053
|
repeatedToolErrors.set(errorKey, count);
|
|
@@ -3946,9 +4063,9 @@ export async function runAgentLoop(opts) {
|
|
|
3946
4063
|
`with different arguments. Last error: ${sanitizedResult}`;
|
|
3947
4064
|
requestedActionStop ??= {
|
|
3948
4065
|
message: `I stopped because the ${toolCall.name} action rejected ${anyArgsCount} different attempts the same way, ` +
|
|
3949
|
-
"so changing the arguments again would not have worked. Anything completed before this is saved.
|
|
3950
|
-
`Error: ${sanitizedResult}`,
|
|
4066
|
+
"so changing the arguments again would not have worked. Anything completed before this is saved.",
|
|
3951
4067
|
errorCode: "repeated_tool_error_across_arguments",
|
|
4068
|
+
details: sanitizedResult,
|
|
3952
4069
|
};
|
|
3953
4070
|
return result;
|
|
3954
4071
|
}
|
|
@@ -3961,15 +4078,23 @@ export async function runAgentLoop(opts) {
|
|
|
3961
4078
|
// can do; never instruct them to "fix the arguments".
|
|
3962
4079
|
requestedActionStop ??= {
|
|
3963
4080
|
message: `I stopped because the ${toolCall.name} action failed ${count} times in a row the same way, ` +
|
|
3964
|
-
"so retrying it again would not have worked. Anything completed before this is saved.
|
|
3965
|
-
`Error: ${sanitizedResult}`,
|
|
4081
|
+
"so retrying it again would not have worked. Anything completed before this is saved.",
|
|
3966
4082
|
errorCode: "repeated_identical_tool_error",
|
|
4083
|
+
details: sanitizedResult,
|
|
3967
4084
|
};
|
|
3968
4085
|
return result;
|
|
3969
4086
|
};
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
4087
|
+
/**
|
|
4088
|
+
* A guard refused to run the tool. Recorded as an ERROR, not as a plain
|
|
4089
|
+
* result: a decline costs a full model round-trip carrying the whole
|
|
4090
|
+
* transcript, and `noteRepeatedToolCall` keys on name+args, so a model
|
|
4091
|
+
* iterating ids mints a fresh key every time and declines forever until
|
|
4092
|
+
* `maxIterations`. Routing through `finalizeToolErrorResult` gives the
|
|
4093
|
+
* existing breakers something to count — no new counter — while the text
|
|
4094
|
+
* the model reads is still the guard's own guidance.
|
|
4095
|
+
*/
|
|
4096
|
+
const declineToolCall = (guidance) => {
|
|
4097
|
+
const result = finalizeToolErrorResult(guidance);
|
|
3973
4098
|
send({
|
|
3974
4099
|
type: "tool_start",
|
|
3975
4100
|
id: toolCall.id,
|
|
@@ -3982,9 +4107,10 @@ export async function runAgentLoop(opts) {
|
|
|
3982
4107
|
tool: toolCall.name,
|
|
3983
4108
|
input: toolCall.input,
|
|
3984
4109
|
result,
|
|
4110
|
+
isError: true,
|
|
3985
4111
|
completedSideEffect: false,
|
|
3986
4112
|
});
|
|
3987
|
-
recordToolResult(result,
|
|
4113
|
+
recordToolResult(result, true);
|
|
3988
4114
|
return {
|
|
3989
4115
|
type: "tool-result",
|
|
3990
4116
|
toolCallId: toolCall.id,
|
|
@@ -3992,31 +4118,13 @@ export async function runAgentLoop(opts) {
|
|
|
3992
4118
|
toolInput: wireToolInput,
|
|
3993
4119
|
content: result,
|
|
3994
4120
|
};
|
|
4121
|
+
};
|
|
4122
|
+
if (sourceSweepGuard) {
|
|
4123
|
+
sourceSweepDelegationGuardActive = true;
|
|
4124
|
+
return declineToolCall(sourceSweepGuard.message);
|
|
3995
4125
|
}
|
|
3996
4126
|
if (sourceSweepDelegationGuard) {
|
|
3997
|
-
|
|
3998
|
-
send({
|
|
3999
|
-
type: "tool_start",
|
|
4000
|
-
id: toolCall.id,
|
|
4001
|
-
tool: toolCall.name,
|
|
4002
|
-
input: toolCall.input,
|
|
4003
|
-
});
|
|
4004
|
-
send({
|
|
4005
|
-
type: "tool_done",
|
|
4006
|
-
id: toolCall.id,
|
|
4007
|
-
tool: toolCall.name,
|
|
4008
|
-
input: toolCall.input,
|
|
4009
|
-
result,
|
|
4010
|
-
completedSideEffect: false,
|
|
4011
|
-
});
|
|
4012
|
-
recordToolResult(result, false);
|
|
4013
|
-
return {
|
|
4014
|
-
type: "tool-result",
|
|
4015
|
-
toolCallId: toolCall.id,
|
|
4016
|
-
toolName: toolCall.name,
|
|
4017
|
-
toolInput: wireToolInput,
|
|
4018
|
-
content: result,
|
|
4019
|
-
};
|
|
4127
|
+
return declineToolCall(sourceSweepDelegationGuard);
|
|
4020
4128
|
}
|
|
4021
4129
|
if (!actionEntry) {
|
|
4022
4130
|
const result = finalizeToolErrorResult(`Error: Unknown tool "${toolCall.name}"`);
|
|
@@ -4817,7 +4925,7 @@ export async function runAgentLoop(opts) {
|
|
|
4817
4925
|
// TypeScript can't track ??= through async closures; cast to known type.
|
|
4818
4926
|
const stop = requestedActionStop;
|
|
4819
4927
|
terminalActionStop = stop;
|
|
4820
|
-
|
|
4928
|
+
sendTerminalActionStop(stop);
|
|
4821
4929
|
break;
|
|
4822
4930
|
}
|
|
4823
4931
|
}
|
|
@@ -4893,6 +5001,10 @@ export async function runAgentLoop(opts) {
|
|
|
4893
5001
|
}
|
|
4894
5002
|
}
|
|
4895
5003
|
}
|
|
5004
|
+
// Assignments happen inside async tool-loop closures, which TypeScript cannot
|
|
5005
|
+
// use for narrowing at this point. Snapshot the runtime value before the
|
|
5006
|
+
// outcome branches so a stop is still represented as a possible value.
|
|
5007
|
+
const finalTerminalActionStop = terminalActionStop;
|
|
4896
5008
|
if (signal.aborted) {
|
|
4897
5009
|
reportOutcome({ state: "canceled", message: "Agent run was aborted." });
|
|
4898
5010
|
}
|
|
@@ -4904,26 +5016,26 @@ export async function runAgentLoop(opts) {
|
|
|
4904
5016
|
message: `Agent stopped after ${maxIterations} iterations.`,
|
|
4905
5017
|
});
|
|
4906
5018
|
}
|
|
4907
|
-
else if (
|
|
5019
|
+
else if (finalTerminalActionStop?.errorCode === "needs-approval") {
|
|
4908
5020
|
reportOutcome({
|
|
4909
5021
|
state: "input_required",
|
|
4910
5022
|
code: "needs_approval",
|
|
4911
|
-
message:
|
|
5023
|
+
message: finalTerminalActionStop.message,
|
|
4912
5024
|
});
|
|
4913
5025
|
}
|
|
4914
|
-
else if (
|
|
5026
|
+
else if (finalTerminalActionStop?.errorCode === "awaiting-user-input") {
|
|
4915
5027
|
reportOutcome({
|
|
4916
5028
|
state: "input_required",
|
|
4917
5029
|
code: "awaiting_user_input",
|
|
4918
|
-
message:
|
|
5030
|
+
message: finalTerminalActionStop.message,
|
|
4919
5031
|
});
|
|
4920
5032
|
}
|
|
4921
|
-
else if (
|
|
5033
|
+
else if (finalTerminalActionStop) {
|
|
4922
5034
|
reportOutcome({
|
|
4923
5035
|
state: "failed",
|
|
4924
|
-
code:
|
|
5036
|
+
code: finalTerminalActionStop.errorCode ?? "tool_failed",
|
|
4925
5037
|
retryable: false,
|
|
4926
|
-
message:
|
|
5038
|
+
message: finalTerminalActionStop.message,
|
|
4927
5039
|
});
|
|
4928
5040
|
}
|
|
4929
5041
|
else {
|
|
@@ -5383,8 +5495,11 @@ async function emitRunText(run, text) {
|
|
|
5383
5495
|
* per-turn journal the resume note uses (no new ledger read shape).
|
|
5384
5496
|
*/
|
|
5385
5497
|
async function describeTurnProgress(threadId, turnId) {
|
|
5386
|
-
const
|
|
5387
|
-
|
|
5498
|
+
const journalRead = await loadPriorTurnToolCallJournal(threadId, turnId);
|
|
5499
|
+
if (journalRead.status === "unreadable") {
|
|
5500
|
+
return "I could not read the run ledger, so I cannot say which steps completed.";
|
|
5501
|
+
}
|
|
5502
|
+
const completed = journalRead.toolCallJournal?.completed ?? [];
|
|
5388
5503
|
if (completed.length === 0)
|
|
5389
5504
|
return "No steps had completed yet.";
|
|
5390
5505
|
const names = [...new Set(completed.map((entry) => entry.tool))];
|
|
@@ -27,7 +27,7 @@ import type { ResolveRunSoftTimeoutOptions } from "./run-manager.js";
|
|
|
27
27
|
* Rebuild the same safe continuation context for a logical turn that resumes
|
|
28
28
|
* in a fresh hosted invocation.
|
|
29
29
|
*/
|
|
30
|
-
export declare function appendDurableContinuationContext(messages: EngineMessage[], reason: AgentLoopContinuationReason, threadId: string): Promise<void>;
|
|
30
|
+
export declare function appendDurableContinuationContext(messages: EngineMessage[], reason: AgentLoopContinuationReason, threadId: string, turnId?: string): Promise<void>;
|
|
31
31
|
/**
|
|
32
32
|
* Cap on continuation iterations inside a single
|
|
33
33
|
* `runAgentLoopDirectWithSoftTimeout` invocation. The host's hard function
|
|
@@ -24,23 +24,35 @@ import { runAgentLoop, appendAgentLoopContinuation, isResumableEngineError, isTr
|
|
|
24
24
|
import { resolveRunSoftTimeoutMs } from "./run-manager.js";
|
|
25
25
|
import { getCurrentTurnEventsForThread } from "./run-store.js";
|
|
26
26
|
import { classifyToolCallJournal, buildResumeJournalNote, } from "./tool-call-journal.js";
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* `persisted: false` means the durable half of the turn could not be read, so
|
|
29
|
+
* `events` holds only what this invocation saw. Callers that decide whether to
|
|
30
|
+
* DISCARD user-visible output must not read that as "nothing else happened".
|
|
31
|
+
*/
|
|
32
|
+
async function readCurrentTurnEventsForResume(threadId, turnId, localEvents = []) {
|
|
28
33
|
let persistedEvents = [];
|
|
34
|
+
let persisted = true;
|
|
29
35
|
try {
|
|
36
|
+
// Without the turnId this can return the PREVIOUS turn's events — see
|
|
37
|
+
// `getCurrentTurnEventsForThread`, whose run row is written without await.
|
|
30
38
|
persistedEvents = threadId
|
|
31
|
-
? await getCurrentTurnEventsForThread(threadId)
|
|
39
|
+
? await getCurrentTurnEventsForThread(threadId, turnId)
|
|
32
40
|
: [];
|
|
33
41
|
}
|
|
34
|
-
catch {
|
|
35
|
-
|
|
42
|
+
catch (err) {
|
|
43
|
+
persisted = false;
|
|
44
|
+
console.warn("[run-loop] current-turn ledger read failed:", err instanceof Error ? err.message : String(err));
|
|
36
45
|
}
|
|
37
46
|
if (localEvents.length === 0)
|
|
38
|
-
return persistedEvents;
|
|
47
|
+
return { events: persistedEvents, persisted };
|
|
39
48
|
const seen = new Set(persistedEvents.map((event) => JSON.stringify(event)));
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
return {
|
|
50
|
+
events: [
|
|
51
|
+
...persistedEvents,
|
|
52
|
+
...localEvents.filter((event) => !seen.has(JSON.stringify(event))),
|
|
53
|
+
],
|
|
54
|
+
persisted,
|
|
55
|
+
};
|
|
44
56
|
}
|
|
45
57
|
function actionPreparationContinuationOptions(events) {
|
|
46
58
|
const actionPreparationTool = lastUnfinishedPreparingActionToolFromEvents(events);
|
|
@@ -86,8 +98,8 @@ function appendToolCallJournalNote(messages, events) {
|
|
|
86
98
|
// up — the model still continues, just without the structured journal.
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
|
-
async function appendContinuationAndJournal(messages, reason, threadId, localEvents = []) {
|
|
90
|
-
const events = await readCurrentTurnEventsForResume(threadId, localEvents);
|
|
101
|
+
async function appendContinuationAndJournal(messages, reason, threadId, turnId, localEvents = []) {
|
|
102
|
+
const { events } = await readCurrentTurnEventsForResume(threadId, turnId, localEvents);
|
|
91
103
|
appendAgentLoopContinuation(messages, reason, actionPreparationContinuationOptions(events));
|
|
92
104
|
appendToolCallJournalNote(messages, events);
|
|
93
105
|
}
|
|
@@ -95,21 +107,23 @@ async function appendContinuationAndJournal(messages, reason, threadId, localEve
|
|
|
95
107
|
* Rebuild the same safe continuation context for a logical turn that resumes
|
|
96
108
|
* in a fresh hosted invocation.
|
|
97
109
|
*/
|
|
98
|
-
export async function appendDurableContinuationContext(messages, reason, threadId) {
|
|
99
|
-
await appendContinuationAndJournal(messages, reason, threadId);
|
|
110
|
+
export async function appendDurableContinuationContext(messages, reason, threadId, turnId) {
|
|
111
|
+
await appendContinuationAndJournal(messages, reason, threadId, turnId);
|
|
100
112
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
/**
|
|
114
|
+
* `"unknown"` is not `"none"`: the only caller uses this to decide whether to
|
|
115
|
+
* emit `clear`, and `clear` WIPES user-visible output. A transient ledger error
|
|
116
|
+
* must not delete the tool cards that are the user's only proof a side effect
|
|
117
|
+
* landed.
|
|
118
|
+
*/
|
|
119
|
+
async function completedSideEffectInCurrentTurn(threadId, turnId, localEvents = []) {
|
|
120
|
+
const { events, persisted } = await readCurrentTurnEventsForResume(threadId, turnId, localEvents);
|
|
121
|
+
const found = events.some((event) => event.type === "tool_done" &&
|
|
122
|
+
event.completedSideEffect === true &&
|
|
123
|
+
event.isError !== true);
|
|
124
|
+
if (found)
|
|
125
|
+
return "some";
|
|
126
|
+
return persisted ? "none" : "unknown";
|
|
113
127
|
}
|
|
114
128
|
function internalContinuationReasonForAttempt(events) {
|
|
115
129
|
const last = events.at(-1);
|
|
@@ -362,15 +376,15 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
362
376
|
if (internalContinuationReason && !upstreamSignal.aborted) {
|
|
363
377
|
lastAttemptWasUnfinishedContinuation = true;
|
|
364
378
|
const continuationEvents = [...localTurnEvents];
|
|
365
|
-
if (
|
|
379
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, continuationEvents)) === "none") {
|
|
366
380
|
opts.send({ type: "clear" });
|
|
367
381
|
}
|
|
368
|
-
await appendContinuationAndJournal(opts.messages, internalContinuationReason, opts.threadId, continuationEvents);
|
|
382
|
+
await appendContinuationAndJournal(opts.messages, internalContinuationReason, opts.threadId, opts.turnId, continuationEvents);
|
|
369
383
|
continue;
|
|
370
384
|
}
|
|
371
385
|
if (softTimedOut && !upstreamSignal.aborted) {
|
|
372
386
|
lastAttemptWasUnfinishedContinuation = true;
|
|
373
|
-
await appendContinuationAndJournal(opts.messages, "run_timeout", opts.threadId, localTurnEvents);
|
|
387
|
+
await appendContinuationAndJournal(opts.messages, "run_timeout", opts.threadId, opts.turnId, localTurnEvents);
|
|
374
388
|
continue;
|
|
375
389
|
}
|
|
376
390
|
reportFinalOutcome(upstreamSignal.aborted
|
|
@@ -383,10 +397,10 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
383
397
|
// Clear partial text the client received before the abort so the
|
|
384
398
|
// resumed model doesn't re-emit it and produce duplicated output.
|
|
385
399
|
lastAttemptWasUnfinishedContinuation = true;
|
|
386
|
-
if (
|
|
400
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
|
|
387
401
|
opts.send({ type: "clear" });
|
|
388
402
|
}
|
|
389
|
-
await appendContinuationAndJournal(opts.messages, "run_timeout", opts.threadId, localTurnEvents);
|
|
403
|
+
await appendContinuationAndJournal(opts.messages, "run_timeout", opts.threadId, opts.turnId, localTurnEvents);
|
|
390
404
|
continue;
|
|
391
405
|
}
|
|
392
406
|
const transientRateLimit = isTransientProviderRateLimitError(err);
|
|
@@ -402,10 +416,10 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
402
416
|
rateLimitRetryFitsBudget) {
|
|
403
417
|
lastAttemptWasUnfinishedContinuation = true;
|
|
404
418
|
backgroundRateLimitContinuations++;
|
|
405
|
-
if (
|
|
419
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
|
|
406
420
|
opts.send({ type: "clear" });
|
|
407
421
|
}
|
|
408
|
-
await appendContinuationAndJournal(opts.messages, "rate_limited", opts.threadId, localTurnEvents);
|
|
422
|
+
await appendContinuationAndJournal(opts.messages, "rate_limited", opts.threadId, opts.turnId, localTurnEvents);
|
|
409
423
|
await waitForBackgroundRateLimitCooldown(upstreamSignal);
|
|
410
424
|
continue;
|
|
411
425
|
}
|
|
@@ -424,10 +438,10 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
424
438
|
// the in-memory messages array, so the next attempt re-emits it).
|
|
425
439
|
if (!upstreamSignal.aborted && isResumableEngineError(err)) {
|
|
426
440
|
lastAttemptWasUnfinishedContinuation = true;
|
|
427
|
-
if (
|
|
441
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
|
|
428
442
|
opts.send({ type: "clear" });
|
|
429
443
|
}
|
|
430
|
-
await appendContinuationAndJournal(opts.messages, continuationReasonForResumableError(err), opts.threadId, localTurnEvents);
|
|
444
|
+
await appendContinuationAndJournal(opts.messages, continuationReasonForResumableError(err), opts.threadId, opts.turnId, localTurnEvents);
|
|
431
445
|
continue;
|
|
432
446
|
}
|
|
433
447
|
if (upstreamSignal.aborted) {
|
|
@@ -467,7 +481,7 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
|
|
|
467
481
|
// the terminal message stands alone instead of trailing a half sentence.
|
|
468
482
|
// Preserve completed tool cards: they are the user's only durable proof
|
|
469
483
|
// that a side effect landed before the final assistant note timed out.
|
|
470
|
-
if (
|
|
484
|
+
if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
|
|
471
485
|
opts.send({ type: "clear" });
|
|
472
486
|
}
|
|
473
487
|
opts.send({
|