@bubblebrain-ai/bubble 0.0.35 → 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/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -0
- package/dist/agent/profiles.js +25 -11
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/workflow/control.d.ts +14 -0
- package/dist/agent/workflow/control.js +30 -9
- package/dist/agent.d.ts +75 -5
- package/dist/agent.js +313 -61
- 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 +4 -0
- package/dist/config.js +4 -0
- 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 +105 -27
- 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 +87 -41
- 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 +17 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.js +22 -3
- 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 +19 -0
- package/dist/prompt/task-reminders.js +49 -0
- 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 +28 -5
- 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.js +9 -11
- 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 +648 -232
- package/dist/tui-ink/approval/approval-dialog.js +36 -0
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- 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 +35 -1
- package/dist/tui-ink/input-box.js +215 -122
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +453 -123
- 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/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +31 -0
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/types.d.ts +5 -5
- 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 +9 -0
- package/dist/variant/variant-resolver.js +17 -0
- package/package.json +2 -1
- 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)
|
|
@@ -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 }]));
|
|
@@ -46,7 +46,7 @@ export class DiscoveryBarrier {
|
|
|
46
46
|
return undefined;
|
|
47
47
|
return {
|
|
48
48
|
content: `Blocked speculative ${target.kind}. The path "${target.path}" has not been discovered in this repository context ` +
|
|
49
|
-
"and was not explicitly requested by the user. Run glob/grep
|
|
49
|
+
"and was not explicitly requested by the user. Run glob/grep discovery first, then read only returned paths. " +
|
|
50
50
|
"Do not infer from this blocked call whether the path exists.",
|
|
51
51
|
isError: true,
|
|
52
52
|
status: "blocked",
|
package/dist/agent/profiles.d.ts
CHANGED
|
@@ -67,3 +67,4 @@ export declare function allowedToolEffectsForMode(mode: AgentProfileMode): Set<s
|
|
|
67
67
|
export declare function selectToolsForAgentProfile(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): ToolRegistryEntry[];
|
|
68
68
|
export declare function validateAgentProfileTools(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): AgentProfileDiagnostic[];
|
|
69
69
|
export declare function mergeUsage(current: SubagentRunResult["usage"], usage: TokenUsage): SubagentRunResult["usage"];
|
|
70
|
+
export declare function canonicalToolName(name: string): string;
|
package/dist/agent/profiles.js
CHANGED
|
@@ -10,14 +10,12 @@ const READONLY_PRESET = [
|
|
|
10
10
|
"lsp",
|
|
11
11
|
"web_search",
|
|
12
12
|
"web_fetch",
|
|
13
|
-
"
|
|
14
|
-
"memory_read_summary",
|
|
13
|
+
"memory",
|
|
15
14
|
"skill",
|
|
16
15
|
"todo_write",
|
|
17
16
|
];
|
|
18
17
|
const SUBAGENT_DENY_TOOLS = new Set([
|
|
19
18
|
"subagent",
|
|
20
|
-
"task",
|
|
21
19
|
"spawn_agent",
|
|
22
20
|
"wait_agent",
|
|
23
21
|
"send_input",
|
|
@@ -126,12 +124,12 @@ export function builtinAgentProfiles() {
|
|
|
126
124
|
"You are an explorer subagent for codebase reconnaissance.",
|
|
127
125
|
"Answer the specific question by inspecting the repository directly. Prefer precise file paths and line-level evidence.",
|
|
128
126
|
"Keep the answer compact and avoid broad refactors or implementation plans unless asked.",
|
|
129
|
-
].join("\n"), ["read", "glob", "grep", "lsp", "
|
|
127
|
+
].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill", "todo_write"]),
|
|
130
128
|
roleProfile("worker", "Implementation-planning worker subagent", [
|
|
131
129
|
"You are a worker subagent. In this Phase 1 runtime you are read-only, so you must not modify files.",
|
|
132
130
|
"Analyze the assigned implementation slice, identify exact files to change, and return a concrete patch plan or findings.",
|
|
133
131
|
"If write-capable worker mode is needed, say so explicitly.",
|
|
134
|
-
].join("\n"), ["read", "glob", "grep", "lsp", "
|
|
132
|
+
].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill", "todo_write"]),
|
|
135
133
|
toProfile("search"),
|
|
136
134
|
toProfile("security_investigation"),
|
|
137
135
|
toProfile("evidence_correlation"),
|
|
@@ -167,7 +165,7 @@ export function allowedToolEffectsForMode(mode) {
|
|
|
167
165
|
: new Set(["read"]);
|
|
168
166
|
}
|
|
169
167
|
export function selectToolsForAgentProfile(tools, profile, approval = profile.approval) {
|
|
170
|
-
const explicitInclude = new Set(profile.tools.include ?? []);
|
|
168
|
+
const explicitInclude = new Set((profile.tools.include ?? []).map(canonicalToolName));
|
|
171
169
|
const selected = requestedToolNames(profile);
|
|
172
170
|
for (const tool of SUBAGENT_DENY_TOOLS)
|
|
173
171
|
selected.delete(tool);
|
|
@@ -183,13 +181,17 @@ export function selectToolsForAgentProfile(tools, profile, approval = profile.ap
|
|
|
183
181
|
continue;
|
|
184
182
|
if (approval === "disabled" && tool.requiresApproval)
|
|
185
183
|
continue;
|
|
186
|
-
|
|
184
|
+
// Explicit include is the profile author pre-unlocking the tool: the
|
|
185
|
+
// child is a fresh Agent with its own (empty) unlock set, so a deferred
|
|
186
|
+
// flag surviving here would re-lock the tool inside the child.
|
|
187
|
+
const entry = tool.deferred ? { ...tool, deferred: false } : tool;
|
|
188
|
+
out.push(wrapApprovalFailTool(entry, approval));
|
|
187
189
|
}
|
|
188
190
|
return out;
|
|
189
191
|
}
|
|
190
192
|
export function validateAgentProfileTools(tools, profile, approval = profile.approval) {
|
|
191
193
|
const available = new Map(tools.map((tool) => [tool.name, tool]));
|
|
192
|
-
const explicitInclude = new Set(profile.tools.include ?? []);
|
|
194
|
+
const explicitInclude = new Set((profile.tools.include ?? []).map(canonicalToolName));
|
|
193
195
|
const allowedEffects = allowedToolEffectsForMode(profile.mode);
|
|
194
196
|
const diagnostics = [];
|
|
195
197
|
for (const name of requestedToolNames(profile)) {
|
|
@@ -256,6 +258,18 @@ function wrapApprovalFailTool(tool, approval) {
|
|
|
256
258
|
}),
|
|
257
259
|
};
|
|
258
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Renamed/merged tools keep working for user-defined profiles written
|
|
263
|
+
* against the old names (memory_search/memory_read_summary merged into
|
|
264
|
+
* "memory", 2026-07-13).
|
|
265
|
+
*/
|
|
266
|
+
const LEGACY_TOOL_ALIASES = {
|
|
267
|
+
memory_search: "memory",
|
|
268
|
+
memory_read_summary: "memory",
|
|
269
|
+
};
|
|
270
|
+
export function canonicalToolName(name) {
|
|
271
|
+
return LEGACY_TOOL_ALIASES[name] ?? name;
|
|
272
|
+
}
|
|
259
273
|
function requestedToolNames(profile) {
|
|
260
274
|
const selected = new Set();
|
|
261
275
|
if (profile.tools.preset === "readonly") {
|
|
@@ -264,12 +278,12 @@ function requestedToolNames(profile) {
|
|
|
264
278
|
}
|
|
265
279
|
else if (profile.tools.preset === "explicit") {
|
|
266
280
|
for (const tool of profile.tools.include ?? [])
|
|
267
|
-
selected.add(tool);
|
|
281
|
+
selected.add(canonicalToolName(tool));
|
|
268
282
|
}
|
|
269
283
|
for (const tool of profile.tools.include ?? [])
|
|
270
|
-
selected.add(tool);
|
|
284
|
+
selected.add(canonicalToolName(tool));
|
|
271
285
|
for (const tool of profile.tools.exclude ?? [])
|
|
272
|
-
selected.delete(tool);
|
|
286
|
+
selected.delete(canonicalToolName(tool));
|
|
273
287
|
return selected;
|
|
274
288
|
}
|
|
275
289
|
function loadProfilesFromDir(dir, source, diagnostics) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing catalog: the single source of catalog truth for subagent model
|
|
3
|
+
* routing (docs/model-routing-design.md §1).
|
|
4
|
+
*
|
|
5
|
+
* No routing consumer reads `listBuiltinModels` / `getConfigured` directly —
|
|
6
|
+
* neither reflects what is actually runnable. Consumers read a
|
|
7
|
+
* `RoutingSnapshot` through the live accessor created here; the accessor
|
|
8
|
+
* caches by the registry's routing revision and rebuilds lazily, so tier
|
|
9
|
+
* resolution, validation, the detector, and the menu are never stale (§1.5).
|
|
10
|
+
*/
|
|
11
|
+
import { type ModelTier } from "../model-catalog.js";
|
|
12
|
+
import type { ProviderRegistry } from "../provider-registry.js";
|
|
13
|
+
import { selectTierCandidates, type AgentCategoriesConfig, type TierRoutingContext } from "./categories.js";
|
|
14
|
+
import type { ThinkingLevel } from "../types.js";
|
|
15
|
+
export interface AgentRoutingConfig {
|
|
16
|
+
/** Automatic builtin-tier downgrade routing (§3.2). */
|
|
17
|
+
autoTier: boolean;
|
|
18
|
+
/** Call-site cross-provider routing; default open, set false to lock (§7.2). */
|
|
19
|
+
allowCrossProvider: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_AGENT_ROUTING: AgentRoutingConfig;
|
|
22
|
+
export declare function sanitizeAgentRouting(value: unknown): AgentRoutingConfig;
|
|
23
|
+
export interface RoutingModelEntry {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
tier?: ModelTier;
|
|
27
|
+
routingPriority?: number;
|
|
28
|
+
/** Index in the builtin catalog order, when the id is builtin (§3.2 tiebreak). */
|
|
29
|
+
builtinIndex?: number;
|
|
30
|
+
source: "builtin" | "dynamic" | "custom";
|
|
31
|
+
}
|
|
32
|
+
export type RoutingMembershipSource = "custom-allowlist" | "complete-discovery" | "fallback-union";
|
|
33
|
+
export interface RoutingSnapshot {
|
|
34
|
+
parent: {
|
|
35
|
+
providerId: string;
|
|
36
|
+
model: string;
|
|
37
|
+
tier?: ModelTier;
|
|
38
|
+
};
|
|
39
|
+
/** Catalog-effective provider id (openai OAuth -> "openai-codex" alias). */
|
|
40
|
+
effectiveProviderId: string;
|
|
41
|
+
membershipSource: RoutingMembershipSource;
|
|
42
|
+
/** Model list for the PARENT provider only (§1.2 scope note). */
|
|
43
|
+
models: RoutingModelEntry[];
|
|
44
|
+
/** True iff membershipSource === "custom-allowlist" — the only catalog
|
|
45
|
+
* that hard-rejects unknown ids (§1.4). */
|
|
46
|
+
authoritative: boolean;
|
|
47
|
+
registryRevision: number;
|
|
48
|
+
/**
|
|
49
|
+
* Earliest expiry of the discovery data this snapshot consumed. TTL expiry
|
|
50
|
+
* is passive — it bumps no revision — so the accessor must rebuild past
|
|
51
|
+
* this instant or it serves a catalog the registry itself no longer would.
|
|
52
|
+
*/
|
|
53
|
+
discoveryExpiresAt?: number;
|
|
54
|
+
/** Provider ids with active credentials (getEnabled, §1.1). */
|
|
55
|
+
runnableProviderIds: string[];
|
|
56
|
+
/** Post-merge, post-tier-resolution category bindings (§3). */
|
|
57
|
+
resolvedCategories: Array<{
|
|
58
|
+
name: string;
|
|
59
|
+
model: string | "inherit";
|
|
60
|
+
thinkingLevel?: ThinkingLevel;
|
|
61
|
+
tierSource?: "builtin" | "user";
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
export type RoutingSnapshotAccessor = (parent: {
|
|
65
|
+
providerId: string;
|
|
66
|
+
model: string;
|
|
67
|
+
}) => RoutingSnapshot;
|
|
68
|
+
/**
|
|
69
|
+
* Membership versus metadata (§1.3): an authoritative source alone decides
|
|
70
|
+
* which ids exist; lower-priority sources only enrich per-field metadata
|
|
71
|
+
* (tier: dynamic ?? custom ?? builtin).
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildRoutingSnapshot(registry: ProviderRegistry, parent: {
|
|
74
|
+
providerId: string;
|
|
75
|
+
model: string;
|
|
76
|
+
}, agentCategories: AgentCategoriesConfig, agentRouting: AgentRoutingConfig): RoutingSnapshot;
|
|
77
|
+
/** Tier-resolution context derived from a snapshot (consumed by categories §3.2). */
|
|
78
|
+
export declare function tierContextFromSnapshot(snapshot: RoutingSnapshot, agentRouting: AgentRoutingConfig): TierRoutingContext;
|
|
79
|
+
/**
|
|
80
|
+
* Live accessor (§1.5): caches by (routing revision, parent route) and
|
|
81
|
+
* rebuilds lazily, so a discovery completion or provider mutation is
|
|
82
|
+
* reflected at the very next read. Building is synchronous over cached
|
|
83
|
+
* registry state — microseconds, never I/O.
|
|
84
|
+
*/
|
|
85
|
+
export declare function createRoutingSnapshotAccessor(registry: ProviderRegistry, getAgentCategories: () => AgentCategoriesConfig, getAgentRouting: () => AgentRoutingConfig): RoutingSnapshotAccessor;
|
|
86
|
+
/** Tier of a model within a snapshot, when known. */
|
|
87
|
+
export declare function snapshotModelTier(snapshot: RoutingSnapshot, modelId: string): ModelTier | undefined;
|
|
88
|
+
export interface RoutableModelEntry {
|
|
89
|
+
/** Routable provider id — what goes before the colon in provider:model. */
|
|
90
|
+
providerId: string;
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
}
|
|
94
|
+
export type RoutableModelIndex = () => RoutableModelEntry[];
|
|
95
|
+
/** Revision-cached index over all runnable providers' catalogs. */
|
|
96
|
+
export declare function createRoutableModelIndex(registry: ProviderRegistry): RoutableModelIndex;
|
|
97
|
+
/** Loose normalization for model-name matching: case/punctuation-insensitive. */
|
|
98
|
+
export declare function normalizeModelToken(value: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Near-match candidates for a (probably mistyped) model id within one
|
|
101
|
+
* provider's catalog. "suggest" matches normalized prefixes in either
|
|
102
|
+
* direction (soft "did you mean" notes). "truncation" matches only inputs
|
|
103
|
+
* the catalog EXTENDS ("gpt-5.6" -> gpt-5.6-sol/-terra/-luna) — the one
|
|
104
|
+
* direction that is typo evidence. An input extending a catalog id
|
|
105
|
+
* (gpt-5.6-sol-20260701 vs cataloged gpt-5.6-sol) is more likely a newly
|
|
106
|
+
* released variant the local catalog lags on, and must not be treated as a
|
|
107
|
+
* typo by hard-reject paths. Capped, deterministic order.
|
|
108
|
+
*/
|
|
109
|
+
export declare function nearModelMatches(input: string, candidates: RoutableModelEntry[], options?: {
|
|
110
|
+
limit?: number;
|
|
111
|
+
mode?: "suggest" | "truncation";
|
|
112
|
+
}): string[];
|
|
113
|
+
export { selectTierCandidates };
|