@bubblebrain-ai/bubble 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/agent/budget-ledger.d.ts +20 -0
  2. package/dist/agent/budget-ledger.js +51 -0
  3. package/dist/agent/execution-governor.js +1 -1
  4. package/dist/agent/profiles.d.ts +59 -0
  5. package/dist/agent/profiles.js +460 -0
  6. package/dist/agent/subagent-control.d.ts +52 -0
  7. package/dist/agent/subagent-control.js +38 -0
  8. package/dist/agent/task-size.d.ts +9 -0
  9. package/dist/agent/task-size.js +33 -0
  10. package/dist/agent/tool-intent.d.ts +1 -0
  11. package/dist/agent/tool-intent.js +1 -1
  12. package/dist/agent.d.ts +60 -1
  13. package/dist/agent.js +648 -55
  14. package/dist/context/budget.js +1 -0
  15. package/dist/context/compact-llm.js +7 -6
  16. package/dist/context/compact.js +6 -6
  17. package/dist/context/projector.d.ts +3 -3
  18. package/dist/context/projector.js +32 -18
  19. package/dist/context/prune.d.ts +2 -2
  20. package/dist/context/prune.js +1 -4
  21. package/dist/main.js +12 -5
  22. package/dist/mcp/manager.js +1 -0
  23. package/dist/orchestrator/default-hooks.js +85 -35
  24. package/dist/orchestrator/hooks.d.ts +5 -3
  25. package/dist/prompt/compose.d.ts +1 -0
  26. package/dist/prompt/compose.js +11 -1
  27. package/dist/prompt/environment.js +23 -2
  28. package/dist/prompt/provider-prompts/deepseek.js +1 -2
  29. package/dist/prompt/provider-prompts/kimi.js +1 -2
  30. package/dist/prompt/reminders.d.ts +21 -2
  31. package/dist/prompt/reminders.js +53 -8
  32. package/dist/prompt/runtime.d.ts +1 -1
  33. package/dist/prompt/runtime.js +17 -23
  34. package/dist/provider-artifacts.d.ts +7 -0
  35. package/dist/provider-artifacts.js +60 -0
  36. package/dist/provider.d.ts +16 -8
  37. package/dist/provider.js +149 -34
  38. package/dist/session-log.js +3 -1
  39. package/dist/system-prompt.d.ts +2 -0
  40. package/dist/tools/agent-lifecycle.d.ts +6 -0
  41. package/dist/tools/agent-lifecycle.js +355 -0
  42. package/dist/tools/bash.d.ts +2 -1
  43. package/dist/tools/bash.js +3 -1
  44. package/dist/tools/edit-apply.d.ts +25 -0
  45. package/dist/tools/edit-apply.js +228 -0
  46. package/dist/tools/edit.d.ts +2 -1
  47. package/dist/tools/edit.js +75 -56
  48. package/dist/tools/exit-plan-mode.js +3 -1
  49. package/dist/tools/file-mutation-queue.d.ts +1 -0
  50. package/dist/tools/file-mutation-queue.js +32 -0
  51. package/dist/tools/file-state.d.ts +25 -0
  52. package/dist/tools/file-state.js +52 -0
  53. package/dist/tools/glob.js +1 -0
  54. package/dist/tools/grep.js +1 -0
  55. package/dist/tools/index.d.ts +3 -1
  56. package/dist/tools/index.js +9 -7
  57. package/dist/tools/lsp.js +2 -0
  58. package/dist/tools/memory.js +2 -0
  59. package/dist/tools/question.js +2 -0
  60. package/dist/tools/read.d.ts +2 -1
  61. package/dist/tools/read.js +6 -1
  62. package/dist/tools/skill.js +1 -0
  63. package/dist/tools/task.js +1 -0
  64. package/dist/tools/todo.js +1 -0
  65. package/dist/tools/tool-search.js +2 -1
  66. package/dist/tools/web-fetch.js +1 -0
  67. package/dist/tools/web-search.js +1 -0
  68. package/dist/tools/write.d.ts +4 -3
  69. package/dist/tools/write.js +135 -54
  70. package/dist/tui/display-history.d.ts +10 -1
  71. package/dist/tui/markdown-inline.d.ts +22 -0
  72. package/dist/tui/markdown-inline.js +68 -0
  73. package/dist/tui/render-signature.d.ts +1 -0
  74. package/dist/tui/render-signature.js +7 -0
  75. package/dist/tui/run.js +811 -274
  76. package/dist/tui/streaming-tool-args.d.ts +15 -0
  77. package/dist/tui/streaming-tool-args.js +30 -0
  78. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  79. package/dist/tui/tool-renderers/fallback.js +75 -0
  80. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  81. package/dist/tui/tool-renderers/registry.js +11 -0
  82. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  83. package/dist/tui/tool-renderers/subagent.js +114 -0
  84. package/dist/tui/tool-renderers/types.d.ts +36 -0
  85. package/dist/tui/tool-renderers/types.js +1 -0
  86. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  87. package/dist/tui/tool-renderers/write-preview.js +30 -0
  88. package/dist/tui/tool-renderers/write.d.ts +6 -0
  89. package/dist/tui/tool-renderers/write.js +88 -0
  90. package/dist/types.d.ts +105 -10
  91. package/package.json +1 -1
