@bubblebrain-ai/bubble 0.0.35 → 0.0.37

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 (144) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/categories.d.ts +66 -2
  3. package/dist/agent/categories.js +120 -19
  4. package/dist/agent/discovery-barrier.js +1 -1
  5. package/dist/agent/profiles.d.ts +1 -0
  6. package/dist/agent/profiles.js +25 -11
  7. package/dist/agent/routing-catalog.d.ts +113 -0
  8. package/dist/agent/routing-catalog.js +240 -0
  9. package/dist/agent/workflow/control.d.ts +14 -0
  10. package/dist/agent/workflow/control.js +30 -9
  11. package/dist/agent.d.ts +75 -5
  12. package/dist/agent.js +313 -61
  13. package/dist/approval/controller.js +28 -0
  14. package/dist/approval/tool-helper.js +2 -0
  15. package/dist/approval/types.d.ts +21 -1
  16. package/dist/cli.d.ts +1 -1
  17. package/dist/cli.js +2 -1
  18. package/dist/config.d.ts +4 -0
  19. package/dist/config.js +4 -0
  20. package/dist/context/compact-llm.d.ts +2 -2
  21. package/dist/external-runtime/grok-acp.d.ts +56 -0
  22. package/dist/external-runtime/grok-acp.js +386 -0
  23. package/dist/external-runtime/grok-binary.d.ts +25 -0
  24. package/dist/external-runtime/grok-binary.js +108 -0
  25. package/dist/external-runtime/grok-errors.d.ts +7 -0
  26. package/dist/external-runtime/grok-errors.js +15 -0
  27. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  28. package/dist/external-runtime/grok-input-policy.js +52 -0
  29. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  30. package/dist/external-runtime/grok-ndjson.js +193 -0
  31. package/dist/external-runtime/grok-network.d.ts +15 -0
  32. package/dist/external-runtime/grok-network.js +81 -0
  33. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  34. package/dist/external-runtime/grok-preflight.js +194 -0
  35. package/dist/external-runtime/grok-process.d.ts +26 -0
  36. package/dist/external-runtime/grok-process.js +184 -0
  37. package/dist/external-runtime/grok-profile.d.ts +32 -0
  38. package/dist/external-runtime/grok-profile.js +419 -0
  39. package/dist/external-runtime/grok-provider.d.ts +31 -0
  40. package/dist/external-runtime/grok-provider.js +41 -0
  41. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  42. package/dist/external-runtime/grok-runtime.js +882 -0
  43. package/dist/external-runtime/index.d.ts +13 -0
  44. package/dist/external-runtime/index.js +13 -0
  45. package/dist/external-runtime/session-policy.d.ts +11 -0
  46. package/dist/external-runtime/session-policy.js +30 -0
  47. package/dist/external-runtime/types.d.ts +65 -0
  48. package/dist/external-runtime/types.js +1 -0
  49. package/dist/feishu/agent-host/approval-card.js +21 -0
  50. package/dist/feishu/agent-host/run-driver.js +19 -3
  51. package/dist/feishu/serve.js +1 -0
  52. package/dist/goal/tools.d.ts +6 -3
  53. package/dist/goal/tools.js +7 -19
  54. package/dist/main.js +105 -27
  55. package/dist/memory/db.d.ts +2 -0
  56. package/dist/memory/db.js +10 -0
  57. package/dist/memory/index.d.ts +1 -0
  58. package/dist/memory/index.js +1 -0
  59. package/dist/memory/phase1.js +14 -2
  60. package/dist/memory/phase2.js +21 -2
  61. package/dist/memory/prompts.js +1 -1
  62. package/dist/memory/session-policy.d.ts +25 -0
  63. package/dist/memory/session-policy.js +55 -0
  64. package/dist/memory/storage.d.ts +1 -0
  65. package/dist/memory/storage.js +13 -0
  66. package/dist/model-catalog.d.ts +18 -0
  67. package/dist/model-catalog.js +87 -41
  68. package/dist/model-config.d.ts +3 -1
  69. package/dist/model-config.js +11 -0
  70. package/dist/oauth/grok.d.ts +25 -0
  71. package/dist/oauth/grok.js +218 -0
  72. package/dist/oauth/index.d.ts +1 -0
  73. package/dist/oauth/index.js +1 -0
  74. package/dist/oauth/storage.d.ts +9 -0
  75. package/dist/oauth/storage.js +25 -0
  76. package/dist/orchestrator/default-hooks.js +17 -7
  77. package/dist/prompt/compose.d.ts +3 -0
  78. package/dist/prompt/compose.js +3 -0
  79. package/dist/prompt/delegation.js +22 -3
  80. package/dist/prompt/environment.js +0 -4
  81. package/dist/prompt/reminders.d.ts +0 -1
  82. package/dist/prompt/reminders.js +1 -11
  83. package/dist/prompt/routing.d.ts +14 -0
  84. package/dist/prompt/routing.js +71 -0
  85. package/dist/prompt/runtime.js +1 -1
  86. package/dist/prompt/task-reminders.d.ts +19 -0
  87. package/dist/prompt/task-reminders.js +49 -0
  88. package/dist/provider-ai-sdk.js +2 -2
  89. package/dist/provider-grok.d.ts +26 -0
  90. package/dist/provider-grok.js +59 -0
  91. package/dist/provider-openai-codex.d.ts +17 -1
  92. package/dist/provider-openai-codex.js +101 -29
  93. package/dist/provider-registry.d.ts +72 -0
  94. package/dist/provider-registry.js +336 -59
  95. package/dist/provider-transform.d.ts +1 -1
  96. package/dist/provider-transform.js +28 -5
  97. package/dist/provider.d.ts +3 -0
  98. package/dist/provider.js +14 -0
  99. package/dist/session-types.d.ts +19 -1
  100. package/dist/session.d.ts +1 -0
  101. package/dist/session.js +4 -0
  102. package/dist/slash-commands/commands.js +224 -14
  103. package/dist/slash-commands/types.d.ts +14 -1
  104. package/dist/system-prompt.d.ts +2 -0
  105. package/dist/tools/agent-lifecycle.js +9 -11
  106. package/dist/tools/bash.js +1 -1
  107. package/dist/tools/child-tools.js +2 -0
  108. package/dist/tools/index.d.ts +2 -2
  109. package/dist/tools/index.js +7 -5
  110. package/dist/tools/lsp.js +1 -0
  111. package/dist/tools/memory.d.ts +7 -2
  112. package/dist/tools/memory.js +28 -43
  113. package/dist/tools/server.js +4 -0
  114. package/dist/tools/tool-search.d.ts +1 -1
  115. package/dist/tools/tool-search.js +23 -3
  116. package/dist/tui/model-switch.d.ts +7 -0
  117. package/dist/tui/model-switch.js +15 -5
  118. package/dist/tui-ink/app.d.ts +3 -1
  119. package/dist/tui-ink/app.js +649 -233
  120. package/dist/tui-ink/approval/approval-dialog.js +36 -0
  121. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  122. package/dist/tui-ink/composer-buffer.js +317 -0
  123. package/dist/tui-ink/footer.d.ts +1 -0
  124. package/dist/tui-ink/footer.js +7 -2
  125. package/dist/tui-ink/input-box.d.ts +35 -1
  126. package/dist/tui-ink/input-box.js +215 -122
  127. package/dist/tui-ink/message-list.js +5 -5
  128. package/dist/tui-ink/model-picker.d.ts +61 -4
  129. package/dist/tui-ink/model-picker.js +486 -133
  130. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  131. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  132. package/dist/tui-ink/run.d.ts +3 -0
  133. package/dist/tui-ink/run.js +1 -1
  134. package/dist/tui-ink/subagent-view.d.ts +31 -0
  135. package/dist/tui-ink/subagent-view.js +82 -1
  136. package/dist/types.d.ts +5 -5
  137. package/dist/types.js +1 -1
  138. package/dist/variant/thinking-level.d.ts +2 -2
  139. package/dist/variant/thinking-level.js +2 -1
  140. package/dist/variant/variant-resolver.d.ts +9 -0
  141. package/dist/variant/variant-resolver.js +17 -0
  142. package/package.json +2 -1
  143. package/dist/tools/task.d.ts +0 -2
  144. 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
