@bubblebrain-ai/bubble 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/abort-errors.d.ts +2 -2
  3. package/dist/agent/budget-ledger.d.ts +4 -45
  4. package/dist/agent/budget-ledger.js +5 -75
  5. package/dist/agent/categories.d.ts +66 -2
  6. package/dist/agent/categories.js +120 -19
  7. package/dist/agent/child-runner.d.ts +1 -3
  8. package/dist/agent/child-runner.js +6 -37
  9. package/dist/agent/discovery-barrier.js +1 -1
  10. package/dist/agent/profiles.d.ts +1 -2
  11. package/dist/agent/profiles.js +25 -14
  12. package/dist/agent/routing-catalog.d.ts +113 -0
  13. package/dist/agent/routing-catalog.js +240 -0
  14. package/dist/agent/subagent-control.d.ts +5 -12
  15. package/dist/agent/subagent-control.js +0 -2
  16. package/dist/agent/subagent-store.d.ts +1 -1
  17. package/dist/agent/subagent-store.js +1 -3
  18. package/dist/agent/workflow/control.d.ts +21 -0
  19. package/dist/agent/workflow/control.js +50 -8
  20. package/dist/agent/workflow/runtime.d.ts +15 -2
  21. package/dist/agent/workflow/runtime.js +112 -30
  22. package/dist/agent.d.ts +82 -48
  23. package/dist/agent.js +360 -232
  24. package/dist/approval/controller.js +28 -0
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +21 -1
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +2 -1
  29. package/dist/config.d.ts +5 -3
  30. package/dist/config.js +10 -5
  31. package/dist/context/compact-llm.d.ts +2 -2
  32. package/dist/external-runtime/grok-acp.d.ts +56 -0
  33. package/dist/external-runtime/grok-acp.js +386 -0
  34. package/dist/external-runtime/grok-binary.d.ts +25 -0
  35. package/dist/external-runtime/grok-binary.js +108 -0
  36. package/dist/external-runtime/grok-errors.d.ts +7 -0
  37. package/dist/external-runtime/grok-errors.js +15 -0
  38. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  39. package/dist/external-runtime/grok-input-policy.js +52 -0
  40. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  41. package/dist/external-runtime/grok-ndjson.js +193 -0
  42. package/dist/external-runtime/grok-network.d.ts +15 -0
  43. package/dist/external-runtime/grok-network.js +81 -0
  44. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  45. package/dist/external-runtime/grok-preflight.js +194 -0
  46. package/dist/external-runtime/grok-process.d.ts +26 -0
  47. package/dist/external-runtime/grok-process.js +184 -0
  48. package/dist/external-runtime/grok-profile.d.ts +32 -0
  49. package/dist/external-runtime/grok-profile.js +419 -0
  50. package/dist/external-runtime/grok-provider.d.ts +31 -0
  51. package/dist/external-runtime/grok-provider.js +41 -0
  52. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  53. package/dist/external-runtime/grok-runtime.js +882 -0
  54. package/dist/external-runtime/index.d.ts +13 -0
  55. package/dist/external-runtime/index.js +13 -0
  56. package/dist/external-runtime/session-policy.d.ts +11 -0
  57. package/dist/external-runtime/session-policy.js +30 -0
  58. package/dist/external-runtime/types.d.ts +65 -0
  59. package/dist/external-runtime/types.js +1 -0
  60. package/dist/feishu/agent-host/approval-card.js +21 -0
  61. package/dist/feishu/agent-host/run-driver.js +19 -3
  62. package/dist/feishu/serve.js +1 -0
  63. package/dist/goal/tools.d.ts +6 -3
  64. package/dist/goal/tools.js +7 -19
  65. package/dist/main.js +114 -28
  66. package/dist/memory/db.d.ts +2 -0
  67. package/dist/memory/db.js +10 -0
  68. package/dist/memory/index.d.ts +1 -0
  69. package/dist/memory/index.js +1 -0
  70. package/dist/memory/phase1.js +14 -2
  71. package/dist/memory/phase2.js +21 -2
  72. package/dist/memory/prompts.js +1 -1
  73. package/dist/memory/session-policy.d.ts +25 -0
  74. package/dist/memory/session-policy.js +55 -0
  75. package/dist/memory/storage.d.ts +1 -0
  76. package/dist/memory/storage.js +13 -0
  77. package/dist/model-catalog.d.ts +18 -0
  78. package/dist/model-catalog.js +92 -52
  79. package/dist/model-config.d.ts +3 -1
  80. package/dist/model-config.js +11 -0
  81. package/dist/oauth/grok.d.ts +25 -0
  82. package/dist/oauth/grok.js +218 -0
  83. package/dist/oauth/index.d.ts +1 -0
  84. package/dist/oauth/index.js +1 -0
  85. package/dist/oauth/storage.d.ts +9 -0
  86. package/dist/oauth/storage.js +25 -0
  87. package/dist/orchestrator/default-hooks.js +23 -7
  88. package/dist/prompt/compose.d.ts +3 -0
  89. package/dist/prompt/compose.js +3 -0
  90. package/dist/prompt/delegation.d.ts +1 -1
  91. package/dist/prompt/delegation.js +41 -8
  92. package/dist/prompt/environment.js +0 -4
  93. package/dist/prompt/reminders.d.ts +0 -1
  94. package/dist/prompt/reminders.js +1 -11
  95. package/dist/prompt/routing.d.ts +14 -0
  96. package/dist/prompt/routing.js +71 -0
  97. package/dist/prompt/runtime.js +1 -1
  98. package/dist/prompt/task-reminders.d.ts +20 -0
  99. package/dist/prompt/task-reminders.js +73 -1
  100. package/dist/provider-ai-sdk.js +2 -2
  101. package/dist/provider-grok.d.ts +26 -0
  102. package/dist/provider-grok.js +59 -0
  103. package/dist/provider-openai-codex.d.ts +17 -1
  104. package/dist/provider-openai-codex.js +101 -29
  105. package/dist/provider-registry.d.ts +72 -0
  106. package/dist/provider-registry.js +336 -59
  107. package/dist/provider-transform.d.ts +1 -1
  108. package/dist/provider-transform.js +35 -17
  109. package/dist/provider.d.ts +3 -0
  110. package/dist/provider.js +14 -0
  111. package/dist/session-types.d.ts +19 -1
  112. package/dist/session.d.ts +1 -0
  113. package/dist/session.js +4 -0
  114. package/dist/slash-commands/commands.js +224 -14
  115. package/dist/slash-commands/types.d.ts +14 -1
  116. package/dist/system-prompt.d.ts +2 -0
  117. package/dist/tools/agent-lifecycle.d.ts +1 -9
  118. package/dist/tools/agent-lifecycle.js +42 -272
  119. package/dist/tools/bash.js +1 -1
  120. package/dist/tools/child-tools.js +2 -0
  121. package/dist/tools/index.d.ts +2 -2
  122. package/dist/tools/index.js +7 -5
  123. package/dist/tools/lsp.js +1 -0
  124. package/dist/tools/memory.d.ts +7 -2
  125. package/dist/tools/memory.js +28 -43
  126. package/dist/tools/server.js +4 -0
  127. package/dist/tools/tool-search.d.ts +1 -1
  128. package/dist/tools/tool-search.js +23 -3
  129. package/dist/tui/model-switch.d.ts +7 -0
  130. package/dist/tui/model-switch.js +15 -5
  131. package/dist/tui-ink/app.d.ts +3 -1
  132. package/dist/tui-ink/app.js +672 -243
  133. package/dist/tui-ink/approval/approval-dialog.js +37 -1
  134. package/dist/tui-ink/approval/diff-view.js +4 -4
  135. package/dist/tui-ink/approval/select.js +2 -2
  136. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  137. package/dist/tui-ink/composer-buffer.js +317 -0
  138. package/dist/tui-ink/feedback-dialog.js +2 -2
  139. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  140. package/dist/tui-ink/footer.d.ts +1 -0
  141. package/dist/tui-ink/footer.js +7 -2
  142. package/dist/tui-ink/input-box.d.ts +36 -7
  143. package/dist/tui-ink/input-box.js +229 -157
  144. package/dist/tui-ink/markdown.js +11 -9
  145. package/dist/tui-ink/message-list.d.ts +5 -1
  146. package/dist/tui-ink/message-list.js +15 -2
  147. package/dist/tui-ink/model-picker.d.ts +61 -4
  148. package/dist/tui-ink/model-picker.js +464 -128
  149. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  150. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  151. package/dist/tui-ink/plan-confirm.js +1 -1
  152. package/dist/tui-ink/run.d.ts +3 -0
  153. package/dist/tui-ink/run.js +1 -1
  154. package/dist/tui-ink/subagent-view.d.ts +34 -1
  155. package/dist/tui-ink/subagent-view.js +82 -1
  156. package/dist/tui-ink/theme.d.ts +19 -2
  157. package/dist/tui-ink/theme.js +25 -8
  158. package/dist/tui-ink/welcome.d.ts +3 -2
  159. package/dist/tui-ink/welcome.js +10 -6
  160. package/dist/types.d.ts +8 -31
  161. package/dist/types.js +1 -1
  162. package/dist/variant/thinking-level.d.ts +2 -2
  163. package/dist/variant/thinking-level.js +2 -1
  164. package/dist/variant/variant-resolver.d.ts +22 -0
  165. package/dist/variant/variant-resolver.js +43 -0
  166. package/package.json +6 -2
  167. package/dist/tools/task.d.ts +0 -2
  168. package/dist/tools/task.js +0 -58
