@bubblebrain-ai/bubble 0.0.46 → 0.0.47

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 (92) hide show
  1. package/dist/agent/child-runner.js +11 -0
  2. package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
  3. package/dist/agent/internal-reminder-sanitizer.js +13 -0
  4. package/dist/agent/subagent/router.d.ts +65 -0
  5. package/dist/agent/subagent/router.js +198 -0
  6. package/dist/agent/subagent/runtime.d.ts +203 -0
  7. package/dist/agent/subagent/runtime.js +789 -0
  8. package/dist/agent/subagent-control.d.ts +5 -0
  9. package/dist/agent/subagent-store.d.ts +18 -2
  10. package/dist/agent/subagent-store.js +32 -1
  11. package/dist/agent/tool-intent.d.ts +10 -0
  12. package/dist/agent/tool-intent.js +18 -0
  13. package/dist/agent/tool-visibility.d.ts +10 -0
  14. package/dist/agent/tool-visibility.js +6 -0
  15. package/dist/agent/workflow/runs.d.ts +72 -0
  16. package/dist/agent/workflow/runs.js +157 -0
  17. package/dist/agent.d.ts +78 -96
  18. package/dist/agent.js +266 -1072
  19. package/dist/context/compact-llm.js +14 -5
  20. package/dist/context/compact.d.ts +9 -0
  21. package/dist/context/compact.js +36 -10
  22. package/dist/context/compaction-files.d.ts +44 -0
  23. package/dist/context/compaction-files.js +148 -0
  24. package/dist/context/llm-compactor.js +24 -5
  25. package/dist/feishu/agent-host/run-driver.js +3 -0
  26. package/dist/feishu/serve.js +13 -9
  27. package/dist/goal/tools.js +12 -3
  28. package/dist/main.js +24 -19
  29. package/dist/model-catalog.js +36 -2
  30. package/dist/model-config.d.ts +2 -0
  31. package/dist/model-data.generated.d.ts +16 -0
  32. package/dist/model-data.generated.js +275 -0
  33. package/dist/model-pricing.d.ts +11 -7
  34. package/dist/model-pricing.js +0 -0
  35. package/dist/oauth/grok.d.ts +1 -1
  36. package/dist/oauth/grok.js +5 -2
  37. package/dist/oauth/storage.d.ts +9 -2
  38. package/dist/oauth/storage.js +16 -9
  39. package/dist/orchestrator/default-hooks.d.ts +20 -0
  40. package/dist/orchestrator/default-hooks.js +47 -295
  41. package/dist/orchestrator/hooks.d.ts +6 -39
  42. package/dist/permission/mode.js +1 -1
  43. package/dist/prompt/reminders.d.ts +9 -35
  44. package/dist/prompt/reminders.js +30 -136
  45. package/dist/prompt/task-reminders.d.ts +0 -31
  46. package/dist/prompt/task-reminders.js +0 -87
  47. package/dist/provider-ai-sdk.d.ts +2 -0
  48. package/dist/provider-ai-sdk.js +1 -0
  49. package/dist/provider-anthropic.d.ts +2 -0
  50. package/dist/provider-anthropic.js +3 -1
  51. package/dist/provider-ark-responses.d.ts +2 -0
  52. package/dist/provider-ark-responses.js +1 -0
  53. package/dist/provider-registry.d.ts +13 -0
  54. package/dist/provider-registry.js +83 -0
  55. package/dist/provider-transform.js +37 -1
  56. package/dist/provider.d.ts +7 -0
  57. package/dist/provider.js +13 -1
  58. package/dist/sdk/index.d.ts +152 -0
  59. package/dist/sdk/index.js +400 -0
  60. package/dist/session.js +9 -1
  61. package/dist/slash-commands/commands.js +33 -19
  62. package/dist/tools/exit-plan-mode.js +8 -0
  63. package/dist/tui/detect-theme.d.ts +1 -0
  64. package/dist/tui/detect-theme.js +11 -5
  65. package/dist/tui-ink/app.d.ts +2 -6
  66. package/dist/tui-ink/app.js +11 -18
  67. package/dist/tui-ink/display-history.d.ts +0 -2
  68. package/dist/tui-ink/display-history.js +0 -52
  69. package/dist/tui-ink/footer.js +2 -2
  70. package/dist/tui-ink/message-list.js +62 -23
  71. package/dist/tui-ink/run.js +10 -5
  72. package/dist/tui-ink/sync-output.d.ts +17 -0
  73. package/dist/tui-ink/sync-output.js +35 -0
  74. package/dist/tui-ink/theme.d.ts +0 -1
  75. package/dist/tui-ink/theme.js +0 -2
  76. package/dist/tui-ink/trace-groups.js +1 -1
  77. package/dist/types.d.ts +21 -1
  78. package/package.json +16 -2
  79. package/dist/agent/discovery-barrier.d.ts +0 -21
  80. package/dist/agent/discovery-barrier.js +0 -173
  81. package/dist/agent/evidence-tracker.d.ts +0 -15
  82. package/dist/agent/evidence-tracker.js +0 -93
  83. package/dist/agent/execution-governor.d.ts +0 -36
  84. package/dist/agent/execution-governor.js +0 -218
  85. package/dist/agent/task-classifier.d.ts +0 -3
  86. package/dist/agent/task-classifier.js +0 -114
  87. package/dist/agent/task-size.d.ts +0 -9
  88. package/dist/agent/task-size.js +0 -33
  89. package/dist/agent/tool-arbiter.d.ts +0 -7
  90. package/dist/agent/tool-arbiter.js +0 -33
  91. package/dist/orchestrator/workflow.d.ts +0 -10
  92. package/dist/orchestrator/workflow.js +0 -22
