@bubblebrain-ai/bubble 0.0.58 → 0.0.60
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/README.md +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
package/dist/sdk/index.js
CHANGED
|
@@ -21,8 +21,11 @@ import { AgentRunInputQueue } from "../agent/input-controller.js";
|
|
|
21
21
|
import { SessionTurnCoordinator, } from "./session-turn-coordinator.js";
|
|
22
22
|
import { ReplayEventLog } from "./replay-event-log.js";
|
|
23
23
|
import { SessionManager } from "../session.js";
|
|
24
|
+
import { isPermissionModeReminder } from "../prompt/reminders.js";
|
|
24
25
|
import { PermissionAwareApprovalController } from "../approval/controller.js";
|
|
25
26
|
import { BashAllowlist } from "../approval/session-cache.js";
|
|
27
|
+
import { SettingsManager } from "../permissions/settings.js";
|
|
28
|
+
import { isRepoConfigTrusted, mergedRepoCapabilities, readRepoSettings, trustRepoConfig, } from "../permissions/trust.js";
|
|
26
29
|
import { createAllTools, buildToolPromptOptions } from "../tools/index.js";
|
|
27
30
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
28
31
|
import { FileStateTracker } from "../tools/file-state.js";
|
|
@@ -36,7 +39,7 @@ import { assertProviderModelAllowed } from "../provider-model-policy.js";
|
|
|
36
39
|
import { SkillRegistry } from "../skills/registry.js";
|
|
37
40
|
import { parseSkillInvocation } from "../skills/invocation.js";
|
|
38
41
|
import { GoalStore } from "../goal/store.js";
|
|
39
|
-
import { McpManager } from "../mcp/manager.js";
|
|
42
|
+
import { McpManager, gateMcpTools } from "../mcp/manager.js";
|
|
40
43
|
import { loadMcpConfig } from "../mcp/config.js";
|
|
41
44
|
import { ExternalHookController } from "../hooks/controller.js";
|
|
42
45
|
import { buildMemoryPrompt } from "../memory/store.js";
|
|
@@ -49,12 +52,17 @@ export class BubbleSdk {
|
|
|
49
52
|
registry = new ProviderRegistry(this.userConfig);
|
|
50
53
|
defaultCwd;
|
|
51
54
|
mcpEnabled;
|
|
55
|
+
sessionEventRetention;
|
|
52
56
|
cwdBySession = new Map();
|
|
53
57
|
bashAllowlists = new Map();
|
|
58
|
+
sessionGrants = new Map();
|
|
54
59
|
turnCoordinator = new SessionTurnCoordinator();
|
|
55
60
|
turnRuntimes = new Map();
|
|
56
61
|
lastTurnOptions = new Map();
|
|
57
62
|
mcpToolsByCwd = new Map();
|
|
63
|
+
mcpManagersByCwd = new Map();
|
|
64
|
+
/** Folders whose trust question was already put to the host. */
|
|
65
|
+
projectTrustAsked = new Set();
|
|
58
66
|
inputIdNonce = randomUUID().slice(0, 6);
|
|
59
67
|
nextTurnInputPrefix = 0;
|
|
60
68
|
nextDetachedInputId = 0;
|
|
@@ -63,6 +71,10 @@ export class BubbleSdk {
|
|
|
63
71
|
constructor(options = {}) {
|
|
64
72
|
this.defaultCwd = options.defaultCwd || process.env.BUBBLE_CWD || os.homedir();
|
|
65
73
|
this.mcpEnabled = options.mcp !== false;
|
|
74
|
+
this.sessionEventRetention = options.sessionEventRetention ?? Infinity;
|
|
75
|
+
if (this.sessionEventRetention !== Infinity && (!Number.isSafeInteger(this.sessionEventRetention) || this.sessionEventRetention < 1)) {
|
|
76
|
+
throw new Error('sessionEventRetention must be a positive integer');
|
|
77
|
+
}
|
|
66
78
|
}
|
|
67
79
|
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
68
80
|
listSessions() {
|
|
@@ -84,12 +96,13 @@ export class BubbleSdk {
|
|
|
84
96
|
if (!resolved)
|
|
85
97
|
throw new Error(`Unknown session: ${sessionId}`);
|
|
86
98
|
const ownerKey = resolved.manager.getSessionFile();
|
|
87
|
-
const eventLog = sessionEventLogFor(ownerKey);
|
|
99
|
+
const eventLog = sessionEventLogFor(ownerKey, this.sessionEventRetention);
|
|
88
100
|
const closed = new AbortController();
|
|
89
101
|
return {
|
|
90
102
|
id: sessionId,
|
|
91
103
|
cwd: resolved.cwd,
|
|
92
|
-
|
|
104
|
+
get latestSequence() { return eventLog.length; },
|
|
105
|
+
get events() { return eventLog.iterate({ signal: closed.signal }); },
|
|
93
106
|
eventsFrom: (afterSequence) => eventLog.iterate({
|
|
94
107
|
from: Math.max(0, afterSequence),
|
|
95
108
|
signal: closed.signal,
|
|
@@ -105,6 +118,31 @@ export class BubbleSdk {
|
|
|
105
118
|
return [];
|
|
106
119
|
return this.resolveSession(sessionId)?.manager.getMessages() ?? [];
|
|
107
120
|
}
|
|
121
|
+
/** Preserve an accepted desktop input cancelled before runTurn was admitted. */
|
|
122
|
+
recordInterruptedInput(sessionId, prompt) {
|
|
123
|
+
const owner = this.ownerForSession(sessionId);
|
|
124
|
+
if (owner && owner !== this)
|
|
125
|
+
return owner.recordInterruptedInput(sessionId, prompt);
|
|
126
|
+
if (this.turnCoordinator.isDeleted(sessionId) || this.getSessionRunState(sessionId).active) {
|
|
127
|
+
throw new Error(`Cannot record interrupted input while session is active or deleted: ${sessionId}`);
|
|
128
|
+
}
|
|
129
|
+
const resolved = this.resolveSession(sessionId);
|
|
130
|
+
if (!resolved)
|
|
131
|
+
throw new Error(`Unknown session: ${sessionId}`);
|
|
132
|
+
this.persistInterruptedInput(resolved, prompt);
|
|
133
|
+
}
|
|
134
|
+
persistInterruptedInput(resolved, prompt) {
|
|
135
|
+
const session = resolved.manager;
|
|
136
|
+
session.getMessages();
|
|
137
|
+
const revision = session.getRevision();
|
|
138
|
+
session.updateMetadata({ cwd: resolved.cwd });
|
|
139
|
+
session.appendMessage({ role: "user", content: prompt }, revision);
|
|
140
|
+
session.appendMessage({
|
|
141
|
+
role: "assistant",
|
|
142
|
+
content: "[Interrupted by user before execution started.]",
|
|
143
|
+
error: { name: "MessageAbortedError", message: "Turn stopped during initialization.", aborted: true },
|
|
144
|
+
}, session.getRevision());
|
|
145
|
+
}
|
|
108
146
|
async deleteSession(sessionId) {
|
|
109
147
|
const owner = this.ownerForSession(sessionId);
|
|
110
148
|
if (owner && owner !== this)
|
|
@@ -117,6 +155,7 @@ export class BubbleSdk {
|
|
|
117
155
|
rmSync(resolved.manager.getSessionFile(), { force: true });
|
|
118
156
|
this.cwdBySession.delete(sessionId);
|
|
119
157
|
this.bashAllowlists.delete(sessionId);
|
|
158
|
+
this.sessionGrants.delete(sessionId);
|
|
120
159
|
this.lastTurnOptions.delete(sessionId);
|
|
121
160
|
this.sessionIndex.delete(sessionId);
|
|
122
161
|
processSessionLocations.delete(sessionId);
|
|
@@ -133,8 +172,21 @@ export class BubbleSdk {
|
|
|
133
172
|
const current = this.turnCoordinator.getCurrent(sessionId);
|
|
134
173
|
const runtime = current ? this.turnRuntimes.get(current.id) : undefined;
|
|
135
174
|
runtime?.inputController.closePendingInputs();
|
|
136
|
-
|
|
137
|
-
|
|
175
|
+
// A reserved turn can release its slot synchronously when cancelled.
|
|
176
|
+
// Clear the queue first so cancellation cannot promote an unwanted input.
|
|
177
|
+
const queued = options.cancelQueued ? this.turnCoordinator.clearQueue(sessionId) : 0;
|
|
178
|
+
return queued + this.turnCoordinator.stopCurrent(sessionId);
|
|
179
|
+
}
|
|
180
|
+
/** Stop and wait for persistence, cleanup, and terminal event publication. */
|
|
181
|
+
async stopAndWait(sessionId, options = {}) {
|
|
182
|
+
const owner = this.ownerForSession(sessionId);
|
|
183
|
+
if (owner && owner !== this)
|
|
184
|
+
return owner.stopAndWait(sessionId, options);
|
|
185
|
+
const current = this.turnCoordinator.getCurrent(sessionId);
|
|
186
|
+
const stopping = this.runtimesForSession(sessionId).filter(runtime => options.cancelQueued || runtime.reservation.id === current?.id);
|
|
187
|
+
const stopped = this.stop(sessionId, options);
|
|
188
|
+
await Promise.all(stopping.map(runtime => runtime.finished));
|
|
189
|
+
return stopped;
|
|
138
190
|
}
|
|
139
191
|
/** Cancel queued turns without interrupting the turn that owns the session slot. */
|
|
140
192
|
clearQueue(sessionId) {
|
|
@@ -191,14 +243,36 @@ export class BubbleSdk {
|
|
|
191
243
|
return this.enqueueTurn(sessionId, options);
|
|
192
244
|
}
|
|
193
245
|
// ── Discovery (composer pickers) ─────────────────────────────────────────
|
|
194
|
-
listSkills(cwd) {
|
|
246
|
+
listSkills(cwd, skillPaths = []) {
|
|
195
247
|
const registry = new SkillRegistry({
|
|
196
248
|
cwd: cwd || this.defaultCwd,
|
|
197
|
-
skillPaths: this.userConfig.getSkillPaths(),
|
|
249
|
+
skillPaths: [...this.userConfig.getSkillPaths(), ...skillPaths],
|
|
198
250
|
disabledSkills: this.userConfig.getDisabledSkills(),
|
|
199
251
|
});
|
|
200
252
|
return registry.summaries();
|
|
201
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Ask the host once per folder (onProjectTrust) before this turn loads the
|
|
256
|
+
* repository's allow rules, MCP servers and LSP servers. Trusting restarts
|
|
257
|
+
* the folder's MCP servers so newly trusted ones are available right away.
|
|
258
|
+
*/
|
|
259
|
+
async resolveProjectTrust(cwd, options, signal) {
|
|
260
|
+
if (!options.onProjectTrust || this.projectTrustAsked.has(cwd))
|
|
261
|
+
return;
|
|
262
|
+
// Read once: what gets trusted is exactly what the host was shown.
|
|
263
|
+
const raw = readRepoSettings(cwd);
|
|
264
|
+
if (isRepoConfigTrusted(cwd, raw))
|
|
265
|
+
return;
|
|
266
|
+
this.projectTrustAsked.add(cwd);
|
|
267
|
+
const trusted = await awaitWithAbort(options.onProjectTrust({ cwd, pending: mergedRepoCapabilities(raw) }), signal);
|
|
268
|
+
if (!trusted)
|
|
269
|
+
return;
|
|
270
|
+
trustRepoConfig(cwd, raw);
|
|
271
|
+
const manager = this.mcpManagersByCwd.get(cwd);
|
|
272
|
+
this.mcpManagersByCwd.delete(cwd);
|
|
273
|
+
this.mcpToolsByCwd.delete(cwd);
|
|
274
|
+
await (await manager?.catch(() => null))?.shutdown().catch(() => undefined);
|
|
275
|
+
}
|
|
202
276
|
/** Configured providers + default model, for a host's model picker. */
|
|
203
277
|
getModelConfig() {
|
|
204
278
|
const configured = this.registry.getConfigured().filter((p) => p.enabled);
|
|
@@ -248,24 +322,54 @@ export class BubbleSdk {
|
|
|
248
322
|
outcomes: new Map(),
|
|
249
323
|
options,
|
|
250
324
|
events: new ReplayEventLog(),
|
|
325
|
+
started: reservation.phase !== "queued",
|
|
326
|
+
inputRecorded: false,
|
|
327
|
+
finished: Promise.resolve(),
|
|
251
328
|
};
|
|
252
329
|
this.lastTurnOptions.set(sessionId, inheritableTurnOptions(options));
|
|
253
330
|
this.turnRuntimes.set(reservation.id, runtime);
|
|
254
|
-
|
|
331
|
+
runtime.finished = this.pumpTurn(runtime, resolved);
|
|
255
332
|
return runtime;
|
|
256
333
|
}
|
|
257
334
|
async pumpTurn(runtime, resolved) {
|
|
258
335
|
let failure;
|
|
336
|
+
let completedEvent;
|
|
259
337
|
try {
|
|
260
338
|
for await (const event of this.runReservedTurn(runtime, runtime.options, resolved)) {
|
|
261
339
|
runtime.events.append(event);
|
|
262
|
-
|
|
340
|
+
// Publish success after the pump's finally releases its slot.
|
|
341
|
+
// A host can then distinguish idle from an automatically queued steer.
|
|
342
|
+
if (event.type === "agent_end")
|
|
343
|
+
completedEvent = event;
|
|
344
|
+
else
|
|
345
|
+
this.publishSessionEvent(runtime, event);
|
|
263
346
|
}
|
|
264
347
|
}
|
|
265
348
|
catch (error) {
|
|
266
349
|
failure = error;
|
|
350
|
+
// Setup can fail before runReservedTurn installs its own cleanup.
|
|
351
|
+
for (const event of this.rejectOutstandingSteers(runtime, runtime.reservation.signal.aborted ? "turn_cancelled" : "turn_failed")) {
|
|
352
|
+
runtime.events.append(event);
|
|
353
|
+
this.publishSessionEvent(runtime, event);
|
|
354
|
+
}
|
|
355
|
+
// Desktop users already submitted this input. Even if setup was
|
|
356
|
+
// interrupted before Agent.run appended it, keep it for a later resume.
|
|
357
|
+
// Cancelled queued inputs never owned the turn and must stay discarded.
|
|
358
|
+
if (runtime.options.preserveInterruptedInput && runtime.started && !runtime.inputRecorded
|
|
359
|
+
&& runtime.reservation.signal.aborted && !this.turnCoordinator.isDeleted(runtime.sessionId)) {
|
|
360
|
+
try {
|
|
361
|
+
this.persistInterruptedInput(resolved, runtime.options.prompt);
|
|
362
|
+
}
|
|
363
|
+
catch (persistError) {
|
|
364
|
+
failure = persistError;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
267
367
|
}
|
|
268
368
|
finally {
|
|
369
|
+
runtime.inputController.closePendingInputs();
|
|
370
|
+
// Release only after the interrupted input is durable. Keep the runtime
|
|
371
|
+
// discoverable until the terminal event is published for stopAndWait.
|
|
372
|
+
runtime.reservation.finish();
|
|
269
373
|
if (failure !== undefined) {
|
|
270
374
|
// The session-level log never closes per turn, so replay subscribers
|
|
271
375
|
// need an explicit terminal record — otherwise a failed turn is
|
|
@@ -277,6 +381,9 @@ export class BubbleSdk {
|
|
|
277
381
|
message: failure instanceof Error ? failure.message : String(failure),
|
|
278
382
|
});
|
|
279
383
|
}
|
|
384
|
+
else {
|
|
385
|
+
this.publishSessionEvent(runtime, completedEvent ?? { type: "agent_end" }, { kind: "completed" });
|
|
386
|
+
}
|
|
280
387
|
runtime.events.close(failure);
|
|
281
388
|
if (this.turnRuntimes.get(runtime.reservation.id) === runtime) {
|
|
282
389
|
this.turnRuntimes.delete(runtime.reservation.id);
|
|
@@ -290,10 +397,18 @@ export class BubbleSdk {
|
|
|
290
397
|
const { sessionId, reservation, inputController } = runtime;
|
|
291
398
|
const { manager: session, cwd } = resolved;
|
|
292
399
|
const mode = options.mode ?? "default";
|
|
400
|
+
const planExitMode = options.planExitMode === "bypassPermissions" ? "bypassPermissions" : "default";
|
|
293
401
|
const abortSignal = reservation.signal;
|
|
402
|
+
await reservation.waitForStart();
|
|
403
|
+
runtime.started = true;
|
|
294
404
|
let agentRef;
|
|
295
405
|
let streamCompleted = false;
|
|
296
406
|
const hookController = new ExternalHookController({ cwd, sessionId });
|
|
407
|
+
await this.resolveProjectTrust(cwd, options, abortSignal);
|
|
408
|
+
// Same allow/deny rules as the TUI (user, project and local settings;
|
|
409
|
+
// repository allow rules only once trusted). Deny rules bind even under
|
|
410
|
+
// bypassPermissions.
|
|
411
|
+
const ruleSet = new SettingsManager(cwd).getMerged().ruleSet;
|
|
297
412
|
// Settles as reject the moment the turn aborts, so a tool blocked on a
|
|
298
413
|
// host approval (or question) can never hang the abort path.
|
|
299
414
|
const abortedDecision = new Promise((resolve) => {
|
|
@@ -314,7 +429,10 @@ export class BubbleSdk {
|
|
|
314
429
|
}),
|
|
315
430
|
},
|
|
316
431
|
bashAllowlist: this.bashAllowlistFor(sessionId),
|
|
432
|
+
sessionGrants: this.sessionGrantsFor(sessionId),
|
|
317
433
|
cwd,
|
|
434
|
+
// Read once per turn: edits to settings files apply from the next turn.
|
|
435
|
+
getRuleSet: () => ruleSet,
|
|
318
436
|
externalHooks: hookController,
|
|
319
437
|
});
|
|
320
438
|
const fileStateTracker = new FileStateTracker(cwd);
|
|
@@ -324,13 +442,13 @@ export class BubbleSdk {
|
|
|
324
442
|
const approved = options.onPlanApproval
|
|
325
443
|
? await awaitWithAbort(options.onPlanApproval(plan), abortSignal)
|
|
326
444
|
: false;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return { action: "reject", reason: "Plan rejected by host" };
|
|
445
|
+
// exit_plan_mode applies getExitMode() itself on approval.
|
|
446
|
+
return approved
|
|
447
|
+
? { action: "approve", plan }
|
|
448
|
+
: { action: "reject", reason: "Plan rejected by host" };
|
|
332
449
|
},
|
|
333
450
|
setMode: (m) => agentRef?.setMode(m),
|
|
451
|
+
getExitMode: () => planExitMode,
|
|
334
452
|
};
|
|
335
453
|
const questionController = new QuestionController();
|
|
336
454
|
const unsubscribeQuestions = questionController.subscribe((event) => {
|
|
@@ -345,10 +463,9 @@ export class BubbleSdk {
|
|
|
345
463
|
});
|
|
346
464
|
abortSignal.addEventListener("abort", () => questionController.rejectAll(), { once: true });
|
|
347
465
|
try {
|
|
348
|
-
await reservation.waitForStart();
|
|
349
466
|
const skillRegistry = new SkillRegistry({
|
|
350
467
|
cwd,
|
|
351
|
-
skillPaths: this.userConfig.getSkillPaths(),
|
|
468
|
+
skillPaths: [...this.userConfig.getSkillPaths(), ...(options.skillPaths ?? [])],
|
|
352
469
|
disabledSkills: this.userConfig.getDisabledSkills(),
|
|
353
470
|
});
|
|
354
471
|
const tools = createAllTools(cwd, skillRegistry, {
|
|
@@ -359,7 +476,23 @@ export class BubbleSdk {
|
|
|
359
476
|
goalStore: new GoalStore(),
|
|
360
477
|
checkpoints: () => session.getCheckpoints(),
|
|
361
478
|
});
|
|
362
|
-
tools.push(...(await awaitWithAbort(this.mcpToolsFor(cwd), abortSignal)));
|
|
479
|
+
tools.push(...gateMcpTools(await awaitWithAbort(this.mcpToolsFor(cwd), abortSignal), approvalController));
|
|
480
|
+
const hostTools = options.hostTools ?? [];
|
|
481
|
+
const registered = new Set(tools.map(tool => tool.name));
|
|
482
|
+
for (const tool of hostTools) {
|
|
483
|
+
if (registered.has(tool.name))
|
|
484
|
+
throw new Error(`Duplicate host tool: ${tool.name}`);
|
|
485
|
+
registered.add(tool.name);
|
|
486
|
+
const guarded = {
|
|
487
|
+
...tool,
|
|
488
|
+
execute: async (args, ctx) => {
|
|
489
|
+
throwAbortSignal(abortSignal);
|
|
490
|
+
return tool.execute(args, { ...ctx, abortSignal });
|
|
491
|
+
},
|
|
492
|
+
cloneForChild: () => ({ ...tool, enabled: () => false, execute: async () => ({ content: "Host capability is not available to this child.", isError: true }) }),
|
|
493
|
+
};
|
|
494
|
+
tools.push(...(tool.readOnly ? [guarded] : gateMcpTools([guarded], approvalController)));
|
|
495
|
+
}
|
|
363
496
|
throwAbortSignal(abortSignal);
|
|
364
497
|
const promptCacheKey = session.getOrCreatePromptCacheKey();
|
|
365
498
|
if (!this.turnCoordinator.isDeleted(sessionId)) {
|
|
@@ -375,6 +508,8 @@ export class BubbleSdk {
|
|
|
375
508
|
const memoryPrompt = buildMemoryPrompt(cwd);
|
|
376
509
|
const builtSystemPrompt = buildSystemPrompt({
|
|
377
510
|
agentName: "Bubble",
|
|
511
|
+
skills: skillRegistry.summaries(),
|
|
512
|
+
skillCatalogChars: this.userConfig.getSkillCatalogChars(),
|
|
378
513
|
configuredProvider: providerId || "none",
|
|
379
514
|
configuredModel: model ? displayModel(model) : "none",
|
|
380
515
|
configuredModelId: model || "none",
|
|
@@ -387,6 +522,39 @@ export class BubbleSdk {
|
|
|
387
522
|
const systemPrompt = options.appendSystemPrompt
|
|
388
523
|
? `${builtSystemPrompt}\n\n${options.appendSystemPrompt.trim()}`
|
|
389
524
|
: builtSystemPrompt;
|
|
525
|
+
// Host-driven mode changes (picker, /plan, leaving plan) go into the log
|
|
526
|
+
// as markers, so the rebuilt history states the mode where it changed —
|
|
527
|
+
// after any older tool result that claimed a different mode.
|
|
528
|
+
if (latestRecordedMode(session) !== mode && !this.turnCoordinator.isDeleted(sessionId)) {
|
|
529
|
+
session.appendMarker("mode_switch", mode);
|
|
530
|
+
}
|
|
531
|
+
const history = session.getMessages();
|
|
532
|
+
let contextRevision = session.getRevision();
|
|
533
|
+
// The constructor's leading context minus its mode reminder: the history
|
|
534
|
+
// carries the mode timeline itself (session-log toMessages).
|
|
535
|
+
let leadingContext = [];
|
|
536
|
+
// A fence rejection means the durable log diverged from this turn's
|
|
537
|
+
// resident snapshot (a foreign append or clear landed mid-flight). Refuse
|
|
538
|
+
// the write, then roll the resident transcript back to the file's truth so
|
|
539
|
+
// the rejected message does not linger and later writes are not wedged
|
|
540
|
+
// behind the stale revision.
|
|
541
|
+
const persistFenced = (write) => {
|
|
542
|
+
try {
|
|
543
|
+
write();
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
// Any refused write (fence rejection, busy lock, I/O) leaves the agent's
|
|
547
|
+
// already-pushed message unpersisted. getMessages() refreshes, so read
|
|
548
|
+
// the history first and adopt the revision of that same snapshot.
|
|
549
|
+
try {
|
|
550
|
+
const reloaded = session.getMessages();
|
|
551
|
+
contextRevision = session.getRevision();
|
|
552
|
+
agent.messages = [...leadingContext, ...reloaded];
|
|
553
|
+
}
|
|
554
|
+
catch { /* surface the original write failure */ }
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
390
558
|
const agent = new Agent({
|
|
391
559
|
provider,
|
|
392
560
|
providerId,
|
|
@@ -400,6 +568,7 @@ export class BubbleSdk {
|
|
|
400
568
|
budgetLedger: new BudgetLedger(),
|
|
401
569
|
fileStateTracker,
|
|
402
570
|
skills: skillRegistry.summaries(),
|
|
571
|
+
skillCatalogChars: this.userConfig.getSkillCatalogChars(),
|
|
403
572
|
memoryPrompt,
|
|
404
573
|
externalHooks: hookController,
|
|
405
574
|
// Cross-provider subagent routes (spawn_agent with "provider:model")
|
|
@@ -413,29 +582,47 @@ export class BubbleSdk {
|
|
|
413
582
|
return;
|
|
414
583
|
if (this.turnCoordinator.isDeleted(sessionId))
|
|
415
584
|
return;
|
|
416
|
-
|
|
585
|
+
if (message.role === "user")
|
|
586
|
+
runtime.inputRecorded = true;
|
|
587
|
+
persistFenced(() => {
|
|
588
|
+
session.appendMessage(message, contextRevision);
|
|
589
|
+
contextRevision = session.getRevision();
|
|
590
|
+
});
|
|
417
591
|
if (message.role === "assistant")
|
|
418
592
|
recordMemoryCitations(cwd, message.content);
|
|
419
593
|
},
|
|
420
594
|
onProviderError: (error) => {
|
|
421
595
|
if (this.turnCoordinator.isDeleted(sessionId))
|
|
422
596
|
return;
|
|
423
|
-
session.appendProviderError(error);
|
|
597
|
+
persistFenced(() => session.appendProviderError(error, contextRevision));
|
|
424
598
|
},
|
|
425
|
-
|
|
599
|
+
getContextRevision: () => contextRevision,
|
|
600
|
+
onContextCheckpoint: (checkpoint) => {
|
|
426
601
|
if (this.turnCoordinator.isDeleted(sessionId))
|
|
427
|
-
|
|
428
|
-
|
|
602
|
+
throw new Error("Session deleted before context commit");
|
|
603
|
+
persistFenced(() => {
|
|
604
|
+
session.commitContextCheckpoint(checkpoint, contextRevision);
|
|
605
|
+
contextRevision = session.getRevision();
|
|
606
|
+
});
|
|
429
607
|
},
|
|
430
608
|
onModeUpdate: (m) => {
|
|
431
|
-
if (!this.turnCoordinator.isDeleted(sessionId))
|
|
432
|
-
|
|
609
|
+
if (!this.turnCoordinator.isDeleted(sessionId)) {
|
|
610
|
+
persistFenced(() => {
|
|
611
|
+
session.appendMarker("mode_switch", m, contextRevision);
|
|
612
|
+
contextRevision = session.getRevision();
|
|
613
|
+
});
|
|
614
|
+
}
|
|
433
615
|
},
|
|
434
616
|
});
|
|
435
617
|
agentRef = agent;
|
|
436
|
-
|
|
618
|
+
leadingContext = agent.messages.filter((message) => !(message.role === "meta" && isPermissionModeReminder(message.content)));
|
|
437
619
|
if (history.length > 0) {
|
|
438
|
-
|
|
620
|
+
// Leading context (system prompt + deferred-tools reminder, never
|
|
621
|
+
// persisted) stays ahead of the history, at the same position every
|
|
622
|
+
// turn, so the provider's prefix cache covers the previous turn. The
|
|
623
|
+
// mode reminder comes from the history's own mode markers instead, so
|
|
624
|
+
// it is ordered after anything older that mentioned a mode.
|
|
625
|
+
agent.messages = [...leadingContext, ...history];
|
|
439
626
|
}
|
|
440
627
|
reservation.markActive();
|
|
441
628
|
options.onStart?.({
|
|
@@ -473,9 +660,6 @@ export class BubbleSdk {
|
|
|
473
660
|
this.rejectOutstandingSteers(runtime, streamCompleted ? "no_continuation" : abortSignal.aborted ? "turn_cancelled" : "turn_failed", streamCompleted ? resolved : undefined);
|
|
474
661
|
unsubscribeQuestions();
|
|
475
662
|
questionController.rejectAll();
|
|
476
|
-
reservation.finish();
|
|
477
|
-
if (this.turnRuntimes.get(reservation.id) === runtime)
|
|
478
|
-
this.turnRuntimes.delete(reservation.id);
|
|
479
663
|
}
|
|
480
664
|
}
|
|
481
665
|
// ── Internals ────────────────────────────────────────────────────────────
|
|
@@ -578,7 +762,7 @@ export class BubbleSdk {
|
|
|
578
762
|
}
|
|
579
763
|
}
|
|
580
764
|
publishSessionEvent(runtime, event, terminal) {
|
|
581
|
-
const log = sessionEventLogFor(runtime.ownerKey);
|
|
765
|
+
const log = sessionEventLogFor(runtime.ownerKey, this.sessionEventRetention);
|
|
582
766
|
log.append({
|
|
583
767
|
sequence: log.length + 1,
|
|
584
768
|
sessionId: runtime.sessionId,
|
|
@@ -599,6 +783,14 @@ export class BubbleSdk {
|
|
|
599
783
|
}
|
|
600
784
|
return allowlist;
|
|
601
785
|
}
|
|
786
|
+
sessionGrantsFor(sessionId) {
|
|
787
|
+
let grants = this.sessionGrants.get(sessionId);
|
|
788
|
+
if (!grants) {
|
|
789
|
+
grants = new Set();
|
|
790
|
+
this.sessionGrants.set(sessionId, grants);
|
|
791
|
+
}
|
|
792
|
+
return grants;
|
|
793
|
+
}
|
|
602
794
|
/**
|
|
603
795
|
* Default cross-provider subagent factory over this SDK instance's own
|
|
604
796
|
* registry — the same semantics as the TUI's createProviderForRoute. The
|
|
@@ -612,10 +804,10 @@ export class BubbleSdk {
|
|
|
612
804
|
}
|
|
613
805
|
// getConfigured() reads config.json from disk every call, so keys
|
|
614
806
|
// added after SDK construction are picked up without a reload call.
|
|
615
|
-
|
|
807
|
+
const target = this.registry.getConfigured().find((item) => item.id === providerId);
|
|
808
|
+
if (target?.authType === "oauth") {
|
|
616
809
|
await this.registry.prepareProvider(providerId);
|
|
617
810
|
}
|
|
618
|
-
const target = this.registry.getConfigured().find((item) => item.id === providerId);
|
|
619
811
|
if (!target?.enabled || !target.apiKey) {
|
|
620
812
|
throw new Error(`Subagent route requires provider "${providerId}", but it is not configured or has no active credentials.`);
|
|
621
813
|
}
|
|
@@ -630,20 +822,25 @@ export class BubbleSdk {
|
|
|
630
822
|
});
|
|
631
823
|
};
|
|
632
824
|
}
|
|
633
|
-
/**
|
|
825
|
+
/**
|
|
826
|
+
* MCP servers are started lazily, once per cwd, and restarted only when the
|
|
827
|
+
* repository's settings get trusted (resolveProjectTrust).
|
|
828
|
+
*/
|
|
634
829
|
mcpToolsFor(cwd) {
|
|
635
830
|
if (!this.mcpEnabled)
|
|
636
831
|
return Promise.resolve([]);
|
|
637
832
|
let cached = this.mcpToolsByCwd.get(cwd);
|
|
638
833
|
if (!cached) {
|
|
639
|
-
|
|
834
|
+
const managerPromise = (async () => {
|
|
640
835
|
const loaded = loadMcpConfig({ cwd });
|
|
641
836
|
if (loaded.servers.length === 0)
|
|
642
|
-
return
|
|
837
|
+
return null;
|
|
643
838
|
const manager = new McpManager({ servers: loaded.servers });
|
|
644
839
|
await manager.start();
|
|
645
|
-
return manager
|
|
646
|
-
})().catch(() =>
|
|
840
|
+
return manager;
|
|
841
|
+
})().catch(() => null);
|
|
842
|
+
this.mcpManagersByCwd.set(cwd, managerPromise);
|
|
843
|
+
cached = managerPromise.then((manager) => manager?.getToolEntries() ?? []);
|
|
647
844
|
this.mcpToolsByCwd.set(cwd, cached);
|
|
648
845
|
}
|
|
649
846
|
return cached;
|
|
@@ -706,6 +903,10 @@ export class BubbleSdk {
|
|
|
706
903
|
promptCacheKey,
|
|
707
904
|
protocol: target.protocol,
|
|
708
905
|
headers: target.headers,
|
|
906
|
+
// Main SDK turns need the same per-request OAuth renewal as the TUI
|
|
907
|
+
// and child provider factory, including calls after a long-running tool.
|
|
908
|
+
openAICodexAuth: this.registry.createOpenAICodexAuthAdapter(activeProviderId),
|
|
909
|
+
grokAuth: this.registry.createGrokAuthAdapter(activeProviderId),
|
|
709
910
|
});
|
|
710
911
|
return { provider, providerId: activeProviderId, model: activeModel };
|
|
711
912
|
}
|
|
@@ -730,10 +931,10 @@ function releaseProcessOwner(ownerKey, sdk) {
|
|
|
730
931
|
if (processSessionOwners.get(ownerKey) === sdk)
|
|
731
932
|
processSessionOwners.delete(ownerKey);
|
|
732
933
|
}
|
|
733
|
-
function sessionEventLogFor(ownerKey) {
|
|
934
|
+
function sessionEventLogFor(ownerKey, retention = Infinity) {
|
|
734
935
|
let log = sessionEventLogs.get(ownerKey);
|
|
735
936
|
if (!log) {
|
|
736
|
-
log = new ReplayEventLog();
|
|
937
|
+
log = new ReplayEventLog(retention);
|
|
737
938
|
sessionEventLogs.set(ownerKey, log);
|
|
738
939
|
}
|
|
739
940
|
return log;
|
|
@@ -767,6 +968,18 @@ function throwAbortSignal(signal) {
|
|
|
767
968
|
throw signal.reason;
|
|
768
969
|
throw new AgentAbortError(typeof signal.reason === "string" ? signal.reason : "SDK turn cancelled.");
|
|
769
970
|
}
|
|
971
|
+
/** The mode the session log last recorded ("default" before any switch). */
|
|
972
|
+
function latestRecordedMode(session) {
|
|
973
|
+
const entries = session.getEntries();
|
|
974
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
975
|
+
const entry = entries[index];
|
|
976
|
+
if (entry.type === "marker" && entry.kind === "mode_switch"
|
|
977
|
+
&& (entry.value === "default" || entry.value === "plan" || entry.value === "bypassPermissions")) {
|
|
978
|
+
return entry.value;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return "default";
|
|
982
|
+
}
|
|
770
983
|
function awaitWithAbort(promise, signal) {
|
|
771
984
|
if (signal.aborted) {
|
|
772
985
|
try {
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
* consumer, and consumers can attach again with their last seen sequence.
|
|
4
4
|
*/
|
|
5
5
|
export declare class ReplayEventLog<T> {
|
|
6
|
+
private readonly retention;
|
|
6
7
|
private readonly entries;
|
|
8
|
+
private nextIndex;
|
|
9
|
+
private firstIndex;
|
|
10
|
+
private readonly readers;
|
|
7
11
|
private signal;
|
|
8
12
|
private closed;
|
|
9
13
|
private failure;
|
|
14
|
+
constructor(retention?: number);
|
|
10
15
|
get length(): number;
|
|
16
|
+
get retainedCount(): number;
|
|
11
17
|
append(value: T): void;
|
|
12
18
|
close(error?: unknown): void;
|
|
13
19
|
iterate(options?: {
|
|
@@ -15,5 +21,6 @@ export declare class ReplayEventLog<T> {
|
|
|
15
21
|
signal?: AbortSignal;
|
|
16
22
|
}): AsyncGenerator<T>;
|
|
17
23
|
private readFrom;
|
|
24
|
+
private prune;
|
|
18
25
|
private wake;
|
|
19
26
|
}
|