@@ -0,0 +1,20 @@
1
+ import type { TokenUsage } from "../types.js";
2
+ export interface BudgetUsageSource {
3
+ runId: string;
4
+ subAgentId?: string;
5
+ }
6
+ export interface BudgetSnapshot {
7
+ spent: number;
8
+ limit?: number;
9
+ exhausted: boolean;
10
+ }
11
+ export declare class BudgetLedger {
12
+ private readonly limit?;
13
+ private spent;
14
+ private readonly controller;
15
+ constructor(limit?: number | undefined);
16
+ get signal(): AbortSignal;
17
+ recordUsage(usage: TokenUsage, source: BudgetUsageSource): void;
18
+ snapshot(): BudgetSnapshot;
19
+ }
20
+ export declare function composeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
@@ -0,0 +1,51 @@
1
+ export class BudgetLedger {
2
+ limit;
3
+ spent = 0;
4
+ controller = new AbortController();
5
+ constructor(limit) {
6
+ this.limit = limit;
7
+ }
8
+ get signal() {
9
+ return this.controller.signal;
10
+ }
11
+ recordUsage(usage, source) {
12
+ const delta = usage.promptTokens + usage.completionTokens;
13
+ this.spent += delta;
14
+ if (this.limit !== undefined && this.spent >= this.limit && !this.controller.signal.aborted) {
15
+ this.controller.abort(budgetAbortError("Budget exhausted"));
16
+ }
17
+ }
18
+ snapshot() {
19
+ return {
20
+ spent: this.spent,
21
+ limit: this.limit,
22
+ exhausted: this.limit !== undefined && this.spent >= this.limit,
23
+ };
24
+ }
25
+ }
26
+ function budgetAbortError(message) {
27
+ const error = new Error(message);
28
+ error.name = "AbortError";
29
+ return error;
30
+ }
31
+ export function composeAbortSignals(signals) {
32
+ const active = signals.filter((signal) => !!signal);
33
+ if (active.length === 0)
34
+ return undefined;
35
+ if (active.length === 1)
36
+ return active[0];
37
+ const controller = new AbortController();
38
+ const abort = (signal) => {
39
+ if (controller.signal.aborted)
40
+ return;
41
+ controller.abort(signal.reason);
42
+ };
43
+ for (const signal of active) {
44
+ if (signal.aborted) {
45
+ abort(signal);
46
+ break;
47
+ }
48
+ signal.addEventListener("abort", () => abort(signal), { once: true });
49
+ }
50
+ return controller.signal;
51
+ }
@@ -93,7 +93,7 @@ const BUDGETS = {
93
93
  },
94
94
  };
95
95
  const SEARCH_TOOLS_DISABLED = new Set(["grep", "web_search", "web_fetch"]);
