@bubblebrain-ai/bubble 0.0.21 → 0.0.23
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 +197 -34
- package/dist/agent/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/internal-reminder-sanitizer.js +29 -9
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +63 -5
- package/dist/agent.js +360 -287
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +1 -0
- package/dist/main.js +38 -2
- package/dist/model-catalog.js +6 -0
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider-transform.js +14 -0
- package/dist/provider.js +23 -3
- package/dist/slash-commands/commands.js +29 -2
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/index.js +1 -1
- package/dist/tui/run.d.ts +17 -1
- package/dist/tui/run.js +155 -10
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/trace-groups.js +41 -5
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- package/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/dist/update/index.d.ts +18 -4
- package/dist/update/index.js +41 -19
- package/package.json +1 -1
|
@@ -49,7 +49,9 @@ export class PermissionAwareApprovalController {
|
|
|
49
49
|
if (mode === "default" && (req.type === "edit" || req.type === "write" || req.type === "patch")) {
|
|
50
50
|
return finalize({ action: "approve" });
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
// Project profile trust is a user decision, not a destructive action:
|
|
53
|
+
// spawn_agent is legal in plan mode, so the gate prompts there too.
|
|
54
|
+
if (mode === "plan" && req.type !== "agent_profile") {
|
|
53
55
|
return finalize({
|
|
54
56
|
action: "reject",
|
|
55
57
|
feedback: "Plan mode is active. Do not call destructive tools directly — propose your changes via exit_plan_mode and wait for user approval.",
|
|
@@ -83,6 +85,8 @@ export class PermissionAwareApprovalController {
|
|
|
83
85
|
return { tool: "Edit", path: req.path, cwd: this.options.cwd };
|
|
84
86
|
case "lsp":
|
|
85
87
|
return { tool: "Lsp", path: req.path, cwd: this.options.cwd };
|
|
88
|
+
case "agent_profile":
|
|
89
|
+
return { tool: "AgentProfile" };
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
checkRequestRules(req) {
|
|
@@ -170,6 +174,8 @@ function approvalTarget(req) {
|
|
|
170
174
|
return "Edit";
|
|
171
175
|
case "lsp":
|
|
172
176
|
return "Lsp";
|
|
177
|
+
case "agent_profile":
|
|
178
|
+
return "AgentProfile";
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
181
|
function summarizeApprovalRequest(req) {
|
|
@@ -184,5 +190,7 @@ function summarizeApprovalRequest(req) {
|
|
|
184
190
|
return { type: req.type, path: req.path, paths: req.paths, files: req.files, diffLength: req.diff.length };
|
|
185
191
|
case "lsp":
|
|
186
192
|
return { type: req.type, path: req.path, operation: req.operation };
|
|
193
|
+
case "agent_profile":
|
|
194
|
+
return { type: req.type, name: req.name, path: req.path, contentHash: req.contentHash };
|
|
187
195
|
}
|
|
188
196
|
}
|
package/dist/approval/types.d.ts
CHANGED
|
@@ -45,7 +45,23 @@ export interface LspApprovalRequest {
|
|
|
45
45
|
path: string;
|
|
46
46
|
operation: string;
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Trust gate for project-local agent profiles (.bubble/agents). The user —
|
|
50
|
+
* not the model — decides whether a repository's profile prompt may drive a
|
|
51
|
+
* subagent; approvals are remembered per content hash for the session.
|
|
52
|
+
*/
|
|
53
|
+
export interface AgentProfileApprovalRequest {
|
|
54
|
+
type: "agent_profile";
|
|
55
|
+
/** Profile name as referenced by spawn_agent. */
|
|
56
|
+
name: string;
|
|
57
|
+
/** Absolute path of the profile file inside the repository. */
|
|
58
|
+
path: string;
|
|
59
|
+
/** Content hash; a changed file re-prompts. */
|
|
60
|
+
contentHash: string;
|
|
61
|
+
/** First lines of the profile prompt so the user can judge it. */
|
|
62
|
+
promptPreview: string;
|
|
63
|
+
}
|
|
64
|
+
export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | PatchApprovalRequest | BashApprovalRequest | LspApprovalRequest | AgentProfileApprovalRequest;
|
|
49
65
|
export type ApprovalDecision = {
|
|
50
66
|
action: "approve";
|
|
51
67
|
feedback?: string;
|
package/dist/config.d.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface UserConfigData {
|
|
|
30
30
|
providers?: ProviderProfile[];
|
|
31
31
|
defaultProvider?: string;
|
|
32
32
|
agentCategories?: AgentCategoriesConfig;
|
|
33
|
+
subagents?: SubagentsUserConfig;
|
|
34
|
+
}
|
|
35
|
+
export interface SubagentsUserConfig {
|
|
36
|
+
/** Global cap on concurrently running children. Default 8. */
|
|
37
|
+
maxActiveSubagents?: number;
|
|
38
|
+
/** Absolute per-child soft token cap. Default 200000. */
|
|
39
|
+
childTokenCap?: number;
|
|
33
40
|
}
|
|
34
41
|
export declare class UserConfig {
|
|
35
42
|
private data;
|
|
@@ -56,6 +63,7 @@ export declare class UserConfig {
|
|
|
56
63
|
setThemeMode(mode: ThemeMode): void;
|
|
57
64
|
setThemeOverrides(overrides: Record<string, string>): void;
|
|
58
65
|
getAgentCategories(): AgentCategoriesConfig;
|
|
66
|
+
getSubagents(): SubagentsUserConfig;
|
|
59
67
|
}
|
|
60
68
|
/** Mask an API key for safe display. */
|
|
61
69
|
export declare function maskKey(key: string): string;
|
package/dist/config.js
CHANGED
|
@@ -37,6 +37,19 @@ function sanitizeDefaultModel(model) {
|
|
|
37
37
|
function sanitizeDefaultProvider(providerId) {
|
|
38
38
|
return isHiddenProviderId(providerId) ? undefined : providerId;
|
|
39
39
|
}
|
|
40
|
+
function sanitizeSubagentsConfig(value) {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
42
|
+
return undefined;
|
|
43
|
+
const raw = value;
|
|
44
|
+
const out = {};
|
|
45
|
+
if (typeof raw.maxActiveSubagents === "number" && Number.isFinite(raw.maxActiveSubagents)) {
|
|
46
|
+
out.maxActiveSubagents = Math.max(1, Math.floor(raw.maxActiveSubagents));
|
|
47
|
+
}
|
|
48
|
+
if (typeof raw.childTokenCap === "number" && Number.isFinite(raw.childTokenCap)) {
|
|
49
|
+
out.childTokenCap = Math.max(1_000, Math.floor(raw.childTokenCap));
|
|
50
|
+
}
|
|
51
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
52
|
+
}
|
|
40
53
|
function sanitizeTheme(value) {
|
|
41
54
|
if (value == null)
|
|
42
55
|
return undefined;
|
|
@@ -89,6 +102,7 @@ export class UserConfig {
|
|
|
89
102
|
providers: sanitizeProviders(parsed.providers),
|
|
90
103
|
defaultProvider: sanitizeDefaultProvider(parsed.defaultProvider),
|
|
91
104
|
agentCategories: sanitizeAgentCategories(parsed.agentCategories),
|
|
105
|
+
subagents: sanitizeSubagentsConfig(parsed.subagents),
|
|
92
106
|
theme: sanitizeTheme(parsed.theme),
|
|
93
107
|
};
|
|
94
108
|
}
|
|
@@ -188,6 +202,9 @@ export class UserConfig {
|
|
|
188
202
|
getAgentCategories() {
|
|
189
203
|
return sanitizeAgentCategories(this.data.agentCategories);
|
|
190
204
|
}
|
|
205
|
+
getSubagents() {
|
|
206
|
+
return sanitizeSubagentsConfig(this.data.subagents) ?? {};
|
|
207
|
+
}
|
|
191
208
|
}
|
|
192
209
|
/** Mask an API key for safe display. */
|
|
193
210
|
export function maskKey(key) {
|
|
@@ -46,6 +46,15 @@ export function formatApprovalRequest(req) {
|
|
|
46
46
|
title: `LSP 操作 (${req.operation})`,
|
|
47
47
|
body: `**path:** \`${truncate(req.path, PATH_PREVIEW_MAX)}\``,
|
|
48
48
|
};
|
|
49
|
+
case "agent_profile":
|
|
50
|
+
return {
|
|
51
|
+
title: `使用项目 agent profile "${req.name}"`,
|
|
52
|
+
body: [
|
|
53
|
+
`**path:** \`${truncate(req.path, PATH_PREVIEW_MAX)}\``,
|
|
54
|
+
`\n**prompt preview:**\n\`\`\`\n${truncate(req.promptPreview, CONTENT_PREVIEW_MAX)}\n\`\`\``,
|
|
55
|
+
"\n该 profile 来自仓库本地 `.bubble/agents`,其 prompt 会驱动一个子代理。仅在信任该仓库时批准。",
|
|
56
|
+
].join("\n"),
|
|
57
|
+
};
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
function truncate(s, max) {
|
|
@@ -140,6 +140,7 @@ export class RunDriver {
|
|
|
140
140
|
memoryPrompt,
|
|
141
141
|
fileStateTracker,
|
|
142
142
|
agentCategories: this.opts.deps.userConfig.getAgentCategories(),
|
|
143
|
+
subagents: this.opts.deps.userConfig.getSubagents(),
|
|
143
144
|
providerFactory: (route) => this.opts.deps.createProviderForRoute(route, promptCacheKey),
|
|
144
145
|
externalHooks: hookController,
|
|
145
146
|
});
|
package/dist/main.js
CHANGED
|
@@ -380,6 +380,7 @@ async function main() {
|
|
|
380
380
|
memoryPrompt,
|
|
381
381
|
fileStateTracker,
|
|
382
382
|
agentCategories: userConfig.getAgentCategories(),
|
|
383
|
+
subagents: userConfig.getSubagents(),
|
|
383
384
|
providerFactory: createProviderForRoute,
|
|
384
385
|
externalHooks: hookController,
|
|
385
386
|
});
|
|
@@ -511,8 +512,41 @@ async function main() {
|
|
|
511
512
|
else {
|
|
512
513
|
detectedTheme = themeConfig.mode;
|
|
513
514
|
}
|
|
515
|
+
// In-place session switch for the /session picker: rebind every closure
|
|
516
|
+
// that persists to the session (onMessageAppend, markers, title updater)
|
|
517
|
+
// by reassigning the outer `sessionManager`, then replace the agent's
|
|
518
|
+
// history the same way startup resume does.
|
|
519
|
+
const switchSession = (sessionFile) => {
|
|
520
|
+
try {
|
|
521
|
+
const next = new SessionManager(sessionFile);
|
|
522
|
+
const history = next.getMessages();
|
|
523
|
+
sessionManager = next;
|
|
524
|
+
sessionPromptCacheKey = next.getOrCreatePromptCacheKey();
|
|
525
|
+
sessionTitleUpdater = createSessionTitleUpdater({
|
|
526
|
+
sessionManager: next,
|
|
527
|
+
complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
|
|
528
|
+
});
|
|
529
|
+
next.updateMetadata({
|
|
530
|
+
...(agent.model ? { model: agent.model } : {}),
|
|
531
|
+
cwd: args.cwd,
|
|
532
|
+
thinkingLevel: agent.thinking,
|
|
533
|
+
reasoningEffort: agent.thinking,
|
|
534
|
+
});
|
|
535
|
+
// Keep the live system/meta head (mode reminders survive the switch),
|
|
536
|
+
// mirroring the /rewind history-replacement pattern.
|
|
537
|
+
const head = agent.messages.filter((m) => m.role === "system" || m.role === "meta");
|
|
538
|
+
agent.messages = [...head, ...history];
|
|
539
|
+
agent.setTodos(next.getTodos());
|
|
540
|
+
agent.resetContextUsageAnchor();
|
|
541
|
+
return { manager: next };
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
545
|
+
}
|
|
546
|
+
};
|
|
514
547
|
const commonOptions = {
|
|
515
548
|
sessionManager,
|
|
549
|
+
switchSession,
|
|
516
550
|
createProvider,
|
|
517
551
|
registry,
|
|
518
552
|
skillRegistry,
|
|
@@ -529,8 +563,9 @@ async function main() {
|
|
|
529
563
|
runMemorySummary,
|
|
530
564
|
runMemoryRefresh,
|
|
531
565
|
};
|
|
532
|
-
const {
|
|
533
|
-
const
|
|
566
|
+
const { startStartupUpdateCheck } = await import("./update/index.js");
|
|
567
|
+
const updateCheck = await startStartupUpdateCheck();
|
|
568
|
+
const updateNotice = updateCheck.notice;
|
|
534
569
|
// Two explicit branches (not a dynamic ternary import) so TypeScript
|
|
535
570
|
// checks each renderer's RunTuiOptions shape independently.
|
|
536
571
|
let exitWallMs;
|
|
@@ -543,6 +578,7 @@ async function main() {
|
|
|
543
578
|
detectedTheme,
|
|
544
579
|
onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
|
|
545
580
|
updateNotice: updateNotice ?? undefined,
|
|
581
|
+
updateNoticeRefresh: updateCheck.refreshed,
|
|
546
582
|
});
|
|
547
583
|
}
|
|
548
584
|
else {
|
package/dist/model-catalog.js
CHANGED
|
@@ -28,6 +28,9 @@ const GPT51_CODEX_MAX_LEVELS = ["off", "low", "medium", "high", "xhigh"];
|
|
|
28
28
|
const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
|
|
29
29
|
const OPENAI_CHAT_LEVELS = ["off"];
|
|
30
30
|
const TOGGLE_THINKING_LEVELS = ["off", "medium"];
|
|
31
|
+
// kimi-k2.7-code only supports thinking mode (disabling it errors), so "off" is
|
|
32
|
+
// not offered — the model is always in its thinking variant.
|
|
33
|
+
const KIMI_THINKING_ONLY_LEVELS = ["medium"];
|
|
31
34
|
const DEEPSEEK_V4_LEVELS = ["high", "max"];
|
|
32
35
|
const STEPFUN_REASONING_LEVELS = ["off", "low", "medium", "high"];
|
|
33
36
|
const MINIMAX_M3_REASONING_LEVELS = ["off", "medium"];
|
|
@@ -105,18 +108,21 @@ export const BUILTIN_MODELS = [
|
|
|
105
108
|
{ id: "step-3.5-flash-2603", name: "Step 3.5 Flash 2603", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
|
|
106
109
|
{ id: "step-3.5-flash", name: "Step 3.5 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
|
|
107
110
|
{ id: "step-router-v1", name: "Step Router V1", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
|
|
111
|
+
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", providerId: "moonshot-cn", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
|
|
108
112
|
{ id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
109
113
|
{ id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
110
114
|
{ id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
111
115
|
{ id: "kimi-k2-turbo-preview", name: "Kimi K2 Turbo", providerId: "moonshot-cn", reasoningLevels: ["off"], contextWindow: 256000 },
|
|
112
116
|
{ id: "kimi-k2-0905-preview", name: "Kimi K2 0905", providerId: "moonshot-cn", reasoningLevels: ["off"], contextWindow: 256000 },
|
|
113
117
|
{ id: "kimi-k2-thinking", name: "Kimi K2 Thinking", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
118
|
+
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", providerId: "moonshot-intl", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
|
|
114
119
|
{ id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
115
120
|
{ id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
116
121
|
{ id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
117
122
|
{ id: "kimi-k2-turbo-preview", name: "Kimi K2 Turbo", providerId: "moonshot-intl", reasoningLevels: ["off"], contextWindow: 256000 },
|
|
118
123
|
{ id: "kimi-k2-0905-preview", name: "Kimi K2 0905", providerId: "moonshot-intl", reasoningLevels: ["off"], contextWindow: 256000 },
|
|
119
124
|
{ id: "kimi-k2-thinking", name: "Kimi K2 Thinking", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
125
|
+
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", providerId: "kimi-for-coding", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
|
|
120
126
|
{ id: "kimi-k2.6", name: "Kimi K2.6", providerId: "kimi-for-coding", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
121
127
|
{ id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "kimi-for-coding", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
122
128
|
{ id: "kimi-k2-turbo-preview", name: "Kimi K2 Turbo", providerId: "kimi-for-coding", reasoningLevels: ["off"], contextWindow: 256000 },
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed transport errors shared between providers and the subagent runtime.
|
|
3
|
+
*
|
|
4
|
+
* `RateLimitError` is the contract for 429 handling (design doc
|
|
5
|
+
* docs/subagent-runtime-design.md §4.5): under `rateLimitPolicy: "defer"` the
|
|
6
|
+
* transport performs no 429 backoff of its own and throws this error
|
|
7
|
+
* immediately so the subagent scheduler can be the single backoff layer.
|
|
8
|
+
*/
|
|
9
|
+
export declare class RateLimitError extends Error {
|
|
10
|
+
readonly isRateLimitError = true;
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly retryAfterMs?: number;
|
|
13
|
+
constructor(message: string, options?: {
|
|
14
|
+
status?: number;
|
|
15
|
+
retryAfterMs?: number;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export declare function isRateLimitError(error: unknown): error is RateLimitError;
|
|
20
|
+
/**
|
|
21
|
+
* How a provider transport should treat HTTP 429 responses.
|
|
22
|
+
*
|
|
23
|
+
* - "handle": retry inside the transport with backoff (parent traffic default).
|
|
24
|
+
* - "defer": do not retry 429 at all; throw RateLimitError immediately so the
|
|
25
|
+
* caller (subagent scheduler) owns the backoff. Other retryable
|
|
26
|
+
* statuses are unaffected.
|
|
27
|
+
*/
|
|
28
|
+
export type RateLimitPolicy = "handle" | "defer";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed transport errors shared between providers and the subagent runtime.
|
|
3
|
+
*
|
|
4
|
+
* `RateLimitError` is the contract for 429 handling (design doc
|
|
5
|
+
* docs/subagent-runtime-design.md §4.5): under `rateLimitPolicy: "defer"` the
|
|
6
|
+
* transport performs no 429 backoff of its own and throws this error
|
|
7
|
+
* immediately so the subagent scheduler can be the single backoff layer.
|
|
8
|
+
*/
|
|
9
|
+
export class RateLimitError extends Error {
|
|
10
|
+
isRateLimitError = true;
|
|
11
|
+
status;
|
|
12
|
+
retryAfterMs;
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
15
|
+
this.name = "RateLimitError";
|
|
16
|
+
this.status = options?.status ?? 429;
|
|
17
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isRateLimitError(error) {
|
|
21
|
+
return !!error
|
|
22
|
+
&& typeof error === "object"
|
|
23
|
+
&& error.isRateLimitError === true;
|
|
24
|
+
}
|
|
@@ -20,7 +20,11 @@ export function createDefaultHooks() {
|
|
|
20
20
|
input: ctx.input,
|
|
21
21
|
enabled: taskType === "repo_orientation",
|
|
22
22
|
});
|
|
23
|
-
const taskReminder = reminderForTaskType(taskType
|
|
23
|
+
const taskReminder = reminderForTaskType(taskType, {
|
|
24
|
+
// Only parent agents carry the delegation tools; the nudge must
|
|
25
|
+
// never reach a child that cannot spawn anything.
|
|
26
|
+
canDelegate: ctx.agent.hasToolAvailable("spawn_agent"),
|
|
27
|
+
});
|
|
24
28
|
if (taskReminder) {
|
|
25
29
|
ctx.queueReminder(taskReminder);
|
|
26
30
|
}
|
package/dist/prompt/compose.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildGeminiProviderPrompt } from "./provider-prompts/gemini.js";
|
|
|
6
6
|
import { buildGlmProviderPrompt } from "./provider-prompts/glm.js";
|
|
7
7
|
import { buildGptProviderPrompt } from "./provider-prompts/gpt.js";
|
|
8
8
|
import { buildKimiProviderPrompt } from "./provider-prompts/kimi.js";
|
|
9
|
+
import { buildDelegationPolicyPrompt } from "./delegation.js";
|
|
9
10
|
import { buildEnvironmentPrompt, defaultToolNames } from "./environment.js";
|
|
10
11
|
import { buildRuntimePrompt } from "./runtime.js";
|
|
11
12
|
export function composeSystemPrompt(options = {}) {
|
|
@@ -25,10 +26,12 @@ export function composeSystemPrompt(options = {}) {
|
|
|
25
26
|
mode: options.mode,
|
|
26
27
|
guidelines: buildGuidelines(options.tools ?? defaultToolNames, options.guidelines ?? []),
|
|
27
28
|
});
|
|
29
|
+
const delegationPrompt = buildDelegationPolicyPrompt(options.tools ?? defaultToolNames);
|
|
28
30
|
return [
|
|
29
31
|
providerPrompt,
|
|
30
32
|
environmentPrompt,
|
|
31
33
|
runtimePrompt,
|
|
34
|
+
delegationPrompt,
|
|
32
35
|
options.agentProfilePrompt,
|
|
33
36
|
options.memoryPrompt,
|
|
34
37
|
].filter(Boolean).join("\n\n");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation policy section for the parent agent's system prompt.
|
|
3
|
+
*
|
|
4
|
+
* Two-sided by design (review 2026-06-12): positive triggers are quantified
|
|
5
|
+
* and read-only-scoped, negative clauses get equal weight — the user's hard
|
|
6
|
+
* constraint is "proactive, but never delegate-everything". Gated on the
|
|
7
|
+
* delegation tools being present, so child agents (whose tool sets never
|
|
8
|
+
* include spawn_agent/agent_team) never see it.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Returns the delegation policy section when the agent actually has the
|
|
12
|
+
* delegation tools; child agents and stripped-down tool sets get nothing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDelegationPolicyPrompt(tools: string[]): string | undefined;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation policy section for the parent agent's system prompt.
|
|
3
|
+
*
|
|
4
|
+
* Two-sided by design (review 2026-06-12): positive triggers are quantified
|
|
5
|
+
* and read-only-scoped, negative clauses get equal weight — the user's hard
|
|
6
|
+
* constraint is "proactive, but never delegate-everything". Gated on the
|
|
7
|
+
* delegation tools being present, so child agents (whose tool sets never
|
|
8
|
+
* include spawn_agent/agent_team) never see it.
|
|
9
|
+
*/
|
|
10
|
+
const DELEGATION_POLICY = `## Delegation policy (subagents)
|
|
11
|
+
|
|
12
|
+
You can delegate work to background subagents (spawn_agent) and batch
|
|
13
|
+
fan-outs (agent_team). Delegate deliberately, not by default.
|
|
14
|
+
|
|
15
|
+
Delegate when:
|
|
16
|
+
- An investigation will clearly require more than four search or read
|
|
17
|
+
operations, or spans multiple files and patterns, and the conversation only
|
|
18
|
+
needs the conclusion — delegate to a subagent so the intermediate noise
|
|
19
|
+
stays out of the main context. Launch multiple subagents concurrently for
|
|
20
|
+
independent questions.
|
|
21
|
+
- The task naturally splits into the same read-only investigation or
|
|
22
|
+
analysis (review, audit, summarize) over several independent items (files,
|
|
23
|
+
modules, endpoints) — use agent_team.
|
|
24
|
+
- A side-investigation is independent of your current main-line work and can
|
|
25
|
+
run in the background while you continue.
|
|
26
|
+
|
|
27
|
+
Briefing a subagent: it starts with zero context, so the task message must be
|
|
28
|
+
a self-contained work order — state the goal, list everything you already
|
|
29
|
+
know, and write known file paths or commands directly into the task. Never
|
|
30
|
+
outsource knowledge you already hold: if the task hinges on a specific path
|
|
31
|
+
or line number, pin it down yourself first and put it in the briefing. When
|
|
32
|
+
earlier work lives in an existing subagent, prefer send_input to resume it
|
|
33
|
+
over spawning a fresh one.
|
|
34
|
+
|
|
35
|
+
Do NOT delegate when:
|
|
36
|
+
- The task requires editing files or running state-changing commands.
|
|
37
|
+
Built-in subagents are read-only; do edits and writes yourself unless a
|
|
38
|
+
write-capable (write_worktree) profile is explicitly available.
|
|
39
|
+
- The task takes one or two tool calls (reading a single file, looking up one
|
|
40
|
+
definition). The handoff overhead costs more than the task.
|
|
41
|
+
- Doing it well depends on conversation context (preferences the user stated
|
|
42
|
+
this session, decisions made in this discussion). Subagents start without
|
|
43
|
+
the conversation, and fork_context is not the fix: it copies only a recent
|
|
44
|
+
slice of the history, re-pays it as child tokens, and still loses earlier
|
|
45
|
+
decisions — do context-heavy work yourself.
|
|
46
|
+
- You already read the relevant files in this conversation; a subagent would
|
|
47
|
+
re-read everything from scratch.
|
|
48
|
+
- You already delegated it. Never redo delegated work locally, and never
|
|
49
|
+
re-spawn the same task to a second subagent.
|
|
50
|
+
|
|
51
|
+
When in doubt about a one-off task, do it yourself. When a task is clearly
|
|
52
|
+
the same read-only operation over three or more independent items — where
|
|
53
|
+
each item alone would take more than a couple of tool calls — prefer
|
|
54
|
+
agent_team over doing them sequentially yourself. For just two small items,
|
|
55
|
+
do them yourself with parallel tool calls.`;
|
|
56
|
+
/**
|
|
57
|
+
* Returns the delegation policy section when the agent actually has the
|
|
58
|
+
* delegation tools; child agents and stripped-down tool sets get nothing.
|
|
59
|
+
*/
|
|
60
|
+
export function buildDelegationPolicyPrompt(tools) {
|
|
61
|
+
if (!tools.includes("spawn_agent"))
|
|
62
|
+
return undefined;
|
|
63
|
+
return DELEGATION_POLICY;
|
|
64
|
+
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { TaskType } from "../agent/task-classifier.js";
|
|
2
|
-
export
|
|
2
|
+
export interface TaskReminderOptions {
|
|
3
|
+
/** Whether this agent has the delegation tools (parent agents only). */
|
|
4
|
+
canDelegate?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function reminderForTaskType(taskType: TaskType, options?: TaskReminderOptions): string | undefined;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { wrapInSystemReminder } from "./reminders.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Delegation nudge for exploration-shaped tasks: injected at the decision
|
|
4
|
+
* point (start of turn), where it carries far more weight for weakly
|
|
5
|
+
* delegating models than the session-start system prompt. Task-type gating
|
|
6
|
+
* keeps it away from ordinary implementation/debugging turns, so it cannot
|
|
7
|
+
* amplify over-delegation.
|
|
8
|
+
*/
|
|
9
|
+
const DELEGATION_NUDGE = "- If answering needs scanning many files and only the conclusion matters, delegate to a background subagent (spawn_agent); when it is the same read-only question over several independent items, fan out with agent_team.";
|
|
10
|
+
export function reminderForTaskType(taskType, options = {}) {
|
|
3
11
|
switch (taskType) {
|
|
4
12
|
case "debugging":
|
|
5
13
|
return wrapInSystemReminder(`
|
|
@@ -39,7 +47,7 @@ Repository orientation workflow:
|
|
|
39
47
|
- Start with the repo purpose and main execution paths.
|
|
40
48
|
- Inspect README/package metadata plus core runtime files before summarizing.
|
|
41
49
|
- Keep the first pass read-only unless the user asks for changes or runtime verification.
|
|
42
|
-
`);
|
|
50
|
+
${options.canDelegate ? `${DELEGATION_NUDGE}\n` : ""}`);
|
|
43
51
|
case "product_discussion":
|
|
44
52
|
return wrapInSystemReminder(`
|
|
45
53
|
Product discussion workflow:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
|
|
2
|
+
import { RateLimitError } from "./network/errors.js";
|
|
2
3
|
import { isProviderTransportError, normalizeProviderNetworkError, providerFetch } from "./network/provider-transport.js";
|
|
3
4
|
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, retryAfterMsFromResponse, sleepBeforeRetry, } from "./network/retry.js";
|
|
4
5
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
@@ -32,6 +33,7 @@ export function createAnthropicMessagesProvider(options) {
|
|
|
32
33
|
method: "POST",
|
|
33
34
|
body: JSON.stringify(body),
|
|
34
35
|
signal: chatOptions.abortSignal,
|
|
36
|
+
rateLimitPolicy: chatOptions.rateLimitPolicy,
|
|
35
37
|
});
|
|
36
38
|
yield* translateAnthropicStream(events);
|
|
37
39
|
yield { type: "done" };
|
|
@@ -428,6 +430,27 @@ async function fetchAnthropicResponseWithRetry(options, request) {
|
|
|
428
430
|
if (response.ok)
|
|
429
431
|
return response;
|
|
430
432
|
const detail = await readAnthropicErrorDetail(response);
|
|
433
|
+
// Rate-limit contract (design §4.5): under "defer" the transport performs
|
|
434
|
+
// no 429 backoff and throws the typed error immediately; under "handle"
|
|
435
|
+
// an exhausted 429 retry budget still surfaces as the typed error so the
|
|
436
|
+
// caller can recognize it without string matching.
|
|
437
|
+
if (response.status === 429) {
|
|
438
|
+
const retryAfterMs = retryAfterMsFromResponse(response);
|
|
439
|
+
if (request.rateLimitPolicy === "defer") {
|
|
440
|
+
throw new RateLimitError(`Anthropic Messages API rate limited (429): ${detail || response.statusText}`, {
|
|
441
|
+
status: 429,
|
|
442
|
+
retryAfterMs,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (request.signal?.aborted || attempt >= maxRetries) {
|
|
446
|
+
throw new RateLimitError(`Anthropic Messages API rate limited (429) after ${attempt + 1} attempts: ${detail || response.statusText}`, {
|
|
447
|
+
status: 429,
|
|
448
|
+
retryAfterMs,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1, { retryAfterMs }), request.signal);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
431
454
|
const error = new Error(`Anthropic Messages API error ${response.status}: ${detail || response.statusText}`);
|
|
432
455
|
if (request.signal?.aborted || attempt >= maxRetries || !isRetryableAnthropicHttpError(response.status, detail)) {
|
|
433
456
|
throw error;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./variant/variant-resolver.js";
|
|
2
2
|
export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
|
|
3
3
|
const MOONSHOT_PROVIDER_IDS = new Set(["moonshot-cn", "moonshot-intl", "kimi-for-coding"]);
|
|
4
|
+
const KIMI_K27_FAMILY = new Set(["kimi-k2.7-code"]);
|
|
4
5
|
const KIMI_K25_FAMILY = new Set(["kimi-k2.5", "k2.6-code-preview", "kimi-k2.6"]);
|
|
5
6
|
const KIMI_THINKING_FAMILY = new Set(["kimi-k2-thinking", "kimi-k2-thinking-turbo"]);
|
|
6
7
|
const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
|
|
@@ -78,6 +79,19 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
|
|
|
78
79
|
// temperature/top_p/n/penalties and exposes thinking via extra_body.thinking;
|
|
79
80
|
// kimi-k2-thinking family locks temperature=1.
|
|
80
81
|
if (MOONSHOT_PROVIDER_IDS.has(providerId)) {
|
|
82
|
+
// kimi-k2.7-code is thinking-only: temperature is locked to 1.0 server-side
|
|
83
|
+
// (any explicit value errors), thinking can never be disabled, and
|
|
84
|
+
// reasoning_content must be echoed back on tool-call turns.
|
|
85
|
+
if (KIMI_K27_FAMILY.has(modelId)) {
|
|
86
|
+
return {
|
|
87
|
+
effectiveThinkingLevel,
|
|
88
|
+
omitTemperature: true,
|
|
89
|
+
reasoningContentEcho: "tool_calls",
|
|
90
|
+
extraBody: {
|
|
91
|
+
thinking: { type: "enabled" },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
81
95
|
if (KIMI_K25_FAMILY.has(modelId)) {
|
|
82
96
|
return {
|
|
83
97
|
effectiveThinkingLevel,
|
package/dist/provider.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-open
|
|
|
10
10
|
import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
|
|
11
11
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
12
12
|
import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
|
|
13
|
+
import { RateLimitError } from "./network/errors.js";
|
|
13
14
|
// Diagnostic logger for tool-args byte-loss investigation. Activate with
|
|
14
15
|
// BUBBLE_DEBUG_TOOL_ARGS=/path/to/log.jsonl (any writable path)
|
|
15
16
|
// Each line is a JSON record describing a transition. When debugging is off,
|
|
@@ -128,9 +129,28 @@ export function createProviderInstance(options) {
|
|
|
128
129
|
if (requestConfig.reasoningEffort && requestConfig.reasoningEffort !== "off") {
|
|
129
130
|
body.reasoning = { enabled: true };
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// Rate-limit contract (design §4.5): "defer" disables the SDK's own
|
|
133
|
+
// retries so the caller is the single 429 backoff layer; either policy
|
|
134
|
+
// surfaces a final 429 as a typed RateLimitError instead of a string.
|
|
135
|
+
let stream;
|
|
136
|
+
try {
|
|
137
|
+
stream = (await client.chat.completions.create(body, {
|
|
138
|
+
signal: chatOptions.abortSignal,
|
|
139
|
+
...(chatOptions.rateLimitPolicy === "defer" ? { maxRetries: 0 } : {}),
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error?.status === 429) {
|
|
144
|
+
const retryAfterHeader = error?.headers?.["retry-after"];
|
|
145
|
+
const retryAfterSeconds = Number(retryAfterHeader);
|
|
146
|
+
throw new RateLimitError(error?.message || "Rate limited (429)", {
|
|
147
|
+
status: 429,
|
|
148
|
+
retryAfterMs: Number.isFinite(retryAfterSeconds) ? Math.round(retryAfterSeconds * 1000) : undefined,
|
|
149
|
+
cause: error,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
134
154
|
yield* translateOpenAIStream(stream, {
|
|
135
155
|
toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL),
|
|
136
156
|
reasoningMergeMode: resolveReasoningMergeMode(options.providerId || "", options.baseURL),
|
|
@@ -5,7 +5,10 @@ import { normalizeNameForMCP } from "../mcp/name.js";
|
|
|
5
5
|
import { parseRule } from "../permissions/rule.js";
|
|
6
6
|
import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
|
|
7
7
|
import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
|
|
8
|
+
import { SessionManager } from "../session.js";
|
|
8
9
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
10
|
+
import { normalizeSingleLine } from "../text-display.js";
|
|
11
|
+
import { formatRelativeTime } from "../tui/recent-activity.js";
|
|
9
12
|
import { HOOK_EVENT_NAMES, isHookEventName } from "../hooks/index.js";
|
|
10
13
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
11
14
|
import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
|
|
@@ -481,9 +484,33 @@ const builtinSlashCommandEntries = [
|
|
|
481
484
|
},
|
|
482
485
|
{
|
|
483
486
|
name: "session",
|
|
484
|
-
description: "
|
|
487
|
+
description: "Browse recent sessions and resume one. /session to pick, /session --list to print",
|
|
485
488
|
async handler(args, ctx) {
|
|
486
|
-
|
|
489
|
+
const flag = args.trim();
|
|
490
|
+
if (flag && flag !== "--list") {
|
|
491
|
+
return "Usage: /session (open the session picker) or /session --list";
|
|
492
|
+
}
|
|
493
|
+
if (!flag && ctx.openSessionPicker) {
|
|
494
|
+
ctx.openSessionPicker();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const summaries = SessionManager.summarizeSessionsForCwd(ctx.cwd);
|
|
498
|
+
if (summaries.length === 0) {
|
|
499
|
+
return "No sessions recorded for this project yet.";
|
|
500
|
+
}
|
|
501
|
+
const activeFile = ctx.sessionManager?.getSessionFile();
|
|
502
|
+
const lines = ["Recent sessions:"];
|
|
503
|
+
for (const summary of summaries.slice(0, 15)) {
|
|
504
|
+
const current = summary.file === activeFile ? " (current)" : "";
|
|
505
|
+
const title = normalizeSingleLine(summary.title || summary.preview || summary.name);
|
|
506
|
+
const count = `${summary.messageCount} message${summary.messageCount === 1 ? "" : "s"}`;
|
|
507
|
+
lines.push(`- ${title} — ${count}, ${formatRelativeTime(summary.mtime)} (${summary.name})${current}`);
|
|
508
|
+
}
|
|
509
|
+
if (summaries.length > 15) {
|
|
510
|
+
lines.push(`- … and ${summaries.length - 15} more`);
|
|
511
|
+
}
|
|
512
|
+
lines.push("", "Resume one with: bubble --resume --session <name>");
|
|
513
|
+
return lines.join("\n");
|
|
487
514
|
},
|
|
488
515
|
},
|
|
489
516
|
{
|
|
@@ -50,6 +50,8 @@ export interface SlashCommandContext {
|
|
|
50
50
|
openFeedback?: (initialDescription: string) => void;
|
|
51
51
|
/** Open the interactive rewind picker. When absent, /rewind falls back to a text listing. */
|
|
52
52
|
openRewindPicker?: () => void;
|
|
53
|
+
/** Open the interactive session picker. When absent, /session falls back to a text listing. */
|
|
54
|
+
openSessionPicker?: () => void;
|
|
53
55
|
/** Replace the composer/input box content (e.g. /rewind restores the rewound message for re-editing). */
|
|
54
56
|
fillComposer?: (text: string) => void;
|
|
55
57
|
/** Open the interactive usage stats panel. */
|