@bubblebrain-ai/bubble 0.0.59 → 0.0.61
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/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +4 -24
- package/dist/agent.d.ts +7 -6
- package/dist/agent.js +18 -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/compact.js +6 -2
- 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 +3 -0
- package/dist/lsp/service.js +3 -2
- package/dist/main.js +11 -1
- package/dist/model-catalog.js +6 -0
- package/dist/model-pricing.js +17 -0
- package/dist/permissions/shell-command.js +35 -3
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +17 -3
- package/dist/provider-ai-sdk.d.ts +2 -1
- package/dist/provider-ai-sdk.js +26 -9
- package/dist/provider-openai-codex.d.ts +4 -0
- package/dist/provider-openai-codex.js +28 -9
- package/dist/provider-registry.js +4 -2
- package/dist/sdk/index.d.ts +21 -2
- package/dist/sdk/index.js +92 -11
- package/dist/session.d.ts +5 -1
- package/dist/session.js +13 -5
- 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 +6 -4
- package/dist/system-prompt.d.ts +3 -1
- 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/grep.js +20 -1
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tui/app.js +6 -0
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/controller/controller.d.ts +7 -0
- package/dist/tui/controller/controller.js +13 -0
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent thread lifecycle + dynamic-workflow execution.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from Agent: spawn/wait/send/close, the scheduler dispatch and its
|
|
5
|
+
* terminal callbacks, admission, ingestion, worktree-backed child construction
|
|
6
|
+
* and the QuickJS workflow driver all lived on the Agent class alongside the
|
|
7
|
+
* conversation loop, sharing its private fields.
|
|
8
|
+
*
|
|
9
|
+
* Two rules keep the boundary honest:
|
|
10
|
+
* - Everything the runtime needs from the parent arrives through
|
|
11
|
+
* `SubagentRuntimeParent` as a LIVE accessor, never a construction-time
|
|
12
|
+
* snapshot: `/model` reassigns the parent's provider/model/thinking
|
|
13
|
+
* mid-session and children must inherit the current values.
|
|
14
|
+
* - Conversation state is never pushed back into the parent. Tool updates
|
|
15
|
+
* and ingestion notices are PULLED (`drainToolUpdates` /
|
|
16
|
+
* `drainIngestionNotices`). The only direct sink is a terminal,
|
|
17
|
+
* fail-closed provider diagnostic that contains no child transcript.
|
|
18
|
+
*
|
|
19
|
+
* `new Agent(...)` stays on Agent, reached through `parent.createChild`. That
|
|
20
|
+
* is the only genuine call back into the parent, and keeping it there is what
|
|
21
|
+
* guarantees the child-construction argument set — including the fields that
|
|
22
|
+
* must stay ABSENT (onMessageAppend, sessionID, routing catalog) — has exactly
|
|
23
|
+
* one definition site.
|
|
24
|
+
*/
|
|
25
|
+
import type { SkillSummary } from "../../skills/types.js";
|
|
1
26
|
import { type AgentProfile, type SubagentRunResult } from "../profiles.js";
|
|
2
27
|
import { type SubagentThreadRecord, type SubagentThreadSnapshot } from "../subagent-control.js";
|
|
3
28
|
import { SubagentStore } from "../subagent-store.js";
|
|
@@ -36,6 +61,9 @@ export interface SubagentRuntimeParent {
|
|
|
36
61
|
readonly apiModel: string;
|
|
37
62
|
readonly thinkingLevel: ThinkingLevel;
|
|
38
63
|
readonly memoryPrompt: string | undefined;
|
|
64
|
+
/** Optional so lightweight parents (tests, embedders) need not provide a catalog. */
|
|
65
|
+
getSkillSummaries?(): SkillSummary[];
|
|
66
|
+
getSkillCatalogChars?(): number | undefined;
|
|
39
67
|
readonly providerFactory: ((route: ResolvedSubagentRoute) => Provider | Promise<Provider>) | undefined;
|
|
40
68
|
/**
|
|
41
69
|
* The UNFILTERED tool map — deferred-but-unlocked entries included. Profile
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subagent thread lifecycle + dynamic-workflow execution.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from Agent: spawn/wait/send/close, the scheduler dispatch and its
|
|
5
|
-
* terminal callbacks, admission, ingestion, worktree-backed child construction
|
|
6
|
-
* and the QuickJS workflow driver all lived on the Agent class alongside the
|
|
7
|
-
* conversation loop, sharing its private fields.
|
|
8
|
-
*
|
|
9
|
-
* Two rules keep the boundary honest:
|
|
10
|
-
* - Everything the runtime needs from the parent arrives through
|
|
11
|
-
* `SubagentRuntimeParent` as a LIVE accessor, never a construction-time
|
|
12
|
-
* snapshot: `/model` reassigns the parent's provider/model/thinking
|
|
13
|
-
* mid-session and children must inherit the current values.
|
|
14
|
-
* - Conversation state is never pushed back into the parent. Tool updates
|
|
15
|
-
* and ingestion notices are PULLED (`drainToolUpdates` /
|
|
16
|
-
* `drainIngestionNotices`). The only direct sink is a terminal,
|
|
17
|
-
* fail-closed provider diagnostic that contains no child transcript.
|
|
18
|
-
*
|
|
19
|
-
* `new Agent(...)` stays on Agent, reached through `parent.createChild`. That
|
|
20
|
-
* is the only genuine call back into the parent, and keeping it there is what
|
|
21
|
-
* guarantees the child-construction argument set — including the fields that
|
|
22
|
-
* must stay ABSENT (onMessageAppend, sessionID, routing catalog) — has exactly
|
|
23
|
-
* one definition site.
|
|
24
|
-
*/
|
|
25
1
|
import { AgentRunInputQueue } from "../input-controller.js";
|
|
26
2
|
import { randomUUID } from "node:crypto";
|
|
27
3
|
import { buildSystemPrompt } from "../../system-prompt.js";
|
|
@@ -714,6 +690,10 @@ export class SubagentRuntime {
|
|
|
714
690
|
mode: childMode,
|
|
715
691
|
workingDir: childCwd,
|
|
716
692
|
...buildToolPromptOptions(tools),
|
|
693
|
+
// A child that can load skills sees the same catalog as its parent.
|
|
694
|
+
...(childToolNames.includes("skill")
|
|
695
|
+
? { skills: this.parent.getSkillSummaries?.(), skillCatalogChars: this.parent.getSkillCatalogChars?.() }
|
|
696
|
+
: {}),
|
|
717
697
|
memoryPrompt: childToolNames.some((name) => name === "memory")
|
|
718
698
|
? this.parent.memoryPrompt
|
|
719
699
|
: undefined,
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent - The core decision loop.
|
|
3
|
-
* It maintains message state, calls the LLM, executes tools, and auto-continues.
|
|
4
|
-
*/
|
|
5
1
|
import { type ContextCheckpoint } from "./context/checkpoint.js";
|
|
6
2
|
import { type ContextUsageSnapshot } from "./context/usage.js";
|
|
7
3
|
import type { AgentEvent, AgentInputController, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
|
|
@@ -79,6 +75,8 @@ export interface AgentOptions {
|
|
|
79
75
|
subAgentId?: string;
|
|
80
76
|
};
|
|
81
77
|
skills?: SkillSummary[];
|
|
78
|
+
/** Skill catalog budget in characters for rebuilt system prompts (0 = off). */
|
|
79
|
+
skillCatalogChars?: number;
|
|
82
80
|
memoryPrompt?: string;
|
|
83
81
|
fileStateTracker?: FileStateTracker;
|
|
84
82
|
agentCategories?: AgentCategoriesConfig;
|
|
@@ -180,6 +178,7 @@ export declare class Agent {
|
|
|
180
178
|
private budgetLedger?;
|
|
181
179
|
private budgetSource;
|
|
182
180
|
private skillSummaries;
|
|
181
|
+
private skillCatalogChars?;
|
|
183
182
|
private memoryPrompt?;
|
|
184
183
|
private fileStateTracker?;
|
|
185
184
|
private agentCategories;
|
|
@@ -242,8 +241,10 @@ export declare class Agent {
|
|
|
242
241
|
unlockDeferredTools(names: string[]): void;
|
|
243
242
|
/** All deferred tools in this session (for tool_search to inspect). */
|
|
244
243
|
listDeferredTools(): ToolRegistryEntry[];
|
|
245
|
-
getSystemPromptToolOptions(): Pick<import("./system-prompt.js").SystemPromptOptions, "tools" | "toolSnippets" | "guidelines" | "modelRoutingPrompt" | "memoryPrompt">;
|
|
246
|
-
|
|
244
|
+
getSystemPromptToolOptions(): Pick<import("./system-prompt.js").SystemPromptOptions, "tools" | "toolSnippets" | "guidelines" | "modelRoutingPrompt" | "memoryPrompt" | "skills" | "skillCatalogChars">;
|
|
245
|
+
getSkillSummaries(): SkillSummary[];
|
|
246
|
+
getSkillCatalogChars(): number | undefined;
|
|
247
|
+
/** Refresh the live Skill catalog after /skills reloads or changes enablement; hosts then rebuild the system prompt. */
|
|
247
248
|
setSkillSummaries(skills: SkillSummary[]): void;
|
|
248
249
|
/** Current routing menu (design §4); undefined when no accessor is wired. */
|
|
249
250
|
buildModelRoutingPromptSection(): string | undefined;
|
package/dist/agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toolImageObservation } from "./context/tool-images.js";
|
|
1
2
|
/**
|
|
2
3
|
* Agent - The core decision loop.
|
|
3
4
|
* It maintains message state, calls the LLM, executes tools, and auto-continues.
|
|
@@ -142,6 +143,7 @@ export class Agent {
|
|
|
142
143
|
budgetLedger;
|
|
143
144
|
budgetSource;
|
|
144
145
|
skillSummaries;
|
|
146
|
+
skillCatalogChars;
|
|
145
147
|
memoryPrompt;
|
|
146
148
|
fileStateTracker;
|
|
147
149
|
agentCategories;
|
|
@@ -195,6 +197,7 @@ export class Agent {
|
|
|
195
197
|
this.budgetLedger = options.budgetLedger;
|
|
196
198
|
this.budgetSource = options.budgetSource ?? { runId: this.sessionID ?? "agent" };
|
|
197
199
|
this.skillSummaries = options.skills ?? [];
|
|
200
|
+
this.skillCatalogChars = options.skillCatalogChars;
|
|
198
201
|
this.memoryPrompt = options.memoryPrompt;
|
|
199
202
|
this.fileStateTracker = options.fileStateTracker;
|
|
200
203
|
this.agentCategories = options.agentCategories ?? {};
|
|
@@ -236,6 +239,8 @@ export class Agent {
|
|
|
236
239
|
// must see deferred-but-registered tools (an explicit include in a
|
|
237
240
|
// profile is the author pre-unlocking them).
|
|
238
241
|
allTools: () => [...this.tools.values()],
|
|
242
|
+
getSkillSummaries: () => this.getSkillSummaries(),
|
|
243
|
+
getSkillCatalogChars: () => this.getSkillCatalogChars(),
|
|
239
244
|
createChild: (spec) => this.createChildAgent(spec),
|
|
240
245
|
recordProviderError: (error, context) => this.recordProviderError(error, context),
|
|
241
246
|
runExternalHook: (input, abortSignal) => this.runExternalHook(input, abortSignal),
|
|
@@ -371,6 +376,9 @@ export class Agent {
|
|
|
371
376
|
}
|
|
372
377
|
getSystemPromptToolOptions() {
|
|
373
378
|
return {
|
|
379
|
+
// Rebuilt prompts (model switch, /skills changes) keep the Skills catalog.
|
|
380
|
+
skills: this.skillSummaries,
|
|
381
|
+
skillCatalogChars: this.skillCatalogChars,
|
|
374
382
|
...buildToolPromptOptions(this.getActiveToolEntries()),
|
|
375
383
|
// Rendered through the live accessor (design §1.5), so every host-
|
|
376
384
|
// triggered prompt rebuild picks up the current catalog.
|
|
@@ -380,7 +388,13 @@ export class Agent {
|
|
|
380
388
|
memoryPrompt: this.memoryPrompt,
|
|
381
389
|
};
|
|
382
390
|
}
|
|
383
|
-
|
|
391
|
+
getSkillSummaries() {
|
|
392
|
+
return this.skillSummaries.slice();
|
|
393
|
+
}
|
|
394
|
+
getSkillCatalogChars() {
|
|
395
|
+
return this.skillCatalogChars;
|
|
396
|
+
}
|
|
397
|
+
/** Refresh the live Skill catalog after /skills reloads or changes enablement; hosts then rebuild the system prompt. */
|
|
384
398
|
setSkillSummaries(skills) {
|
|
385
399
|
this.skillSummaries = skills.slice();
|
|
386
400
|
this.notifyContextChanged();
|
|
@@ -1489,6 +1503,9 @@ export class Agent {
|
|
|
1489
1503
|
throwIfAborted(abortSignal);
|
|
1490
1504
|
}
|
|
1491
1505
|
}
|
|
1506
|
+
const imageObservation = toolImageObservation(executedResults);
|
|
1507
|
+
if (imageObservation)
|
|
1508
|
+
this.appendMessage(imageObservation);
|
|
1492
1509
|
// A checkpoint must contain the whole batch, never provider repair's
|
|
1493
1510
|
// placeholder for a sibling that has not executed yet.
|
|
1494
1511
|
this.compactResidentHistory();
|
package/dist/cli.d.ts
CHANGED
|
@@ -30,3 +30,8 @@ export interface CliArgs {
|
|
|
30
30
|
}
|
|
31
31
|
export declare function parseArgs(argv: string[]): CliArgs;
|
|
32
32
|
export declare function printHelp(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Make `cwd` the process working directory, so --cwd behaves exactly like
|
|
35
|
+
* launching Bubble there. Returns an error message instead of throwing.
|
|
36
|
+
*/
|
|
37
|
+
export declare function enterWorkingDirectory(cwd: string): string | undefined;
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI argument parsing.
|
|
3
3
|
*/
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
4
6
|
import { THINKING_LEVELS } from "./types.js";
|
|
5
7
|
import { isThinkingLevel } from "./variant/thinking-level.js";
|
|
6
8
|
export function parseArgs(argv) {
|
|
@@ -27,9 +29,13 @@ export function parseArgs(argv) {
|
|
|
27
29
|
case "-m":
|
|
28
30
|
args.model = argv[++i];
|
|
29
31
|
break;
|
|
30
|
-
case "--cwd":
|
|
31
|
-
|
|
32
|
+
case "--cwd": {
|
|
33
|
+
// Absolute, so it survives the chdir in main() and matches session metadata.
|
|
34
|
+
const dir = argv[++i];
|
|
35
|
+
if (dir)
|
|
36
|
+
args.cwd = resolve(dir);
|
|
32
37
|
break;
|
|
38
|
+
}
|
|
33
39
|
case "--api-key":
|
|
34
40
|
case "-k":
|
|
35
41
|
args.apiKey = argv[++i];
|
|
@@ -150,3 +156,20 @@ Options (serve --feishu):
|
|
|
150
156
|
--dry-run Connect once, then exit (smoke test)
|
|
151
157
|
`);
|
|
152
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Make `cwd` the process working directory, so --cwd behaves exactly like
|
|
161
|
+
* launching Bubble there. Returns an error message instead of throwing.
|
|
162
|
+
*/
|
|
163
|
+
export function enterWorkingDirectory(cwd) {
|
|
164
|
+
if (cwd === process.cwd())
|
|
165
|
+
return undefined;
|
|
166
|
+
try {
|
|
167
|
+
if (!statSync(cwd).isDirectory())
|
|
168
|
+
return `--cwd ${cwd} is not a directory.`;
|
|
169
|
+
process.chdir(cwd);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return `--cwd ${cwd} does not exist or cannot be opened.`;
|
|
174
|
+
}
|
|
175
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface UserConfigData {
|
|
|
20
20
|
/** Skill names disabled from invocation/search until re-enabled in /skills. */
|
|
21
21
|
skills?: {
|
|
22
22
|
disabled?: string[];
|
|
23
|
+
catalogChars?: number;
|
|
23
24
|
};
|
|
24
25
|
/**
|
|
25
26
|
* Three shapes are accepted on disk so we can evolve without breaking
|
|
@@ -76,6 +77,8 @@ export declare class UserConfig {
|
|
|
76
77
|
setDefaultProvider(id: string): void;
|
|
77
78
|
getSkillPaths(): string[];
|
|
78
79
|
setSkillPaths(paths: string[]): void;
|
|
80
|
+
/** Skill catalog budget in characters; undefined uses the default, 0 turns the catalog off. */
|
|
81
|
+
getSkillCatalogChars(): number | undefined;
|
|
79
82
|
getDisabledSkills(): string[];
|
|
80
83
|
setDisabledSkills(names: string[]): void;
|
|
81
84
|
getTheme(): ThemeConfig;
|
package/dist/config.js
CHANGED
|
@@ -202,6 +202,13 @@ export class UserConfig {
|
|
|
202
202
|
this.data.skillPaths = paths.slice();
|
|
203
203
|
this.save();
|
|
204
204
|
}
|
|
205
|
+
/** Skill catalog budget in characters; undefined uses the default, 0 turns the catalog off. */
|
|
206
|
+
getSkillCatalogChars() {
|
|
207
|
+
const value = this.data.skills?.catalogChars;
|
|
208
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
209
|
+
? Math.min(Math.floor(value), 200_000)
|
|
210
|
+
: undefined;
|
|
211
|
+
}
|
|
205
212
|
getDisabledSkills() {
|
|
206
213
|
const disabled = this.data.skills?.disabled;
|
|
207
214
|
if (!Array.isArray(disabled))
|
package/dist/context/compact.js
CHANGED
|
@@ -59,7 +59,9 @@ export function isCompactionSummaryMessage(message) {
|
|
|
59
59
|
}
|
|
60
60
|
/** A message the user actually typed (not a projected reminder, not a summary). */
|
|
61
61
|
export function isRealUserMessage(message) {
|
|
62
|
-
|
|
62
|
+
// Tool screenshots ride in a user-role message for the provider, but they
|
|
63
|
+
// belong to the tool turn: never a turn boundary.
|
|
64
|
+
if (message.role !== "user" || message.toolObservation)
|
|
63
65
|
return false;
|
|
64
66
|
const text = messageText(message);
|
|
65
67
|
if (!text.trim() || isInternalBlockContent(text))
|
|
@@ -302,7 +304,9 @@ export function compactCurrentTurnToolGroups(messages, options = {}) {
|
|
|
302
304
|
groups.push(current);
|
|
303
305
|
current = { assistant: msg, toolResults: [] };
|
|
304
306
|
}
|
|
305
|
-
else if (msg.role === "tool" && current) {
|
|
307
|
+
else if ((msg.role === "tool" || (msg.role === "user" && msg.toolObservation)) && current) {
|
|
308
|
+
// A screenshot observation follows its tool result: it lives and dies
|
|
309
|
+
// with that group instead of vanishing from kept groups.
|
|
306
310
|
current.toolResults.push(msg);
|
|
307
311
|
}
|
|
308
312
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** All tool responses must precede the observation (valid for every provider). */
|
|
2
|
+
export function toolImageObservation(results) {
|
|
3
|
+
const content = [];
|
|
4
|
+
let bytes = 0;
|
|
5
|
+
for (const result of results) {
|
|
6
|
+
if (result.isError)
|
|
7
|
+
continue;
|
|
8
|
+
for (const image of result.images ?? []) {
|
|
9
|
+
if (content.length >= 8 ||
|
|
10
|
+
!["image/png", "image/jpeg"].includes(image.mimeType))
|
|
11
|
+
continue;
|
|
12
|
+
if (typeof image.data !== "string" ||
|
|
13
|
+
image.data.length > 4_000_000 ||
|
|
14
|
+
!/^[a-zA-Z0-9+/]+={0,2}$/.test(image.data))
|
|
15
|
+
continue;
|
|
16
|
+
const raw = Buffer.from(image.data, "base64");
|
|
17
|
+
if (bytes + raw.length > 6_000_000)
|
|
18
|
+
continue;
|
|
19
|
+
if (image.mimeType === "image/png"
|
|
20
|
+
? raw.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a"
|
|
21
|
+
: raw.subarray(0, 3).toString("hex") !== "ffd8ff")
|
|
22
|
+
continue;
|
|
23
|
+
bytes += raw.length;
|
|
24
|
+
content.push({
|
|
25
|
+
type: "text",
|
|
26
|
+
text: `Screenshot observation from the preceding tool result (not a new user request):\n${result.content.slice(0, 2000)}`,
|
|
27
|
+
});
|
|
28
|
+
content.push({
|
|
29
|
+
type: "image_url",
|
|
30
|
+
image_url: { url: `data:${image.mimeType};base64,${image.data}` },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return content.length ? { role: "user", content, toolObservation: true } : null;
|
|
35
|
+
}
|
package/dist/context/usage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getModelContextWindow } from "../model-catalog.js";
|
|
2
|
-
import {
|
|
2
|
+
import { SKILL_CATALOG_HEADING, SKILL_CATALOG_INTRO } from "../skills/format.js";
|
|
3
3
|
import { buildDeferredToolsReminder } from "../prompt/reminders.js";
|
|
4
4
|
import { AUTOCOMPACT_BUFFER_TOKENS, estimateMessageTokens, estimateTextTokens, MIN_WINDOW_FOR_RESERVE, OUTPUT_RESERVE_TOKENS, } from "./budget.js";
|
|
5
5
|
export function buildContextUsageSnapshot(input) {
|
|
@@ -7,8 +7,12 @@ export function buildContextUsageSnapshot(input) {
|
|
|
7
7
|
const otherMessages = input.messages.filter((message) => message.role !== "system");
|
|
8
8
|
const deferredToolEntries = input.deferredToolEntries ?? [];
|
|
9
9
|
const systemContent = systemMessages.map((message) => message.content).join("\n\n");
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
// The catalog is whatever Skills section the prompt actually carries, whatever its budget.
|
|
11
|
+
const skillsPrompt = extractSkillCatalog(systemContent);
|
|
12
|
+
const skillsInSystemPrompt = !!skillsPrompt;
|
|
13
|
+
const advertisedSkills = skillsPrompt
|
|
14
|
+
? skillsPrompt.split("\n").filter((line) => line.startsWith("- ") && !line.startsWith("- … ")).length
|
|
15
|
+
: 0;
|
|
12
16
|
const skillsTokens = skillsInSystemPrompt ? estimateTextTokens(skillsPrompt, input.providerId) : 0;
|
|
13
17
|
const systemPromptTokens = Math.max(0, estimateTextTokens(systemContent, input.providerId) - skillsTokens);
|
|
14
18
|
const toolsTokens = estimateToolEntriesTokens(input.toolEntries, input.providerId);
|
|
@@ -37,7 +41,7 @@ export function buildContextUsageSnapshot(input) {
|
|
|
37
41
|
skills: {
|
|
38
42
|
label: "Skills",
|
|
39
43
|
tokens: skillsTokens,
|
|
40
|
-
detail:
|
|
44
|
+
detail: advertisedSkills > 0 ? `${advertisedSkills} advertised skill${advertisedSkills === 1 ? "" : "s"}` : "none in current prompt",
|
|
41
45
|
},
|
|
42
46
|
deferredTools: {
|
|
43
47
|
label: "Deferred/MCP",
|
|
@@ -54,7 +58,7 @@ export function buildContextUsageSnapshot(input) {
|
|
|
54
58
|
},
|
|
55
59
|
toolCount: input.toolEntries.length,
|
|
56
60
|
deferredToolCount: deferredToolEntries.length,
|
|
57
|
-
skillCount:
|
|
61
|
+
skillCount: advertisedSkills,
|
|
58
62
|
messageCount: input.messages.length,
|
|
59
63
|
};
|
|
60
64
|
}
|
|
@@ -211,3 +215,16 @@ function colorForLabel(label) {
|
|
|
211
215
|
return ANSI_DARK_GRAY;
|
|
212
216
|
return ANSI_GRAY;
|
|
213
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* The Skills catalog the prompt carries: heading plus its fixed intro line, so a
|
|
220
|
+
* "## Skills" heading in AGENTS.md or memory is never mistaken for it. The last
|
|
221
|
+
* match wins (the catalog is appended after memory); it ends at the next blank line.
|
|
222
|
+
*/
|
|
223
|
+
function extractSkillCatalog(systemContent) {
|
|
224
|
+
const marker = `${SKILL_CATALOG_HEADING}\n${SKILL_CATALOG_INTRO}\n`;
|
|
225
|
+
const at = systemContent.lastIndexOf(marker);
|
|
226
|
+
if (at < 0 || (at > 0 && !systemContent.slice(0, at).endsWith("\n\n")))
|
|
227
|
+
return "";
|
|
228
|
+
const end = systemContent.indexOf("\n\n", at);
|
|
229
|
+
return systemContent.slice(at, end < 0 ? undefined : end);
|
|
230
|
+
}
|
|
@@ -99,6 +99,8 @@ export class RunDriver {
|
|
|
99
99
|
const routingSnapshotAccessor = createRoutingSnapshotAccessor(this.opts.deps.providerRegistry, () => this.opts.deps.userConfig.getAgentCategories(), () => agentRouting);
|
|
100
100
|
const systemPrompt = buildSystemPrompt({
|
|
101
101
|
agentName: "Bubble",
|
|
102
|
+
skills,
|
|
103
|
+
skillCatalogChars: this.opts.deps.userConfig.getSkillCatalogChars(),
|
|
102
104
|
configuredProvider: providerId || "none",
|
|
103
105
|
configuredModel: model ? displayModel(model) : "none",
|
|
104
106
|
configuredModelId: model || "none",
|
|
@@ -188,6 +190,7 @@ export class RunDriver {
|
|
|
188
190
|
},
|
|
189
191
|
budgetLedger,
|
|
190
192
|
skills,
|
|
193
|
+
skillCatalogChars: this.opts.deps.userConfig.getSkillCatalogChars(),
|
|
191
194
|
memoryPrompt,
|
|
192
195
|
fileStateTracker,
|
|
193
196
|
agentCategories: this.opts.deps.userConfig.getAgentCategories(),
|
package/dist/lsp/service.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { access, readFile } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
8
8
|
import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node.js";
|
|
@@ -500,7 +500,8 @@ export class ProjectLspService {
|
|
|
500
500
|
resolveInsideCwd(filePath) {
|
|
501
501
|
const file = normalize(isAbsolute(filePath) ? filePath : resolve(this.cwd, filePath));
|
|
502
502
|
const rel = relative(this.cwd, file);
|
|
503
|
-
|
|
503
|
+
// Only a whole `..` segment climbs out; `..notes.md` is a file in the project.
|
|
504
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
504
505
|
return undefined;
|
|
505
506
|
return file;
|
|
506
507
|
}
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { Agent } from "./agent.js";
|
|
7
7
|
import { BudgetLedger } from "./agent/budget-ledger.js";
|
|
8
|
-
import { parseArgs, printHelp } from "./cli.js";
|
|
8
|
+
import { enterWorkingDirectory, parseArgs, printHelp } from "./cli.js";
|
|
9
9
|
import { effectiveThemeModeForTerminal, shouldProbeTerminalTheme, UserConfig } from "./config.js";
|
|
10
10
|
import { createProviderInstance, createUnavailableProvider } from "./provider.js";
|
|
11
11
|
import { resolveConfiguredModel } from "./model-selection.js";
|
|
@@ -56,6 +56,13 @@ async function main() {
|
|
|
56
56
|
console.log(`v${getCurrentVersion()}`);
|
|
57
57
|
process.exit(0);
|
|
58
58
|
}
|
|
59
|
+
// --cwd means "run as if launched there" (like git -C): every layer, including
|
|
60
|
+
// the TUI, shell children and relative paths, then sees the same directory.
|
|
61
|
+
const cwdError = enterWorkingDirectory(args.cwd);
|
|
62
|
+
if (cwdError) {
|
|
63
|
+
console.error(`bubble: ${cwdError}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
59
66
|
if (args.command === "update") {
|
|
60
67
|
const { runUpdateCommand } = await import("./update/index.js");
|
|
61
68
|
const code = await runUpdateCommand({ checkOnly: args.checkOnly });
|
|
@@ -379,6 +386,8 @@ async function main() {
|
|
|
379
386
|
: undefined;
|
|
380
387
|
const systemPrompt = buildSystemPrompt({
|
|
381
388
|
agentName: "Bubble",
|
|
389
|
+
skills: skillSummaries,
|
|
390
|
+
skillCatalogChars: userConfig.getSkillCatalogChars(),
|
|
382
391
|
configuredProvider: activeProviderId || "none",
|
|
383
392
|
configuredModel: activeModel ? displayModel(activeModel) : "none",
|
|
384
393
|
configuredModelId: activeModel || "none",
|
|
@@ -493,6 +502,7 @@ async function main() {
|
|
|
493
502
|
},
|
|
494
503
|
budgetLedger,
|
|
495
504
|
skills: skillSummaries,
|
|
505
|
+
skillCatalogChars: userConfig.getSkillCatalogChars(),
|
|
496
506
|
memoryPrompt,
|
|
497
507
|
fileStateTracker,
|
|
498
508
|
agentCategories: userConfig.getAgentCategories(),
|
package/dist/model-catalog.js
CHANGED
|
@@ -88,6 +88,9 @@ const ANTHROPIC_CHAT_LEVELS = ["off"];
|
|
|
88
88
|
// Grok 4.5 exposes low/medium/high effort with no "off" (thinking always on);
|
|
89
89
|
// the composer model declares no effort support at all.
|
|
90
90
|
const GROK_45_LEVELS = ["low", "medium", "high"];
|
|
91
|
+
// Gemini 4 documents only low/medium (default)/high. Verified live on
|
|
92
|
+
// 2026-10-13: thinkingLevel=minimal returns 400, so there is no off/minimal.
|
|
93
|
+
const GEMINI_4_LEVELS = ["low", "medium", "high"];
|
|
91
94
|
const GEMINI_3_LEVELS = ["low", "medium", "high"];
|
|
92
95
|
const GEMINI_3_FLASH_LEVELS = ["minimal", "low", "medium", "high"];
|
|
93
96
|
// Verified against the native API on 2026-09-02: Gemini 3.7/3.8 Flash reject
|
|
@@ -149,6 +152,9 @@ export const BUILTIN_MODELS = [
|
|
|
149
152
|
// list via fetchGeminiModels (GET /v1beta/models, newest five). Gemini 3
|
|
150
153
|
// thinking support varies by release: 3.7/3.8 Flash support off/low/medium/high
|
|
151
154
|
// while older Flash releases expose minimal/low/medium/high.
|
|
155
|
+
// Gemini 4 Argon: 1,048,576-token window shared by input, thinking and output.
|
|
156
|
+
// https://ai.google.dev/gemini-api/docs/models/gemini-4-argon
|
|
157
|
+
{ id: "gemini-4-argon", name: "Gemini 4 Argon", providerId: "google", tier: "strong", reasoningLevels: GEMINI_4_LEVELS, defaultReasoningLevel: "medium", contextWindow: 1048576 },
|
|
152
158
|
{ id: "gemini-3.8-flash", name: "Gemini 3.8 Flash", providerId: "google", tier: "fast", reasoningLevels: GEMINI_37_38_FLASH_LEVELS, contextWindow: 1048576 },
|
|
153
159
|
{ id: "gemini-3.7-flash", name: "Gemini 3.7 Flash", providerId: "google", tier: "fast", reasoningLevels: GEMINI_37_38_FLASH_LEVELS, contextWindow: 1048576 },
|
|
154
160
|
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", providerId: "google", tier: "fast", reasoningLevels: GEMINI_3_FLASH_LEVELS, contextWindow: 1048576 },
|
package/dist/model-pricing.js
CHANGED
|
@@ -64,6 +64,23 @@ export const MODEL_PRICING_OVERRIDES = [
|
|
|
64
64
|
outputPerMillion: 7.5,
|
|
65
65
|
},
|
|
66
66
|
})),
|
|
67
|
+
// Gemini 4 Argon introductory pricing through 2026-11-30; standard rates from
|
|
68
|
+
// 2026-12-01. Implicit caching only (95% off input), so no cache-write rate.
|
|
69
|
+
// https://ai.google.dev/gemini-api/docs/pricing
|
|
70
|
+
{
|
|
71
|
+
providerId: "google",
|
|
72
|
+
modelId: "gemini-4-argon",
|
|
73
|
+
currency: "USD",
|
|
74
|
+
inputCacheHitPerMillion: 0.1,
|
|
75
|
+
inputCacheMissPerMillion: 2,
|
|
76
|
+
outputPerMillion: 10,
|
|
77
|
+
effectiveUntil: "2026-12-01T00:00:00Z",
|
|
78
|
+
original: {
|
|
79
|
+
inputCacheHitPerMillion: 0.2,
|
|
80
|
+
inputCacheMissPerMillion: 4,
|
|
81
|
+
outputPerMillion: 20,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
67
84
|
{
|
|
68
85
|
providerId: "deepseek",
|
|
69
86
|
modelId: "deepseek-flash",
|
|
@@ -160,6 +160,33 @@ export function shellTokens(segment) {
|
|
|
160
160
|
}
|
|
161
161
|
/** Wrappers that run their argument as the real command. */
|
|
162
162
|
const COMMAND_WRAPPERS = new Set(["command", "exec", "nohup", "builtin", "env", "sudo", "doas", "nice", "xargs"]);
|
|
163
|
+
/**
|
|
164
|
+
* Wrapper options that take a separate argument (`sudo -u root`), so that
|
|
165
|
+
* argument is not mistaken for the command. `env -S` is deliberately absent:
|
|
166
|
+
* its argument is the command line itself.
|
|
167
|
+
*/
|
|
168
|
+
const WRAPPER_OPTION_ARGS = {
|
|
169
|
+
sudo: { short: "CDghpRrTtUu", long: ["chdir", "chroot", "close-from", "command-timeout", "group", "host", "other-user", "prompt", "role", "type", "user"] },
|
|
170
|
+
doas: { short: "Cu", long: [] },
|
|
171
|
+
env: { short: "aCPu", long: ["argv0", "chdir", "unset"] },
|
|
172
|
+
nice: { short: "n", long: ["adjustment"] },
|
|
173
|
+
// GNU --eof/--replace/--max-lines take a value only as `--opt=value`.
|
|
174
|
+
xargs: { short: "adEIJLnPRSs", long: ["arg-file", "delimiter", "max-args", "max-chars", "max-procs", "process-slot-var"] },
|
|
175
|
+
exec: { short: "a", long: [] },
|
|
176
|
+
};
|
|
177
|
+
/** getopt rules: in `-Eu root` the last letter takes the next word; in `-uroot` it is attached. */
|
|
178
|
+
function optionTakesNextWord(wrapper, option) {
|
|
179
|
+
const spec = WRAPPER_OPTION_ARGS[wrapper];
|
|
180
|
+
if (!spec)
|
|
181
|
+
return false;
|
|
182
|
+
if (option.startsWith("--"))
|
|
183
|
+
return !option.includes("=") && spec.long.includes(option.slice(2));
|
|
184
|
+
for (let index = 1; index < option.length; index++) {
|
|
185
|
+
if (spec.short.includes(option[index]))
|
|
186
|
+
return index === option.length - 1;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
163
190
|
/**
|
|
164
191
|
* The words of a segment with leading `VAR=value` assignments and command
|
|
165
192
|
* wrappers (`sudo -u x`, `env`, `nohup`, ...) removed. Deny rules match
|
|
@@ -177,9 +204,14 @@ export function effectiveCommandTokens(segment) {
|
|
|
177
204
|
changed = true;
|
|
178
205
|
}
|
|
179
206
|
if (tokens.length > 0 && COMMAND_WRAPPERS.has(tokens[0])) {
|
|
180
|
-
tokens.shift();
|
|
181
|
-
while (tokens.length > 0 && tokens[0].startsWith("-"))
|
|
182
|
-
tokens.shift();
|
|
207
|
+
const wrapper = tokens.shift();
|
|
208
|
+
while (tokens.length > 0 && tokens[0].startsWith("-")) {
|
|
209
|
+
const option = tokens.shift();
|
|
210
|
+
if (option === "--")
|
|
211
|
+
break;
|
|
212
|
+
if (optionTakesNextWord(wrapper, option))
|
|
213
|
+
tokens.shift();
|
|
214
|
+
}
|
|
183
215
|
changed = true;
|
|
184
216
|
}
|
|
185
217
|
}
|
package/dist/prompt/compose.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export interface ComposeSystemPromptOptions extends EnvironmentPromptOptions {
|
|
|
6
6
|
guidelines?: string[];
|
|
7
7
|
thinkingLevel?: ThinkingLevel;
|
|
8
8
|
mode?: PermissionMode;
|
|
9
|
+
/** Prompt-visible skills; when given (and `skill` is a tool), listed as a catalog. */
|
|
9
10
|
skills?: SkillSummary[];
|
|
11
|
+
/** Catalog budget in characters (default SKILL_CATALOG_CHARS); 0 falls back to search only. */
|
|
12
|
+
skillCatalogChars?: number;
|
|
10
13
|
memoryPrompt?: string;
|
|
11
14
|
agentProfilePrompt?: string;
|
|
12
15
|
/** Rendered subagent model-routing menu (design §4). Placed right after the
|
package/dist/prompt/compose.js
CHANGED
|
@@ -9,6 +9,7 @@ import { buildKimiProviderPrompt } from "./provider-prompts/kimi.js";
|
|
|
9
9
|
import { buildDelegationPolicyPrompt } from "./delegation.js";
|
|
10
10
|
import { buildEnvironmentPrompt, defaultToolNames } from "./environment.js";
|
|
11
11
|
import { buildRuntimePrompt } from "./runtime.js";
|
|
12
|
+
import { buildSkillCatalog } from "../skills/format.js";
|
|
12
13
|
export function composeSystemPrompt(options = {}) {
|
|
13
14
|
const agentName = options.agentName ?? "Bubble";
|
|
14
15
|
const providerPrompt = buildProviderPrompt(agentName, options.configuredProvider, options.configuredModelId, options.configuredModel);
|
|
@@ -21,10 +22,20 @@ export function composeSystemPrompt(options = {}) {
|
|
|
21
22
|
tools: options.tools ?? defaultToolNames,
|
|
22
23
|
toolSnippets: options.toolSnippets,
|
|
23
24
|
});
|
|
25
|
+
const tools = options.tools ?? defaultToolNames;
|
|
26
|
+
// Level one of skill loading: names and one-line descriptions are always in
|
|
27
|
+
// context; bodies load on demand with `skill`. Last section, so the prefix
|
|
28
|
+
// above stays byte-identical while the skill set is unchanged.
|
|
29
|
+
const skillCatalog = options.skills && tools.includes("skill")
|
|
30
|
+
? buildSkillCatalog(options.skills, {
|
|
31
|
+
budgetChars: options.skillCatalogChars,
|
|
32
|
+
searchable: tools.includes("skill_search"),
|
|
33
|
+
})
|
|
34
|
+
: undefined;
|
|
24
35
|
const runtimePrompt = buildRuntimePrompt({
|
|
25
36
|
thinkingLevel: options.thinkingLevel,
|
|
26
37
|
mode: options.mode,
|
|
27
|
-
guidelines: buildGuidelines(
|
|
38
|
+
guidelines: buildGuidelines(tools, options.guidelines ?? [], skillCatalog),
|
|
28
39
|
});
|
|
29
40
|
const delegationPrompt = buildDelegationPolicyPrompt(options.tools ?? defaultToolNames);
|
|
30
41
|
// Same gate as the delegation policy: children (no spawn_agent) never see the menu.
|
|
@@ -37,6 +48,7 @@ export function composeSystemPrompt(options = {}) {
|
|
|
37
48
|
modelRoutingPrompt,
|
|
38
49
|
options.agentProfilePrompt,
|
|
39
50
|
options.memoryPrompt,
|
|
51
|
+
skillCatalog?.text,
|
|
40
52
|
].filter(Boolean).join("\n\n");
|
|
41
53
|
}
|
|
42
54
|
function buildProviderPrompt(agentName, providerId, modelId, modelName) {
|
|
@@ -67,7 +79,7 @@ function buildProviderPrompt(agentName, providerId, modelId, modelName) {
|
|
|
67
79
|
}
|
|
68
80
|
return buildDefaultProviderPrompt(agentName);
|
|
69
81
|
}
|
|
70
|
-
function buildGuidelines(tools, extraGuidelines) {
|
|
82
|
+
function buildGuidelines(tools, extraGuidelines, skillCatalog) {
|
|
71
83
|
const guidelines = [];
|
|
72
84
|
const add = (item) => {
|
|
73
85
|
if (!guidelines.includes(item)) {
|
|
@@ -86,7 +98,9 @@ function buildGuidelines(tools, extraGuidelines) {
|
|
|
86
98
|
if (tools.includes("question")) {
|
|
87
99
|
add("When the user is explicitly discussing, brainstorming, or shaping an approach instead of asking for immediate execution, use the question tool for targeted clarification or preference choices when it would materially improve the discussion; do not use it for generic permission-to-proceed questions");
|
|
88
100
|
}
|
|
89
|
-
|
|
101
|
+
// With a catalog the Skills section itself says how to load; search remains
|
|
102
|
+
// the only route when the host gave no catalog or turned it off.
|
|
103
|
+
if (!skillCatalog?.listed && tools.includes("skill_search") && tools.includes("skill")) {
|
|
90
104
|
add("Skills may provide specialized workflows. When a task appears to match a specialized workflow, call skill_search to find relevant skills, then call skill with the exact name to load the selected skill before applying it");
|
|
91
105
|
}
|
|
92
106
|
for (const item of extraGuidelines) {
|
|
@@ -50,7 +50,8 @@ export declare function fetchGeminiModels(options: {
|
|
|
50
50
|
}): Promise<GeminiModelDescriptor[]>;
|
|
51
51
|
/**
|
|
52
52
|
* Ranking: one entry per (version, tier) family — GA ids beat dated previews —
|
|
53
|
-
* then newest version first, pro before flash before
|
|
53
|
+
* then newest version first, flagship (pro/argon) before flash before
|
|
54
|
+
* flash-lite, top N.
|
|
54
55
|
*/
|
|
55
56
|
export declare function selectLatestGeminiModels(entries: GeminiModelListEntry[], limit?: number): GeminiModelDescriptor[];
|
|
56
57
|
/** Mirrors the static catalog's per-family thinking support. */
|