@bubblebrain-ai/bubble 0.0.21 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/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/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 +34 -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.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 +11 -1
- package/dist/tui/run.js +92 -4
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- 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/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,
|
|
@@ -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;
|
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. */
|
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { AgentProfile } from "../agent/profiles.js";
|
|
2
|
+
import type { ApprovalController } from "../approval/types.js";
|
|
3
|
+
import type { ToolRegistryEntry, ToolResult } from "../types.js";
|
|
4
|
+
export interface AgentLifecycleToolOptions {
|
|
5
|
+
/** Working directory used for profile discovery in tool descriptions. */
|
|
6
|
+
cwd?: string;
|
|
7
|
+
/** Trust gate for project-local .bubble/agents profiles (design §10.2). */
|
|
8
|
+
approval?: ApprovalController;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Session-scoped trust decisions for project profiles, keyed by file path +
|
|
12
|
+
* content hash so an edited file re-prompts (design §10.2). Shared across the
|
|
13
|
+
* lifecycle tools created by one factory call.
|
|
14
|
+
*/
|
|
15
|
+
declare class ProjectProfileTrust {
|
|
16
|
+
private readonly approval?;
|
|
17
|
+
private readonly approved;
|
|
18
|
+
constructor(approval?: ApprovalController | undefined);
|
|
19
|
+
/** Returns undefined when trusted, else a blocked ToolResult. */
|
|
20
|
+
ensureTrusted(profile: AgentProfile): Promise<ToolResult | undefined>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createSpawnAgentTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
|
|
3
23
|
export declare function createWaitAgentTool(): ToolRegistryEntry;
|
|
4
24
|
export declare function createSendInputTool(): ToolRegistryEntry;
|
|
5
25
|
export declare function createCloseAgentTool(): ToolRegistryEntry;
|
|
6
|
-
export declare function
|
|
26
|
+
export declare function createListAgentsTool(): ToolRegistryEntry;
|
|
27
|
+
/** Items bound for one agent_team call (design §1.2). */
|
|
28
|
+
export declare const AGENT_TEAM_MIN_ITEMS = 2;
|
|
29
|
+
export declare const AGENT_TEAM_MAX_ITEMS = 32;
|
|
30
|
+
export declare function createAgentTeamTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
|
|
31
|
+
export declare function createAgentLifecycleTools(options?: AgentLifecycleToolOptions): ToolRegistryEntry[];
|
|
32
|
+
export {};
|