@@ -296,15 +314,30 @@ async function main() {
296
314
  activeProviderId = "";
297
315
  }
298
316
  const initialThinkingLevel = activeModel
299
- ? (sessionThinkingLevel
300
- ?? args.thinkingLevel
301
- ?? configuredThinkingLevel
302
- ?? getDefaultThinkingLevel(activeProviderId, effectiveModelId))
303
- : (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");
304
325
  const restoredTodos = sessionManager?.getTodos() ?? [];
305
326
  const initialMode = args.mode ?? "default";
306
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);
307
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;
308
341
  const systemPrompt = buildSystemPrompt({
309
342
  agentName: "Bubble",
310
343
  configuredProvider: activeProviderId || "none",
@@ -315,6 +348,9 @@ async function main() {
315
348
  workingDir: args.cwd,
316
349
  ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
317
350
  memoryPrompt,
351
+ modelRoutingPrompt: initialRoutingParent
352
+ ? buildModelRoutingPrompt(routingSnapshotAccessor(initialRoutingParent), agentRouting)
353
+ : undefined,
318
354
  });
319
355
  const traceInfo = configureDebugTrace({
320
356
  cwd: args.cwd,
@@ -389,6 +425,9 @@ async function main() {
389
425
  memoryPrompt,
390
426
  fileStateTracker,
391
427
  agentCategories: userConfig.getAgentCategories(),
428
+ agentRouting,
429
+ routingSnapshot: routingSnapshotAccessor,
430
+ routableModels: createRoutableModelIndex(registry),
392
431
  subagents: userConfig.getSubagents(),
393
432
  providerFactory: createProviderForRoute,
394
433
  externalHooks: hookController,
@@ -426,20 +465,26 @@ async function main() {
426
465
  // not perform an ad-hoc extraction of the just-finished session.
427
466
  };
428
467
  const shutdownRuntime = async () => {
429
- await hookController.runEvent({
430
- eventName: "SessionEnd",
431
- cwd: args.cwd,
432
- sessionId: sessionManager?.getSessionFile(),
433
- agentRole: "driver",
434
- target: "session",
435
- payload: {
436
- providerId: agent.providerId,
437
- model: agent.apiModel,
438
- },
439
- });
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
+ }
440
484
  const results = await Promise.allSettled([
441
485
  flushMemory(),
442
486
  shutdownMcp(),
487
+ shutdownExternalRuntime(),
443
488
  lspService.shutdown(),
444
489
  ]);
445
490
  for (const result of results) {
@@ -480,12 +525,18 @@ async function main() {
480
525
  if (agent.mode === "plan") {
481
526
  agent.injectModeReminder();
482
527
  }
528
+ agent.injectDeferredToolsReminder();
483
529
  console.log(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
484
530
  }
485
531
  }
486
532
  try {
487
533
  // Print mode: single prompt, then exit
488
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
+ }
489
540
  const prompt = args.prompt || (await readPipedStdin()) || "";
490
541
  if (!prompt) {
491
542
  console.error(chalk.red("Error: No prompt provided."));
@@ -543,9 +594,8 @@ async function main() {
543
594
  try {
544
595
  const next = new SessionManager(sessionFile);
545
596
  const history = next.getMessages();
546
- sessionManager = next;
547
- sessionPromptCacheKey = next.getOrCreatePromptCacheKey();
548
- sessionTitleUpdater = createSessionTitleUpdater({
597
+ const nextPromptCacheKey = next.getOrCreatePromptCacheKey();
598
+ const nextTitleUpdater = createSessionTitleUpdater({
549
599
  sessionManager: next,
550
600
  complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
551
601
  });
@@ -558,9 +608,26 @@ async function main() {
558
608
  // Keep the live system/meta head (mode reminders survive the switch),
559
609
  // mirroring the /rewind history-replacement pattern.
560
610
  const head = agent.messages.filter((m) => m.role === "system" || m.role === "meta");
561
- agent.messages = [...head, ...history];
562
- agent.setTodos(next.getTodos());
563
- 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 */ }
564
631
  return { manager: next };
565
632
  }
566
633
  catch (error) {
@@ -587,6 +654,16 @@ async function main() {
587
654
  runMemorySummary,
588
655
  runMemoryRefresh,
589
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
+ });
590
667
  const { startStartupUpdateCheck } = await import("./update/index.js");
591
668
  const updateCheck = await startStartupUpdateCheck();
592
669
  const updateNotice = updateCheck.notice;
@@ -599,6 +676,7 @@ async function main() {
599
676
  onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
600
677
  updateNotice: updateNotice ?? undefined,
601
678
  updateNoticeRefresh: updateCheck.refreshed,
679
+ externalRuntime,
602
680
  });
603
681
  const exitWallMs = summary?.wallMs;
604
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";
@@ -1,10 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { SessionManager } from "../session.js";
5
4
  import { MemoryDatabase } from "./db.js";
6
5
  import { getBubbleHome } from "./paths.js";
7
6
  import { buildStageOneMessages, parseJsonObject } from "./prompts.js";
7
+ import { classifyMemorySession } from "./session-policy.js";
8
+ import { clearGeneratedMemoryOutputs } from "./storage.js";
8
9
  import { redactSecrets } from "./store.js";
9
10
  const DEFAULT_MIN_ENTRIES = 4;
10
11
  const DEFAULT_LIMIT = 24;
@@ -22,11 +23,22 @@ export async function runMemoryPhase1(options) {
22
23
  const sessions = listEligibleSessionFiles(options.limit ?? DEFAULT_LIMIT);
23
24
  result.scanned = sessions.length;
24
25
  for (const sessionFile of sessions) {
26
+ const source = classifyMemorySession(sessionFile);
27
+ if (source.kind !== "native") {
28
+ if (db.getStage1Output(sessionFile)) {
29
+ // Clear before deleting the provenance row so interruption cannot
30
+ // leave an unsafe generated artifact that looks source-less later.
31
+ clearGeneratedMemoryOutputs(options.cwd);
32
+ db.deleteStage1Output(sessionFile);
33
+ }
34
+ result.skipped++;
35
+ continue;
36
+ }
25
37
  if (db.getThreadMemoryMode(sessionFile) === "disabled") {
26
38
  result.skipped++;
27
39
  continue;
28
40
  }
29
- const session = new SessionManager(sessionFile);
41
+ const session = source.session;
30
42
  const sessionCwd = session.getMetadata().cwd ?? options.cwd;
31
43
  const entries = session.getEntries();
32
44
  const entryCount = entries.length;