@bubblebrain-ai/bubble 0.0.45 → 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.
- package/dist/agent/child-runner.js +11 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
- package/dist/agent/internal-reminder-sanitizer.js +13 -0
- package/dist/agent/subagent/router.d.ts +65 -0
- package/dist/agent/subagent/router.js +198 -0
- package/dist/agent/subagent/runtime.d.ts +203 -0
- package/dist/agent/subagent/runtime.js +789 -0
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-store.d.ts +18 -2
- package/dist/agent/subagent-store.js +32 -1
- package/dist/agent/tool-intent.d.ts +10 -0
- package/dist/agent/tool-intent.js +18 -0
- package/dist/agent/tool-visibility.d.ts +10 -0
- package/dist/agent/tool-visibility.js +6 -0
- package/dist/agent/workflow/runs.d.ts +72 -0
- package/dist/agent/workflow/runs.js +157 -0
- package/dist/agent.d.ts +78 -96
- package/dist/agent.js +266 -1072
- package/dist/context/compact-llm.js +14 -5
- package/dist/context/compact.d.ts +9 -0
- package/dist/context/compact.js +36 -10
- package/dist/context/compaction-files.d.ts +44 -0
- package/dist/context/compaction-files.js +148 -0
- package/dist/context/llm-compactor.js +24 -5
- package/dist/feishu/agent-host/run-driver.js +3 -0
- package/dist/feishu/serve.js +13 -9
- package/dist/goal/tools.js +12 -3
- package/dist/main.js +24 -19
- package/dist/model-catalog.js +36 -2
- package/dist/model-config.d.ts +2 -0
- package/dist/model-data.generated.d.ts +16 -0
- package/dist/model-data.generated.js +275 -0
- package/dist/model-pricing.d.ts +11 -7
- package/dist/model-pricing.js +0 -0
- package/dist/oauth/grok.d.ts +1 -1
- package/dist/oauth/grok.js +58 -29
- package/dist/oauth/openai-codex.js +13 -0
- package/dist/oauth/storage.d.ts +9 -2
- package/dist/oauth/storage.js +16 -9
- package/dist/orchestrator/default-hooks.d.ts +20 -0
- package/dist/orchestrator/default-hooks.js +47 -295
- package/dist/orchestrator/hooks.d.ts +6 -39
- package/dist/permission/mode.js +1 -1
- package/dist/prompt/reminders.d.ts +9 -35
- package/dist/prompt/reminders.js +30 -136
- package/dist/prompt/task-reminders.d.ts +0 -31
- package/dist/prompt/task-reminders.js +0 -87
- package/dist/provider-ai-sdk.d.ts +2 -0
- package/dist/provider-ai-sdk.js +1 -0
- package/dist/provider-anthropic.d.ts +2 -0
- package/dist/provider-anthropic.js +3 -1
- package/dist/provider-ark-responses.d.ts +2 -0
- package/dist/provider-ark-responses.js +1 -0
- package/dist/provider-registry.d.ts +13 -0
- package/dist/provider-registry.js +83 -0
- package/dist/provider-transform.js +37 -1
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +13 -1
- package/dist/sdk/index.d.ts +152 -0
- package/dist/sdk/index.js +400 -0
- package/dist/session.js +9 -1
- package/dist/slash-commands/commands.js +33 -19
- package/dist/tools/exit-plan-mode.js +8 -0
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +11 -5
- package/dist/tui-ink/app.d.ts +2 -6
- package/dist/tui-ink/app.js +11 -18
- package/dist/tui-ink/display-history.d.ts +0 -2
- package/dist/tui-ink/display-history.js +0 -52
- package/dist/tui-ink/footer.js +2 -2
- package/dist/tui-ink/message-list.js +62 -23
- package/dist/tui-ink/run.js +10 -5
- package/dist/tui-ink/sync-output.d.ts +17 -0
- package/dist/tui-ink/sync-output.js +35 -0
- package/dist/tui-ink/theme.d.ts +0 -1
- package/dist/tui-ink/theme.js +0 -2
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/types.d.ts +21 -1
- package/package.json +16 -2
- package/dist/agent/discovery-barrier.d.ts +0 -21
- package/dist/agent/discovery-barrier.js +0 -173
- package/dist/agent/evidence-tracker.d.ts +0 -15
- package/dist/agent/evidence-tracker.js +0 -93
- package/dist/agent/execution-governor.d.ts +0 -36
- package/dist/agent/execution-governor.js +0 -218
- package/dist/agent/task-classifier.d.ts +0 -3
- package/dist/agent/task-classifier.js +0 -114
- package/dist/agent/task-size.d.ts +0 -9
- package/dist/agent/task-size.js +0 -33
- package/dist/agent/tool-arbiter.d.ts +0 -7
- package/dist/agent/tool-arbiter.js +0 -33
- package/dist/orchestrator/workflow.d.ts +0 -10
- 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
|
-
|
|
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
|
}
|