@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.
- package/dist/agent/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +51 -0
- package/dist/agent/execution-governor.js +1 -1
- package/dist/agent/profiles.d.ts +59 -0
- package/dist/agent/profiles.js +460 -0
- package/dist/agent/subagent-control.d.ts +52 -0
- package/dist/agent/subagent-control.js +38 -0
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +602 -53
- package/dist/context/budget.js +1 -0
- package/dist/context/compact-llm.js +7 -6
- package/dist/context/compact.js +6 -6
- package/dist/context/projector.d.ts +3 -3
- package/dist/context/projector.js +32 -18
- package/dist/context/prune.d.ts +2 -2
- package/dist/context/prune.js +1 -4
- package/dist/main.js +12 -5
- package/dist/mcp/manager.js +1 -0
- package/dist/orchestrator/default-hooks.js +48 -9
- package/dist/orchestrator/hooks.d.ts +5 -0
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +8 -1
- package/dist/prompt/environment.js +21 -2
- package/dist/prompt/reminders.d.ts +3 -1
- package/dist/prompt/reminders.js +23 -4
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +6 -7
- package/dist/provider.js +77 -15
- package/dist/session-log.js +3 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +355 -0
- package/dist/tools/bash.js +2 -0
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +197 -0
- package/dist/tools/edit.js +63 -56
- package/dist/tools/exit-plan-mode.js +3 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +32 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/lsp.js +2 -0
- package/dist/tools/memory.js +2 -0
- package/dist/tools/question.js +2 -0
- package/dist/tools/read.js +1 -0
- package/dist/tools/skill.js +1 -0
- package/dist/tools/task.js +1 -0
- package/dist/tools/todo.js +1 -0
- package/dist/tools/tool-search.js +2 -1
- package/dist/tools/web-fetch.js +1 -0
- package/dist/tools/web-search.js +1 -0
- package/dist/tools/write.js +2 -0
- package/dist/tui/display-history.d.ts +8 -1
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.js +712 -267
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +114 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +22 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +82 -0
- package/dist/types.d.ts +90 -10
- package/package.json +1 -1
package/dist/provider.d.ts
CHANGED
|
@@ -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 {
|
|
6
|
+
import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
|
|
7
7
|
type ReasoningContentEcho = "tool_calls" | "all";
|
|
8
|
-
export declare function toChatCompletionsMessage(message:
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
244
|
-
|
|
265
|
+
const cleaned = textFilter.push(remaining);
|
|
266
|
+
if (cleaned) {
|
|
267
|
+
yield { type: "text", content: cleaned };
|
|
245
268
|
}
|
|
246
269
|
}
|
|
247
270
|
else {
|
|
248
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
+
}
|
package/dist/session-log.js
CHANGED
|
@@ -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;
|
package/dist/system-prompt.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -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;
|