@@ -0,0 +1,13 @@
1
+ export * from "./types.js";
2
+ export * from "./grok-errors.js";
3
+ export * from "./grok-network.js";
4
+ export * from "./grok-provider.js";
5
+ export * from "./grok-input-policy.js";
6
+ export * from "./session-policy.js";
7
+ export * from "./grok-binary.js";
8
+ export * from "./grok-profile.js";
9
+ export * from "./grok-preflight.js";
10
+ export * from "./grok-process.js";
11
+ export * from "./grok-ndjson.js";
12
+ export * from "./grok-acp.js";
13
+ export * from "./grok-runtime.js";
@@ -0,0 +1,13 @@
1
+ export * from "./types.js";
2
+ export * from "./grok-errors.js";
3
+ export * from "./grok-network.js";
4
+ export * from "./grok-provider.js";
5
+ export * from "./grok-input-policy.js";
6
+ export * from "./session-policy.js";
7
+ export * from "./grok-binary.js";
8
+ export * from "./grok-profile.js";
9
+ export * from "./grok-preflight.js";
10
+ export * from "./grok-process.js";
11
+ export * from "./grok-ndjson.js";
12
+ export * from "./grok-acp.js";
13
+ export * from "./grok-runtime.js";
@@ -0,0 +1,11 @@
1
+ import type { ExternalRuntimeManager } from "./types.js";
2
+ export type ExternalRuntimeBindingKind = "none" | "grok" | "unsupported";
3
+ /**
4
+ * Treat every persisted external-runtime marker as non-native. Known aliases
5
+ * normalize to Grok; malformed or future IDs stay fail-closed instead of
6
+ * falling through to Bubble's native agent/tool path.
7
+ */
8
+ export declare function classifyExternalRuntimeBinding(binding: unknown): ExternalRuntimeBindingKind;
9
+ export declare function stopExternalRuntimeForSessionSwitch(runtime: ExternalRuntimeManager | undefined, sessionId?: string): Promise<void>;
10
+ /** Never replay a Grok-owned transcript through Bubble's native print path. */
11
+ export declare function shouldRejectGrokSessionInPrintMode(externalRuntimeBinding: unknown, printMode: boolean): boolean;
@@ -0,0 +1,30 @@
1
+ import { isGrokSubscriptionProviderId } from "./grok-provider.js";
2
+ /**
3
+ * Treat every persisted external-runtime marker as non-native. Known aliases
4
+ * normalize to Grok; malformed or future IDs stay fail-closed instead of
5
+ * falling through to Bubble's native agent/tool path.
6
+ */
7
+ export function classifyExternalRuntimeBinding(binding) {
8
+ if (binding === undefined)
9
+ return "none";
10
+ const id = typeof binding === "string"
11
+ ? binding
12
+ : binding !== null && typeof binding === "object" && "id" in binding
13
+ ? binding.id
14
+ : undefined;
15
+ return typeof id === "string" && isGrokSubscriptionProviderId(id)
16
+ ? "grok"
17
+ : "unsupported";
18
+ }
19
+ export async function stopExternalRuntimeForSessionSwitch(runtime, sessionId) {
20
+ if (!runtime) {
21
+ throw new Error("The external runtime manager is unavailable; the current session was not changed.");
22
+ }
23
+ if (sessionId)
24
+ await runtime.cancel(sessionId);
25
+ await runtime.dispose();
26
+ }
27
+ /** Never replay a Grok-owned transcript through Bubble's native print path. */
28
+ export function shouldRejectGrokSessionInPrintMode(externalRuntimeBinding, printMode) {
29
+ return printMode && classifyExternalRuntimeBinding(externalRuntimeBinding) !== "none";
30
+ }
@@ -0,0 +1,65 @@
1
+ import type { AgentEvent } from "../types.js";
2
+ import type { ThinkingLevel } from "../types.js";
3
+ export type ExternalRuntimeState = "unavailable" | "signed_out" | "ready" | "running" | "failed" | "disposed";
4
+ export interface ExternalRuntimeCapabilities {
5
+ chat: boolean;
6
+ tools: boolean;
7
+ memory: boolean;
8
+ subagents: boolean;
9
+ plan: boolean;
10
+ web: boolean;
11
+ sessionLoad: boolean;
12
+ workspace: boolean;
13
+ modelControl: boolean;
14
+ reasoningControl: boolean;
15
+ }
16
+ export interface ExternalRuntimeBinaryInfo {
17
+ path: string;
18
+ version: string;
19
+ sha256: string;
20
+ }
21
+ export interface ExternalRuntimeStatus {
22
+ provider: "grok";
23
+ state: ExternalRuntimeState;
24
+ capabilities: ExternalRuntimeCapabilities;
25
+ binary?: ExternalRuntimeBinaryInfo;
26
+ sessionId?: string;
27
+ /** A deliberately terse, credential-free diagnostic suitable for the TUI. */
28
+ message?: string;
29
+ }
30
+ export interface ExternalRuntimeSession {
31
+ id: string;
32
+ provider: "grok";
33
+ modelId?: string;
34
+ reasoningEffort?: ThinkingLevel;
35
+ }
36
+ export interface ExternalRuntimeModel {
37
+ id: string;
38
+ name: string;
39
+ reasoningLevels: ThinkingLevel[];
40
+ defaultReasoningLevel: ThinkingLevel;
41
+ }
42
+ export interface ExternalRuntimeModelSelection {
43
+ modelId?: string;
44
+ reasoningEffort: ThinkingLevel;
45
+ }
46
+ export interface ExternalRuntimeRunOptions {
47
+ sessionId?: string;
48
+ signal?: AbortSignal;
49
+ /** Lets callers invalidate late ACP notifications after a UI generation swap. */
50
+ generation?: number;
51
+ }
52
+ export interface ExternalRuntimeManager {
53
+ inspect(): Promise<ExternalRuntimeStatus>;
54
+ login(signal?: AbortSignal, onBrowserOpened?: () => void): Promise<void>;
55
+ logout(): Promise<void>;
56
+ newSession(): Promise<ExternalRuntimeSession>;
57
+ loadSession(id: string): Promise<ExternalRuntimeSession>;
58
+ hydrateSession(id: string, modelId?: string, reasoningEffort?: ThinkingLevel): Promise<ExternalRuntimeSession>;
59
+ listModels(): Promise<ExternalRuntimeModel[]>;
60
+ getModelSelection(): ExternalRuntimeModelSelection;
61
+ setModel(modelId: string, reasoningEffort?: ThinkingLevel): Promise<ExternalRuntimeModelSelection>;
62
+ run(prompt: string, options?: ExternalRuntimeRunOptions): AsyncIterable<AgentEvent>;
63
+ cancel(sessionId?: string): Promise<void>;
64
+ dispose(): Promise<void>;
65
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -55,6 +55,27 @@ export function formatApprovalRequest(req) {
55
55
  "\n该 profile 来自仓库本地 `.bubble/agents`,其 prompt 会驱动一个子代理。仅在信任该仓库时批准。",
56
56
  ].join("\n"),
57
57
  };
