@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.
Files changed (65) hide show
  1. package/README.md +197 -34
  2. package/dist/agent/abort-errors.d.ts +14 -0
  3. package/dist/agent/abort-errors.js +21 -0
  4. package/dist/agent/budget-ledger.d.ts +41 -0
  5. package/dist/agent/budget-ledger.js +64 -0
  6. package/dist/agent/child-runner.d.ts +55 -0
  7. package/dist/agent/child-runner.js +312 -0
  8. package/dist/agent/internal-reminder-sanitizer.js +29 -9
  9. package/dist/agent/profiles.d.ts +8 -0
  10. package/dist/agent/profiles.js +27 -5
  11. package/dist/agent/result-integrator.d.ts +22 -0
  12. package/dist/agent/result-integrator.js +50 -0
  13. package/dist/agent/subagent-control.d.ts +31 -0
  14. package/dist/agent/subagent-control.js +27 -0
  15. package/dist/agent/subagent-lifecycle-reminder.js +11 -2
  16. package/dist/agent/subagent-scheduler.d.ts +95 -0
  17. package/dist/agent/subagent-scheduler.js +256 -0
  18. package/dist/agent/subagent-store.d.ts +41 -0
  19. package/dist/agent/subagent-store.js +149 -0
  20. package/dist/agent/subagent-summary.d.ts +30 -0
  21. package/dist/agent/subagent-summary.js +74 -0
  22. package/dist/agent/worktree.d.ts +29 -0
  23. package/dist/agent/worktree.js +73 -0
  24. package/dist/agent.d.ts +63 -5
  25. package/dist/agent.js +360 -287
  26. package/dist/approval/controller.js +9 -1
  27. package/dist/approval/tool-helper.js +2 -0
  28. package/dist/approval/types.d.ts +17 -1
  29. package/dist/config.d.ts +8 -0
  30. package/dist/config.js +17 -0
  31. package/dist/feishu/agent-host/approval-card.js +9 -0
  32. package/dist/feishu/agent-host/run-driver.js +1 -0
  33. package/dist/main.js +38 -2
  34. package/dist/model-catalog.js +6 -0
  35. package/dist/network/errors.d.ts +28 -0
  36. package/dist/network/errors.js +24 -0
  37. package/dist/orchestrator/default-hooks.js +5 -1
  38. package/dist/prompt/compose.js +3 -0
  39. package/dist/prompt/delegation.d.ts +14 -0
  40. package/dist/prompt/delegation.js +64 -0
  41. package/dist/prompt/task-reminders.d.ts +5 -1
  42. package/dist/prompt/task-reminders.js +10 -2
  43. package/dist/provider-anthropic.js +23 -0
  44. package/dist/provider-transform.js +14 -0
  45. package/dist/provider.js +23 -3
  46. package/dist/slash-commands/commands.js +29 -2
  47. package/dist/slash-commands/types.d.ts +2 -0
  48. package/dist/tools/agent-lifecycle.d.ts +29 -3
  49. package/dist/tools/agent-lifecycle.js +394 -40
  50. package/dist/tools/child-tools.d.ts +31 -0
  51. package/dist/tools/child-tools.js +106 -0
  52. package/dist/tools/index.js +1 -1
  53. package/dist/tui/run.d.ts +17 -1
  54. package/dist/tui/run.js +155 -10
  55. package/dist/tui/session-picker-data.d.ts +18 -0
  56. package/dist/tui/session-picker-data.js +21 -0
  57. package/dist/tui/trace-groups.js +41 -5
  58. package/dist/tui/wordmark.d.ts +2 -0
  59. package/dist/tui/wordmark.js +31 -4
  60. package/dist/tui-ink/approval/approval-dialog.js +10 -0
  61. package/dist/tui-opentui/approval/approval-dialog.js +10 -0
  62. package/dist/types.d.ts +17 -0
  63. package/dist/update/index.d.ts +18 -4
  64. package/dist/update/index.js +41 -19
  65. 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
- if (mode === "plan") {
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
  }
@@ -30,5 +30,7 @@ function approvalRequestLabel(req) {
30
30
  return `Bash command \`${req.command}\``;
31
31
  case "lsp":
32
32
  return `LSP ${req.operation} on ${req.path}`;
33
+ case "agent_profile":
34
+ return `Project agent profile "${req.name}"`;
33
35
  }
34
36
  }
@@ -45,7 +45,23 @@ export interface LspApprovalRequest {
45
45
  path: string;
46
46
  operation: string;
47
47
  }
48
- export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | PatchApprovalRequest | BashApprovalRequest | LspApprovalRequest;
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 { getStartupUpdateNotice } = await import("./update/index.js");
533
- const updateNotice = await getStartupUpdateNotice();
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 {
@@ -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
  }
@@ -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 declare function reminderForTaskType(taskType: TaskType): string | undefined;
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
- export function reminderForTaskType(taskType) {
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
- const stream = (await client.chat.completions.create(body, {
132
- signal: chatOptions.abortSignal,
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: "Show current session information",
487
+ description: "Browse recent sessions and resume one. /session to pick, /session --list to print",
485
488
  async handler(args, ctx) {
486
- return `Session info not implemented yet.`;
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. */