@bubblebrain-ai/bubble 0.0.4 → 0.0.5

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 (77) 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.d.ts +60 -1
  9. package/dist/agent.js +602 -53
  10. package/dist/context/budget.js +1 -0
  11. package/dist/context/compact-llm.js +7 -6
  12. package/dist/context/compact.js +6 -6
  13. package/dist/context/projector.d.ts +3 -3
  14. package/dist/context/projector.js +32 -18
  15. package/dist/context/prune.d.ts +2 -2
  16. package/dist/context/prune.js +1 -4
  17. package/dist/main.js +12 -5
  18. package/dist/mcp/manager.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +48 -9
  20. package/dist/orchestrator/hooks.d.ts +5 -0
  21. package/dist/prompt/compose.d.ts +1 -0
  22. package/dist/prompt/compose.js +8 -1
  23. package/dist/prompt/environment.js +21 -2
  24. package/dist/prompt/reminders.d.ts +3 -1
  25. package/dist/prompt/reminders.js +23 -4
  26. package/dist/prompt/runtime.d.ts +1 -1
  27. package/dist/prompt/runtime.js +1 -1
  28. package/dist/provider-artifacts.d.ts +7 -0
  29. package/dist/provider-artifacts.js +60 -0
  30. package/dist/provider.d.ts +6 -7
  31. package/dist/provider.js +77 -15
  32. package/dist/session-log.js +3 -1
  33. package/dist/system-prompt.d.ts +2 -0
  34. package/dist/tools/agent-lifecycle.d.ts +6 -0
  35. package/dist/tools/agent-lifecycle.js +355 -0
  36. package/dist/tools/bash.js +2 -0
  37. package/dist/tools/edit-apply.d.ts +25 -0
  38. package/dist/tools/edit-apply.js +197 -0
  39. package/dist/tools/edit.js +63 -56
  40. package/dist/tools/exit-plan-mode.js +3 -1
  41. package/dist/tools/file-mutation-queue.d.ts +1 -0
  42. package/dist/tools/file-mutation-queue.js +32 -0
  43. package/dist/tools/glob.js +1 -0
  44. package/dist/tools/grep.js +1 -0
  45. package/dist/tools/index.d.ts +1 -1
  46. package/dist/tools/index.js +3 -3
  47. package/dist/tools/lsp.js +2 -0
  48. package/dist/tools/memory.js +2 -0
  49. package/dist/tools/question.js +2 -0
  50. package/dist/tools/read.js +1 -0
  51. package/dist/tools/skill.js +1 -0
  52. package/dist/tools/task.js +1 -0
  53. package/dist/tools/todo.js +1 -0
  54. package/dist/tools/tool-search.js +2 -1
  55. package/dist/tools/web-fetch.js +1 -0
  56. package/dist/tools/web-search.js +1 -0
  57. package/dist/tools/write.js +2 -0
  58. package/dist/tui/display-history.d.ts +8 -1
  59. package/dist/tui/markdown-inline.d.ts +22 -0
  60. package/dist/tui/markdown-inline.js +68 -0
  61. package/dist/tui/render-signature.d.ts +1 -0
  62. package/dist/tui/render-signature.js +7 -0
  63. package/dist/tui/run.js +712 -267
  64. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  65. package/dist/tui/tool-renderers/fallback.js +75 -0
  66. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  67. package/dist/tui/tool-renderers/registry.js +11 -0
  68. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  69. package/dist/tui/tool-renderers/subagent.js +114 -0
  70. package/dist/tui/tool-renderers/types.d.ts +36 -0
  71. package/dist/tui/tool-renderers/types.js +1 -0
  72. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  73. package/dist/tui/tool-renderers/write-preview.js +22 -0
  74. package/dist/tui/tool-renderers/write.d.ts +6 -0
  75. package/dist/tui/tool-renderers/write.js +82 -0
  76. package/dist/types.d.ts +90 -10
  77. package/package.json +1 -1
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
5
5
  */
6
- import type { Message, Provider, StreamChunk, ThinkingLevel } from "./types.js";
6
+ import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
7
7
  type ReasoningContentEcho = "tool_calls" | "all";
