@agent-native/core 0.153.4 → 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/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 +40 -34
- 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/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/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- 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/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.js +2 -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
|
@@ -12,9 +12,33 @@ export interface PriorTurnToolCallSummary {
|
|
|
12
12
|
* `PriorTurnToolCallSummary` for why this is a local shape, not an import. */
|
|
13
13
|
export interface PriorTurnToolResultSummary {
|
|
14
14
|
name: string;
|
|
15
|
+
/**
|
|
16
|
+
* Input of the `tool_start` this result was matched to, so the caller can
|
|
17
|
+
* rebuild the same `(tool, arguments)` breaker key its live counters use.
|
|
18
|
+
* Legacy `tool_done` events carry no input; those keep the positional
|
|
19
|
+
* FIFO-per-tool pairing documented in `tool-call-journal.ts`, so this is
|
|
20
|
+
* absent only when neither event carried one.
|
|
21
|
+
*/
|
|
22
|
+
input?: unknown;
|
|
15
23
|
content: string;
|
|
16
24
|
isError: boolean;
|
|
17
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Outcome of reading the per-turn ledger. `unreadable` is deliberately NOT the
|
|
28
|
+
* same value as a turn with no history: the repetition ledger, the read-only
|
|
29
|
+
* result cache and the write-replay hard block are all seeded from this read,
|
|
30
|
+
* so a swallowed ledger failure silently restores the per-chunk ceilings those
|
|
31
|
+
* mechanisms exist to remove.
|
|
32
|
+
*/
|
|
33
|
+
export type PriorTurnToolCallJournalRead = {
|
|
34
|
+
status: "read";
|
|
35
|
+
toolCallJournal: ToolCallJournal | null;
|
|
36
|
+
priorToolCalls: PriorTurnToolCallSummary[];
|
|
37
|
+
priorToolResults: PriorTurnToolResultSummary[];
|
|
38
|
+
} | {
|
|
39
|
+
status: "unreadable";
|
|
40
|
+
error: string;
|
|
41
|
+
};
|
|
18
42
|
/**
|
|
19
43
|
* Tool-call journal hard-block (resume safety). Snapshot the per-turn journal
|
|
20
44
|
* ONCE here, before any tool runs in this chunk, so it reflects only PRIOR
|
|
@@ -25,9 +49,8 @@ export interface PriorTurnToolResultSummary {
|
|
|
25
49
|
*
|
|
26
50
|
* Loaded eagerly (not lazily mid-loop) so the current chunk's own
|
|
27
51
|
* asynchronously-persisted tool_done events can never leak in and make a
|
|
28
|
-
* same-chunk call wrongly short-circuit.
|
|
29
|
-
*
|
|
30
|
-
* see an empty journal and are unaffected.
|
|
52
|
+
* same-chunk call wrongly short-circuit. Fresh first-turn calls see an empty
|
|
53
|
+
* journal and are unaffected.
|
|
31
54
|
*
|
|
32
55
|
* Also returns the prior chunks' tool calls/results so the caller can seed
|
|
33
56
|
* its own `toolCallHistory` / `toolResultHistory` accumulators — final
|
|
@@ -35,11 +58,5 @@ export interface PriorTurnToolResultSummary {
|
|
|
35
58
|
* tools executed after the latest handoff. Otherwise a guard can reject a
|
|
36
59
|
* grounded answer (or a successfully-created artifact) after the
|
|
37
60
|
* evidence-producing query completed in a predecessor run.
|
|
38
|
-
*
|
|
39
|
-
* Moved verbatim out of `runAgentLoop`'s per-turn setup — behavior unchanged.
|
|
40
61
|
*/
|
|
41
|
-
export declare function loadPriorTurnToolCallJournal(threadId: string | undefined, turnId?: string): Promise<
|
|
42
|
-
toolCallJournal: ToolCallJournal | null;
|
|
43
|
-
priorToolCalls: PriorTurnToolCallSummary[];
|
|
44
|
-
priorToolResults: PriorTurnToolResultSummary[];
|
|
45
|
-
}>;
|
|
62
|
+
export declare function loadPriorTurnToolCallJournal(threadId: string | undefined, turnId?: string): Promise<PriorTurnToolCallJournalRead>;
|
|
@@ -10,9 +10,8 @@ import { classifyToolCallJournal, } from "../tool-call-journal.js";
|
|
|
10
10
|
*
|
|
11
11
|
* Loaded eagerly (not lazily mid-loop) so the current chunk's own
|
|
12
12
|
* asynchronously-persisted tool_done events can never leak in and make a
|
|
13
|
-
* same-chunk call wrongly short-circuit.
|
|
14
|
-
*
|
|
15
|
-
* see an empty journal and are unaffected.
|
|
13
|
+
* same-chunk call wrongly short-circuit. Fresh first-turn calls see an empty
|
|
14
|
+
* journal and are unaffected.
|
|
16
15
|
*
|
|
17
16
|
* Also returns the prior chunks' tool calls/results so the caller can seed
|
|
18
17
|
* its own `toolCallHistory` / `toolResultHistory` accumulators — final
|
|
@@ -20,40 +19,55 @@ import { classifyToolCallJournal, } from "../tool-call-journal.js";
|
|
|
20
19
|
* tools executed after the latest handoff. Otherwise a guard can reject a
|
|
21
20
|
* grounded answer (or a successfully-created artifact) after the
|
|
22
21
|
* evidence-producing query completed in a predecessor run.
|
|
23
|
-
*
|
|
24
|
-
* Moved verbatim out of `runAgentLoop`'s per-turn setup — behavior unchanged.
|
|
25
22
|
*/
|
|
26
23
|
export async function loadPriorTurnToolCallJournal(threadId, turnId) {
|
|
27
|
-
const priorToolCalls = [];
|
|
28
|
-
const priorToolResults = [];
|
|
29
|
-
let toolCallJournal = null;
|
|
30
24
|
if (!threadId) {
|
|
31
|
-
|
|
25
|
+
// No thread means no durable ledger exists for this turn at all — a true
|
|
26
|
+
// "no prior history", not a read that failed.
|
|
27
|
+
return {
|
|
28
|
+
status: "read",
|
|
29
|
+
toolCallJournal: null,
|
|
30
|
+
priorToolCalls: [],
|
|
31
|
+
priorToolResults: [],
|
|
32
|
+
};
|
|
32
33
|
}
|
|
34
|
+
let priorEvents;
|
|
33
35
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
input: event.input,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
else if (event.type === "tool_done") {
|
|
44
|
-
priorToolResults.push({
|
|
45
|
-
name: event.tool,
|
|
46
|
-
content: event.result,
|
|
47
|
-
isError: event.isError === true,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
toolCallJournal = classifyToolCallJournal(priorEvents);
|
|
52
|
-
}
|
|
36
|
+
priorEvents = await getCurrentTurnEventsForThread(threadId, turnId);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
40
|
+
console.warn(`[tool-call-journal] per-turn ledger read failed for thread ${threadId}: ${error}`);
|
|
41
|
+
return { status: "unreadable", error };
|
|
53
42
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
const priorToolCalls = [];
|
|
44
|
+
const priorToolResults = [];
|
|
45
|
+
// Open `tool_start` inputs per tool, so a legacy `tool_done` with no input of
|
|
46
|
+
// its own can still be attributed to the arguments it ran with.
|
|
47
|
+
const openInputsByTool = new Map();
|
|
48
|
+
for (const event of priorEvents) {
|
|
49
|
+
if (event.type === "tool_start") {
|
|
50
|
+
priorToolCalls.push({ name: event.tool, input: event.input });
|
|
51
|
+
const queue = openInputsByTool.get(event.tool);
|
|
52
|
+
if (queue)
|
|
53
|
+
queue.push(event.input);
|
|
54
|
+
else
|
|
55
|
+
openInputsByTool.set(event.tool, [event.input]);
|
|
56
|
+
}
|
|
57
|
+
else if (event.type === "tool_done") {
|
|
58
|
+
const fifoInput = openInputsByTool.get(event.tool)?.shift();
|
|
59
|
+
priorToolResults.push({
|
|
60
|
+
name: event.tool,
|
|
61
|
+
input: event.input ?? fifoInput,
|
|
62
|
+
content: event.result,
|
|
63
|
+
isError: event.isError === true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
57
66
|
}
|
|
58
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
status: "read",
|
|
69
|
+
toolCallJournal: priorEvents.length > 0 ? classifyToolCallJournal(priorEvents) : null,
|
|
70
|
+
priorToolCalls,
|
|
71
|
+
priorToolResults,
|
|
72
|
+
};
|
|
59
73
|
}
|
|
@@ -183,6 +183,8 @@ export interface ActionEntry {
|
|
|
183
183
|
/** If true, completion does NOT trigger a screen-refresh change event.
|
|
184
184
|
* Set automatically by `defineAction` when `http.method === "GET"`. */
|
|
185
185
|
readOnly?: boolean;
|
|
186
|
+
/** If true, a successful call returns evidence retrieved from a live source. */
|
|
187
|
+
grounding?: boolean;
|
|
186
188
|
/** False keeps a read-only tool available in Act mode but hides/blocks it in
|
|
187
189
|
* Plan mode. Use for tools that perform substantive work even without
|
|
188
190
|
* mutating state. */
|
|
@@ -613,6 +615,21 @@ export declare const MAX_SAME_ERROR_ACROSS_ARGUMENTS = 6;
|
|
|
613
615
|
* legitimate re-check after a write.
|
|
614
616
|
*/
|
|
615
617
|
export declare const MAX_IDENTICAL_TOOL_CALLS = 8;
|
|
618
|
+
/**
|
|
619
|
+
* A stop that ends the turn from inside the tool loop.
|
|
620
|
+
*
|
|
621
|
+
* `message` is the last thing the user reads, so it must name the remedy and
|
|
622
|
+
* must NOT quote a raw provider/tool error: both `isAutoRecoverableError`
|
|
623
|
+
* (client) and `isRecoverableContinuationError` (below) sniff this string for
|
|
624
|
+
* "timeout"/"connection"/"network", and a stop whose text happens to contain
|
|
625
|
+
* one is auto-continued into the very spiral it was raised to end. The raw
|
|
626
|
+
* error belongs in `details`, which nothing retries on.
|
|
627
|
+
*/
|
|
628
|
+
export interface TerminalActionStop {
|
|
629
|
+
message: string;
|
|
630
|
+
errorCode?: string;
|
|
631
|
+
details?: string;
|
|
632
|
+
}
|
|
616
633
|
/**
|
|
617
634
|
* Convert ActionEntry registry to EngineTool array.
|
|
618
635
|
*/
|
|
@@ -687,6 +704,22 @@ export declare function findApprovedStructuredToolCall(history: AgentChatStructu
|
|
|
687
704
|
input: unknown;
|
|
688
705
|
callId: string;
|
|
689
706
|
} | undefined;
|
|
707
|
+
/**
|
|
708
|
+
* Tool errors the model has no way to clear: the missing thing lives outside
|
|
709
|
+
* the turn (a credential, a role grant, a connected account, a runtime, the
|
|
710
|
+
* user's own approval). Every retry costs a full round-trip carrying the whole
|
|
711
|
+
* transcript and lands on the identical error, so these stop on the FIRST
|
|
712
|
+
* occurrence rather than after `MAX_SAME_ERROR_ACROSS_ARGUMENTS` of them.
|
|
713
|
+
*
|
|
714
|
+
* The distinction is the one `stopForBigQueryNotConfigured` draws in
|
|
715
|
+
* templates/analytics: a *configuration* gap is permanent for this turn, a
|
|
716
|
+
* *query* failure is not. Anything the model can act on — a stale read to
|
|
717
|
+
* rebase, a bad argument, a full quota it can free, a wrong dataset name, a
|
|
718
|
+
* statement timeout — must stay out of this list and keep its retries.
|
|
719
|
+
* Precision over recall: a wrong match kills a turn that would have succeeded,
|
|
720
|
+
* which is worse than one more retry.
|
|
721
|
+
*/
|
|
722
|
+
export declare function permanentPreconditionRemedy(message: string): string | null;
|
|
690
723
|
export declare function repeatedSourceSweepGuardMessage(opts: {
|
|
691
724
|
toolName: string;
|
|
692
725
|
priorCalls: number;
|
|
@@ -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
|