package/dist/provider.js CHANGED
@@ -102,13 +102,19 @@ export function createProviderInstance(options) {
102
102
  // CLI identity headers the proxy gates on, plus a fetch that keeps the
103
103
  // short-lived OAuth bearer fresh (and proxy-aware) across long sessions.
104
104
  const grokSubscription = options.providerId === "grok" || isGrokSubscriptionBaseUrl(options.baseURL);
105
+ // User-configured headers merge over the built-in identity headers so a
106
+ // config entry can override even the Grok defaults deliberately.
107
+ const defaultHeaders = {
108
+ ...(grokSubscription ? buildGrokSubscriptionHeaders() : {}),
109
+ ...(options.headers ?? {}),
110
+ };
105
111
  const client = new OpenAI({
106
112
  apiKey: options.apiKey,
107
113
  baseURL: options.baseURL,
108
114
  timeout: resolveRequestTimeoutMs(process.env.BUBBLE_PROVIDER_REQUEST_TIMEOUT_MS),
115
+ ...(Object.keys(defaultHeaders).length > 0 ? { defaultHeaders } : {}),
109
116
  ...(grokSubscription
110
117
  ? {
111
- defaultHeaders: buildGrokSubscriptionHeaders(),
112
118
  fetch: (options.grokAuth
113
119
  ? createGrokSubscriptionFetch(options.grokAuth)
114
120
  : getChatGptFetch()),
@@ -266,10 +272,16 @@ function shouldRequestStreamUsage(options) {
266
272
  || providerId === "deepseek"
267
273
  || providerId === "moonshot-cn"
268
274
  || providerId === "moonshot-intl"
275
+ // Verified 2026-08-04: this endpoint streams NO usage at all without the
276
+ // flag, so every coding-plan session was silently unpriced and left the
277
+ // context-budget anchor guessing.
278
+ || providerId === "kimi-for-coding"
269
279
  || providerId === "zhipuai"
270
280
  || providerId === "zhipuai-coding-plan"
271
281
  || providerId === "zai"
272
282
  || providerId === "zai-coding-plan"
283
+ // Bailian sends usage either way; ask explicitly so it stays guaranteed.
284
+ || providerId === "bailian-token-plan"
273
285
  || isMiniMaxOpenAICompatible(options);
274
286
  }
275
287
  function shouldUseNonStreamingToolCalls(options, tools, toolChoice) {
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Bubble SDK — the programmatic embedding surface for external hosts.
3
+ *
4
+ * Two layers:
5
+ * 1. `BubbleSdk` — a high-level facade that owns config/provider resolution and
6
+ * assembles a full agent turn (tools, approvals, questions, plan mode, MCP,
7
+ * skills, hooks, session persistence) behind a single `runTurn()` async
8
+ * iterator of `AgentEvent`s. This is what a host like an Electron app should
9
+ * consume: wire the three interaction callbacks (approval / question / plan)
10
+ * to its own UI and map the event stream to its own message format.
11
+ * 2. Curated re-exports of the underlying building blocks, for hosts that need
12
+ * to assemble a custom loop the facade doesn't cover.
13
+ *
14
+ * Runs under plain Node (>=20); nothing here touches the TUI layer.
15
+ */
16
+ import { type SessionSummary } from "../session.js";
17
+ import type { ApprovalDecision, ApprovalRequest } from "../approval/types.js";
18
+ import { UserConfig } from "../config.js";
19
+ import { ProviderRegistry } from "../provider-registry.js";
20
+ import { type QuestionAnswer, type QuestionRequest } from "../question/controller.js";
21
+ import { SkillRegistry } from "../skills/registry.js";
22
+ import type { SkillSummary } from "../skills/types.js";
23
+ import type { AgentEvent, ContentPart, Message, PermissionMode, ThinkingLevel } from "../types.js";
24
+ export interface BubbleSdkOptions {
25
+ /** Fallback working directory for sessions created without an explicit cwd. */
26
+ defaultCwd?: string;
27
+ /**
28
+ * Load MCP servers from user/project settings (default true). Hermetic hosts
29
+ * (evals, tests) set false so runs never depend on ambient MCP config.
30
+ */
31
+ mcp?: boolean;
32
+ }
33
+ /** Resolved turn configuration, reported via onStart (e.g. for a host's system-init event). */
34
+ export interface TurnStartInfo {
35
+ providerId: string;
36
+ model: string;
37
+ thinkingLevel: ThinkingLevel;
38
+ mode: PermissionMode;
39
+ tools: string[];
40
+ skills: string[];
41
+ }
42
+ /** Host-side interaction callbacks for one turn. All optional; missing handlers fail safe (reject). */
43
+ export interface TurnHandlers {
44
+ /**
45
+ * Tool call needs user permission (bash / edit / write / …). When the turn is
46
+ * aborted, any still-pending request auto-resolves to reject — the host's
47
+ * promise may settle later and is then ignored.
48
+ */
49
+ onApproval?: (req: ApprovalRequest) => Promise<ApprovalDecision>;
50
+ /** Agent asked the user a structured question. Return answers, or null to decline. */
51
+ onQuestion?: (req: QuestionRequest) => Promise<QuestionAnswer[] | null>;
52
+ /** Plan-mode proposal. Return true to approve executing the plan. */
53
+ onPlanApproval?: (planMarkdown: string) => Promise<boolean>;
54
+ /** Fired once per turn after provider/model/tools are resolved, before the first event. */
55
+ onStart?: (info: TurnStartInfo) => void;
56
+ }
57
+ export interface RunTurnOptions extends TurnHandlers {
58
+ /** Plain text, or content parts (text + base64 images) for attachments. */
59
+ prompt: string | ContentPart[];
60
+ /** "provider:model" or bare model id (resolved against the default provider). */
61
+ model?: string;
62
+ mode?: PermissionMode;
63
+ thinkingLevel?: ThinkingLevel;
64
+ /**
65
+ * Extra text appended to the built system prompt. Lets hosts (and the eval
66
+ * harness) A/B prompt variants without forking the prompt builder.
67
+ */
68
+ appendSystemPrompt?: string;
69
+ signal?: AbortSignal;
70
+ }
71
+ export interface SdkSessionRef {
72
+ id: string;
73
+ cwd: string;
74
+ }
75
+ export declare class BubbleSdk {
76
+ readonly userConfig: UserConfig;
77
+ readonly registry: ProviderRegistry;
78
+ private readonly defaultCwd;
79
+ private readonly mcpEnabled;
80
+ private readonly cwdBySession;
81
+ private readonly bashAllowlists;
82
+ private readonly activeTurns;
83
+ private readonly mcpToolsByCwd;
84
+ /** id -> {cwd,file} rebuilt from disk so sessions survive host restarts. */
85
+ private sessionIndex;
86
+ constructor(options?: BubbleSdkOptions);
87
+ listSessions(): SessionSummary[];
88
+ createSession(options?: {
89
+ cwd?: string;
90
+ id?: string;
91
+ }): SdkSessionRef;
92
+ getHistory(sessionId: string): Message[];
93
+ deleteSession(sessionId: string): void;
94
+ /** Abort the in-flight turn of a session, if any. */
95
+ stop(sessionId: string): void;
96
+ listSkills(cwd?: string): SkillSummary[];
97
+ /** Configured providers + default model, for a host's model picker. */
98
+ getModelConfig(): {
99
+ defaultProviderId: string;
100
+ defaultModel: string;
101
+ providers: Array<{
102
+ id: string;
103
+ baseURL?: string;
104
+ hasApiKey: boolean;
105
+ }>;
106
+ };
107
+ runTurn(sessionId: string, options: RunTurnOptions): AsyncGenerator<AgentEvent>;
108
+ private bashAllowlistFor;
109
+ /** MCP servers are started lazily, once per cwd (McpManager has no stop). */
110
+ private mcpToolsFor;
111
+ private resolveSession;
112
+ private resolveProvider;
113
+ }
114
+ /**
115
+ * Attach a `cost` field to turn_end events that carry usage, priced via the
116
+ * static pricing table. Pricing is an accounting concern, so it rides along
117
+ * here in the SDK rather than in the agent core. Multi-step turns emit several
118
+ * turn_end events; each cost covers only that step's usage, so hosts sum them.
119
+ * Events for unpriced models (or without usage) pass through untouched.
120
+ */
121
+ export declare function attachTurnCost(event: AgentEvent, providerId: string, modelId: string): AgentEvent;
122
+ /**
123
+ * If the prompt (or its first text part) is a "/<skill-name> <task>" skill
124
+ * invocation, rewrite it into the same explicit execution prompt the TUI
125
+ * sends. Non-matching prompts pass through untouched.
126
+ */
127
+ export declare function rewriteSkillInvocationPrompt(prompt: string | ContentPart[], registry: SkillRegistry): string | ContentPart[];
128
+ export { Agent, AgentAbortError, type AgentOptions, type AgentRunOptions } from "../agent.js";
129
+ export { SessionManager, getSessionsDir, type SessionSummary, type UserTurn, type RewindResult, } from "../session.js";
130
+ export { PermissionAwareApprovalController } from "../approval/controller.js";
131
+ export { BashAllowlist } from "../approval/session-cache.js";
132
+ export type { ApprovalController, ApprovalDecision, ApprovalRequest } from "../approval/types.js";
133
+ export { createAllTools, buildToolPromptOptions, type PlanController } from "../tools/index.js";
134
+ export { buildSystemPrompt } from "../system-prompt.js";
135
+ export { FileStateTracker } from "../tools/file-state.js";
136
+ export { BudgetLedger } from "../agent/budget-ledger.js";
137
+ export { UserConfig } from "../config.js";
138
+ export { ProviderRegistry, encodeModel, decodeModel, displayModel, type ProviderProfile, type ModelInfo, } from "../provider-registry.js";
139
+ export { createProviderInstance } from "../provider.js";
140
+ export { getDefaultThinkingLevel } from "../variant/variant-resolver.js";
141
+ export { QuestionController, QuestionRejectedError, type QuestionAnswer, type QuestionEvent, type QuestionRequest, } from "../question/controller.js";
142
+ export { SkillRegistry } from "../skills/registry.js";
143
+ export { parseSkillInvocation, type SkillInvocation } from "../skills/invocation.js";
144
+ export type { SkillSummary } from "../skills/types.js";
145
+ export { GoalStore } from "../goal/store.js";
146
+ export { McpManager } from "../mcp/manager.js";
147
+ export { loadMcpConfig } from "../mcp/config.js";
148
+ export { ExternalHookController } from "../hooks/controller.js";
149
+ export { calculateUsageCost, getModelPricing, type ModelPricing, type PricingCurrency, } from "../model-pricing.js";
150
+ export { registry as slashCommandRegistry } from "../slash-commands/index.js";
151
+ export type { SlashCommandContext } from "../slash-commands/types.js";
152
+ export type { AgentEvent, ContentPart, Message, PermissionMode, Provider, ThinkingLevel, Todo, TokenUsage, UsageCost, ToolRegistryEntry, ToolResult, ToolUpdate, } from "../types.js";
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Bubble SDK — the programmatic embedding surface for external hosts.
3
+ *
4
+ * Two layers:
5
+ * 1. `BubbleSdk` — a high-level facade that owns config/provider resolution and
6
+ * assembles a full agent turn (tools, approvals, questions, plan mode, MCP,
7
+ * skills, hooks, session persistence) behind a single `runTurn()` async
8
+ * iterator of `AgentEvent`s. This is what a host like an Electron app should
9
+ * consume: wire the three interaction callbacks (approval / question / plan)
10
+ * to its own UI and map the event stream to its own message format.
11
+ * 2. Curated re-exports of the underlying building blocks, for hosts that need
12
+ * to assemble a custom loop the facade doesn't cover.
13
+ *
14
+ * Runs under plain Node (>=20); nothing here touches the TUI layer.
15
+ */
16
+ import os from "node:os";
17
+ import { rmSync } from "node:fs";
18
+ import { randomUUID } from "node:crypto";
19
+ import { Agent } from "../agent.js";
20
+ import { SessionManager } from "../session.js";
21
+ import { PermissionAwareApprovalController } from "../approval/controller.js";
22
+ import { BashAllowlist } from "../approval/session-cache.js";
23
+ import { createAllTools, buildToolPromptOptions } from "../tools/index.js";
24
+ import { buildSystemPrompt } from "../system-prompt.js";
25
+ import { FileStateTracker } from "../tools/file-state.js";
26
+ import { BudgetLedger } from "../agent/budget-ledger.js";
27
+ import { UserConfig } from "../config.js";
28
+ import { ProviderRegistry, encodeModel, decodeModel, displayModel } from "../provider-registry.js";
29
+ import { createProviderInstance } from "../provider.js";
30
+ import { getDefaultThinkingLevel } from "../variant/variant-resolver.js";
31
+ import { QuestionController } from "../question/controller.js";
32
+ import { SkillRegistry } from "../skills/registry.js";
33
+ import { parseSkillInvocation } from "../skills/invocation.js";
34
+ import { GoalStore } from "../goal/store.js";
35
+ import { McpManager } from "../mcp/manager.js";
36
+ import { loadMcpConfig } from "../mcp/config.js";
37
+ import { ExternalHookController } from "../hooks/controller.js";
38
+ import { buildMemoryPrompt } from "../memory/store.js";
39
+ import { calculateUsageCost } from "../model-pricing.js";
40
+ import { purgeUnsafeMemorySources } from "../memory/session-policy.js";
41
+ import { recordMemoryCitations } from "../memory/usage.js";
42
+ // ── Facade ─────────────────────────────────────────────────────────────────
43
+ export class BubbleSdk {
44
+ userConfig = new UserConfig();
45
+ registry = new ProviderRegistry(this.userConfig);
46
+ defaultCwd;
47
+ mcpEnabled;
48
+ cwdBySession = new Map();
49
+ bashAllowlists = new Map();
50
+ activeTurns = new Map();
51
+ mcpToolsByCwd = new Map();
52
+ /** id -> {cwd,file} rebuilt from disk so sessions survive host restarts. */
53
+ sessionIndex = new Map();
54
+ constructor(options = {}) {
55
+ this.defaultCwd = options.defaultCwd || process.env.BUBBLE_CWD || os.homedir();
56
+ this.mcpEnabled = options.mcp !== false;
57
+ }
58
+ // ── Sessions ─────────────────────────────────────────────────────────────
59
+ listSessions() {
60
+ const sessions = SessionManager.listAllSessions();
61
+ this.sessionIndex = new Map(sessions.map((s) => [s.name, { cwd: s.cwd ?? s.cwdLabel ?? this.defaultCwd, file: s.file }]));
62
+ return sessions;
63
+ }
64
+ createSession(options = {}) {
65
+ const cwd = options.cwd || this.defaultCwd;
66
+ const id = options.id || `sdk-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
67
+ SessionManager.create(cwd, sessionFileName(id));
68
+ this.cwdBySession.set(id, cwd);
69
+ return { id, cwd };
70
+ }
71
+ getHistory(sessionId) {
72
+ return this.resolveSession(sessionId)?.manager.getMessages() ?? [];
73
+ }
74
+ deleteSession(sessionId) {
75
+ const resolved = this.resolveSession(sessionId);
76
+ if (resolved)
77
+ rmSync(resolved.manager.getSessionFile(), { force: true });
78
+ this.cwdBySession.delete(sessionId);
79
+ this.bashAllowlists.delete(sessionId);
80
+ this.sessionIndex.delete(sessionId);
81
+ }
82
+ /** Abort the in-flight turn of a session, if any. */
83
+ stop(sessionId) {
84
+ this.activeTurns.get(sessionId)?.abort();
85
+ }
86
+ // ── Discovery (composer pickers) ─────────────────────────────────────────
87
+ listSkills(cwd) {
88
+ const registry = new SkillRegistry({
89
+ cwd: cwd || this.defaultCwd,
90
+ skillPaths: this.userConfig.getSkillPaths(),
91
+ });
92
+ return registry.summaries();
93
+ }
94
+ /** Configured providers + default model, for a host's model picker. */
95
+ getModelConfig() {
96
+ const configured = this.registry.getConfigured().filter((p) => p.enabled);
97
+ return {
98
+ defaultProviderId: this.registry.getDefault()?.id ?? "",
99
+ defaultModel: this.userConfig.getDefaultModel() ?? "",
100
+ providers: configured.map((p) => ({ id: p.id, baseURL: p.baseURL, hasApiKey: Boolean(p.apiKey) })),
101
+ };
102
+ }
103
+ // ── The turn ─────────────────────────────────────────────────────────────
104
+ async *runTurn(sessionId, options) {
105
+ const resolved = this.resolveSession(sessionId);
106
+ if (!resolved)
107
+ throw new Error(`Unknown session: ${sessionId}`);
108
+ const { manager: session, cwd } = resolved;
109
+ const mode = options.mode ?? "default";
110
+ const abort = new AbortController();
111
+ if (options.signal) {
112
+ if (options.signal.aborted)
113
+ abort.abort();
114
+ else
115
+ options.signal.addEventListener("abort", () => abort.abort(), { once: true });
116
+ }
117
+ this.activeTurns.set(sessionId, abort);
118
+ let agentRef;
119
+ const hookController = new ExternalHookController({ cwd, sessionId });
120
+ // Settles as reject the moment the turn aborts, so a tool blocked on a
121
+ // host approval (or question) can never hang the abort path.
122
+ const abortedDecision = new Promise((resolve) => {
123
+ const decide = () => resolve({ action: "reject", feedback: "Turn aborted" });
124
+ if (abort.signal.aborted)
125
+ decide();
126
+ else
127
+ abort.signal.addEventListener("abort", decide, { once: true });
128
+ });
129
+ const approvalController = new PermissionAwareApprovalController({
130
+ getMode: () => agentRef?.mode ?? mode,
131
+ handlerRef: {
132
+ current: (req) => options.onApproval
133
+ ? Promise.race([options.onApproval(req), abortedDecision])
134
+ : Promise.resolve({
135
+ action: "reject",
136
+ feedback: "Host provided no approval handler",
137
+ }),
138
+ },
139
+ bashAllowlist: this.bashAllowlistFor(sessionId),
140
+ cwd,
141
+ externalHooks: hookController,
142
+ });
143
+ const fileStateTracker = new FileStateTracker(cwd);
144
+ const planController = {
145
+ getMode: () => agentRef?.mode ?? mode,
146
+ requestApproval: async (plan) => {
147
+ const approved = options.onPlanApproval ? await options.onPlanApproval(plan) : false;
148
+ if (approved) {
149
+ agentRef?.setMode("default");
150
+ return { action: "approve", plan };
151
+ }
152
+ return { action: "reject", reason: "Plan rejected by host" };
153
+ },
154
+ setMode: (m) => agentRef?.setMode(m),
155
+ };
156
+ const questionController = new QuestionController();
157
+ const unsubscribeQuestions = questionController.subscribe((event) => {
158
+ if (event.type !== "asked")
159
+ return;
160
+ const request = event.request;
161
+ if (!options.onQuestion) {
162
+ questionController.reject(request.id);
163
+ return;
164
+ }
165
+ options.onQuestion(request).then((answers) => answers ? questionController.reply(request.id, answers) : questionController.reject(request.id), () => questionController.reject(request.id));
166
+ });
167
+ abort.signal.addEventListener("abort", () => questionController.rejectAll(), { once: true });
168
+ try {
169
+ const skillRegistry = new SkillRegistry({ cwd, skillPaths: this.userConfig.getSkillPaths() });
170
+ const tools = createAllTools(cwd, skillRegistry, {
171
+ approvalController,
172
+ fileStateTracker,
173
+ planController,
174
+ todoStore: {
175
+ getTodos: () => agentRef?.getTodos() ?? [],
176
+ setTodos: (todos) => agentRef?.setTodos(todos),
177
+ },
178
+ questionController,
179
+ goalStore: new GoalStore(),
180
+ checkpoints: () => session.getCheckpoints(),
181
+ });
182
+ tools.push(...(await this.mcpToolsFor(cwd)));
183
+ const promptCacheKey = session.getOrCreatePromptCacheKey();
184
+ session.updateMetadata({ cwd }); // recoverable by cwd after host restart
185
+ const { provider, providerId, model } = this.resolveProvider(promptCacheKey, options.model);
186
+ const thinkingLevel = options.thinkingLevel ??
187
+ this.userConfig.getDefaultThinkingLevel() ??
188
+ getDefaultThinkingLevel(providerId, decodeModel(model).modelId);
189
+ // Same ordering as the TUI: the provenance gate must run before memories
190
+ // enter the system prompt.
191
+ purgeUnsafeMemorySources(cwd);
192
+ const memoryPrompt = buildMemoryPrompt(cwd);
193
+ const builtSystemPrompt = buildSystemPrompt({
194
+ agentName: "Bubble",
195
+ configuredProvider: providerId || "none",
196
+ configuredModel: model ? displayModel(model) : "none",
197
+ configuredModelId: model || "none",
198
+ thinkingLevel,
199
+ mode,
200
+ workingDir: cwd,
201
+ ...buildToolPromptOptions(tools.filter((t) => !t.deferred)),
202
+ memoryPrompt,
203
+ });
204
+ const systemPrompt = options.appendSystemPrompt
205
+ ? `${builtSystemPrompt}\n\n${options.appendSystemPrompt.trim()}`
206
+ : builtSystemPrompt;
207
+ const agent = new Agent({
208
+ provider,
209
+ providerId,
210
+ model,
211
+ sessionID: session.getSessionFile(),
212
+ tools,
213
+ systemPrompt,
214
+ temperature: 0.2,
215
+ thinkingLevel,
216
+ mode,
217
+ todos: session.getTodos(),
218
+ budgetLedger: new BudgetLedger(),
219
+ fileStateTracker,
220
+ skills: skillRegistry.summaries(),
221
+ memoryPrompt,
222
+ externalHooks: hookController,
223
+ onMessageAppend: (message) => {
224
+ if (message.role === "system" || message.role === "meta")
225
+ return;
226
+ session.appendMessage(message);
227
+ if (message.role === "assistant")
228
+ recordMemoryCitations(cwd, message.content);
229
+ },
230
+ onCompactionApplied: (summary) => {
231
+ session.applyLLMCompaction(summary);
232
+ },
233
+ onTodosUpdate: (todos) => session.appendTodosSnapshot(todos),
234
+ onModeUpdate: (m) => session.appendMarker("mode_switch", m),
235
+ });
236
+ agentRef = agent;
237
+ const history = session.getMessages();
238
+ if (history.length > 0) {
239
+ agent.messages = [{ role: "system", content: systemPrompt }, ...history];
240
+ }
241
+ options.onStart?.({
242
+ providerId,
243
+ model,
244
+ thinkingLevel,
245
+ mode,
246
+ tools: tools.map((t) => t.name),
247
+ skills: skillRegistry.summaries().map((s) => s.name),
248
+ });
249
+ // Match the TUI: a leading "/<skill-name> <task>" becomes an explicit
250
+ // load-this-skill instruction, so invocation never depends on the model
251
+ // recognizing the slash syntax on its own.
252
+ const prompt = rewriteSkillInvocationPrompt(options.prompt, skillRegistry);
253
+ const bareModelId = decodeModel(model).modelId;
254
+ for await (const event of agent.run(prompt, cwd, { abortSignal: abort.signal })) {
255
+ yield attachTurnCost(event, providerId, bareModelId);
256
+ if (abort.signal.aborted)
257
+ break;
258
+ }
259
+ }
260
+ finally {
261
+ // Also reached via generator.return() when the host stops consuming
262
+ // early: aborting here unblocks any tool still awaiting an approval.
263
+ abort.abort();
264
+ unsubscribeQuestions();
265
+ questionController.rejectAll();
266
+ if (this.activeTurns.get(sessionId) === abort)
267
+ this.activeTurns.delete(sessionId);
268
+ }
269
+ }
270
+ // ── Internals ────────────────────────────────────────────────────────────
271
+ bashAllowlistFor(sessionId) {
272
+ let allowlist = this.bashAllowlists.get(sessionId);
273
+ if (!allowlist) {
274
+ allowlist = new BashAllowlist();
275
+ this.bashAllowlists.set(sessionId, allowlist);
276
+ }
277
+ return allowlist;
278
+ }
279
+ /** MCP servers are started lazily, once per cwd (McpManager has no stop). */
280
+ mcpToolsFor(cwd) {
281
+ if (!this.mcpEnabled)
282
+ return Promise.resolve([]);
283
+ let cached = this.mcpToolsByCwd.get(cwd);
284
+ if (!cached) {
285
+ cached = (async () => {
286
+ const loaded = loadMcpConfig({ cwd });
287
+ if (loaded.servers.length === 0)
288
+ return [];
289
+ const manager = new McpManager({ servers: loaded.servers });
290
+ await manager.start();
291
+ return manager.getToolEntries();
292
+ })().catch(() => []);
293
+ this.mcpToolsByCwd.set(cwd, cached);
294
+ }
295
+ return cached;
296
+ }
297
+ resolveSession(sessionId) {
298
+ const memCwd = this.cwdBySession.get(sessionId);
299
+ if (memCwd) {
300
+ // create() only resolves the path and loads iff the file exists, so this
301
+ // covers both fresh (lazily persisted, not yet on disk) and resumed sessions.
302
+ return { manager: SessionManager.create(memCwd, sessionFileName(sessionId)), cwd: memCwd };
303
+ }
304
+ if (this.sessionIndex.size === 0)
305
+ this.listSessions();
306
+ const entry = this.sessionIndex.get(sessionId);
307
+ if (!entry)
308
+ return undefined;
309
+ this.cwdBySession.set(sessionId, entry.cwd);
310
+ return { manager: new SessionManager(entry.file), cwd: entry.cwd };
311
+ }
312
+ resolveProvider(promptCacheKey, explicitModel) {
313
+ const configuredModel = explicitModel || this.userConfig.getDefaultModel();
314
+ const defaultProvider = this.registry.getDefault();
315
+ const fallbackProviderId = defaultProvider?.id ?? "";
316
+ const normalized = configuredModel
317
+ ? configuredModel.includes(":")
318
+ ? configuredModel
319
+ : fallbackProviderId
320
+ ? encodeModel(fallbackProviderId, configuredModel)
321
+ : ""
322
+ : "";
323
+ const { providerId: effId, modelId: effModelId } = normalized
324
+ ? decodeModel(normalized)
325
+ : { providerId: undefined, modelId: "" };
326
+ const activeProviderId = effId || fallbackProviderId;
327
+ const target = this.registry.getConfigured().find((p) => p.id === activeProviderId) || defaultProvider;
328
+ if (!target?.apiKey) {
329
+ throw new Error("No provider with an API key configured — run the Bubble CLI once to set one up");
330
+ }
331
+ const activeModel = effModelId ? encodeModel(activeProviderId, effModelId) : "";
332
+ const provider = createProviderInstance({
333
+ providerId: activeProviderId,
334
+ apiKey: target.apiKey,
335
+ baseURL: target.baseURL,
336
+ promptCacheKey,
337
+ protocol: target.protocol,
338
+ headers: target.headers,
339
+ });
340
+ return { provider, providerId: activeProviderId, model: activeModel };
341
+ }
342
+ }
343
+ function sessionFileName(sessionId) {
344
+ return sessionId.endsWith(".jsonl") ? sessionId : `${sessionId}.jsonl`;
345
+ }
346
+ /**
347
+ * Attach a `cost` field to turn_end events that carry usage, priced via the
348
+ * static pricing table. Pricing is an accounting concern, so it rides along
349
+ * here in the SDK rather than in the agent core. Multi-step turns emit several
350
+ * turn_end events; each cost covers only that step's usage, so hosts sum them.
351
+ * Events for unpriced models (or without usage) pass through untouched.
352
+ */
353
+ export function attachTurnCost(event, providerId, modelId) {
354
+ if (event.type !== "turn_end" || !event.usage)
355
+ return event;
356
+ const cost = calculateUsageCost(providerId, modelId, event.usage);
357
+ return cost ? { ...event, cost } : event;
358
+ }
359
+ /**
360
+ * If the prompt (or its first text part) is a "/<skill-name> <task>" skill
361
+ * invocation, rewrite it into the same explicit execution prompt the TUI
362
+ * sends. Non-matching prompts pass through untouched.
363
+ */
364
+ export function rewriteSkillInvocationPrompt(prompt, registry) {
365
+ if (typeof prompt === "string") {
366
+ return parseSkillInvocation(prompt, registry)?.actualPrompt ?? prompt;
367
+ }
368
+ const index = prompt.findIndex((part) => part.type === "text");
369
+ if (index === -1)
370
+ return prompt;
371
+ const part = prompt[index];
372
+ const rewritten = parseSkillInvocation(part.text, registry)?.actualPrompt;
373
+ if (!rewritten)
374
+ return prompt;
375
+ const next = prompt.slice();
376
+ next[index] = { ...part, text: rewritten };
377
+ return next;
378
+ }
379
+ // ── Building blocks (escape hatch for custom hosts) ────────────────────────
380
+ export { Agent, AgentAbortError } from "../agent.js";
381
+ export { SessionManager, getSessionsDir, } from "../session.js";
382
+ export { PermissionAwareApprovalController } from "../approval/controller.js";
383
+ export { BashAllowlist } from "../approval/session-cache.js";
384
+ export { createAllTools, buildToolPromptOptions } from "../tools/index.js";
385
+ export { buildSystemPrompt } from "../system-prompt.js";
386
+ export { FileStateTracker } from "../tools/file-state.js";
387
+ export { BudgetLedger } from "../agent/budget-ledger.js";
388
+ export { UserConfig } from "../config.js";
389
+ export { ProviderRegistry, encodeModel, decodeModel, displayModel, } from "../provider-registry.js";
390
+ export { createProviderInstance } from "../provider.js";
391
+ export { getDefaultThinkingLevel } from "../variant/variant-resolver.js";
392
+ export { QuestionController, QuestionRejectedError, } from "../question/controller.js";
393
+ export { SkillRegistry } from "../skills/registry.js";
394
+ export { parseSkillInvocation } from "../skills/invocation.js";
395
+ export { GoalStore } from "../goal/store.js";
396
+ export { McpManager } from "../mcp/manager.js";
397
+ export { loadMcpConfig } from "../mcp/config.js";
398
+ export { ExternalHookController } from "../hooks/controller.js";
399
+ export { calculateUsageCost, getModelPricing, } from "../model-pricing.js";
400
+ export { registry as slashCommandRegistry } from "../slash-commands/index.js";
package/dist/session.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
6
  import { basename, dirname, join } from "node:path";
7
+ import { isInternalBlockOnlyContent } from "./agent/internal-reminder-sanitizer.js";
7
8
  import { getBubbleHome } from "./bubble-home.js";
8
9
  import { CheckpointStore } from "./checkpoints.js";
9
10
  import { buildCompactedEntries, compactSessionEntries, planOldMessages, planSessionCompaction, } from "./context/compact.js";
@@ -237,6 +238,10 @@ export class SessionManager {
237
238
  const entry = entries[i];
238
239
  if (entry.type !== "user_message")
239
240
  continue;
241
+ // Harness-injected turns (goal kicks, task wakes) are internal blocks —
242
+ // not rewind anchors, and their markup must never render in the picker.
243
+ if (isInternalBlockOnlyContent(entry.message.content))
244
+ continue;
240
245
  const text = messageText(entry.message);
241
246
  turns.push({
242
247
  id: entry.id,
@@ -354,8 +359,11 @@ function firstUserEntryAfterLatestClear(entries) {
354
359
  const startIndex = latestClearIndex(entries) + 1;
355
360
  for (let i = startIndex; i < entries.length; i++) {
356
361
  const entry = entries[i];
357
- if (entry.type === "user_message")
362
+ // Skip harness-injected internal blocks (goal kicks, task wakes): they
363
+ // must not become the /resume preview or the deterministic title.
364
+ if (entry.type === "user_message" && !isInternalBlockOnlyContent(entry.message.content)) {
358
365
  return entry;
366
+ }
359
367
  }
360
368
  return undefined;
361
369
  }