8
- export declare function toChatCompletionsMessage(message: Message, options?: {
8
+ export declare function toChatCompletionsMessage(message: ProviderMessage, options?: {
9
9
  reasoningContentEcho?: ReasoningContentEcho;
10
10
  }): Record<string, unknown>;
11
11
  export interface ProviderInstanceOptions {
@@ -21,11 +21,10 @@ export declare function normalizeToolArgs(raw: string): string;
21
21
  /**
22
22
  * Convert an OpenAI-compatible chat-completions stream into our internal StreamChunk events.
23
23
  *
24
- * Multi-tool-call streams are buffered by `index` and emitted in index order at
25
- * `finish_reason === "tool_calls"`, so the agent layer always sees a clean
26
- * (isStart -> args -> isEnd) sequence per call. This matters for providers like
27
- * Kimi K2.5 that emit several parallel tool calls per assistant turn -- the
28
- * previous single-slot implementation silently dropped every call but the last.
24
+ * Multi-tool-call streams are tracked by `index`, but tool-call starts and
25
+ * argument deltas are emitted as soon as they arrive so the TUI can render
26
+ * partial write previews before the tool executes. End events are still flushed
27
+ * in index order to keep multi-call turns deterministic.
29
28
  */
30
29
  export declare function translateOpenAIStream(stream: AsyncIterable<any>): AsyncIterable<StreamChunk>;
31
30
  export {};
package/dist/provider.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import OpenAI from "openai";
7
7
  import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
8
+ import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
8
9
  import { resolveProviderRequestConfig } from "./provider-transform.js";
9
10
  export function toChatCompletionsMessage(message, options = {}) {
10
11
  const reasoningContentEcho = options.reasoningContentEcho ?? "tool_calls";
@@ -189,14 +190,14 @@ function extractBalancedJson(s, start) {
189
190
  /**
190
191
  * Convert an OpenAI-compatible chat-completions stream into our internal StreamChunk events.
191
192
  *
192
- * Multi-tool-call streams are buffered by `index` and emitted in index order at
193
- * `finish_reason === "tool_calls"`, so the agent layer always sees a clean
194
- * (isStart -> args -> isEnd) sequence per call. This matters for providers like
195
- * Kimi K2.5 that emit several parallel tool calls per assistant turn -- the
196
- * previous single-slot implementation silently dropped every call but the last.
193
+ * Multi-tool-call streams are tracked by `index`, but tool-call starts and
194
+ * argument deltas are emitted as soon as they arrive so the TUI can render
195
+ * partial write previews before the tool executes. End events are still flushed
196
+ * in index order to keep multi-call turns deterministic.
197
197
  */
198
198
  export async function* translateOpenAIStream(stream) {
199
199
  const toolCalls = new Map();
200
+ const textFilter = createProviderProtocolArtifactFilter();
200
201
  function* flushToolCalls() {
201
202
  if (toolCalls.size === 0)
202
203
  return;
@@ -204,13 +205,34 @@ export async function* translateOpenAIStream(stream) {
204
205
  for (const [, entry] of sorted) {
205
206
  if (!entry.id || !entry.name)
206
207
  continue;
207
- const args = normalizeToolArgs(entry.args);
208
- yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
209
- yield { type: "tool_call", id: entry.id, name: entry.name, arguments: args, isStart: false, isEnd: false };
210
- yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: false, isEnd: true };
208
+ if (!entry.started) {
209
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
210
+ entry.started = true;
211
+ if (entry.args) {
212
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: entry.args, isStart: false, isEnd: false };
213
+ }
214
+ }
215
+ yield {
216
+ type: "tool_call",
217
+ id: entry.id,
218
+ name: entry.name,
219
+ arguments: "",
220
+ argumentsFull: normalizeToolArgs(entry.args),
221
+ isStart: false,
222
+ isEnd: true,
223
+ };
211
224
  }
212
225
  toolCalls.clear();
213
226
  }
227
+ function* startToolCallIfReady(entry) {
228
+ if (entry.started || !entry.id || !entry.name)
229
+ return;
230
+ entry.started = true;
231
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
232
+ if (entry.args) {
233
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: entry.args, isStart: false, isEnd: false };
234
+ }
235
+ }
214
236
  for await (const chunk of stream) {
215
237
  const delta = chunk.choices?.[0]?.delta;
216
238
  const usage = chunk.usage;
@@ -240,12 +262,16 @@ export async function* translateOpenAIStream(stream) {
240
262
  yield { type: "reasoning_delta", content: thinkMatch[1] };
241
263
  }
242
264
  const remaining = delta.content.replace(/<think>[\s\S]*?<\/think>/, "");
243
- if (remaining) {
244
- yield { type: "text", content: remaining };
265
+ const cleaned = textFilter.push(remaining);
266
+ if (cleaned) {
267
+ yield { type: "text", content: cleaned };
245
268
  }
246
269
  }
247
270
  else {
248
- yield { type: "text", content: delta.content };
271
+ const cleaned = textFilter.push(delta.content);
272
+ if (cleaned) {
273
+ yield { type: "text", content: cleaned };
274
+ }
249
275
  }
250
276
  }
251
277
  if (delta?.tool_calls) {
@@ -253,15 +279,28 @@ export async function* translateOpenAIStream(stream) {
253
279
  const idx = typeof tc.index === "number" ? tc.index : 0;
254
280
  let entry = toolCalls.get(idx);
255
281
  if (!entry) {
256
- entry = { id: "", name: "", args: "" };
282
+ entry = { id: "", name: "", args: "", started: false };
257
283
  toolCalls.set(idx, entry);
258
284
  }
259
285
  if (tc.id)
260
286
  entry.id = tc.id;
261
287
  if (tc.function?.name)
262
288
  entry.name = tc.function.name;
263
- if (typeof tc.function?.arguments === "string")
264
- entry.args += tc.function.arguments;
289
+ yield* startToolCallIfReady(entry);
290
+ if (typeof tc.function?.arguments === "string" && tc.function.arguments) {
291
+ const merged = mergeToolArgumentDelta(entry.args, tc.function.arguments);
292
+ entry.args = merged.args;
293
+ if (entry.started && merged.delta) {
294
+ yield {
295
+ type: "tool_call",
296
+ id: entry.id,
297
+ name: entry.name,
298
+ arguments: merged.delta,
299
+ isStart: false,
300
+ isEnd: false,
301
+ };
302
+ }
303
+ }
265
304
  }
266
305
  }
267
306
  const finishReason = chunk.choices?.[0]?.finish_reason;
@@ -269,5 +308,28 @@ export async function* translateOpenAIStream(stream) {
269
308
  yield* flushToolCalls();
270
309
  }
271
310
  }
311
+ const remainingText = textFilter.flush();
312
+ if (remainingText) {
313
+ yield { type: "text", content: remainingText };
314
+ }
272
315
  yield* flushToolCalls();
273
316
  }
317
+ function mergeToolArgumentDelta(current, incoming) {
318
+ if (!current)
319
+ return { args: incoming, delta: incoming };
320
+ if (!incoming)
321
+ return { args: current, delta: "" };
322
+ // Standard OpenAI-compatible streams send incremental argument deltas. Some
323
+ // providers send cumulative snapshots instead. If the incoming chunk already
324
+ // contains what we have, emit only the new suffix so downstream state remains
325
+ // append-only.
326
+ if (incoming.startsWith(current)) {
327
+ return { args: incoming, delta: incoming.slice(current.length) };
328
+ }
329
+ // Repeated identical snapshots should not duplicate the TUI preview or final
330
+ // JSON arguments.
331
+ if (incoming === current || current.endsWith(incoming)) {
332
+ return { args: current, delta: "" };
333
+ }
334
+ return { args: current + incoming, delta: incoming };
335
+ }
@@ -193,6 +193,8 @@ function normalizeMessageToEntries(message, id, timestamp) {
193
193
  }
194
194
  case "tool":
195
195
  return [{ id, type: "tool_result", message, timestamp }];
196
+ case "meta":
197
+ return [];
196
198
  case "system":
197
199
  return [{
198
200
  id,
@@ -241,7 +243,7 @@ function pruneIncompleteTail(messages) {
241
243
  let sawNonUserInCurrentTurn = false;
242
244
  for (let i = 0; i < messages.length; i++) {
243
245
  const message = messages[i];
244
- if (message.role === "system")
246
+ if (message.role === "system" || message.role === "meta")
245
247
  continue;
246
248
  if (message.role === "user") {
247
249
  currentTurnStart = i;
@@ -30,5 +30,7 @@ export interface SystemPromptOptions {
30
30
  skills?: SkillSummary[];
31
31
  /** Prompt-visible memory guidance and summaries */
32
32
  memoryPrompt?: string;
33
+ /** Durable child-agent profile prompt used for subagents. */
34
+ agentProfilePrompt?: string;
33
35
  }
34
36
  export declare function buildSystemPrompt(options?: SystemPromptOptions): string;
@@ -0,0 +1,6 @@
1
+ import type { ToolRegistryEntry } from "../types.js";
2
+ export declare function createSpawnAgentTool(): ToolRegistryEntry;
3
+ export declare function createWaitAgentTool(): ToolRegistryEntry;
4
+ export declare function createSendInputTool(): ToolRegistryEntry;
5
+ export declare function createCloseAgentTool(): ToolRegistryEntry;
6
+ export declare function createAgentLifecycleTools(): ToolRegistryEntry[];
@@ -0,0 +1,355 @@
1
+ import { discoverAgentProfiles, findAgentProfile } from "../agent/profiles.js";
2
+ export function createSpawnAgentTool() {
3
+ return {
4
+ name: "spawn_agent",
5
+ readOnly: true,
6
+ effect: "read",
7
+ description: [
8
+ "Start a child subagent in the background and return its agent_id plus random nickname.",
9
+ "Use this for Codex-style delegation. The child has an independent thread; call wait_agent later to collect its result.",
10
+ "When the user asks to use a subagent, spawn first with a clear task instead of doing the delegated investigation yourself.",
11
+ "After spawning, do not duplicate the same delegated work locally; either wait for the child or do clearly non-overlapping work.",
12
+ "agent_type defaults to default. Built-in types include default, explorer, and worker.",
13
+ ].join(" "),
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ agent_type: { type: "string", description: "Subagent profile or role name. Defaults to default." },
18
+ agent: { type: "string", description: "Alias for agent_type." },
19
+ message: { type: "string", description: "Initial task for the subagent." },
20
+ task: { type: "string", description: "Alias for message." },
21
+ fork_context: { type: "boolean", description: "When true, copy recent parent conversation into the child thread." },
22
+ agentScope: {
23
+ type: "string",
24
+ enum: ["user", "project", "both"],
25
+ description: "Which profile locations to load. Defaults to user profiles plus built-ins.",
26
+ },
27
+ allowProjectAgents: {
28
+ type: "boolean",
29
+ description: "Required to run profiles loaded from project-local .bubble/agents.",
30
+ },
31
+ approval: {
32
+ type: "string",
33
+ enum: ["fail", "disabled"],
34
+ description: "How this child handles tools that need interactive approval.",
35
+ },
36
+ },
37
+ additionalProperties: false,
38
+ },
39
+ async execute(args, ctx) {
40
+ if (!ctx.agent?.spawnSubAgent) {
41
+ return toolRuntimeMissing("spawn_agent");
42
+ }
43
+ const message = stringArg(args.message) ?? stringArg(args.task);
44
+ if (!message) {
45
+ return { content: "Error: spawn_agent requires message or task.", isError: true };
46
+ }
47
+ const profileName = stringArg(args.agent_type) ?? stringArg(args.agent) ?? "default";
48
+ const resolved = resolveProfile(ctx.cwd, profileName, parseScope(args.agentScope), args.allowProjectAgents === true);
49
+ if ("error" in resolved)
50
+ return resolved.error;
51
+ if (resolved.profile.mode !== "readonly") {
52
+ return unsupportedProfile(resolved.profile);
53
+ }
54
+ try {
55
+ const snapshot = await ctx.agent.spawnSubAgent(message, ctx.cwd, {
56
+ profile: resolved.profile,
57
+ parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
58
+ approval: parseApproval(args.approval),
59
+ abortSignal: ctx.abortSignal,
60
+ forkContext: args.fork_context === true,
61
+ });
62
+ return formatLifecycleResult("spawn_agent", [snapshot], [
63
+ `Spawned ${snapshot.nickname} (${snapshot.agentName})`,
64
+ `agent_id: ${snapshot.agentId}`,
65
+ `status: ${snapshot.status}`,
66
+ `next: call wait_agent for ${snapshot.agentId} to collect the delegated result`,
67
+ ]);
68
+ }
69
+ catch (error) {
70
+ return toolError("spawn_agent", error);
71
+ }
72
+ },
73
+ };
74
+ }
75
+ export function createWaitAgentTool() {
76
+ return {
77
+ name: "wait_agent",
78
+ readOnly: true,
79
+ effect: "read",
80
+ description: [
81
+ "Wait for one or more spawned subagents to reach a final status and return snapshots.",
82
+ "If the wait times out while children are still running, call wait_agent again with a longer timeout instead of redoing the same delegated work locally.",
83
+ ].join(" "),
84
+ parameters: {
85
+ type: "object",
86
+ properties: {
87
+ agent_id: { type: "string", description: "A single agent id to wait for." },
88
+ agent_ids: {
89
+ type: "array",
90
+ description: "Agent ids to wait for. If omitted, waits for any active subagent.",
91
+ items: { type: "string" },
92
+ },
93
+ timeout_ms: { type: "number", description: "Maximum wait time in milliseconds. Defaults to 30000." },
94
+ },
95
+ additionalProperties: false,
96
+ },
97
+ async execute(args, ctx) {
98
+ if (!ctx.agent?.waitSubAgents) {
99
+ return toolRuntimeMissing("wait_agent");
100
+ }
101
+ const agentIds = normalizeAgentIds(args.agent_ids, args.agent_id);
102
+ try {
103
+ const snapshots = await ctx.agent.waitSubAgents({
104
+ agentIds,
105
+ timeoutMs: typeof args.timeout_ms === "number" ? args.timeout_ms : undefined,
106
+ });
107
+ if (snapshots.length === 0) {
108
+ return {
109
+ content: "No subagents reached a final status before the timeout.",
110
+ status: "timeout",
111
+ metadata: { kind: "subagent", mode: "lifecycle", subagents: [] },
112
+ };
113
+ }
114
+ if (snapshots.some((snapshot) => !isFinalSnapshotStatus(snapshot.status))) {
115
+ return formatLifecycleResult("wait_agent", snapshots, [
116
+ "wait_agent timed out before a delegated result was ready.",
117
+ "The subagent is still running; call wait_agent again with a longer timeout instead of duplicating the same work locally.",
118
+ "",
119
+ ...snapshots.flatMap((snapshot) => [...formatSnapshot(snapshot), ""]),
120
+ ]);
121
+ }
122
+ return formatLifecycleResult("wait_agent", snapshots);
123
+ }
124
+ catch (error) {
125
+ return toolError("wait_agent", error);
126
+ }
127
+ },
128
+ };
129
+ }
130
+ export function createSendInputTool() {
131
+ return {
132
+ name: "send_input",
133
+ readOnly: true,
134
+ effect: "read",
135
+ description: "Send a follow-up message to an existing subagent thread. If it is still running, pass interrupt:true to cancel and redirect it.",
136
+ parameters: {
137
+ type: "object",
138
+ properties: {
139
+ agent_id: { type: "string", description: "Target subagent id." },
140
+ message: { type: "string", description: "Follow-up message." },
141
+ task: { type: "string", description: "Alias for message." },
142
+ interrupt: { type: "boolean", description: "Cancel a running child before applying this input." },
143
+ },
144
+ required: ["agent_id"],
145
+ additionalProperties: false,
146
+ },
147
+ async execute(args, ctx) {
148
+ if (!ctx.agent?.sendSubAgentInput) {
149
+ return toolRuntimeMissing("send_input");
150
+ }
151
+ const agentId = stringArg(args.agent_id);
152
+ const message = stringArg(args.message) ?? stringArg(args.task);
153
+ if (!agentId || !message) {
154
+ return { content: "Error: send_input requires agent_id and message.", isError: true };
155
+ }
156
+ try {
157
+ const snapshot = await ctx.agent.sendSubAgentInput(agentId, message, ctx.cwd, {
158
+ interrupt: args.interrupt === true,
159
+ parentToolCallId: ctx.toolCall?.id,
160
+ abortSignal: ctx.abortSignal,
161
+ });
162
+ return formatLifecycleResult("send_input", [snapshot], [
163
+ `Sent input to ${snapshot.nickname} (${snapshot.agentName})`,
164
+ `agent_id: ${snapshot.agentId}`,
165
+ `status: ${snapshot.status}`,
166
+ ]);
167
+ }
168
+ catch (error) {
169
+ return toolError("send_input", error);
170
+ }
171
+ },
172
+ };
173
+ }
174
+ export function createCloseAgentTool() {
175
+ return {
176
+ name: "close_agent",
177
+ readOnly: true,
178
+ effect: "read",
179
+ description: "Close a spawned subagent only when the delegated task is cancelled, stale, or no longer needed. Running children are cancelled before closing; do not close a child just because you started doing the same delegated work locally.",
180
+ parameters: {
181
+ type: "object",
182
+ properties: {
183
+ agent_id: { type: "string", description: "Subagent id to close." },
184
+ },
185
+ required: ["agent_id"],
186
+ additionalProperties: false,
187
+ },
188
+ async execute(args, ctx) {
189
+ if (!ctx.agent?.closeSubAgent) {
190
+ return toolRuntimeMissing("close_agent");
191
+ }
192
+ const agentId = stringArg(args.agent_id);
193
+ if (!agentId) {
194
+ return { content: "Error: close_agent requires agent_id.", isError: true };
195
+ }
196
+ try {
197
+ const snapshot = await ctx.agent.closeSubAgent(agentId);
198
+ return formatLifecycleResult("close_agent", [snapshot], [
199
+ `Closed ${snapshot.nickname} (${snapshot.agentName})`,
200
+ `agent_id: ${snapshot.agentId}`,
201
+ `status: ${snapshot.status}`,
202
+ ]);
203
+ }
204
+ catch (error) {
205
+ return toolError("close_agent", error);
206
+ }
207
+ },
208
+ };
209
+ }
210
+ export function createAgentLifecycleTools() {
211
+ return [
212
+ createSpawnAgentTool(),
213
+ createWaitAgentTool(),
214
+ createSendInputTool(),
215
+ createCloseAgentTool(),
216
+ ];
217
+ }
218
+ function resolveProfile(cwd, name, scope, allowProjectAgents) {
219
+ const discovered = discoverAgentProfiles(cwd, scope);
220
+ const profile = findAgentProfile(discovered.profiles, name);
221
+ if (!profile) {
222
+ const available = discovered.profiles.map((item) => item.name).sort().join(", ") || "none";
223
+ return {
224
+ error: {
225
+ content: `Error: unknown subagent profile "${name}". Available profiles: ${available}`,
226
+ isError: true,
227
+ },
228
+ };
229
+ }
230
+ if (profile.source === "project" && !allowProjectAgents) {
231
+ return {
232
+ error: {
233
+ content: [
234
+ `Blocked: subagent profile "${profile.name}" was loaded from project-local .bubble/agents.`,
235
+ "Pass allowProjectAgents: true only when you trust this repository's agent profile prompts.",
236
+ ].join("\n"),
237
+ isError: true,
238
+ status: "blocked",
239
+ },
240
+ };
241
+ }
242
+ return { profile };
243
+ }
244
+ function formatLifecycleResult(toolName, snapshots, header) {
245
+ const lines = header ?? [`${toolName}: ${snapshots.length} subagent${snapshots.length === 1 ? "" : "s"}`];
246
+ if (!header)
247
+ lines.push("");
248
+ if (!header) {
249
+ for (const snapshot of snapshots) {
250
+ lines.push(...formatSnapshot(snapshot), "");
251
+ }
252
+ }
253
+ return {
254
+ content: lines.join("\n").trim(),
255
+ status: lifecycleStatus(toolName, snapshots),
256
+ isError: snapshots.length > 0 && snapshots.every((snapshot) => snapshot.status === "failed" || snapshot.status === "blocked"),
257
+ metadata: {
258
+ kind: "subagent",
259
+ mode: "lifecycle",
260
+ subagents: snapshots.map(snapshotToMetadata),
261
+ },
262
+ };
263
+ }
264
+ function lifecycleStatus(toolName, snapshots) {
265
+ if (toolName === "spawn_agent" || toolName === "send_input" || toolName === "close_agent") {
266
+ return "success";
267
+ }
268
+ if (snapshots.some((snapshot) => !isFinalSnapshotStatus(snapshot.status))) {
269
+ return "timeout";
270
+ }
271
+ return snapshots.every((snapshot) => snapshot.status === "completed" || snapshot.status === "closed") ? "success" : "partial";
272
+ }
273
+ function isFinalSnapshotStatus(status) {
274
+ return status === "completed"
275
+ || status === "failed"
276
+ || status === "blocked"
277
+ || status === "cancelled"
278
+ || status === "closed";
279
+ }
280
+ function formatSnapshot(snapshot) {
281
+ const label = `${snapshot.nickname} (${snapshot.agentName})`;
282
+ const lines = [
283
+ `## ${label}`,
284
+ `agent_id: ${snapshot.agentId}`,
285
+ `status: ${snapshot.status}`,
286
+ `task: ${snapshot.task}`,
287
+ ];
288
+ if (snapshot.summary) {
289
+ lines.push("", "Summary:", snapshot.summary);
290
+ }
291
+ else if (snapshot.status === "completed") {
292
+ lines.push("", "Summary: (no final text summary was produced)");
293
+ }
294
+ if (snapshot.toolNotes.length > 0) {
295
+ lines.push("", "Recent tool notes:", ...snapshot.toolNotes.slice(-8).map((note) => `- ${note}`));
296
+ }
297
+ if (snapshot.error) {
298
+ lines.push("", `Error: ${snapshot.error}`);
299
+ }
300
+ return lines;
301
+ }
302
+ function snapshotToMetadata(snapshot) {
303
+ return {
304
+ subAgentId: snapshot.agentId,
305
+ agentName: snapshot.agentName,
306
+ nickname: snapshot.nickname,
307
+ status: snapshot.status === "closed" ? "cancelled" : snapshot.status,
308
+ profileSource: snapshot.profileSource,
309
+ task: snapshot.task,
310
+ summary: snapshot.summary,
311
+ toolNotes: snapshot.toolNotes,
312
+ usage: snapshot.usage,
313
+ error: snapshot.error,
314
+ };
315
+ }
316
+ function unsupportedProfile(profile) {
317
+ return {
318
+ content: `Error: subagent profile "${profile.name}" uses mode "${profile.mode}", but this runtime only supports readonly lifecycle subagents.`,
319
+ isError: true,
320
+ status: "blocked",
321
+ };
322
+ }
323
+ function parseScope(value) {
324
+ return value === "project" || value === "both" ? value : "user";
325
+ }
326
+ function parseApproval(value) {
327
+ return value === "fail" || value === "disabled" ? value : undefined;
328
+ }
329
+ function normalizeAgentIds(value, single) {
330
+ const out = [];
331
+ if (typeof single === "string" && single.trim())
332
+ out.push(single.trim());
333
+ if (Array.isArray(value)) {
334
+ for (const item of value) {
335
+ if (typeof item === "string" && item.trim())
336
+ out.push(item.trim());
337
+ }
338
+ }
339
+ return out.length > 0 ? [...new Set(out)] : undefined;
340
+ }
341
+ function stringArg(value) {
342
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
343
+ }
344
+ function snapshotFallbackId() {
345
+ return `spawn_${Date.now().toString(36)}`;
346
+ }
347
+ function toolRuntimeMissing(name) {
348
+ return { content: `Error: ${name} requires an agent runtime`, isError: true };
349
+ }
350
+ function toolError(name, error) {
351
+ return {
352
+ content: `Error executing ${name}: ${error?.message || String(error)}`,
353
+ isError: true,
354
+ };
355
+ }
@@ -11,6 +11,8 @@ const MAX_OUTPUT = 50 * 1024;
11
11
  export function createBashTool(cwd, approval) {
12
12
  return {
13
13
  name: "bash",
14
+ effect: "unknown",
15
+ requiresApproval: true,
14
16
  description: "Execute a bash command in the working directory. Use timeout for long-running commands.",
15
17
  parameters: {
16
18
  type: "object",
@@ -0,0 +1,25 @@
1
+ export interface EditOperation {
2
+ oldText: string;
3
+ newText: string;
4
+ }
5
+ export type EditMatchMode = "exact" | "normalized-line";
6
+ export interface EditMatchInfo {
7
+ editIndex: number;
8
+ mode: EditMatchMode;
9
+ start: number;
10
+ end: number;
11
+ }
12
+ export interface AppliedEditResult {
13
+ content: string;
14
+ normalizedOriginal: string;
15
+ normalizedNext: string;
16
+ bom: string;
17
+ lineEnding: "\n" | "\r\n";
18
+ matches: EditMatchInfo[];
19
+ }
20
+ export declare class EditApplyError extends Error {
21
+ readonly status: "no_match" | "blocked";
22
+ constructor(message: string, status?: "no_match" | "blocked");
23
+ }
24
+ export declare function applyEditsToContent(rawContent: string, edits: EditOperation[]): AppliedEditResult;
25
+ export declare function formatEditMatchNotes(matches: EditMatchInfo[]): string;