@bubblebrain-ai/bubble 0.0.34 → 0.0.36
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/README.md +27 -7
- package/dist/agent/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -2
- package/dist/agent/profiles.js +25 -14
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +21 -0
- package/dist/agent/workflow/control.js +50 -8
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +82 -48
- package/dist/agent.js +360 -232
- package/dist/approval/controller.js +28 -0
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +21 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -1
- package/dist/config.d.ts +5 -3
- package/dist/config.js +10 -5
- package/dist/context/compact-llm.d.ts +2 -2
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.js +21 -0
- package/dist/feishu/agent-host/run-driver.js +19 -3
- package/dist/feishu/serve.js +1 -0
- package/dist/goal/tools.d.ts +6 -3
- package/dist/goal/tools.js +7 -19
- package/dist/main.js +114 -28
- package/dist/memory/db.d.ts +2 -0
- package/dist/memory/db.js +10 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/phase1.js +14 -2
- package/dist/memory/phase2.js +21 -2
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/storage.d.ts +1 -0
- package/dist/memory/storage.js +13 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +92 -52
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +11 -0
- package/dist/oauth/grok.d.ts +25 -0
- package/dist/oauth/grok.js +218 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/storage.d.ts +9 -0
- package/dist/oauth/storage.js +25 -0
- package/dist/orchestrator/default-hooks.js +23 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +41 -8
- package/dist/prompt/environment.js +0 -4
- package/dist/prompt/reminders.d.ts +0 -1
- package/dist/prompt/reminders.js +1 -11
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.js +1 -1
- package/dist/prompt/task-reminders.d.ts +20 -0
- package/dist/prompt/task-reminders.js +73 -1
- package/dist/provider-ai-sdk.js +2 -2
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +59 -0
- package/dist/provider-openai-codex.d.ts +17 -1
- package/dist/provider-openai-codex.js +101 -29
- package/dist/provider-registry.d.ts +72 -0
- package/dist/provider-registry.js +336 -59
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +35 -17
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +14 -0
- package/dist/session-types.d.ts +19 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +4 -0
- package/dist/slash-commands/commands.js +224 -14
- package/dist/slash-commands/types.d.ts +14 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +42 -272
- package/dist/tools/bash.js +1 -1
- package/dist/tools/child-tools.js +2 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +7 -5
- package/dist/tools/lsp.js +1 -0
- package/dist/tools/memory.d.ts +7 -2
- package/dist/tools/memory.js +28 -43
- package/dist/tools/server.js +4 -0
- package/dist/tools/tool-search.d.ts +1 -1
- package/dist/tools/tool-search.js +23 -3
- package/dist/tui/model-switch.d.ts +7 -0
- package/dist/tui/model-switch.js +15 -5
- package/dist/tui-ink/app.d.ts +3 -1
- package/dist/tui-ink/app.js +672 -243
- package/dist/tui-ink/approval/approval-dialog.js +37 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +7 -2
- package/dist/tui-ink/input-box.d.ts +36 -7
- package/dist/tui-ink/input-box.js +229 -157
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +464 -128
- package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
- package/dist/tui-ink/paste-operation-tracker.js +25 -0
- package/dist/tui-ink/plan-confirm.js +1 -1
- package/dist/tui-ink/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +34 -1
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +8 -31
- package/dist/types.js +1 -1
- package/dist/variant/thinking-level.d.ts +2 -2
- package/dist/variant/thinking-level.js +2 -1
- package/dist/variant/variant-resolver.d.ts +22 -0
- package/dist/variant/variant-resolver.js +43 -0
- package/package.json +6 -2
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Bubble is a terminal coding agent. It works inside a local project folder: it reads and edits files, runs commands behind configurable approval controls, searches and navigates code with language-server intelligence, browses the web, loads reusable skills, connects MCP tools, fans work out to subagents, and keeps persistent memory across sessions.
|
|
4
4
|
|
|
5
|
-
It is provider-agnostic. Bring an API key for OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Zhipu, Z.AI, MiniMax, Groq, Together, Fireworks, a local OpenAI-compatible endpoint, and more
|
|
5
|
+
It is provider-agnostic. Bring an API key for OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Zhipu, Z.AI, MiniMax, Groq, Together, Fireworks, a local OpenAI-compatible endpoint, and more; sign in to ChatGPT with OAuth; or sign in with OAuth to use a ChatGPT or Grok subscription directly.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -39,10 +39,10 @@ Start Bubble in the current directory:
|
|
|
39
39
|
bubble
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
On first launch, connect a
|
|
42
|
+
On first launch, connect a provider:
|
|
43
43
|
|
|
44
44
|
- Run `/login` to sign in to ChatGPT (OAuth) and use the Codex models, or
|
|
45
|
-
- Run `/provider` to add
|
|
45
|
+
- Run `/provider` to select Grok Subscription or add a provider with an API key.
|
|
46
46
|
|
|
47
47
|
Then just type what you want done. Bubble plans, edits files, and runs commands, asking for approval where the current permission mode requires it.
|
|
48
48
|
|
|
@@ -64,10 +64,30 @@ Bubble ships with a catalog of built-in providers. Configure them inside the app
|
|
|
64
64
|
|
|
65
65
|
| How | What it does |
|
|
66
66
|
| --- | --- |
|
|
67
|
-
| `/login` | OAuth sign-in for ChatGPT
|
|
68
|
-
| `/provider` | Open a picker to connect, switch, add, or remove a provider. |
|
|
67
|
+
| `/login` | OAuth sign-in for ChatGPT or Grok Subscription; `/login grok` starts the Grok flow directly. |
|
|
68
|
+
| `/provider` | Open a picker to connect, switch, add, or remove a provider, including Grok Subscription. |
|
|
69
69
|
| `/model` | Pick the active model and reasoning effort. |
|
|
70
70
|
|
|
71
|
+
### Grok Subscription Provider
|
|
72
|
+
|
|
73
|
+
A Grok subscription works as a native model provider, exactly like ChatGPT
|
|
74
|
+
OAuth: sign in once in the browser and the subscription models (`grok-4.5`,
|
|
75
|
+
`grok-composer-2.5-fast`) drive Bubble's own agent loop — Bubble's tools,
|
|
76
|
+
skills, MCP servers, subagents, memory, permission modes, and approval UI all
|
|
77
|
+
apply as usual. `/model` selects the model and reasoning effort.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bubble
|
|
81
|
+
# In the TUI: /login grok
|
|
82
|
+
# or choose Grok Subscription from /provider
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`/login grok` opens the xAI sign-in page in the default browser. If you have
|
|
86
|
+
already signed in with the official Grok CLI, Bubble imports that login and
|
|
87
|
+
skips the browser. Tokens are stored in `~/.bubble/auth.json` alongside other
|
|
88
|
+
provider credentials and refresh automatically. Use `/logout grok` to remove
|
|
89
|
+
this device's login.
|
|
90
|
+
|
|
71
91
|
Built-in providers include OpenAI, Anthropic, Google, DeepSeek, Moonshot (CN and international), Kimi for Coding, Zhipu AI, Z.AI, Alibaba DashScope, Doubao (Volcengine Ark), MiniMax, StepFun, Groq, Together AI, Fireworks, and a `local` profile for any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.).
|
|
72
92
|
|
|
73
93
|
For Doubao Seed models on Volcengine Ark, run `/provider --add doubao` and paste your Ark API key. The built-in endpoint is `https://ark.cn-beijing.volces.com/api/v3` and uses Ark's Responses API. The model picker exposes `minimal`, `low`, `medium`, and `high`, defaulting to `high`; `minimal` disables Ark thinking, while the other levels enable it.
|
|
@@ -95,7 +115,7 @@ For full control — custom base URLs, self-hosted gateways, extra models, or pi
|
|
|
95
115
|
|
|
96
116
|
### Reasoning effort
|
|
97
117
|
|
|
98
|
-
Models that support it expose a reasoning-effort control: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Set it with `/model <id> --reasoning-effort <level>` in the app, or at launch with `--reasoning` (medium) or `--reasoning-effort <level>`.
|
|
118
|
+
Models that support it expose a reasoning-effort control: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `ultra`. Set it with `/model <id> --reasoning-effort <level>` in the app, or at launch with `--reasoning` (medium) or `--reasoning-effort <level>`.
|
|
99
119
|
|
|
100
120
|
## Permission modes
|
|
101
121
|
|
|
@@ -191,7 +211,7 @@ Options (default):
|
|
|
191
211
|
-r, --resume Resume a previous session (latest by default)
|
|
192
212
|
--session <name> Session name to create or resume
|
|
193
213
|
--reasoning Enable reasoning mode at medium effort
|
|
194
|
-
--reasoning-effort <l> Set reasoning effort: off|minimal|low|medium|high|xhigh|max
|
|
214
|
+
--reasoning-effort <l> Set reasoning effort: off|minimal|low|medium|high|xhigh|max|ultra
|
|
195
215
|
--plan Start in plan mode (read-only investigation; propose before executing)
|
|
196
216
|
--dangerously-skip-permissions
|
|
197
217
|
Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
|
|
@@ -7,8 +7,8 @@ export declare class AgentAbortError extends Error {
|
|
|
7
7
|
* message strings.
|
|
8
8
|
*/
|
|
9
9
|
export declare class SubagentAbortError extends AgentAbortError {
|
|
10
|
-
readonly subagentReason: "interrupt" | "user_close"
|
|
11
|
-
constructor(message: string, subagentReason: "interrupt" | "user_close"
|
|
10
|
+
readonly subagentReason: "interrupt" | "user_close";
|
|
11
|
+
constructor(message: string, subagentReason: "interrupt" | "user_close");
|
|
12
12
|
}
|
|
13
13
|
/** Shown when the model produced no user-visible content despite recovery attempts. */
|
|
14
14
|
export declare const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
|
|
@@ -1,61 +1,20 @@
|
|
|
1
1
|
import type { TokenUsage } from "../types.js";
|
|
2
|
-
import type { SubagentTokenCap } from "./subagent-control.js";
|
|
3
2
|
export interface BudgetUsageSource {
|
|
4
3
|
runId: string;
|
|
5
4
|
subAgentId?: string;
|
|
6
5
|
}
|
|
7
|
-
export interface BudgetSnapshot {
|
|
8
|
-
spent: number;
|
|
9
|
-
limit?: number;
|
|
10
|
-
exhausted: boolean;
|
|
11
|
-
}
|
|
12
6
|
/**
|
|
13
7
|
* Shared token ledger for a parent and all of its children, with per-source
|
|
14
|
-
* accounting
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* "pool remaining"; see computeChildTokenCap.
|
|
8
|
+
* accounting. Pure bookkeeping: nothing is ever stopped for token usage —
|
|
9
|
+
* a child's only resource bound is the model context window, absorbed by
|
|
10
|
+
* compaction (design doc §6, revised 2026-07-06).
|
|
18
11
|
*/
|
|
19
12
|
export declare class BudgetLedger {
|
|
20
|
-
private readonly limit?;
|
|
21
13
|
private spent;
|
|
22
14
|
private readonly spentBySource;
|
|
23
|
-
private readonly controller;
|
|
24
|
-
constructor(limit?: number | undefined);
|
|
25
|
-
get signal(): AbortSignal;
|
|
26
15
|
recordUsage(usage: TokenUsage, source: BudgetUsageSource): void;
|
|
27
16
|
/** Tokens attributed to one child (or the parent when subAgentId is omitted). */
|
|
28
17
|
spentBy(subAgentId?: string): number;
|
|
29
|
-
|
|
30
|
-
remaining(): number | undefined;
|
|
31
|
-
get poolLimit(): number | undefined;
|
|
32
|
-
snapshot(): BudgetSnapshot;
|
|
18
|
+
totalSpent(): number;
|
|
33
19
|
}
|
|
34
|
-
/** Default absolute per-child soft cap; applies even on limit-free hosts. */
|
|
35
|
-
export declare const DEFAULT_CHILD_TOKEN_CAP = 200000;
|
|
36
|
-
/** Share of a limited pool reserved for the parent's own turns. */
|
|
37
|
-
export declare const PARENT_POOL_RESERVE_RATIO = 0.2;
|
|
38
|
-
/** Hard cap sits at least this many tokens above the soft cap (≈ 2 turns). */
|
|
39
|
-
export declare const CHILD_HARD_CAP_FLOOR = 20000;
|
|
40
|
-
/**
|
|
41
|
-
* Per-child token cap, fixed at dispatch (design doc §6). The soft cap is an
|
|
42
|
-
* absolute number (config default 200k) so it is effective on limit-free
|
|
43
|
-
* hosts; when the pool *is* limited, the fair share of what remains after the
|
|
44
|
-
* parent's reserve further bounds it. The cap never shrinks mid-run because
|
|
45
|
-
* siblings spawned later.
|
|
46
|
-
*/
|
|
47
|
-
export declare function computeChildTokenCap(options: {
|
|
48
|
-
ledger?: BudgetLedger;
|
|
49
|
-
subAgentId: string;
|
|
50
|
-
activeChildren: number;
|
|
51
|
-
configCap?: number;
|
|
52
|
-
profileMaxTokens?: number;
|
|
53
|
-
}): SubagentTokenCap;
|
|
54
|
-
/**
|
|
55
|
-
* Hard cap recomputed at each turn-boundary check: at least ~2 of this
|
|
56
|
-
* child's average turns above the soft cap, never below the absolute floor
|
|
57
|
-
* (design doc §6 — replaces the fixed 25% ratio that could be smaller than a
|
|
58
|
-
* single turn).
|
|
59
|
-
*/
|
|
60
|
-
export declare function childHardCap(soft: number, avgTurnTokens: number): number;
|
|
61
20
|
export declare function composeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
|
|
@@ -1,96 +1,26 @@
|
|
|
1
1
|
const PARENT_SOURCE_KEY = "__parent__";
|
|
2
2
|
/**
|
|
3
3
|
* Shared token ledger for a parent and all of its children, with per-source
|
|
4
|
-
* accounting
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* "pool remaining"; see computeChildTokenCap.
|
|
4
|
+
* accounting. Pure bookkeeping: nothing is ever stopped for token usage —
|
|
5
|
+
* a child's only resource bound is the model context window, absorbed by
|
|
6
|
+
* compaction (design doc §6, revised 2026-07-06).
|
|
8
7
|
*/
|
|
9
8
|
export class BudgetLedger {
|
|
10
|
-
limit;
|
|
11
9
|
spent = 0;
|
|
12
10
|
spentBySource = new Map();
|
|
13
|
-
controller = new AbortController();
|
|
14
|
-
constructor(limit) {
|
|
15
|
-
this.limit = limit;
|
|
16
|
-
}
|
|
17
|
-
get signal() {
|
|
18
|
-
return this.controller.signal;
|
|
19
|
-
}
|
|
20
11
|
recordUsage(usage, source) {
|
|
21
12
|
const delta = usage.promptTokens + usage.completionTokens;
|
|
22
13
|
this.spent += delta;
|
|
23
14
|
const key = source.subAgentId ?? PARENT_SOURCE_KEY;
|
|
24
15
|
this.spentBySource.set(key, (this.spentBySource.get(key) ?? 0) + delta);
|
|
25
|
-
if (this.limit !== undefined && this.spent >= this.limit && !this.controller.signal.aborted) {
|
|
26
|
-
this.controller.abort(budgetAbortError("Budget exhausted"));
|
|
27
|
-
}
|
|
28
16
|
}
|
|
29
17
|
/** Tokens attributed to one child (or the parent when subAgentId is omitted). */
|
|
30
18
|
spentBy(subAgentId) {
|
|
31
19
|
return this.spentBySource.get(subAgentId ?? PARENT_SOURCE_KEY) ?? 0;
|
|
32
20
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (this.limit === undefined)
|
|
36
|
-
return undefined;
|
|
37
|
-
return Math.max(0, this.limit - this.spent);
|
|
38
|
-
}
|
|
39
|
-
get poolLimit() {
|
|
40
|
-
return this.limit;
|
|
21
|
+
totalSpent() {
|
|
22
|
+
return this.spent;
|
|
41
23
|
}
|
|
42
|
-
snapshot() {
|
|
43
|
-
return {
|
|
44
|
-
spent: this.spent,
|
|
45
|
-
limit: this.limit,
|
|
46
|
-
exhausted: this.limit !== undefined && this.spent >= this.limit,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/** Default absolute per-child soft cap; applies even on limit-free hosts. */
|
|
51
|
-
export const DEFAULT_CHILD_TOKEN_CAP = 200_000;
|
|
52
|
-
/** Share of a limited pool reserved for the parent's own turns. */
|
|
53
|
-
export const PARENT_POOL_RESERVE_RATIO = 0.2;
|
|
54
|
-
/** Hard cap sits at least this many tokens above the soft cap (≈ 2 turns). */
|
|
55
|
-
export const CHILD_HARD_CAP_FLOOR = 20_000;
|
|
56
|
-
/**
|
|
57
|
-
* Per-child token cap, fixed at dispatch (design doc §6). The soft cap is an
|
|
58
|
-
* absolute number (config default 200k) so it is effective on limit-free
|
|
59
|
-
* hosts; when the pool *is* limited, the fair share of what remains after the
|
|
60
|
-
* parent's reserve further bounds it. The cap never shrinks mid-run because
|
|
61
|
-
* siblings spawned later.
|
|
62
|
-
*/
|
|
63
|
-
export function computeChildTokenCap(options) {
|
|
64
|
-
let soft = options.configCap ?? DEFAULT_CHILD_TOKEN_CAP;
|
|
65
|
-
if (options.profileMaxTokens !== undefined && options.profileMaxTokens > 0) {
|
|
66
|
-
soft = Math.min(soft, options.profileMaxTokens);
|
|
67
|
-
}
|
|
68
|
-
const limit = options.ledger?.poolLimit;
|
|
69
|
-
if (options.ledger && limit !== undefined) {
|
|
70
|
-
const reserve = Math.floor(limit * PARENT_POOL_RESERVE_RATIO);
|
|
71
|
-
const available = Math.max(0, (options.ledger.remaining() ?? 0) - reserve);
|
|
72
|
-
const share = Math.floor(available / (options.activeChildren + 1));
|
|
73
|
-
soft = Math.max(1, Math.min(soft, share));
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
soft,
|
|
77
|
-
hard: soft + CHILD_HARD_CAP_FLOOR,
|
|
78
|
-
baseline: options.ledger?.spentBy(options.subAgentId) ?? 0,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Hard cap recomputed at each turn-boundary check: at least ~2 of this
|
|
83
|
-
* child's average turns above the soft cap, never below the absolute floor
|
|
84
|
-
* (design doc §6 — replaces the fixed 25% ratio that could be smaller than a
|
|
85
|
-
* single turn).
|
|
86
|
-
*/
|
|
87
|
-
export function childHardCap(soft, avgTurnTokens) {
|
|
88
|
-
return soft + Math.max(CHILD_HARD_CAP_FLOOR, Math.ceil(avgTurnTokens * 2));
|
|
89
|
-
}
|
|
90
|
-
function budgetAbortError(message) {
|
|
91
|
-
const error = new Error(message);
|
|
92
|
-
error.name = "AbortError";
|
|
93
|
-
return error;
|
|
94
24
|
}
|
|
95
25
|
export function composeAbortSignals(signals) {
|
|
96
26
|
const active = signals.filter((signal) => !!signal);
|
|
@@ -1,18 +1,50 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelTier } from "../model-catalog.js";
|
|
2
|
+
import { type ThinkingLevel } from "../types.js";
|
|
2
3
|
export interface AgentCategoryConfig {
|
|
3
4
|
model?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the model by tier within the parent provider's catalog
|
|
7
|
+
* (docs/model-routing-design.md §3.1). Mutually exclusive with `model`;
|
|
8
|
+
* `model` wins if both are set.
|
|
9
|
+
*/
|
|
10
|
+
tier?: ModelTier | "inherit";
|
|
4
11
|
thinkingLevel?: ThinkingLevel;
|
|
5
12
|
maxConcurrent?: number;
|
|
6
13
|
}
|
|
7
14
|
export type AgentCategoriesConfig = Record<string, AgentCategoryConfig>;
|
|
15
|
+
/**
|
|
16
|
+
* Field-level provenance for the model-determining fields (§3.3): a value is
|
|
17
|
+
* "user" ONLY when the user's config object literally contained that key.
|
|
18
|
+
* thinkingLevel/maxConcurrent overrides carry no routing authority, so a
|
|
19
|
+
* partially-overridden builtin category keeps its builtin tier under the
|
|
20
|
+
* rank guard.
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentCategoryProvenance {
|
|
23
|
+
tierSource?: "builtin" | "user";
|
|
24
|
+
modelSource?: "builtin" | "user";
|
|
25
|
+
}
|
|
26
|
+
export interface MergedAgentCategory {
|
|
27
|
+
config: AgentCategoryConfig;
|
|
28
|
+
provenance: AgentCategoryProvenance;
|
|
29
|
+
}
|
|
8
30
|
export interface ModelRoute {
|
|
9
31
|
providerId: string;
|
|
10
32
|
model: string;
|
|
11
33
|
thinkingLevel: ThinkingLevel;
|
|
12
34
|
}
|
|
35
|
+
/** Which precedence layer decided the model at the category level. */
|
|
36
|
+
export type CategoryModelSource = "builtin-tier" | "user-category";
|
|
37
|
+
/** Which precedence layer decided the FINAL model (§3.4, chain-tracked). */
|
|
38
|
+
export type RouteModelSource = "inherit" | CategoryModelSource | "profile" | "callsite";
|
|
13
39
|
export interface ResolvedSubagentRoute extends ModelRoute {
|
|
14
40
|
category?: string;
|
|
15
41
|
inherited: boolean;
|
|
42
|
+
/** Set when the category layer decided the model (undefined = inherit). */
|
|
43
|
+
categoryModelSource?: CategoryModelSource;
|
|
44
|
+
/** True iff providerId+model equal the parent's — regardless of effort (§3.4). */
|
|
45
|
+
modelInherited?: boolean;
|
|
46
|
+
/** Assigned while applying the precedence chain, never inferred from final values. */
|
|
47
|
+
modelSource?: RouteModelSource;
|
|
16
48
|
}
|
|
17
49
|
export type CategoryResolution = {
|
|
18
50
|
route: ResolvedSubagentRoute;
|
|
@@ -23,10 +55,42 @@ export interface ResolvedModelSelection {
|
|
|
23
55
|
providerId: string;
|
|
24
56
|
model: string | "inherit";
|
|
25
57
|
}
|
|
58
|
+
/** Catalog entry visible to tier resolution (built by routing-catalog.ts). */
|
|
59
|
+
export interface TierCatalogEntry {
|
|
60
|
+
id: string;
|
|
61
|
+
tier?: ModelTier;
|
|
62
|
+
/** models.json user-authored ordering; wins over everything (§3.2). */
|
|
63
|
+
routingPriority?: number;
|
|
64
|
+
/** Index in the builtin catalog order, when the id is builtin. */
|
|
65
|
+
builtinIndex?: number;
|
|
66
|
+
}
|
|
67
|
+
/** Context for rank-guarded automatic tier routing (§3.2). */
|
|
68
|
+
export interface TierRoutingContext {
|
|
69
|
+
/** Tier of the parent's own model, if annotated. */
|
|
70
|
+
parentTier?: ModelTier;
|
|
71
|
+
/** Same-provider catalog the tier is resolved against. */
|
|
72
|
+
models: TierCatalogEntry[];
|
|
73
|
+
/** agentRouting.autoTier kill switch (default true). */
|
|
74
|
+
autoTier: boolean;
|
|
75
|
+
}
|
|
76
|
+
export declare const TIER_RANK: Record<ModelTier, number>;
|
|
26
77
|
export declare function builtinAgentCategories(): AgentCategoriesConfig;
|
|
27
78
|
export declare function mergeAgentCategories(userCategories?: AgentCategoriesConfig): AgentCategoriesConfig;
|
|
79
|
+
/**
|
|
80
|
+
* Field-level merge with provenance (§3.3). The merge itself is identical to
|
|
81
|
+
* mergeAgentCategories; provenance records whether the user literally wrote
|
|
82
|
+
* `tier` / `model`, so builtin tiers flowing through a partial override never
|
|
83
|
+
* masquerade as user intent.
|
|
84
|
+
*/
|
|
85
|
+
export declare function mergeAgentCategoriesWithProvenance(userCategories?: AgentCategoriesConfig): Record<string, MergedAgentCategory>;
|
|
28
86
|
export declare function sanitizeAgentCategories(value: unknown): AgentCategoriesConfig;
|
|
29
|
-
export declare function resolveSubagentRoute(category: string | undefined, parent: ModelRoute, categories?: AgentCategoriesConfig): CategoryResolution;
|
|
87
|
+
export declare function resolveSubagentRoute(category: string | undefined, parent: ModelRoute, categories?: AgentCategoriesConfig, tierContext?: TierRoutingContext): CategoryResolution;
|
|
88
|
+
/**
|
|
89
|
+
* Deterministic within-tier ordering (§3.2): user routingPriority, then
|
|
90
|
+
* builtin catalog order, then normalized id — never a remote response's
|
|
91
|
+
* array order.
|
|
92
|
+
*/
|
|
93
|
+
export declare function selectTierCandidates(models: TierCatalogEntry[], tier: ModelTier): TierCatalogEntry[];
|
|
30
94
|
export declare function resolveModelRoute(model: string | undefined, parentProviderId: string): ResolvedModelSelection;
|
|
31
95
|
export declare function resolveSameProviderModelRoute(model: string | undefined, parentProviderId: string): {
|
|
32
96
|
model: string | "inherit";
|
package/dist/agent/categories.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
"minimal",
|
|
4
|
-
"low",
|
|
5
|
-
"medium",
|
|
6
|
-
"high",
|
|
7
|
-
"xhigh",
|
|
8
|
-
"max",
|
|
9
|
-
]);
|
|
1
|
+
import { THINKING_LEVELS } from "../types.js";
|
|
2
|
+
export const TIER_RANK = { fast: 0, balanced: 1, strong: 2 };
|
|
10
3
|
const BUILTIN_CATEGORIES = {
|
|
11
|
-
quick: { model: "inherit", thinkingLevel: "low", maxConcurrent: 3 },
|
|
4
|
+
quick: { model: "inherit", tier: "fast", thinkingLevel: "low", maxConcurrent: 3 },
|
|
5
|
+
explore: { model: "inherit", tier: "fast", thinkingLevel: "low", maxConcurrent: 3 },
|
|
12
6
|
deep: { model: "inherit", thinkingLevel: "high", maxConcurrent: 2 },
|
|
13
|
-
explore: { model: "inherit", thinkingLevel: "low", maxConcurrent: 3 },
|
|
14
7
|
review: { model: "inherit", thinkingLevel: "high", maxConcurrent: 2 },
|
|
15
8
|
frontend: { model: "inherit", thinkingLevel: "high", maxConcurrent: 1 },
|
|
16
9
|
writing: { model: "inherit", thinkingLevel: "medium", maxConcurrent: 2 },
|
|
@@ -28,6 +21,36 @@ export function mergeAgentCategories(userCategories) {
|
|
|
28
21
|
}
|
|
29
22
|
return merged;
|
|
30
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Field-level merge with provenance (§3.3). The merge itself is identical to
|
|
26
|
+
* mergeAgentCategories; provenance records whether the user literally wrote
|
|
27
|
+
* `tier` / `model`, so builtin tiers flowing through a partial override never
|
|
28
|
+
* masquerade as user intent.
|
|
29
|
+
*/
|
|
30
|
+
export function mergeAgentCategoriesWithProvenance(userCategories) {
|
|
31
|
+
const sanitizedUser = sanitizeAgentCategories(userCategories);
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [name, config] of Object.entries(builtinAgentCategories())) {
|
|
34
|
+
out[name] = {
|
|
35
|
+
config: { ...config },
|
|
36
|
+
provenance: {
|
|
37
|
+
tierSource: config.tier !== undefined ? "builtin" : undefined,
|
|
38
|
+
modelSource: config.model !== undefined && config.model !== "inherit" ? "builtin" : undefined,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
for (const [name, config] of Object.entries(sanitizedUser)) {
|
|
43
|
+
const existing = out[name] ?? { config: {}, provenance: {} };
|
|
44
|
+
out[name] = {
|
|
45
|
+
config: { ...existing.config, ...config },
|
|
46
|
+
provenance: {
|
|
47
|
+
tierSource: config.tier !== undefined ? "user" : existing.provenance.tierSource,
|
|
48
|
+
modelSource: config.model !== undefined ? "user" : existing.provenance.modelSource,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
31
54
|
export function sanitizeAgentCategories(value) {
|
|
32
55
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
33
56
|
return {};
|
|
@@ -41,6 +64,9 @@ export function sanitizeAgentCategories(value) {
|
|
|
41
64
|
if (typeof record.model === "string" && record.model.trim()) {
|
|
42
65
|
config.model = record.model.trim();
|
|
43
66
|
}
|
|
67
|
+
if (isCategoryTier(record.tier)) {
|
|
68
|
+
config.tier = record.tier;
|
|
69
|
+
}
|
|
44
70
|
if (isThinkingLevel(record.thinkingLevel)) {
|
|
45
71
|
config.thinkingLevel = record.thinkingLevel;
|
|
46
72
|
}
|
|
@@ -51,27 +77,99 @@ export function sanitizeAgentCategories(value) {
|
|
|
51
77
|
}
|
|
52
78
|
return out;
|
|
53
79
|
}
|
|
54
|
-
export function resolveSubagentRoute(category, parent, categories) {
|
|
80
|
+
export function resolveSubagentRoute(category, parent, categories, tierContext) {
|
|
55
81
|
const normalizedCategory = normalizeCategoryName(category);
|
|
56
82
|
if (!normalizedCategory) {
|
|
57
83
|
return { route: { ...parent, inherited: true } };
|
|
58
84
|
}
|
|
59
|
-
const merged =
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
85
|
+
const merged = mergeAgentCategoriesWithProvenance(categories);
|
|
86
|
+
const entry = merged[normalizedCategory];
|
|
87
|
+
if (!entry) {
|
|
62
88
|
return { error: `Unknown subagent category "${normalizedCategory}".` };
|
|
63
89
|
}
|
|
90
|
+
const { config, provenance } = entry;
|
|
91
|
+
// Explicit model binding wins over tier (config surface contract §3.1).
|
|
64
92
|
const modelSelection = parseModelSelection(config.model, parent.providerId);
|
|
93
|
+
if (modelSelection.model !== "inherit") {
|
|
94
|
+
return {
|
|
95
|
+
route: {
|
|
96
|
+
category: normalizedCategory,
|
|
97
|
+
providerId: modelSelection.providerId,
|
|
98
|
+
model: modelSelection.model,
|
|
99
|
+
thinkingLevel: config.thinkingLevel ?? parent.thinkingLevel,
|
|
100
|
+
inherited: false,
|
|
101
|
+
categoryModelSource: provenance.modelSource === "user" ? "user-category" : "builtin-tier",
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Tier binding: rank-guarded for builtin-sourced tiers, free for
|
|
106
|
+
// user-authored ones (§3.2).
|
|
107
|
+
const tierModel = resolveTierModel(config.tier, provenance.tierSource, tierContext);
|
|
108
|
+
if (tierModel) {
|
|
109
|
+
return {
|
|
110
|
+
route: {
|
|
111
|
+
category: normalizedCategory,
|
|
112
|
+
providerId: parent.providerId,
|
|
113
|
+
model: tierModel,
|
|
114
|
+
thinkingLevel: config.thinkingLevel ?? parent.thinkingLevel,
|
|
115
|
+
inherited: false,
|
|
116
|
+
categoryModelSource: provenance.tierSource === "user" ? "user-category" : "builtin-tier",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
65
120
|
return {
|
|
66
121
|
route: {
|
|
67
122
|
category: normalizedCategory,
|
|
68
|
-
providerId:
|
|
69
|
-
model:
|
|
123
|
+
providerId: parent.providerId,
|
|
124
|
+
model: parent.model,
|
|
70
125
|
thinkingLevel: config.thinkingLevel ?? parent.thinkingLevel,
|
|
71
|
-
inherited:
|
|
126
|
+
inherited: config.thinkingLevel === undefined,
|
|
72
127
|
},
|
|
73
128
|
};
|
|
74
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Rank-guarded tier -> model resolution (§3.2). Returns undefined whenever
|
|
132
|
+
* the route must inherit: no tier context wired, kill switch off (builtin
|
|
133
|
+
* tiers only), unknown parent tier, missing target tier, or a target that is
|
|
134
|
+
* not strictly cheaper than the parent.
|
|
135
|
+
*/
|
|
136
|
+
function resolveTierModel(tier, tierSource, tierContext) {
|
|
137
|
+
if (!tier || tier === "inherit" || !tierContext)
|
|
138
|
+
return undefined;
|
|
139
|
+
const candidates = selectTierCandidates(tierContext.models, tier);
|
|
140
|
+
if (candidates.length === 0)
|
|
141
|
+
return undefined;
|
|
142
|
+
if (tierSource !== "user") {
|
|
143
|
+
// Builtin-sourced tier: downgrade-only, enforced by rank comparison.
|
|
144
|
+
if (!tierContext.autoTier)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (!tierContext.parentTier)
|
|
147
|
+
return undefined;
|
|
148
|
+
if (TIER_RANK[tier] >= TIER_RANK[tierContext.parentTier])
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return candidates[0].id;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Deterministic within-tier ordering (§3.2): user routingPriority, then
|
|
155
|
+
* builtin catalog order, then normalized id — never a remote response's
|
|
156
|
+
* array order.
|
|
157
|
+
*/
|
|
158
|
+
export function selectTierCandidates(models, tier) {
|
|
159
|
+
return models
|
|
160
|
+
.filter((entry) => entry.tier === tier)
|
|
161
|
+
.sort((a, b) => {
|
|
162
|
+
const priorityA = a.routingPriority ?? Number.POSITIVE_INFINITY;
|
|
163
|
+
const priorityB = b.routingPriority ?? Number.POSITIVE_INFINITY;
|
|
164
|
+
if (priorityA !== priorityB)
|
|
165
|
+
return priorityA - priorityB;
|
|
166
|
+
const builtinA = a.builtinIndex ?? Number.POSITIVE_INFINITY;
|
|
167
|
+
const builtinB = b.builtinIndex ?? Number.POSITIVE_INFINITY;
|
|
168
|
+
if (builtinA !== builtinB)
|
|
169
|
+
return builtinA - builtinB;
|
|
170
|
+
return a.id.localeCompare(b.id);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
75
173
|
export function resolveModelRoute(model, parentProviderId) {
|
|
76
174
|
return parseModelSelection(model, parentProviderId);
|
|
77
175
|
}
|
|
@@ -95,7 +193,10 @@ function parseModelSelection(model, parentProviderId) {
|
|
|
95
193
|
return { providerId: parentProviderId, model };
|
|
96
194
|
}
|
|
97
195
|
function isThinkingLevel(value) {
|
|
98
|
-
return typeof value === "string" && THINKING_LEVELS.
|
|
196
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
197
|
+
}
|
|
198
|
+
function isCategoryTier(value) {
|
|
199
|
+
return value === "fast" || value === "balanced" || value === "strong" || value === "inherit";
|
|
99
200
|
}
|
|
100
201
|
function cloneCategories(categories) {
|
|
101
202
|
return Object.fromEntries(Object.entries(categories).map(([name, config]) => [name, { ...config }]));
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* turn-boundary budget enforcement, the handoff completeness guard, and the
|
|
9
9
|
* mapping of failures to SubagentFinalReason.
|
|
10
10
|
*/
|
|
11
|
-
import { BudgetLedger } from "./budget-ledger.js";
|
|
12
11
|
import type { SubagentRunOutcome } from "./subagent-scheduler.js";
|
|
13
12
|
import type { SubagentFinalReason, SubagentThreadRecord } from "./subagent-control.js";
|
|
14
13
|
import type { AgentEvent, Message, ToolRegistryEntry, ToolResult, ToolUpdate } from "../types.js";
|
|
@@ -24,7 +23,6 @@ export interface ChildRunOptions {
|
|
|
24
23
|
}
|
|
25
24
|
export interface ChildRunnerHost {
|
|
26
25
|
allTools(): ToolRegistryEntry[];
|
|
27
|
-
budgetLedger(): BudgetLedger | undefined;
|
|
28
26
|
emit(record: SubagentThreadRecord, options: ChildRunOptions, status: ToolUpdate["status"], event?: AgentEvent, message?: string): void;
|
|
29
27
|
runLifecycleHook(record: SubagentThreadRecord, cwd: string, eventName: "SubagentStart" | "SubagentStop", status?: string, error?: string, abortSignal?: AbortSignal): Promise<void>;
|
|
30
28
|
finalizeBlocked(record: SubagentThreadRecord, error: string, options: ChildRunOptions): void;
|
|
@@ -47,7 +45,7 @@ export declare function sanitizeSubagentSummary(value: string): string;
|
|
|
47
45
|
* to a trivial question is a complete handoff.
|
|
48
46
|
*/
|
|
49
47
|
export declare function needsExplicitFinalSummary(record: SubagentThreadRecord, executedAnyTool: boolean): boolean;
|
|
50
|
-
export declare function classifySubagentAbortReason(reason: unknown, parentSignal: AbortSignal | undefined
|
|
48
|
+
export declare function classifySubagentAbortReason(reason: unknown, parentSignal: AbortSignal | undefined): SubagentFinalReason;
|
|
51
49
|
/**
|
|
52
50
|
* Drops trailing "[model request interrupted ...]" boundary messages so a
|
|
53
51
|
* rate-limit re-entry resumes from clean history (design §4.5).
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* mapping of failures to SubagentFinalReason.
|
|
10
10
|
*/
|
|
11
11
|
import { AgentAbortError, EMPTY_ASSISTANT_FALLBACK, SubagentAbortError } from "./abort-errors.js";
|
|
12
|
-
import {
|
|
12
|
+
import { composeAbortSignals } from "./budget-ledger.js";
|
|
13
13
|
import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "../provider-artifacts.js";
|
|
14
14
|
import { isRateLimitError } from "../network/errors.js";
|
|
15
15
|
import { isProviderTransportError } from "../network/provider-transport.js";
|
|
@@ -68,13 +68,6 @@ export class ChildRunner {
|
|
|
68
68
|
let turnSummaryBuffer = "";
|
|
69
69
|
let turnHadToolCall = false;
|
|
70
70
|
let executedAnyTool = false;
|
|
71
|
-
// Per-child budget enforcement happens at turn boundaries (design §6):
|
|
72
|
-
// turn_end already carries usage, and a reminder injected here is seen by
|
|
73
|
-
// the very next provider call — unlike chunk-level aborts.
|
|
74
|
-
const cap = record.tokenCap;
|
|
75
|
-
let runTokens = 0;
|
|
76
|
-
let runTurns = 0;
|
|
77
|
-
let budgetSoftWarned = false;
|
|
78
71
|
// Re-entry after a rate limit: the input was applied on attempt 1, so the
|
|
79
72
|
// child history must not gain a second copy, and any stale interruption
|
|
80
73
|
// boundary from the failed call is stripped (design §4.5).
|
|
@@ -110,21 +103,6 @@ export class ChildRunner {
|
|
|
110
103
|
}
|
|
111
104
|
if (event.type === "turn_end" && event.usage) {
|
|
112
105
|
record.usage = mergeUsage(record.usage, event.usage);
|
|
113
|
-
runTokens += event.usage.promptTokens + event.usage.completionTokens;
|
|
114
|
-
runTurns += 1;
|
|
115
|
-
if (cap) {
|
|
116
|
-
if (!budgetSoftWarned && runTokens >= cap.soft) {
|
|
117
|
-
budgetSoftWarned = true;
|
|
118
|
-
// The hard cap is fixed when the warning fires: soft + ~2 of
|
|
119
|
-
// this child's average turns (absolute floor), so the child
|
|
120
|
-
// gets a real chance to wrap up before the kill (design §6).
|
|
121
|
-
cap.hard = childHardCap(cap.soft, runTokens / Math.max(1, runTurns));
|
|
122
|
-
subAgent.injectSystemReminder(buildChildBudgetWrapUpReminder(runTokens, cap.soft));
|
|
123
|
-
}
|
|
124
|
-
else if (budgetSoftWarned && runTokens >= cap.hard) {
|
|
125
|
-
record.abortController.abort(new SubagentAbortError(`Subagent ${record.agentId} exceeded its hard token cap (${cap.hard}).`, "budget"));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
106
|
}
|
|
129
107
|
if (event.type === "turn_end") {
|
|
130
108
|
const turnSummary = stripProviderProtocolArtifacts(turnSummaryBuffer).trim();
|
|
@@ -176,7 +154,7 @@ export class ChildRunner {
|
|
|
176
154
|
const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
|
|
177
155
|
record.status = cancelled ? "cancelled" : "failed";
|
|
178
156
|
record.finalReason = cancelled
|
|
179
|
-
? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal
|
|
157
|
+
? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal)
|
|
180
158
|
: "failed_transient";
|
|
181
159
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
182
160
|
record.error = error?.message || String(error);
|
|
@@ -267,23 +245,21 @@ export function needsExplicitFinalSummary(record, executedAnyTool) {
|
|
|
267
245
|
return false;
|
|
268
246
|
if (record.summary === EMPTY_ASSISTANT_FALLBACK)
|
|
269
247
|
return true;
|
|
270
|
-
|
|
248
|
+
// A schema-bearing child's handoff is a compact JSON value; short is
|
|
249
|
+
// complete by construction, and a restate turn would risk breaking the JSON.
|
|
250
|
+
if (!record.expectsStructuredOutput && estimateHandoffTokens(record.summary) < HANDOFF_TOKEN_FLOOR)
|
|
271
251
|
return true;
|
|
272
252
|
return isIntermediateHandoff(record.summary);
|
|
273
253
|
}
|
|
274
|
-
export function classifySubagentAbortReason(reason, parentSignal
|
|
254
|
+
export function classifySubagentAbortReason(reason, parentSignal) {
|
|
275
255
|
if (reason instanceof SubagentAbortError) {
|
|
276
256
|
switch (reason.subagentReason) {
|
|
277
257
|
case "interrupt":
|
|
278
258
|
return "cancelled_interrupt";
|
|
279
259
|
case "user_close":
|
|
280
260
|
return "cancelled_user";
|
|
281
|
-
case "budget":
|
|
282
|
-
return "cancelled_budget";
|
|
283
261
|
}
|
|
284
262
|
}
|
|
285
|
-
if (ledger?.snapshot().exhausted)
|
|
286
|
-
return "cancelled_budget";
|
|
287
263
|
if (parentSignal?.aborted)
|
|
288
264
|
return "cancelled_parent_abort";
|
|
289
265
|
return "cancelled_user";
|
|
@@ -302,13 +278,6 @@ export function stripTrailingModelInterruptedBoundary(messages) {
|
|
|
302
278
|
break;
|
|
303
279
|
}
|
|
304
280
|
}
|
|
305
|
-
function buildChildBudgetWrapUpReminder(spentTokens, softCap) {
|
|
306
|
-
return [
|
|
307
|
-
`Token budget notice: this subagent has used ~${Math.round(spentTokens)} tokens, crossing its ${softCap}-token budget.`,
|
|
308
|
-
"Wrap up now: stop opening new lines of investigation and produce your complete final handoff",
|
|
309
|
-
"(findings, conclusions, unfinished items) in your next message.",
|
|
310
|
-
].join(" ");
|
|
311
|
-
}
|
|
312
281
|
export function summarizeSubagentToolEnd(event) {
|
|
313
282
|
const metadata = (event.result.metadata ?? {});
|
|
314
283
|
const reason = readString(metadata.reason);
|