@agent-native/core 0.168.13 → 0.169.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/engine/first-event-timeout.d.ts +8 -0
- package/dist/agent/engine/first-event-timeout.js +8 -0
- package/dist/agent/production-agent.d.ts +0 -30
- package/dist/agent/production-agent.js +17 -38
- package/dist/agent/run-loop-with-resume.d.ts +38 -25
- package/dist/agent/run-loop-with-resume.js +140 -55
- package/dist/agent/run-manager.d.ts +83 -68
- package/dist/agent/run-manager.js +280 -94
- package/dist/agent/run-store.d.ts +31 -0
- package/dist/agent/run-store.js +42 -12
- package/dist/app-config/agent.d.ts +2 -0
- package/dist/app-config/agent.js +33 -0
- package/dist/app-config/run-lifecycle-invariants.d.ts +248 -0
- package/dist/app-config/run-lifecycle-invariants.js +342 -0
- package/dist/app-config/schema.d.ts +2 -0
- package/dist/app-config/store.js +9 -1
- package/dist/client/agent-chat-adapter.d.ts +0 -2
- package/dist/client/agent-chat-adapter.js +7 -23
- package/dist/jobs/background-automation-runner.d.ts +25 -0
- package/dist/jobs/background-automation-runner.js +104 -21
- package/dist/jobs/run-history.d.ts +7 -1
- package/dist/jobs/run-history.js +57 -14
- package/dist/observability/traces.d.ts +13 -0
- package/dist/observability/traces.js +369 -317
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/agent-chat-plugin.js +2 -4
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +1 -1
|
@@ -148,6 +148,29 @@ export declare const UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS: number;
|
|
|
148
148
|
export declare const UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS = 20000;
|
|
149
149
|
/** Test seam — the probe cache is module state, so suites must clear it. */
|
|
150
150
|
export declare function __resetNoRunningRunsProbeForTests(): void;
|
|
151
|
+
/**
|
|
152
|
+
* Ceiling on run ROWS for one logical turn — the number the continuation-chain
|
|
153
|
+
* guard and stale-run recovery must agree on.
|
|
154
|
+
*
|
|
155
|
+
* This was a literal `25` here plus `MAX_BACKGROUND_RUN_CONTINUATIONS + 5` in
|
|
156
|
+
* production-agent.ts, kept in step by a comment asking the next editor to
|
|
157
|
+
* remember, because importing back from this file would have been circular.
|
|
158
|
+
* It no longer needs to be: the base value is configuration, and `app-config`
|
|
159
|
+
* imports no agent code, so both sites can read the same resolver.
|
|
160
|
+
*/
|
|
161
|
+
export declare function resolveTurnRunLedgerBudget(): number;
|
|
162
|
+
/**
|
|
163
|
+
* True when a turn holding `turnRunCount` run rows must not be given another.
|
|
164
|
+
*
|
|
165
|
+
* A predicate rather than a number the callers compare themselves, because both
|
|
166
|
+
* call sites had `turnRunCount > budget` and both were off by one: the current
|
|
167
|
+
* run's row is already inserted when they check, and the successor's row is
|
|
168
|
+
* inserted after — so at equality they permitted a row past the documented
|
|
169
|
+
* ceiling. Two sites, one comparison, no way for them to disagree about the
|
|
170
|
+
* boundary again. That is the third time in this area that one number had two
|
|
171
|
+
* spellings.
|
|
172
|
+
*/
|
|
173
|
+
export declare function turnRunLedgerExhausted(turnRunCount: number): boolean;
|
|
151
174
|
/**
|
|
152
175
|
* Maximum time the stale reapers (`reapIfStale`, `reapAllStaleRuns`,
|
|
153
176
|
* `cleanupOldRuns`'s heartbeat-stale pass) will suspend reaping a "running"
|
|
@@ -449,6 +472,14 @@ export declare const RUN_DIAG_STAGE: {
|
|
|
449
472
|
* the per-turn budget is exhausted). See `attemptStaleRunRecovery`.
|
|
450
473
|
*/
|
|
451
474
|
readonly staleRunRecoveryAttempted: "stale_run_recovery_attempted";
|
|
475
|
+
/**
|
|
476
|
+
* The run manager reached a server-owned chunk boundary (`no_progress` or
|
|
477
|
+
* `run_timeout`). Detail carries the reason, whether it was recovered in the
|
|
478
|
+
* same invocation or terminated the turn, how long the run had been silent,
|
|
479
|
+
* and the last event type seen — the segment that went quiet, which is what
|
|
480
|
+
* no boundary previously recorded anywhere.
|
|
481
|
+
*/
|
|
482
|
+
readonly runBoundaryReached: "run_boundary_reached";
|
|
452
483
|
};
|
|
453
484
|
export type RunDiagStage = (typeof RUN_DIAG_STAGE)[keyof typeof RUN_DIAG_STAGE];
|
|
454
485
|
/**
|
package/dist/agent/run-store.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL persistence for agent runs and events.
|
|
3
|
+
* Enables cross-isolate access on Cloudflare Workers and
|
|
4
|
+
* reliable reconnection after page refreshes.
|
|
5
|
+
*/
|
|
6
|
+
import { MAX_BACKGROUND_RUN_CONTINUATIONS, TURN_RUN_LEDGER_SLACK, } from "../app-config/run-lifecycle-invariants.js";
|
|
1
7
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
2
8
|
import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
|
|
3
9
|
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
@@ -190,15 +196,32 @@ async function hasRunningRuns() {
|
|
|
190
196
|
return false;
|
|
191
197
|
}
|
|
192
198
|
/**
|
|
193
|
-
*
|
|
194
|
-
* stale-run recovery
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
199
|
+
* Ceiling on run ROWS for one logical turn — the number the continuation-chain
|
|
200
|
+
* guard and stale-run recovery must agree on.
|
|
201
|
+
*
|
|
202
|
+
* This was a literal `25` here plus `MAX_BACKGROUND_RUN_CONTINUATIONS + 5` in
|
|
203
|
+
* production-agent.ts, kept in step by a comment asking the next editor to
|
|
204
|
+
* remember, because importing back from this file would have been circular.
|
|
205
|
+
* It no longer needs to be: the base value is configuration, and `app-config`
|
|
206
|
+
* imports no agent code, so both sites can read the same resolver.
|
|
207
|
+
*/
|
|
208
|
+
export function resolveTurnRunLedgerBudget() {
|
|
209
|
+
return MAX_BACKGROUND_RUN_CONTINUATIONS + TURN_RUN_LEDGER_SLACK;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* True when a turn holding `turnRunCount` run rows must not be given another.
|
|
213
|
+
*
|
|
214
|
+
* A predicate rather than a number the callers compare themselves, because both
|
|
215
|
+
* call sites had `turnRunCount > budget` and both were off by one: the current
|
|
216
|
+
* run's row is already inserted when they check, and the successor's row is
|
|
217
|
+
* inserted after — so at equality they permitted a row past the documented
|
|
218
|
+
* ceiling. Two sites, one comparison, no way for them to disagree about the
|
|
219
|
+
* boundary again. That is the third time in this area that one number had two
|
|
220
|
+
* spellings.
|
|
200
221
|
*/
|
|
201
|
-
|
|
222
|
+
export function turnRunLedgerExhausted(turnRunCount) {
|
|
223
|
+
return turnRunCount >= resolveTurnRunLedgerBudget();
|
|
224
|
+
}
|
|
202
225
|
/**
|
|
203
226
|
* Circuit breaker for a DETERMINISTIC dead-on-arrival loop: some request
|
|
204
227
|
* shapes make the worker hang almost immediately every single time (e.g. an
|
|
@@ -206,7 +229,7 @@ const STALE_RUN_RECOVERY_MAX_TURN_RUNS = 25;
|
|
|
206
229
|
* hitting a transient blip. Because `attemptStaleRunRecovery` replays the
|
|
207
230
|
* SAME captured `dispatch_payload` on every successor (never a fresh
|
|
208
231
|
* request), such a turn was retrying an unwinnable request up to
|
|
209
|
-
* `
|
|
232
|
+
* `resolveTurnRunLedgerBudget()` (25) times — ~25 * 53s ≈ 22 minutes,
|
|
210
233
|
* each cycle re-billing the full input context — before finally giving up.
|
|
211
234
|
* Confirmed live in prod (assets: one turn cycled 24x, each attempt an
|
|
212
235
|
* identical ~32K-token request that made a token of real progress around
|
|
@@ -1271,6 +1294,14 @@ export const RUN_DIAG_STAGE = {
|
|
|
1271
1294
|
* the per-turn budget is exhausted). See `attemptStaleRunRecovery`.
|
|
1272
1295
|
*/
|
|
1273
1296
|
staleRunRecoveryAttempted: "stale_run_recovery_attempted",
|
|
1297
|
+
/**
|
|
1298
|
+
* The run manager reached a server-owned chunk boundary (`no_progress` or
|
|
1299
|
+
* `run_timeout`). Detail carries the reason, whether it was recovered in the
|
|
1300
|
+
* same invocation or terminated the turn, how long the run had been silent,
|
|
1301
|
+
* and the last event type seen — the segment that went quiet, which is what
|
|
1302
|
+
* no boundary previously recorded anywhere.
|
|
1303
|
+
*/
|
|
1304
|
+
runBoundaryReached: "run_boundary_reached",
|
|
1274
1305
|
};
|
|
1275
1306
|
/**
|
|
1276
1307
|
* Record the last reached pipeline stage (+ optional short detail) for a run.
|
|
@@ -1491,7 +1522,7 @@ function staleRecoveryDispatchPayload(payload) {
|
|
|
1491
1522
|
* caller's own atomic "did I win the reap" gate, this guarantees AT MOST
|
|
1492
1523
|
* ONE recovery successor per reaped run even under concurrent reapers.
|
|
1493
1524
|
* - the per-turn run ledger (`countRunsForTurn`'s underlying query) has
|
|
1494
|
-
* room (`
|
|
1525
|
+
* room (`resolveTurnRunLedgerBudget`) — mirrors
|
|
1495
1526
|
* `chainServerDrivenContinuation`'s own budget guard so a pathological
|
|
1496
1527
|
* turn can't loop forever through reaper-driven recovery either.
|
|
1497
1528
|
*/
|
|
@@ -1535,8 +1566,7 @@ async function attemptStaleRunRecovery(db, runId) {
|
|
|
1535
1566
|
args: [threadId, turnId],
|
|
1536
1567
|
});
|
|
1537
1568
|
const turnRunCount = Number(countRows?.[0]?.run_count);
|
|
1538
|
-
if (Number.isFinite(turnRunCount) &&
|
|
1539
|
-
turnRunCount > STALE_RUN_RECOVERY_MAX_TURN_RUNS) {
|
|
1569
|
+
if (Number.isFinite(turnRunCount) && turnRunLedgerExhausted(turnRunCount)) {
|
|
1540
1570
|
return { outcome: "budget_exhausted" };
|
|
1541
1571
|
}
|
|
1542
1572
|
// See `STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT`: a run whose last
|
|
@@ -15,4 +15,6 @@ export declare const agentConfig: z.ZodObject<{
|
|
|
15
15
|
runSoftTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
16
16
|
completedRunRetentionMs: z.ZodOptional<z.ZodNumber>;
|
|
17
17
|
erroredRunRetentionMs: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
backgroundNoProgressTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
backgroundRunHardTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
18
20
|
}, z.core.$strip>;
|
package/dist/app-config/agent.js
CHANGED
|
@@ -66,4 +66,37 @@ export const agentConfig = z.object({
|
|
|
66
66
|
env: ["AGENT_ERRORED_RUN_RETENTION_MS"],
|
|
67
67
|
doc: "How long an errored agent run row is kept, in milliseconds.",
|
|
68
68
|
}),
|
|
69
|
+
// ── Run-lifecycle bounds ────────────────────────────────────────────────
|
|
70
|
+
//
|
|
71
|
+
// These are the numbers that can TERMINATE a run, or that encode an
|
|
72
|
+
// assumption about the host it runs on. They carry today's shipped values as
|
|
73
|
+
// declared defaults, so a deployment that configures nothing sees no
|
|
74
|
+
// behaviour change; `agent/run-lifecycle.ts` is the only place that reads
|
|
75
|
+
// them, one resolver per field, and `assertRunLifecycleInvariants` checks the
|
|
76
|
+
// ordering between them every time configuration resolves.
|
|
77
|
+
//
|
|
78
|
+
// Derived values (the foreground backstop fraction, tool-timeout headroom)
|
|
79
|
+
// stay internal on purpose: they are relationships, not host facts, and
|
|
80
|
+
// making them settable is how the ordering below stops being checkable.
|
|
81
|
+
//
|
|
82
|
+
// Each default here is the value that shipped as a module constant, and the
|
|
83
|
+
// constant still exists under its historical name where its reasoning is
|
|
84
|
+
// written down. `agent-run-lifecycle-config.spec.ts` pins the two together so
|
|
85
|
+
// editing one alone is a failing test, not a silent divergence.
|
|
86
|
+
backgroundNoProgressTimeoutMs: z
|
|
87
|
+
.number()
|
|
88
|
+
.nonnegative()
|
|
89
|
+
.default(150_000)
|
|
90
|
+
.meta({
|
|
91
|
+
env: ["AGENT_BACKGROUND_NO_PROGRESS_TIMEOUT_MS"],
|
|
92
|
+
doc: "No-progress backstop for a background-function run, in milliseconds. 0 disables it.",
|
|
93
|
+
}),
|
|
94
|
+
backgroundRunHardTimeoutMs: z
|
|
95
|
+
.number()
|
|
96
|
+
.positive()
|
|
97
|
+
.default(10 * 60_000)
|
|
98
|
+
.meta({
|
|
99
|
+
env: ["AGENT_BACKGROUND_RUN_HARD_TIMEOUT_MS"],
|
|
100
|
+
doc: "Hard abort for one in-process background automation run, in milliseconds. This is the host's real function budget for scheduled work.",
|
|
101
|
+
}),
|
|
69
102
|
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { AppConfig } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wall-clock reserved between a background automation's round budget and its
|
|
4
|
+
* own hard abort.
|
|
5
|
+
*
|
|
6
|
+
* It covers wind-down only — emit the terminal event, persist the turn, let
|
|
7
|
+
* `finalized` settle — because the recoverable boundary on this path belongs to
|
|
8
|
+
* the agent-loop wrapper's own per-round timer, not to a second timer in the
|
|
9
|
+
* run manager.
|
|
10
|
+
*
|
|
11
|
+
* This is the constant that makes `automation soft timeout < automation hard
|
|
12
|
+
* abort` true by construction rather than by hoping two independently chosen
|
|
13
|
+
* numbers happen to be ordered — they were not: the shipped build gave the
|
|
14
|
+
* automation path a 13-minute soft timeout under a 10-minute hard abort, so
|
|
15
|
+
* the recoverable boundary was dead code and the only boundary an automation
|
|
16
|
+
* could reach was the terminal one.
|
|
17
|
+
*/
|
|
18
|
+
export declare const BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS = 20000;
|
|
19
|
+
/**
|
|
20
|
+
* The host's hard kill for a background function (Netlify: 15 minutes).
|
|
21
|
+
*
|
|
22
|
+
* Not configuration — a deployment does not choose it, the platform does. It is
|
|
23
|
+
* here because `backgroundSoftTimeoutCeilingMs` IS the clamp that
|
|
24
|
+
* `resolveRunSoftTimeoutMs` reduces every background soft timeout to, so once
|
|
25
|
+
* that ceiling became configurable nothing was left bounding it: a deployment
|
|
26
|
+
* could set 60 minutes and push its own chunk boundary past the wall the
|
|
27
|
+
* ceiling exists to stay inside, turning every long background turn back into
|
|
28
|
+
* the silent platform kill it was introduced to prevent. Configurable must not
|
|
29
|
+
* mean unclamped.
|
|
30
|
+
*/
|
|
31
|
+
export declare const BACKGROUND_FUNCTION_WALL_MS: number;
|
|
32
|
+
/**
|
|
33
|
+
* Wall-clock a background chunk must leave itself to abort, persist the partial
|
|
34
|
+
* turn, write the terminal event, and chain a successor before the host kills
|
|
35
|
+
* the invocation. The shipped 13-minute ceiling under a 15-minute wall is
|
|
36
|
+
* exactly this margin.
|
|
37
|
+
*/
|
|
38
|
+
export declare const BACKGROUND_FUNCTION_WALL_HEADROOM_MS: number;
|
|
39
|
+
/**
|
|
40
|
+
* Slack between the CHAIN bound (`agent.maxBackgroundRunContinuations`) and the
|
|
41
|
+
* per-turn LEDGER bound below.
|
|
42
|
+
*
|
|
43
|
+
* They count different things. The chain bound counts handoffs a chunk decided
|
|
44
|
+
* to make; the ledger counts every run ROW the turn produced, which also
|
|
45
|
+
* includes sweep redispatches and stale-run recoveries no chunk ever decided.
|
|
46
|
+
* Without slack the ledger would refuse a turn before the chain bound it is
|
|
47
|
+
* meant to sit above, so a turn recovered once would die holding unused chain
|
|
48
|
+
* budget.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Shipped run-lifecycle bounds.
|
|
52
|
+
*
|
|
53
|
+
* They live beside the relationships that constrain them so a change to one is
|
|
54
|
+
* checked against the others in the same file. `run-manager.ts` and
|
|
55
|
+
* `production-agent.ts` re-export them under their historical names; this
|
|
56
|
+
* module imports no agent code, so nothing here can become circular.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Hard ceiling for the soft timeout when a run executes inside a Netlify
|
|
60
|
+
* background function (any deployed function whose name ends in `-background`).
|
|
61
|
+
* Background functions return 202 immediately and run detached for up to 15
|
|
62
|
+
* minutes, so the ~60s synchronous function wall that 40s defends against does
|
|
63
|
+
* NOT apply. 13 minutes leaves ~2 min of headroom under Netlify's 15-min hard
|
|
64
|
+
* kill to abort, persist the partial turn, write the terminal event, and (for
|
|
65
|
+
* the rare >13-min turn) self-fire another background continuation.
|
|
66
|
+
*
|
|
67
|
+
* This ceiling is used ONLY when a caller explicitly opts in with
|
|
68
|
+
* `backgroundFunction: true`. It does not change the foreground/interactive
|
|
69
|
+
* ceiling and does not fire unless the durable-background path dispatched the
|
|
70
|
+
* run into a background function. Per the design doc Guardrail, the 40s
|
|
71
|
+
* interactive clamp stays correct for every non-background run.
|
|
72
|
+
*/
|
|
73
|
+
export declare const BACKGROUND_SOFT_TIMEOUT_CEILING_MS: number;
|
|
74
|
+
/**
|
|
75
|
+
* AUTHORITATIVE no-progress backstop for a run, enforced by the run manager
|
|
76
|
+
* itself (timer-driven, independent of any layer below).
|
|
77
|
+
*
|
|
78
|
+
* The finer-grained watchdogs inside the agent loop (model-stream and
|
|
79
|
+
* action-preparation no-progress, both 90s) only guard the model event stream
|
|
80
|
+
* — a stall in any segment OUTSIDE that guarded loop (engine-call
|
|
81
|
+
* establishment, worker setup between continuation chunks, a wedged transport
|
|
82
|
+
* that emits keepalives while the loop never runs) previously hung forever
|
|
83
|
+
* with the client watching keepalives. This backstop covers every segment by
|
|
84
|
+
* construction: if no REAL progress event (see `shouldBumpProgressForEvent`;
|
|
85
|
+
* keepalives and zero-byte prep activity don't count) lands for this long —
|
|
86
|
+
* and no unit of work is in flight (see `inFlightWorkDelta`: tool calls,
|
|
87
|
+
* cross-app calls, and the model stream all legitimately emit nothing for
|
|
88
|
+
* minutes and each carry a bound of their own) — the run manager emits
|
|
89
|
+
* `auto_continue { reason: "no_progress" }` and aborts the chunk, exactly
|
|
90
|
+
* like the soft timeout, so the normal continuation machinery recovers it.
|
|
91
|
+
*
|
|
92
|
+
* Being numerically larger than the in-loop watchdogs is NOT what keeps this
|
|
93
|
+
* from killing a healthy run, and treating it that way is what made it do so:
|
|
94
|
+
* this clock and the loop's `lastModelStreamProgressAt` measure DIFFERENT
|
|
95
|
+
* events. An extended-thinking phase bumps the inner clock on every engine
|
|
96
|
+
* frame while forwarding nothing, so the inner watchdog correctly stayed quiet
|
|
97
|
+
* and this one saw pure silence — runs whose worst gap crossed 150s died while
|
|
98
|
+
* still streaming, some by a single second. Ordering between two clocks only
|
|
99
|
+
* means something when they watch the same events; suspending on in-flight
|
|
100
|
+
* work is what actually makes the two agree.
|
|
101
|
+
*
|
|
102
|
+
* This is now only the CEILING, not the value: `resolveRunNoProgressTimeoutMs`
|
|
103
|
+
* clamps the foreground backstop to a fraction of the chunk's soft timeout
|
|
104
|
+
* (~30s at a 40s chunk), which is BELOW the 90s in-loop watchdogs rather than
|
|
105
|
+
* above them. That ordering is deliberate — the in-loop watchdogs could never
|
|
106
|
+
* fire inside a hosted foreground chunk anyway, since the serverless wall
|
|
107
|
+
* (~57-59s) arrives first. Proven durable-background chunks keep the full
|
|
108
|
+
* `DEFAULT_BACKGROUND_NO_PROGRESS_TIMEOUT_MS` so large outputs can use the
|
|
109
|
+
* background budget. Only armed when a soft-timeout regime is active (hosted
|
|
110
|
+
* runs); local dev stays unbounded.
|
|
111
|
+
*/
|
|
112
|
+
export declare const RUN_NO_PROGRESS_HARD_TIMEOUT_MS = 150000;
|
|
113
|
+
/**
|
|
114
|
+
* Default in-loop watchdog for silence while an action's arguments stream in.
|
|
115
|
+
* Read through `resolveActionPreparationNoProgressTimeoutMs`, never directly:
|
|
116
|
+
* a host diagnosing a timeout has to be able to see and change this number.
|
|
117
|
+
*/
|
|
118
|
+
export declare const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90000;
|
|
119
|
+
/**
|
|
120
|
+
* Default in-loop watchdog for silence between engine stream frames. Read
|
|
121
|
+
* through `resolveModelStreamNoProgressTimeoutMs`, never directly.
|
|
122
|
+
*/
|
|
123
|
+
export declare const MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS = 90000;
|
|
124
|
+
/**
|
|
125
|
+
* Consecutive chunks allowed to end on the SAME terminal error code having
|
|
126
|
+
* produced nothing before the chain stops.
|
|
127
|
+
*
|
|
128
|
+
* Two, because two independent recovery layers multiply here and neither can
|
|
129
|
+
* see the other: the engine already retried this identical request 3x with
|
|
130
|
+
* backoff before the error was ever emitted, and a recoverable error is also a
|
|
131
|
+
* continuation boundary, so every chunk that fails costs 4 gateway attempts
|
|
132
|
+
* and dispatches a fresh one. A production turn spent 27 background runs and
|
|
133
|
+
* 15 minutes on one message this way. The first repeat is the retry this path
|
|
134
|
+
* exists for; a second identical failure that moved nothing is evidence the
|
|
135
|
+
* retrying itself is what is broken, not the request.
|
|
136
|
+
*/
|
|
137
|
+
export declare const MAX_CONSECUTIVE_NO_PROGRESS_CONTINUATIONS = 2;
|
|
138
|
+
/**
|
|
139
|
+
* Wall-clock ceiling on a single logical turn. The run-count ledger alone is
|
|
140
|
+
* not a time bound: in durable mode each of the ~25 permitted chunks may burn
|
|
141
|
+
* ~780s, so the ledger's real worst case is over five hours (production has an
|
|
142
|
+
* observed 2h34m turn). Nobody is waiting that long, and every minute past
|
|
143
|
+
* this point is spend on a request the user has abandoned.
|
|
144
|
+
*/
|
|
145
|
+
export declare const MAX_TURN_WALL_CLOCK_MS: number;
|
|
146
|
+
/**
|
|
147
|
+
* Cap on continuation iterations inside a single
|
|
148
|
+
* `runAgentLoopDirectWithSoftTimeout` invocation. The host's hard function
|
|
149
|
+
* timeout usually bounds this naturally — but a defensive cap prevents an
|
|
150
|
+
* instant-error spiral from looping forever inside hosting environments with a
|
|
151
|
+
* generous budget.
|
|
152
|
+
*
|
|
153
|
+
* 6 leaves room for: 1 normal completion + a few resume rounds for design
|
|
154
|
+
* generation (prompt + 3 variants ≈ 4 LLM calls), with a small safety margin.
|
|
155
|
+
*/
|
|
156
|
+
export declare const MAX_RUN_LOOP_CONTINUATIONS = 6;
|
|
157
|
+
/**
|
|
158
|
+
* A delegated turn that is proven to be running inside a durable background
|
|
159
|
+
* function has the same 15-minute host budget as main chat, but this wrapper
|
|
160
|
+
* historically kept the foreground-sized six-continuation cap. A healthy
|
|
161
|
+
* child A2A call can consume several minutes and the receiving model may then
|
|
162
|
+
* need more than six recovery/model-stream boundaries to finish its own tool
|
|
163
|
+
* work. Keep a hard cap, but give the proven background path the same bounded
|
|
164
|
+
* continuation allowance as the durable main-chat runner. The cumulative
|
|
165
|
+
* soft-timeout below still prevents these rounds from exceeding the one real
|
|
166
|
+
* background-function wall-clock budget.
|
|
167
|
+
*/
|
|
168
|
+
export declare const MAX_BACKGROUND_RUN_LOOP_CONTINUATIONS = 20;
|
|
169
|
+
export declare const TURN_RUN_LEDGER_SLACK = 5;
|
|
170
|
+
/**
|
|
171
|
+
* Hard cap on server-driven background→background continuation chunks for a
|
|
172
|
+
* single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout,
|
|
173
|
+
* so reaching this boundary at all is the rare exception (most turns finish in
|
|
174
|
+
* one chunk). The cap bounds a pathological turn that would otherwise chain
|
|
175
|
+
* background invocations forever, mirroring `MAX_AGENT_TEAM_CONTINUATIONS`.
|
|
176
|
+
*/
|
|
177
|
+
export declare const MAX_BACKGROUND_RUN_CONTINUATIONS = 20;
|
|
178
|
+
/**
|
|
179
|
+
* Per-TURN follow budgets the browser applies while reading a background turn.
|
|
180
|
+
*
|
|
181
|
+
* They live here, not in `client/agent-chat-adapter.ts`, because they are one
|
|
182
|
+
* half of an ordering relationship whose other half is server configuration —
|
|
183
|
+
* and a relationship checked in only one of its two homes is the failure this
|
|
184
|
+
* module exists to prevent. This file has no runtime imports (the `AppConfig`
|
|
185
|
+
* import is type-only and erased), so the browser bundle pays nothing to read
|
|
186
|
+
* them from here.
|
|
187
|
+
*
|
|
188
|
+
* CLIENT-ABOVE-SERVER: these MUST stay above the server's own ceilings. The
|
|
189
|
+
* client fires on a clock and cannot tell looping from working; the server can,
|
|
190
|
+
* so the server must always terminate a turn first and write a truthful
|
|
191
|
+
* terminal reason. They shipped at 10 min / 6 runs while ONE legal background
|
|
192
|
+
* chunk may run 13 minutes — so the client killed healthy turns the server was
|
|
193
|
+
* still streaming, measured in production as aborts at 11-25 minutes with
|
|
194
|
+
* progress recorded right up to the abort. That was the top non-auth cause of
|
|
195
|
+
* "the chat just stopped".
|
|
196
|
+
*
|
|
197
|
+
* Do NOT tighten these to catch a stuck turn. A turn that is not progressing is
|
|
198
|
+
* already caught twice by mechanisms that read progress rather than a clock:
|
|
199
|
+
* `BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS` and the repeated-terminal-reason
|
|
200
|
+
* detector.
|
|
201
|
+
*/
|
|
202
|
+
export declare const MAX_FOLLOWED_BACKGROUND_RUNS = 30;
|
|
203
|
+
export declare const MAX_BACKGROUND_FOLLOW_WALL_TIME_MS: number;
|
|
204
|
+
/**
|
|
205
|
+
* Ordering relationships between the run-lifecycle bounds.
|
|
206
|
+
*
|
|
207
|
+
* Every one of these was already argued for in a source comment somewhere and
|
|
208
|
+
* enforced by nothing, which is how the framework shipped a violated pair. The
|
|
209
|
+
* check runs on resolved configuration — including the all-defaults case — so
|
|
210
|
+
* a relationship broken by a new default fails the same way a relationship
|
|
211
|
+
* broken by a deployment does.
|
|
212
|
+
*
|
|
213
|
+
* DECLARED EXCEPTION, deliberately not asserted:
|
|
214
|
+
* `backgroundSoftTimeoutCeilingMs` (13 min) sits ABOVE
|
|
215
|
+
* `backgroundRunHardTimeoutMs` (10 min). Those two bound different paths — the
|
|
216
|
+
* ceiling belongs to a durable background CHAT chunk, whose wall is the host's
|
|
217
|
+
* 15-minute background-function budget, while the hard abort belongs to the
|
|
218
|
+
* in-process automation runner. The automation path does not inherit the
|
|
219
|
+
* ceiling: it derives its budget from its own hard abort minus
|
|
220
|
+
* `BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS` (see
|
|
221
|
+
* `resolveBackgroundAutomationSoftTimeoutMs`), which is what invariant 4 below
|
|
222
|
+
* checks is possible at all.
|
|
223
|
+
*/
|
|
224
|
+
interface Invariant {
|
|
225
|
+
name: string;
|
|
226
|
+
smaller: {
|
|
227
|
+
key: string;
|
|
228
|
+
value: number;
|
|
229
|
+
};
|
|
230
|
+
larger: {
|
|
231
|
+
key: string;
|
|
232
|
+
value: number;
|
|
233
|
+
};
|
|
234
|
+
relation: "<" | "<=";
|
|
235
|
+
why: string;
|
|
236
|
+
}
|
|
237
|
+
export declare class RunLifecycleInvariantError extends Error {
|
|
238
|
+
constructor(violations: readonly Invariant[]);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Throws when the resolved run-lifecycle bounds cannot all do their job.
|
|
242
|
+
*
|
|
243
|
+
* Called from configuration resolution, so it fails at startup naming both
|
|
244
|
+
* constants and the relationship rather than at 3am when a run dies inside the
|
|
245
|
+
* window a mis-ordered pair opened.
|
|
246
|
+
*/
|
|
247
|
+
export declare function assertRunLifecycleInvariants(agent: AppConfig["agent"]): void;
|
|
248
|
+
export {};
|