96
- const EXPLORATION_TOOLS_DISABLED = new Set(["read", "glob", "grep", "web_search", "web_fetch", "task", "tool_search"]);
96
+ const EXPLORATION_TOOLS_DISABLED = new Set(["read", "glob", "grep", "web_search", "web_fetch", "spawn_agent", "wait_agent", "send_input", "tool_search"]);
97
97
  export class ExecutionGovernor {
98
98
  taskType;
99
99
  budget;
@@ -0,0 +1,59 @@
1
+ import type { ToolRegistryEntry, TokenUsage } from "../types.js";
2
+ import { type SubtaskType } from "./subtask-policy.js";
3
+ export type AgentProfileSource = "user" | "project" | "builtin";
4
+ export type AgentProfileMode = "readonly" | "write_patch" | "write_worktree";
5
+ export type AgentProfileApproval = "fail" | "disabled";
6
+ export type AgentProfileToolPreset = "readonly" | "none" | "explicit";
7
+ export interface AgentProfileTools {
8
+ preset: AgentProfileToolPreset;
9
+ include?: string[];
10
+ exclude?: string[];
11
+ }
12
+ export interface AgentProfile {
13
+ name: string;
14
+ description: string;
15
+ source: AgentProfileSource;
16
+ filePath?: string;
17
+ mode: AgentProfileMode;
18
+ model?: string | "inherit";
19
+ tools: AgentProfileTools;
20
+ maxTurns?: number;
21
+ approval: AgentProfileApproval;
22
+ nicknameCandidates?: string[];
23
+ prompt: string;
24
+ subtaskType?: SubtaskType;
25
+ }
26
+ export interface SubagentRunResult {
27
+ subAgentId: string;
28
+ agentName: string;
29
+ nickname?: string;
30
+ status: "completed" | "failed" | "blocked" | "cancelled";
31
+ profileSource: AgentProfileSource;
32
+ task: string;
33
+ summary: string;
34
+ toolNotes: string[];
35
+ usage?: {
36
+ promptTokens: number;
37
+ completionTokens: number;
38
+ totalTokens: number;
39
+ };
40
+ error?: string;
41
+ }
42
+ export interface DiscoverAgentProfilesResult {
43
+ profiles: AgentProfile[];
44
+ projectAgentsDir: string | null;
45
+ diagnostics: string[];
46
+ }
47
+ export interface AgentProfileDiagnostic {
48
+ severity: "warning" | "error";
49
+ message: string;
50
+ toolName?: string;
51
+ }
52
+ export type AgentProfileScope = "user" | "project" | "both";
53
+ export declare function discoverAgentProfiles(cwd: string, scope?: AgentProfileScope): DiscoverAgentProfilesResult;
54
+ export declare function builtinAgentProfiles(): AgentProfile[];
55
+ export declare function findAgentProfile(profiles: AgentProfile[], name: string): AgentProfile | undefined;
56
+ export declare function assignAgentNickname(profile: AgentProfile, activeNicknames?: Iterable<string>): string;
57
+ export declare function selectToolsForAgentProfile(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): ToolRegistryEntry[];
58
+ export declare function validateAgentProfileTools(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): AgentProfileDiagnostic[];
59
+ export declare function mergeUsage(current: SubagentRunResult["usage"], usage: TokenUsage): SubagentRunResult["usage"];
@@ -0,0 +1,460 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getBubbleHome } from "../bubble-home.js";
4
+ import { randomInt } from "node:crypto";
5
+ import { getSubtaskPolicy } from "./subtask-policy.js";
6
+ const READONLY_PRESET = [
7
+ "read",
8
+ "glob",
9
+ "grep",
10
+ "lsp",
11
+ "web_search",
12
+ "web_fetch",
13
+ "memory_search",
14
+ "memory_read_summary",
15
+ "skill",
16
+ "todo_write",
17
+ ];
18
+ const SUBAGENT_DENY_TOOLS = new Set(["subagent", "task", "spawn_agent", "wait_agent", "send_input", "close_agent"]);
19
+ const DEFAULT_NICKNAME_CANDIDATES = [
20
+ "Ada",
21
+ "Alan",
22
+ "Grace",
23
+ "Katherine",
24
+ "Claude",
25
+ "Edsger",
26
+ "Barbara",
27
+ "Donald",
28
+ "Margaret",
29
+ "Ken",
30
+ "Radia",
31
+ "Leslie",
32
+ "Mary",
33
+ "Dennis",
34
+ "Frances",
35
+ "Niklaus",
36
+ "Jean",
37
+ "Linus",
38
+ "Anita",
39
+ "Yukihiro",
40
+ "Brenda",
41
+ "Guido",
42
+ "Sophie",
43
+ "Tim",
44
+ "Hedy",
45
+ "John",
46
+ "Evelyn",
47
+ "Bjarne",
48
+ "Karen",
49
+ "Vint",
50
+ "Adele",
51
+ "Fernando",
52
+ ];
53
+ export function discoverAgentProfiles(cwd, scope = "user") {
54
+ const diagnostics = [];
55
+ const profiles = [...builtinAgentProfiles()];
56
+ const userDir = join(getBubbleHome(), "agents");
57
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
58
+ if (scope !== "project") {
59
+ profiles.push(...loadProfilesFromDir(userDir, "user", diagnostics));
60
+ }
61
+ if (scope !== "user" && projectAgentsDir) {
62
+ profiles.push(...loadProfilesFromDir(projectAgentsDir, "project", diagnostics));
63
+ }
64
+ const map = new Map();
65
+ for (const profile of profiles) {
66
+ map.set(profile.name, profile);
67
+ }
68
+ return {
69
+ profiles: [...map.values()],
70
+ projectAgentsDir,
71
+ diagnostics,
72
+ };
73
+ }
74
+ export function builtinAgentProfiles() {
75
+ const toProfile = (type) => {
76
+ const policy = getSubtaskPolicy(type);
77
+ return {
78
+ name: `builtin:${type}`,
79
+ description: `${type} read-only subagent`,
80
+ source: "builtin",
81
+ mode: "readonly",
82
+ model: "inherit",
83
+ tools: {
84
+ preset: "explicit",
85
+ include: [...policy.allowedTools],
86
+ exclude: [],
87
+ },
88
+ maxTurns: policy.maxTurns,
89
+ approval: "fail",
90
+ nicknameCandidates: DEFAULT_NICKNAME_CANDIDATES,
91
+ prompt: policy.reminder,
92
+ subtaskType: type,
93
+ };
94
+ };
95
+ const roleProfile = (name, description, prompt, include = READONLY_PRESET) => ({
96
+ name,
97
+ description,
98
+ source: "builtin",
99
+ mode: "readonly",
100
+ model: "inherit",
101
+ tools: {
102
+ preset: "explicit",
103
+ include,
104
+ exclude: [],
105
+ },
106
+ maxTurns: 8,
107
+ approval: "fail",
108
+ nicknameCandidates: DEFAULT_NICKNAME_CANDIDATES,
109
+ prompt,
110
+ });
111
+ return [
112
+ roleProfile("default", "General read-only subagent", [
113
+ "You are a focused child agent. Complete the assigned task independently using only read-only tools.",
114
+ "Return a concise result with concrete evidence, file paths, and any uncertainty.",
115
+ "Do not ask the user questions. If blocked by policy or missing data, state the blocker plainly.",
116
+ ].join("\n")),
117
+ roleProfile("explorer", "Fast codebase exploration subagent", [
118
+ "You are an explorer subagent for codebase reconnaissance.",
119
+ "Answer the specific question by inspecting the repository directly. Prefer precise file paths and line-level evidence.",
120
+ "Keep the answer compact and avoid broad refactors or implementation plans unless asked.",
121
+ ].join("\n"), ["read", "glob", "grep", "lsp", "memory_search", "memory_read_summary", "skill", "todo_write"]),
122
+ roleProfile("worker", "Bounded implementation worker subagent", [
123
+ "You are a worker subagent. In this Phase 1 runtime you are read-only, so you must not modify files.",
124
+ "Analyze the assigned implementation slice, identify exact files to change, and return a concrete patch plan or findings.",
125
+ "If write-capable worker mode is needed, say so explicitly.",
126
+ ].join("\n"), ["read", "glob", "grep", "lsp", "memory_search", "memory_read_summary", "skill", "todo_write"]),
127
+ toProfile("search"),
128
+ toProfile("security_investigation"),
129
+ toProfile("evidence_correlation"),
130
+ toProfile("general_readonly"),
131
+ ];
132
+ }
133
+ export function findAgentProfile(profiles, name) {
134
+ return profiles.find((profile) => profile.name === name)
135
+ ?? profiles.find((profile) => profile.name === `builtin:${name}`);
136
+ }
137
+ export function assignAgentNickname(profile, activeNicknames = []) {
138
+ const active = new Set([...activeNicknames].map((item) => item.toLowerCase()));
139
+ const candidates = (profile.nicknameCandidates && profile.nicknameCandidates.length > 0
140
+ ? profile.nicknameCandidates
141
+ : DEFAULT_NICKNAME_CANDIDATES)
142
+ .map((item) => item.trim())
143
+ .filter(Boolean);
144
+ const available = candidates.filter((item) => !active.has(item.toLowerCase()));
145
+ const pool = available.length > 0 ? available : candidates;
146
+ if (pool.length === 0) {
147
+ return `Agent-${randomInt(1000, 9999)}`;
148
+ }
149
+ return pool[randomInt(pool.length)];
150
+ }
151
+ export function selectToolsForAgentProfile(tools, profile, approval = profile.approval) {
152
+ const explicitInclude = new Set(profile.tools.include ?? []);
153
+ const selected = requestedToolNames(profile);
154
+ for (const tool of SUBAGENT_DENY_TOOLS)
155
+ selected.delete(tool);
156
+ const out = [];
157
+ for (const tool of tools) {
158
+ if (!selected.has(tool.name))
159
+ continue;
160
+ if (SUBAGENT_DENY_TOOLS.has(tool.name))
161
+ continue;
162
+ if ((tool.effect ?? "unknown") !== "read")
163
+ continue;
164
+ if (tool.deferred && !explicitInclude.has(tool.name))
165
+ continue;
166
+ if (approval === "disabled" && tool.requiresApproval)
167
+ continue;
168
+ out.push(wrapApprovalFailTool(tool, approval));
169
+ }
170
+ return out;
171
+ }
172
+ export function validateAgentProfileTools(tools, profile, approval = profile.approval) {
173
+ const available = new Map(tools.map((tool) => [tool.name, tool]));
174
+ const explicitInclude = new Set(profile.tools.include ?? []);
175
+ const diagnostics = [];
176
+ for (const name of requestedToolNames(profile)) {
177
+ if (SUBAGENT_DENY_TOOLS.has(name)) {
178
+ diagnostics.push({
179
+ severity: "error",
180
+ toolName: name,
181
+ message: `Tool "${name}" is not allowed inside subagents because recursive delegation is disabled in Phase 1.`,
182
+ });
183
+ continue;
184
+ }
185
+ const tool = available.get(name);
186
+ if (!tool) {
187
+ if (explicitInclude.has(name)) {
188
+ diagnostics.push({
189
+ severity: "warning",
190
+ toolName: name,
191
+ message: `Tool "${name}" is listed by profile "${profile.name}" but is not available in this session.`,
192
+ });
193
+ }
194
+ continue;
195
+ }
196
+ const effect = tool.effect ?? "unknown";
197
+ if (effect !== "read") {
198
+ diagnostics.push({
199
+ severity: "error",
200
+ toolName: name,
201
+ message: `Tool "${name}" has effect "${effect}" and cannot run in Phase 1 read-only subagents.`,
202
+ });
203
+ }
204
+ else if (approval === "disabled" && tool.requiresApproval) {
205
+ diagnostics.push({
206
+ severity: "warning",
207
+ toolName: name,
208
+ message: `Tool "${name}" requires approval and will be removed because approval is disabled for this subagent.`,
209
+ });
210
+ }
211
+ }
212
+ return diagnostics;
213
+ }
214
+ export function mergeUsage(current, usage) {
215
+ const promptTokens = (current?.promptTokens ?? 0) + usage.promptTokens;
216
+ const completionTokens = (current?.completionTokens ?? 0) + usage.completionTokens;
217
+ return {
218
+ promptTokens,
219
+ completionTokens,
220
+ totalTokens: promptTokens + completionTokens,
221
+ };
222
+ }
223
+ function wrapApprovalFailTool(tool, approval) {
224
+ if (approval !== "fail" || !tool.requiresApproval)
225
+ return tool;
226
+ return {
227
+ ...tool,
228
+ execute: async () => ({
229
+ content: `Blocked: tool "${tool.name}" requires interactive approval, which is disabled for subagents.`,
230
+ isError: true,
231
+ status: "blocked",
232
+ metadata: {
233
+ kind: "security",
234
+ reason: "Subagents cannot request interactive approval.",
235
+ },
236
+ }),
237
+ };
238
+ }
239
+ function requestedToolNames(profile) {
240
+ const selected = new Set();
241
+ if (profile.tools.preset === "readonly") {
242
+ for (const tool of READONLY_PRESET)
243
+ selected.add(tool);
244
+ }
245
+ else if (profile.tools.preset === "explicit") {
246
+ for (const tool of profile.tools.include ?? [])
247
+ selected.add(tool);
248
+ }
249
+ for (const tool of profile.tools.include ?? [])
250
+ selected.add(tool);
251
+ for (const tool of profile.tools.exclude ?? [])
252
+ selected.delete(tool);
253
+ return selected;
254
+ }
255
+ function loadProfilesFromDir(dir, source, diagnostics) {
256
+ if (!existsSync(dir))
257
+ return [];
258
+ let entries = [];
259
+ try {
260
+ entries = readdirSync(dir);
261
+ }
262
+ catch (error) {
263
+ diagnostics.push(`Failed to read agent profile directory ${dir}: ${error.message || String(error)}`);
264
+ return [];
265
+ }
266
+ const profiles = [];
267
+ for (const entry of entries) {
268
+ if (!entry.endsWith(".md"))
269
+ continue;
270
+ const filePath = join(dir, entry);
271
+ try {
272
+ if (!statSync(filePath).isFile())
273
+ continue;
274
+ const parsed = parseAgentProfileFile(readFileSync(filePath, "utf8"), source, filePath);
275
+ if (parsed)
276
+ profiles.push(parsed);
277
+ }
278
+ catch (error) {
279
+ diagnostics.push(`Failed to load agent profile ${filePath}: ${error.message || String(error)}`);
280
+ }
281
+ }
282
+ return profiles;
283
+ }
284
+ function parseAgentProfileFile(raw, source, filePath) {
285
+ const parsed = splitFrontmatter(raw);
286
+ if (!parsed)
287
+ return undefined;
288
+ const frontmatter = parseProfileFrontmatter(parsed.frontmatter);
289
+ const fallbackName = filePath.split("/").pop()?.replace(/\.md$/, "") ?? "agent";
290
+ const name = stringValue(frontmatter.name) || fallbackName;
291
+ const description = stringValue(frontmatter.description);
292
+ if (!name || !description)
293
+ return undefined;
294
+ return {
295
+ name,
296
+ description,
297
+ source,
298
+ filePath,
299
+ mode: modeValue(frontmatter.mode),
300
+ model: stringValue(frontmatter.model) || "inherit",
301
+ tools: toolsValue(frontmatter.tools),
302
+ maxTurns: numberValue(frontmatter.maxTurns),
303
+ approval: approvalValue(frontmatter.approval),
304
+ nicknameCandidates: stringArray(frontmatter.nicknameCandidates) ?? stringArray(frontmatter.nicknames),
305
+ prompt: parsed.body.trim(),
306
+ };
307
+ }
308
+ function splitFrontmatter(raw) {
309
+ const normalized = raw.replace(/\r\n/g, "\n");
310
+ if (!normalized.startsWith("---\n"))
311
+ return undefined;
312
+ const end = normalized.indexOf("\n---\n", 4);
313
+ if (end < 0)
314
+ return undefined;
315
+ return {
316
+ frontmatter: normalized.slice(4, end),
317
+ body: normalized.slice(end + 5),
318
+ };
319
+ }
320
+ function parseProfileFrontmatter(block) {
321
+ const out = {};
322
+ const lines = block.split("\n");
323
+ for (let i = 0; i < lines.length; i++) {
324
+ const line = lines[i];
325
+ if (!line.trim())
326
+ continue;
327
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
328
+ if (!match)
329
+ continue;
330
+ const [, key, value] = match;
331
+ if (value.trim()) {
332
+ out[key] = parseScalar(value.trim());
333
+ continue;
334
+ }
335
+ i++;
336
+ const listItems = [];
337
+ for (; i < lines.length; i++) {
338
+ const itemLine = lines[i];
339
+ if (!itemLine.startsWith(" - ")) {
340
+ i--;
341
+ break;
342
+ }
343
+ const item = itemLine.slice(" - ".length).trim();
344
+ if (item)
345
+ listItems.push(String(parseScalar(item)));
346
+ }
347
+ if (listItems.length > 0) {
348
+ out[key] = listItems;
349
+ continue;
350
+ }
351
+ const nested = {};
352
+ i++;
353
+ for (; i < lines.length; i++) {
354
+ const nestedLine = lines[i];
355
+ if (!nestedLine.startsWith(" ")) {
356
+ i--;
357
+ break;
358
+ }
359
+ const nestedMatch = nestedLine.trim().match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
360
+ if (nestedMatch) {
361
+ const nestedKey = nestedMatch[1];
362
+ const nestedValue = nestedMatch[2].trim();
363
+ if (nestedValue) {
364
+ nested[nestedKey] = parseScalar(nestedValue);
365
+ continue;
366
+ }
367
+ const items = [];
368
+ i++;
369
+ for (; i < lines.length; i++) {
370
+ const itemLine = lines[i];
371
+ if (!itemLine.startsWith(" - ")) {
372
+ i--;
373
+ break;
374
+ }
375
+ const item = itemLine.slice(" - ".length).trim();
376
+ if (item)
377
+ items.push(String(parseScalar(item)));
378
+ }
379
+ nested[nestedKey] = items.length > 0 ? items : "";
380
+ }
381
+ }
382
+ out[key] = nested;
383
+ }
384
+ return out;
385
+ }
386
+ function parseScalar(value) {
387
+ if (!value)
388
+ return "";
389
+ const unquoted = value.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
390
+ if (unquoted.startsWith("[") && unquoted.endsWith("]")) {
391
+ return unquoted.slice(1, -1).split(",").map((item) => String(parseScalar(item.trim()))).filter(Boolean);
392
+ }
393
+ if (unquoted === "true")
394
+ return true;
395
+ if (unquoted === "false")
396
+ return false;
397
+ if (/^-?\d+$/.test(unquoted))
398
+ return Number.parseInt(unquoted, 10);
399
+ return unquoted;
400
+ }
401
+ function stringValue(value) {
402
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
403
+ }
404
+ function numberValue(value) {
405
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
406
+ }
407
+ function modeValue(value) {
408
+ return value === "write_patch" || value === "write_worktree" ? value : "readonly";
409
+ }
410
+ function approvalValue(value) {
411
+ return value === "disabled" ? "disabled" : "fail";
412
+ }
413
+ function toolsValue(value) {
414
+ if (typeof value === "string") {
415
+ if (value === "none" || value === "explicit" || value === "readonly")
416
+ return { preset: value };
417
+ return { preset: "explicit", include: [value] };
418
+ }
419
+ if (Array.isArray(value)) {
420
+ return { preset: "explicit", include: stringArray(value) ?? [] };
421
+ }
422
+ if (value && typeof value === "object" && !Array.isArray(value)) {
423
+ const raw = value;
424
+ const preset = raw.preset === "none" || raw.preset === "explicit" || raw.preset === "readonly"
425
+ ? raw.preset
426
+ : "readonly";
427
+ return {
428
+ preset,
429
+ include: stringArray(raw.include),
430
+ exclude: stringArray(raw.exclude),
431
+ };
432
+ }
433
+ return { preset: "readonly", include: [], exclude: [] };
434
+ }
435
+ function stringArray(value) {
436
+ if (typeof value === "string" && value.trim())
437
+ return [value.trim()];
438
+ if (!Array.isArray(value))
439
+ return undefined;
440
+ return value.filter((item) => typeof item === "string" && !!item.trim()).map((item) => item.trim());
441
+ }
442
+ function findNearestProjectAgentsDir(cwd) {
443
+ let current = cwd;
444
+ while (true) {
445
+ const candidate = join(current, ".bubble", "agents");
446
+ if (existsSync(candidate)) {
447
+ try {
448
+ if (statSync(candidate).isDirectory())
449
+ return candidate;
450
+ }
451
+ catch {
452
+ // ignore
453
+ }
454
+ }
455
+ const parent = dirname(current);
456
+ if (parent === current)
457
+ return null;
458
+ current = parent;
459
+ }
460
+ }
@@ -0,0 +1,52 @@
1
+ import type { AgentProfile, AgentProfileSource, SubagentRunResult } from "./profiles.js";
2
+ import type { AgentEvent, ContentPart, Message, ToolUpdate } from "../types.js";
3
+ export type SubagentThreadStatus = "queued" | "running" | "completed" | "failed" | "blocked" | "cancelled" | "closed";
4
+ export interface SubagentThreadSnapshot {
5
+ agentId: string;
6
+ runId: string;
7
+ nickname: string;
8
+ agentName: string;
9
+ profileSource: AgentProfileSource;
10
+ status: SubagentThreadStatus;
11
+ task: string;
12
+ summary: string;
13
+ toolNotes: string[];
14
+ usage?: SubagentRunResult["usage"];
15
+ error?: string;
16
+ createdAt: number;
17
+ updatedAt: number;
18
+ }
19
+ export interface SubagentThreadRecord {
20
+ agentId: string;
21
+ runId: string;
22
+ nickname: string;
23
+ profile: AgentProfile;
24
+ parentToolCallId: string;
25
+ parentToolName: string;
26
+ status: SubagentThreadStatus;
27
+ task: string;
28
+ summary: string;
29
+ toolNotes: string[];
30
+ usage?: SubagentRunResult["usage"];
31
+ error?: string;
32
+ createdAt: number;
33
+ updatedAt: number;
34
+ abortController: AbortController;
35
+ waiters: Set<() => void>;
36
+ agent?: {
37
+ messages: Message[];
38
+ injectSystemReminder(content: string): void;
39
+ run(input: string | ContentPart[], cwd: string, options?: {
40
+ abortSignal?: AbortSignal;
41
+ }): AsyncIterable<AgentEvent>;
42
+ };
43
+ messages?: Message[];
44
+ promise?: Promise<void>;
45
+ }
46
+ export interface PendingSubagentToolUpdate {
47
+ id: string;
48
+ name: string;
49
+ update: ToolUpdate;
50
+ }
51
+ export declare function snapshotSubagentThread(record: SubagentThreadRecord): SubagentThreadSnapshot;
52
+ export declare function subagentResultFromThread(record: SubagentThreadRecord): SubagentRunResult;
@@ -0,0 +1,38 @@
1
+ export function snapshotSubagentThread(record) {
2
+ return {
3
+ agentId: record.agentId,
4
+ runId: record.runId,
5
+ nickname: record.nickname,
6
+ agentName: record.profile.name,
7
+ profileSource: record.profile.source,
8
+ status: record.status,
9
+ task: record.task,
10
+ summary: record.summary,
11
+ toolNotes: [...record.toolNotes],
12
+ usage: record.usage,
13
+ error: record.error,
14
+ createdAt: record.createdAt,
15
+ updatedAt: record.updatedAt,
16
+ };
17
+ }
18
+ export function subagentResultFromThread(record) {
19
+ const status = record.status === "completed"
20
+ ? "completed"
21
+ : record.status === "blocked"
22
+ ? "blocked"
23
+ : record.status === "cancelled" || record.status === "closed"
24
+ ? "cancelled"
25
+ : "failed";
26
+ return {
27
+ subAgentId: record.agentId,
28
+ agentName: record.profile.name,
29
+ nickname: record.nickname,
30
+ status,
31
+ profileSource: record.profile.source,
32
+ task: record.task,
33
+ summary: record.summary,
34
+ toolNotes: [...record.toolNotes],
35
+ usage: record.usage,
36
+ error: record.error,
37
+ };
38
+ }