58
+ case "external_tool":
59
+ return {
60
+ title: `外部工具:${req.title}`,
61
+ body: [
62
+ `**kind:** \`${truncate(req.kind, 80)}\``,
63
+ req.locations?.length
64
+ ? `**paths:** ${req.locations.map((location) => `\`${truncate(location.path, PATH_PREVIEW_MAX)}\``).join(", ")}`
65
+ : "",
66
+ req.rawInput === undefined
67
+ ? ""
68
+ : `\n**input:**\n\`\`\`json\n${truncate(safeJson(req.rawInput), CONTENT_PREVIEW_MAX)}\n\`\`\``,
69
+ ].filter(Boolean).join("\n"),
70
+ };
71
+ }
72
+ }
73
+ function safeJson(value) {
74
+ try {
75
+ return JSON.stringify(value, null, 2);
76
+ }
77
+ catch {
78
+ return "[unserializable input]";
58
79
  }
59
80
  }
60
81
  function truncate(s, max) {
@@ -19,11 +19,13 @@ import { BashAllowlist } from "../../approval/session-cache.js";
19
19
  import { getLspService } from "../../lsp/index.js";
20
20
  import { ExternalHookController } from "../../hooks/index.js";
21
21
  import { buildSystemPrompt } from "../../system-prompt.js";
22
+ import { createRoutableModelIndex, createRoutingSnapshotAccessor } from "../../agent/routing-catalog.js";
23
+ import { buildModelRoutingPrompt } from "../../prompt/routing.js";
22
24
  import { FileStateTracker } from "../../tools/file-state.js";
23
25
  import { buildToolPromptOptions, createAllTools } from "../../tools/index.js";
24
26
  import { displayModel, encodeModel, decodeModel } from "../../provider-registry.js";
25
27
  import { buildMemoryPrompt, recordMemoryCitations } from "../../memory/index.js";
26
- import { getDefaultThinkingLevel } from "../../provider-transform.js";
28
+ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../../provider-transform.js";
27
29
  import { createSessionTitleUpdater } from "../../session-title.js";
28
30
  import { applyCardBudget } from "../card/budget.js";
29
31
  import { renderCard } from "../card/renderer.js";
@@ -87,9 +89,16 @@ export class RunDriver {
87
89
  const { provider, providerId, model } = await this.resolveProvider(session, promptCacheKey);
88
90
  const skills = this.opts.deps.skillRegistry.summaries();
89
91
  const memoryPrompt = buildMemoryPrompt(session.cwd);
90
- const thinkingLevel = this.opts.deps.userConfig.getDefaultThinkingLevel()
91
- ?? getDefaultThinkingLevel(providerId, decodeModel(model).modelId);
92
+ const modelId = decodeModel(model).modelId;
93
+ const configuredThinkingLevel = this.opts.deps.userConfig.getDefaultThinkingLevel();
94
+ const thinkingLevel = configuredThinkingLevel !== undefined
95
+ ? normalizeThinkingLevel(configuredThinkingLevel, getAvailableThinkingLevels(providerId, modelId))
96
+ : getDefaultThinkingLevel(providerId, modelId);
92
97
  const initialMode = session.permissionMode;
98
+ // Routing accessor built before the prompt (design §1.5): the prompt is
99
+ // composed before the Agent exists, same ordering constraint as main.ts.
100
+ const agentRouting = this.opts.deps.userConfig.getAgentRouting();
101
+ const routingSnapshotAccessor = createRoutingSnapshotAccessor(this.opts.deps.providerRegistry, () => this.opts.deps.userConfig.getAgentCategories(), () => agentRouting);
93
102
  const systemPrompt = buildSystemPrompt({
94
103
  agentName: "Bubble",
95
104
  configuredProvider: providerId || "none",
@@ -100,6 +109,9 @@ export class RunDriver {
100
109
  workingDir: session.cwd,
101
110
  ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
102
111
  memoryPrompt,
112
+ modelRoutingPrompt: providerId && modelId
113
+ ? buildModelRoutingPrompt(routingSnapshotAccessor({ providerId, model: modelId }), agentRouting)
114
+ : undefined,
103
115
  });
104
116
  const budgetLedger = new BudgetLedger();
105
117
  let sessionTitleUpdater;
@@ -140,6 +152,9 @@ export class RunDriver {
140
152
  memoryPrompt,
141
153
  fileStateTracker,
142
154
  agentCategories: this.opts.deps.userConfig.getAgentCategories(),
155
+ agentRouting,
156
+ routingSnapshot: routingSnapshotAccessor,
157
+ routableModels: createRoutableModelIndex(this.opts.deps.providerRegistry),
143
158
  subagents: this.opts.deps.userConfig.getSubagents(),
144
159
  providerFactory: (route) => this.opts.deps.createProviderForRoute(route, promptCacheKey),
145
160
  externalHooks: hookController,
@@ -175,6 +190,7 @@ export class RunDriver {
175
190
  agent.messages = [{ role: "system", content: systemPrompt }, ...history];
176
191
  if (agent.mode === "plan")
177
192
  agent.injectModeReminder();
193
+ agent.injectDeferredToolsReminder();
178
194
  }
179
195
  }
180
196
  // 4. Build RunState + stream the card.
@@ -98,6 +98,7 @@ export async function serveFeishu(opts = {}) {
98
98
  promptCacheKey,
99
99
  protocol: providerRegistry.getConfigured().find((provider) => provider.id === providerId)?.protocol,
100
100
  openAICodexAuth: providerRegistry.createOpenAICodexAuthAdapter(providerId),
101
+ grokAuth: providerRegistry.createGrokAuthAdapter(providerId),
101
102
  });
102
103
  const createProviderForRoute = async (route, promptCacheKey) => {
103
104
  const target = providerRegistry.getConfigured().find((p) => p.id === route.providerId);
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Model-facing goal tools: get_goal and update_goal.
2
+ * Model-facing goal tool: update_goal.
3
3
  *
4
- * Both read/write the shared GoalStore so the model's completion/blocked signal
4
+ * It writes the shared GoalStore so the model's completion/blocked signal
5
5
  * stops the TUI's auto-continuation loop. The user sets goals via `/goal`, so
6
- * there is intentionally no model-facing create_goal tool.
6
+ * there is intentionally no model-facing create_goal tool — and no read tool:
7
+ * the goal engine already injects the objective plus budget state into every
8
+ * goal turn's prompt, so a get_goal call could only return what the model was
9
+ * just told.
7
10
  */
8
11
  import type { ToolRegistryEntry } from "../types.js";
9
12
  import type { GoalStore } from "./store.js";
@@ -1,30 +1,18 @@
1
1
  /**
2
- * Model-facing goal tools: get_goal and update_goal.
2
+ * Model-facing goal tool: update_goal.
3
3
  *
4
- * Both read/write the shared GoalStore so the model's completion/blocked signal
4
+ * It writes the shared GoalStore so the model's completion/blocked signal
5
5
  * stops the TUI's auto-continuation loop. The user sets goals via `/goal`, so
6
- * there is intentionally no model-facing create_goal tool.
6
+ * there is intentionally no model-facing create_goal tool — and no read tool:
7
+ * the goal engine already injects the objective plus budget state into every
8
+ * goal turn's prompt, so a get_goal call could only return what the model was
9
+ * just told.
7
10
  */
8
- import { goalSummaryText } from "./format.js";
9
11
  const UPDATE_GOAL_DESCRIPTION = `Update the active thread goal's status. Use this tool only to mark the goal achieved or genuinely blocked; it returns an error if there is no active goal.
10
12
  Set status to "complete" only when the objective has actually been achieved and no required work remains — never merely because the budget is nearly exhausted or you are stopping.
11
13
  Set status to "blocked" only when the same blocking condition has repeated for at least three consecutive goal turns (counting the original turn and automatic continuations) and you cannot make meaningful progress without user input or an external-state change. Do not use "blocked" because work is hard, slow, uncertain, or incomplete.
12
14
  You cannot pause, resume, or set a budget through this tool; those are controlled by the user.`;
13
15
  export function createGoalTools(store) {
14
- const getGoal = {
15
- name: "get_goal",
16
- description: "Get the current thread goal: objective, status, turns and tokens used, and remaining token budget. Returns an error if there is no goal.",
17
- parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
18
- readOnly: true,
19
- effect: "read",
20
- promptSnippet: "Inspect the active goal's status and remaining token budget.",
21
- async execute() {
22
- const goal = store.snapshot();
23
- if (!goal)
24
- return { content: "No active goal.", isError: true };
25
- return { content: goalSummaryText(goal) };
26
- },
27
- };
28
16
  const updateGoal = {
29
17
  name: "update_goal",
30
18
  description: UPDATE_GOAL_DESCRIPTION,
@@ -66,5 +54,5 @@ export function createGoalTools(store) {
66
54
  };
67
55
  },
68
56
  };
69
- return [getGoal, updateGoal];
57
+ return [updateGoal];
70
58
  }
package/dist/main.js CHANGED
@@ -9,11 +9,13 @@ import { parseArgs, printHelp } from "./cli.js";
9
9
  import { effectiveThemeModeForTerminal, shouldProbeTerminalTheme, UserConfig } from "./config.js";
10
10
  import { createProviderInstance, createUnavailableProvider } from "./provider.js";
11
11
  import { resolveConfiguredModel } from "./model-selection.js";
12
- import { getDefaultThinkingLevel } from "./provider-transform.js";
12
+ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "./provider-transform.js";
13
13
  import { ProviderRegistry, displayModel, encodeModel, decodeModel } from "./provider-registry.js";
14
14
  import { SessionManager } from "./session.js";
15
15
  import { createSessionTitleUpdater } from "./session-title.js";
16
16
  import { buildSystemPrompt } from "./system-prompt.js";
17
+ import { createRoutableModelIndex, createRoutingSnapshotAccessor } from "./agent/routing-catalog.js";
18
+ import { buildModelRoutingPrompt } from "./prompt/routing.js";
17
19
  import { SkillRegistry } from "./skills/registry.js";
18
20
  import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
19
21
  import { FileStateTracker } from "./tools/file-state.js";
@@ -25,12 +27,14 @@ import { ExternalHookController } from "./hooks/index.js";
25
27
  import { getLspService } from "./lsp/index.js";
26
28
  import { loadMcpConfig } from "./mcp/config.js";
27
29
  import { McpManager } from "./mcp/manager.js";
30
+ import { normalizeInheritedThinkingLevel } from "./variant/variant-resolver.js";
28
31
  import { QuestionController } from "./question/index.js";
29
- import { buildMemoryPrompt, formatMemoryStartupResult, recordMemoryCitations, runMemoryPhase2, runMemoryStartupPipeline, startMemoryStartupTask, } from "./memory/index.js";
32
+ import { buildMemoryPrompt, formatMemoryStartupResult, purgeUnsafeMemorySources, recordMemoryCitations, runMemoryPhase2, runMemoryStartupPipeline, startMemoryStartupTask, } from "./memory/index.js";
30
33
  import { basename } from "node:path";
31
34
  import { normalizeSingleLine, truncateVisual } from "./text-display.js";
32
35
  import { BUBBLE_WORDMARK } from "./tui/wordmark.js";
33
36
  import { configureDebugTrace, summarizeAgentEventForTrace, summarizeTraceMessage, traceEvent, } from "./debug-trace.js";
37
+ import { shouldRejectGrokSessionInPrintMode } from "./external-runtime/session-policy.js";
34
38
  async function main() {
35
39
  const args = parseArgs(process.argv.slice(2));
36
40
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
@@ -88,6 +92,7 @@ async function main() {
88
92
  promptCacheKey: sessionPromptCacheKey,
89
93
  protocol: defaultProvider.protocol,
90
94
  openAICodexAuth: registry.createOpenAICodexAuthAdapter(defaultProvider.id),
95
+ grokAuth: registry.createGrokAuthAdapter(defaultProvider.id),
91
96
  })
92
97
  : createUnavailableProvider(unavailableProviderMessage);
93
98
  const createProvider = (providerId, apiKey, baseURL) => createProviderInstance({
@@ -98,6 +103,7 @@ async function main() {
98
103
  promptCacheKey: sessionPromptCacheKey,
99
104
  protocol: registry.getConfigured().find((provider) => provider.id === providerId)?.protocol,
100
105
  openAICodexAuth: registry.createOpenAICodexAuthAdapter(providerId),
106
+ grokAuth: registry.createGrokAuthAdapter(providerId),
101
107
  });
102
108
  const createProviderForRoute = async (route) => {
103
109
  const providerId = route.providerId;
@@ -156,7 +162,7 @@ async function main() {
156
162
  };
157
163
  const lspService = getLspService(args.cwd, settingsManager.getMerged().lsp);
158
164
  const fileStateTracker = new FileStateTracker(args.cwd);
159
- // Shared between the goal tools (model-facing get_goal/update_goal) and the
165
+ // Shared between the goal tool (model-facing update_goal) and the
160
166
  // TUI's auto-continuation engine / status-line indicator.
161
167
  const goalStore = new GoalStore();
162
168
  const tools = createAllTools(args.cwd, skillRegistry, {
@@ -178,6 +184,7 @@ async function main() {
178
184
  console.error(chalk.yellow(`[mcp:${d.scope}] ${d.path}: ${d.message}`));
179
185
  }
180
186
  const mcpManager = new McpManager({ servers: mcpLoaded.servers });
187
+ let externalRuntime;
181
188
  if (mcpLoaded.servers.length > 0) {
182
189
  await mcpManager.start();
183
190
  // Only surface failures at startup. Successful connections would push the
@@ -206,8 +213,19 @@ async function main() {
206
213
  // ignore — we're exiting anyway
207
214
  }
208
215
  };
209
- process.once("SIGINT", () => { void shutdownMcp().then(() => process.exit(130)); });
210
- process.once("SIGTERM", () => { void shutdownMcp().then(() => process.exit(143)); });
216
+ const shutdownExternalRuntime = async () => {
217
+ try {
218
+ await externalRuntime?.dispose();
219
+ }
220
+ catch {
221
+ // ignore — we're exiting anyway
222
+ }
223
+ };
224
+ const shutdownSidecars = async () => {
225
+ await Promise.allSettled([shutdownMcp(), shutdownExternalRuntime()]);
226
+ };
227
+ process.once("SIGINT", () => { void shutdownSidecars().then(() => process.exit(130)); });
228
+ process.once("SIGTERM", () => { void shutdownSidecars().then(() => process.exit(143)); });
211
229
  // Session management:
212
230
  // - default: always start a fresh session
213
231
  // - --resume --session <name>: restore the named session
@@ -235,12 +253,20 @@ async function main() {
235
253
  const pickerThemeMode = effectiveThemeModeForTerminal(themeConfig, preResolvedTheme);
236
254
  const pickerResolvedTheme = pickerThemeMode === "auto" ? preResolvedTheme : pickerThemeMode;
237
255
  const { runSessionPicker } = await import("./tui-ink/run-session-picker.js");
256
+ const { canvasBackgroundFor } = await import("./tui-ink/theme.js");
257
+ // Same rule as the main TUI: a forced theme mismatching the terminal
258
+ // paints its canvas so its foregrounds stay readable.
259
+ const pickerCanvas = themeConfig.overrides?.background
260
+ ? undefined
261
+ : canvasBackgroundFor(pickerThemeMode, pickerResolvedTheme, preResolvedTheme);
238
262
  const picked = await runSessionPicker({
239
263
  currentCwd: args.cwd,
240
264
  currentSessions,
241
265
  allSessions,
242
266
  resolvedTheme: pickerResolvedTheme,
243
- themeOverrides: themeConfig.overrides,
267
+ themeOverrides: pickerCanvas
268
+ ? { ...themeConfig.overrides, background: pickerCanvas }
269
+ : themeConfig.overrides,
244
270
  });
245
271
  if (picked) {
246
272
  sessionManager = new SessionManager(picked);
@@ -288,15 +314,30 @@ async function main() {
288
314
  activeProviderId = "";
289
315
  }
290
316
  const initialThinkingLevel = activeModel
291
- ? (sessionThinkingLevel
292
- ?? args.thinkingLevel
293
- ?? configuredThinkingLevel
294
- ?? getDefaultThinkingLevel(activeProviderId, effectiveModelId))
295
- : (sessionThinkingLevel ?? args.thinkingLevel ?? configuredThinkingLevel ?? "off");
317
+ ? args.thinkingLevel !== undefined
318
+ ? normalizeThinkingLevel(args.thinkingLevel, getAvailableThinkingLevels(activeProviderId, effectiveModelId))
319
+ : sessionThinkingLevel !== undefined
320
+ ? normalizeInheritedThinkingLevel(activeProviderId, effectiveModelId, sessionThinkingLevel)
321
+ : configuredThinkingLevel !== undefined
322
+ ? normalizeThinkingLevel(configuredThinkingLevel, getAvailableThinkingLevels(activeProviderId, effectiveModelId))
323
+ : getDefaultThinkingLevel(activeProviderId, effectiveModelId)
324
+ : (args.thinkingLevel ?? sessionThinkingLevel ?? configuredThinkingLevel ?? "off");
296
325
  const restoredTodos = sessionManager?.getTodos() ?? [];
297
326
  const initialMode = args.mode ?? "default";
298
327
  const skillSummaries = skillRegistry.summaries();
328
+ // This synchronous provenance gate must run before buildMemoryPrompt: the
329
+ // background startup pipeline is too late once text has entered the native
330
+ // provider's system prompt.
331
+ purgeUnsafeMemorySources(args.cwd);
299
332
  const memoryPrompt = buildMemoryPrompt(args.cwd);
333
+ // Routing snapshot accessor (model-routing design §1.5): built before the
334
+ // system prompt because startup composes the prompt before the Agent exists;
335
+ // the same accessor is then handed to the Agent constructor.
336
+ const agentRouting = userConfig.getAgentRouting();
337
+ const routingSnapshotAccessor = createRoutingSnapshotAccessor(registry, () => userConfig.getAgentCategories(), () => agentRouting);
338
+ const initialRoutingParent = activeProviderId && activeModel
339
+ ? { providerId: activeProviderId, model: decodeModel(activeModel).modelId }
340
+ : undefined;
300
341
  const systemPrompt = buildSystemPrompt({
301
342
  agentName: "Bubble",
302
343
  configuredProvider: activeProviderId || "none",
@@ -307,6 +348,9 @@ async function main() {
307
348
  workingDir: args.cwd,
308
349
  ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
309
350
  memoryPrompt,
351
+ modelRoutingPrompt: initialRoutingParent
352
+ ? buildModelRoutingPrompt(routingSnapshotAccessor(initialRoutingParent), agentRouting)
353
+ : undefined,
310
354
  });
311
355
  const traceInfo = configureDebugTrace({
312
356
  cwd: args.cwd,
@@ -381,6 +425,9 @@ async function main() {
381
425
  memoryPrompt,
382
426
  fileStateTracker,
383
427
  agentCategories: userConfig.getAgentCategories(),
428
+ agentRouting,
429
+ routingSnapshot: routingSnapshotAccessor,
430
+ routableModels: createRoutableModelIndex(registry),
384
431
  subagents: userConfig.getSubagents(),
385
432
  providerFactory: createProviderForRoute,
386
433
  externalHooks: hookController,
@@ -418,20 +465,26 @@ async function main() {
418
465
  // not perform an ad-hoc extraction of the just-finished session.
419
466
  };
420
467
  const shutdownRuntime = async () => {
421
- await hookController.runEvent({
422
- eventName: "SessionEnd",
423
- cwd: args.cwd,
424
- sessionId: sessionManager?.getSessionFile(),
425
- agentRole: "driver",
426
- target: "session",
427
- payload: {
428
- providerId: agent.providerId,
429
- model: agent.apiModel,
430
- },
431
- });
468
+ try {
469
+ await hookController.runEvent({
470
+ eventName: "SessionEnd",
471
+ cwd: args.cwd,
472
+ sessionId: sessionManager?.getSessionFile(),
473
+ agentRole: "driver",
474
+ target: "session",
475
+ payload: {
476
+ providerId: agent.providerId,
477
+ model: agent.apiModel,
478
+ },
479
+ });
480
+ }
481
+ catch {
482
+ // Shutdown must still release child processes when a hook fails.
483
+ }
432
484
  const results = await Promise.allSettled([
433
485
  flushMemory(),
434
486
  shutdownMcp(),
487
+ shutdownExternalRuntime(),
435
488
  lspService.shutdown(),
436
489
  ]);
437
490
  for (const result of results) {
@@ -472,12 +525,18 @@ async function main() {
472
525
  if (agent.mode === "plan") {
473
526
  agent.injectModeReminder();
474
527
  }
528
+ agent.injectDeferredToolsReminder();
475
529
  console.log(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
476
530
  }
477
531
  }
478
532
  try {
479
533
  // Print mode: single prompt, then exit
480
534
  if (args.print || args.prompt) {
535
+ if (shouldRejectGrokSessionInPrintMode(sessionManager?.getMetadata().externalRuntime, true)) {
536
+ console.error(chalk.red("Error: Grok subscription sessions are interactive. Resume this workspace session in the TUI, or start a fresh native session for --print."));
537
+ process.exitCode = 1;
538
+ return;
539
+ }
481
540
  const prompt = args.prompt || (await readPipedStdin()) || "";
482
541
  if (!prompt) {
483
542
  console.error(chalk.red("Error: No prompt provided."));
@@ -535,9 +594,8 @@ async function main() {
535
594
  try {
536
595
  const next = new SessionManager(sessionFile);
537
596
  const history = next.getMessages();
538
- sessionManager = next;
539
- sessionPromptCacheKey = next.getOrCreatePromptCacheKey();
540
- sessionTitleUpdater = createSessionTitleUpdater({
597
+ const nextPromptCacheKey = next.getOrCreatePromptCacheKey();
598
+ const nextTitleUpdater = createSessionTitleUpdater({
541
599
  sessionManager: next,
542
600
  complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
543
601
  });
@@ -550,9 +608,26 @@ async function main() {
550
608
  // Keep the live system/meta head (mode reminders survive the switch),
551
609
  // mirroring the /rewind history-replacement pattern.
552
610
  const head = agent.messages.filter((m) => m.role === "system" || m.role === "meta");
553
- agent.messages = [...head, ...history];
554
- agent.setTodos(next.getTodos());
555
- agent.resetContextUsageAnchor();
611
+ const nextMessages = [...head, ...history];
612
+ const nextTodos = next.getTodos();
613
+ // Commit only after every file read/write and reconstruction step has
614
+ // succeeded. Callers can safely prepare candidate sessions without a
615
+ // failed switch rebinding persistence or replacing the live history.
616
+ sessionManager = next;
617
+ sessionPromptCacheKey = nextPromptCacheKey;
618
+ sessionTitleUpdater = nextTitleUpdater;
619
+ agent.messages = nextMessages;
620
+ // These update only live in-memory/UI mirrors after the persistence
621
+ // commit. A best-effort callback failure must not report the switch as
622
+ // rolled back after the outer session binding has already changed.
623
+ try {
624
+ agent.setTodos(nextTodos);
625
+ }
626
+ catch { /* candidate already committed */ }
627
+ try {
628
+ agent.resetContextUsageAnchor();
629
+ }
630
+ catch { /* derived counter only */ }
556
631
  return { manager: next };
557
632
  }
558
633
  catch (error) {
@@ -579,6 +654,16 @@ async function main() {
579
654
  runMemorySummary,
580
655
  runMemoryRefresh,
581
656
  };
657
+ // Grok Subscription is a first-class interactive provider target. Manager
658
+ // construction is deliberately lazy: it does not inspect, spawn, or reach
659
+ // the network until the user selects Grok or runs /login grok. Print,
660
+ // Feishu, help, and version paths return before this import.
661
+ const { createGrokRuntimeManager } = await import("./external-runtime/grok-runtime.js");
662
+ externalRuntime = createGrokRuntimeManager({
663
+ workspace: args.cwd,
664
+ approvalController,
665
+ getPermissionMode: () => agentRef?.mode ?? "default",
666
+ });
582
667
  const { startStartupUpdateCheck } = await import("./update/index.js");
583
668
  const updateCheck = await startStartupUpdateCheck();
584
669
  const updateNotice = updateCheck.notice;
@@ -591,6 +676,7 @@ async function main() {
591
676
  onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
592
677
  updateNotice: updateNotice ?? undefined,
593
678
  updateNoticeRefresh: updateCheck.refreshed,
679
+ externalRuntime,
594
680
  });
595
681
  const exitWallMs = summary?.wallMs;
596
682
  if (sessionManager) {
@@ -40,6 +40,8 @@ export declare class MemoryDatabase {
40
40
  };
41
41
  finishPhase1Job(sessionFile: string, ok: boolean, error?: string): void;
42
42
  listStage1Outputs(limit?: number): Stage1Output[];
43
+ listAllStage1Outputs(): Stage1Output[];
44
+ deleteStage1Output(sessionFile: string): boolean;
43
45
  listPreviouslySelectedNotIn(sessionFiles: string[]): Stage1Output[];
44
46
  markSelectedForPhase2(outputs: Stage1Output[]): void;
45
47
  recordUsage(sessionFiles: string[], now?: Date): number;
package/dist/memory/db.js CHANGED
@@ -95,6 +95,16 @@ export class MemoryDatabase {
95
95
  LIMIT ?
96
96
  `).all(limit).map((row) => mapStage1(row));
97
97
  }
98
+ listAllStage1Outputs() {
99
+ return this.db.prepare(`
100
+ SELECT * FROM memory_stage1_outputs
101
+ ORDER BY usage_count DESC, COALESCE(last_usage, generated_at) DESC, source_updated_at DESC
102
+ `).all().map((row) => mapStage1(row));
103
+ }
104
+ deleteStage1Output(sessionFile) {
105
+ return (this.db.prepare("DELETE FROM memory_stage1_outputs WHERE session_file = ?")
106
+ .run(sessionFile).changes ?? 0) > 0;
107
+ }
98
108
  listPreviouslySelectedNotIn(sessionFiles) {
99
109
  if (sessionFiles.length === 0) {
100
110
  return this.db.prepare("SELECT * FROM memory_stage1_outputs WHERE selected_for_phase2 = 1")
@@ -7,3 +7,4 @@ export * from "./start.js";
7
7
  export * from "./storage.js";
8
8
  export * from "./reset.js";
9
9
  export * from "./usage.js";
10
+ export * from "./session-policy.js";
@@ -7,3 +7,4 @@ export * from "./start.js";
7
7
  export * from "./storage.js";
8
8
  export * from "./reset.js";
9
9
  export * from "./usage.js";
10
+ export * from "./session-policy.js";