@bubblebrain-ai/bubble 0.0.20 → 0.0.22
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/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +64 -5
- package/dist/agent.js +365 -288
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +2 -0
- package/dist/main.js +88 -13
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider.js +23 -3
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +109 -2
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/bash.js +4 -0
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +2 -1
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +3 -3
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- package/dist/tui/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -0
- package/dist/tui/run.d.ts +11 -1
- package/dist/tui/run.js +399 -71
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +42 -1
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +301 -247
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +50 -2
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- package/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
|
@@ -1,21 +1,122 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
1
3
|
import { discoverAgentProfiles, findAgentProfile } from "../agent/profiles.js";
|
|
2
4
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
3
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Session-scoped trust decisions for project profiles, keyed by file path +
|
|
7
|
+
* content hash so an edited file re-prompts (design §10.2). Shared across the
|
|
8
|
+
* lifecycle tools created by one factory call.
|
|
9
|
+
*/
|
|
10
|
+
class ProjectProfileTrust {
|
|
11
|
+
approval;
|
|
12
|
+
approved = new Set();
|
|
13
|
+
constructor(approval) {
|
|
14
|
+
this.approval = approval;
|
|
15
|
+
}
|
|
16
|
+
/** Returns undefined when trusted, else a blocked ToolResult. */
|
|
17
|
+
async ensureTrusted(profile) {
|
|
18
|
+
if (profile.source !== "project")
|
|
19
|
+
return undefined;
|
|
20
|
+
const filePath = profile.filePath ?? "<unknown>";
|
|
21
|
+
let content;
|
|
22
|
+
try {
|
|
23
|
+
content = profile.filePath ? readFileSync(profile.filePath, "utf8") : profile.prompt;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
content = profile.prompt;
|
|
27
|
+
}
|
|
28
|
+
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
29
|
+
const key = `${filePath}:${contentHash}`;
|
|
30
|
+
if (this.approved.has(key))
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!this.approval) {
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
`Blocked: subagent profile "${profile.name}" comes from project-local .bubble/agents and needs the user's approval,`,
|
|
36
|
+
"but no approval flow is available in this session. Use a built-in or user-level profile instead.",
|
|
37
|
+
].join("\n"),
|
|
38
|
+
isError: true,
|
|
39
|
+
status: "blocked",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const decision = await this.approval.request({
|
|
43
|
+
type: "agent_profile",
|
|
44
|
+
name: profile.name,
|
|
45
|
+
path: filePath,
|
|
46
|
+
contentHash,
|
|
47
|
+
promptPreview: profile.prompt.split("\n").slice(0, 6).join("\n").slice(0, 600),
|
|
48
|
+
});
|
|
49
|
+
if (decision.action === "approve") {
|
|
50
|
+
this.approved.add(key);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const feedback = decision.feedback?.trim();
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
`Blocked: the user declined to trust project agent profile "${profile.name}".`,
|
|
57
|
+
feedback ? `User feedback: ${feedback}` : "Use a built-in or user-level profile instead.",
|
|
58
|
+
].join("\n"),
|
|
59
|
+
isError: true,
|
|
60
|
+
status: "blocked",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const PROFILE_DESCRIPTION_TTL_MS = 5_000;
|
|
65
|
+
/**
|
|
66
|
+
* Custom profile descriptions must reach the model or the whole custom-profile
|
|
67
|
+
* system is unreachable (design §10.1): the agent_type description is built
|
|
68
|
+
* from live profile discovery, refreshed with a short TTL so file edits are
|
|
69
|
+
* picked up on the next turn.
|
|
70
|
+
*/
|
|
71
|
+
function createProfileLister(cwd) {
|
|
72
|
+
let cachedAt = 0;
|
|
73
|
+
let cached = "";
|
|
74
|
+
return () => {
|
|
75
|
+
if (!cwd)
|
|
76
|
+
return "";
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (now - cachedAt < PROFILE_DESCRIPTION_TTL_MS && cached)
|
|
79
|
+
return cached;
|
|
80
|
+
cachedAt = now;
|
|
81
|
+
try {
|
|
82
|
+
const { profiles } = discoverAgentProfiles(cwd, "both");
|
|
83
|
+
const lines = profiles
|
|
84
|
+
.filter((profile) => !profile.name.startsWith("builtin:"))
|
|
85
|
+
.map((profile) => {
|
|
86
|
+
const tag = profile.source === "project" ? " [project: requires user approval on first use]" : "";
|
|
87
|
+
return `- ${profile.name}${tag} — ${truncateText(profile.description, 120)}`;
|
|
88
|
+
});
|
|
89
|
+
cached = lines.length > 0 ? ` Available profiles:\n${lines.join("\n")}` : "";
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
cached = "";
|
|
93
|
+
}
|
|
94
|
+
return cached;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function createSpawnAgentTool(options = {}, sharedTrust) {
|
|
98
|
+
const trust = sharedTrust ?? new ProjectProfileTrust(options.approval);
|
|
99
|
+
const listProfiles = createProfileLister(options.cwd);
|
|
100
|
+
const baseDescription = [
|
|
101
|
+
"Start a child subagent in the background and return its agent_id plus random nickname.",
|
|
102
|
+
"The child has an independent thread; call wait_agent later to collect its result.",
|
|
103
|
+
"Proactively delegate multi-file investigations whose intermediate steps would be noise in the main conversation.",
|
|
104
|
+
"Do the work yourself when it takes only a couple of tool calls or needs conversation context — unless the user explicitly asks for a subagent, in which case spawn one.",
|
|
105
|
+
"The child starts with zero context: write the task as a self-contained work order — state the goal, include known file paths or commands, and never make it rediscover knowledge you already hold.",
|
|
106
|
+
"After spawning, do not duplicate the same delegated work locally; either wait for the child or do clearly non-overlapping work.",
|
|
107
|
+
"A child may start as queued when concurrency slots are busy; it starts automatically, no action needed.",
|
|
108
|
+
].join(" ");
|
|
4
109
|
return {
|
|
5
110
|
name: "spawn_agent",
|
|
6
111
|
readOnly: true,
|
|
7
112
|
effect: "read",
|
|
8
|
-
description
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"When the user asks to use a subagent, spawn first with a clear task instead of doing the delegated investigation yourself.",
|
|
12
|
-
"After spawning, do not duplicate the same delegated work locally; either wait for the child or do clearly non-overlapping work.",
|
|
13
|
-
"agent_type defaults to default. Built-in types include default, explorer, and worker.",
|
|
14
|
-
].join(" "),
|
|
113
|
+
get description() {
|
|
114
|
+
return baseDescription + listProfiles();
|
|
115
|
+
},
|
|
15
116
|
parameters: {
|
|
16
117
|
type: "object",
|
|
17
118
|
properties: {
|
|
18
|
-
agent_type: { type: "string", description: "Subagent profile or role name. Defaults to default." },
|
|
119
|
+
agent_type: { type: "string", description: "Subagent profile or role name. Defaults to default. Built-in types include default, explorer, and worker; see the tool description for custom profiles." },
|
|
19
120
|
agent: { type: "string", description: "Alias for agent_type." },
|
|
20
121
|
category: { type: "string", description: "Optional semantic category for model/thinking routing, such as quick, deep, explore, review, frontend, or writing." },
|
|
21
122
|
message: { type: "string", description: "Initial task for the subagent." },
|
|
@@ -24,11 +125,7 @@ export function createSpawnAgentTool() {
|
|
|
24
125
|
agentScope: {
|
|
25
126
|
type: "string",
|
|
26
127
|
enum: ["user", "project", "both"],
|
|
27
|
-
description: "Which profile locations to load. Defaults to user profiles
|
|
28
|
-
},
|
|
29
|
-
allowProjectAgents: {
|
|
30
|
-
type: "boolean",
|
|
31
|
-
description: "Required to run profiles loaded from project-local .bubble/agents.",
|
|
128
|
+
description: "Which profile locations to load. Defaults to built-ins plus user and project profiles; project profiles need the user's one-time approval.",
|
|
32
129
|
},
|
|
33
130
|
approval: {
|
|
34
131
|
type: "string",
|
|
@@ -47,12 +144,15 @@ export function createSpawnAgentTool() {
|
|
|
47
144
|
return { content: "Error: spawn_agent requires message or task.", isError: true };
|
|
48
145
|
}
|
|
49
146
|
const profileName = stringArg(args.agent_type) ?? stringArg(args.agent) ?? "default";
|
|
50
|
-
const resolved = resolveProfile(ctx.cwd, profileName, parseScope(args.agentScope)
|
|
147
|
+
const resolved = resolveProfile(ctx.cwd, profileName, parseScope(args.agentScope));
|
|
51
148
|
if ("error" in resolved)
|
|
52
149
|
return resolved.error;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
150
|
+
const modeBlock = unsupportedProfile(resolved.profile);
|
|
151
|
+
if (modeBlock)
|
|
152
|
+
return modeBlock;
|
|
153
|
+
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
154
|
+
if (trustBlock)
|
|
155
|
+
return trustBlock;
|
|
56
156
|
try {
|
|
57
157
|
const snapshot = await ctx.agent.spawnSubAgent(message, ctx.cwd, {
|
|
58
158
|
profile: resolved.profile,
|
|
@@ -67,7 +167,7 @@ export function createSpawnAgentTool() {
|
|
|
67
167
|
`agent_id: ${snapshot.agentId}`,
|
|
68
168
|
`status: ${snapshot.status}`,
|
|
69
169
|
...formatRouteLines(snapshot),
|
|
70
|
-
|
|
170
|
+
...spawnNextSteps(snapshot),
|
|
71
171
|
"counting: this spawn result creates one unique subagent; later wait_agent results for the same agent_id are updates, not additional subagents",
|
|
72
172
|
]);
|
|
73
173
|
}
|
|
@@ -84,7 +184,7 @@ export function createWaitAgentTool() {
|
|
|
84
184
|
effect: "read",
|
|
85
185
|
description: [
|
|
86
186
|
"Wait for one or more spawned subagents to reach a final status and return snapshots.",
|
|
87
|
-
"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.",
|
|
187
|
+
"If the wait times out while children are still queued or running, call wait_agent again with a longer timeout instead of redoing the same delegated work locally.",
|
|
88
188
|
].join(" "),
|
|
89
189
|
parameters: {
|
|
90
190
|
type: "object",
|
|
@@ -119,7 +219,7 @@ export function createWaitAgentTool() {
|
|
|
119
219
|
if (snapshots.some((snapshot) => !isFinalSnapshotStatus(snapshot.status))) {
|
|
120
220
|
return formatLifecycleResult("wait_agent", snapshots, [
|
|
121
221
|
"wait_agent timed out before a delegated result was ready.",
|
|
122
|
-
|
|
222
|
+
...waitTimeoutGuidance(snapshots),
|
|
123
223
|
"",
|
|
124
224
|
...snapshots.flatMap((snapshot) => [...formatSnapshot(snapshot), ""]),
|
|
125
225
|
]);
|
|
@@ -137,7 +237,7 @@ export function createSendInputTool() {
|
|
|
137
237
|
name: "send_input",
|
|
138
238
|
readOnly: true,
|
|
139
239
|
effect: "read",
|
|
140
|
-
description: "Send a follow-up message to an existing subagent thread. If it is still running, pass interrupt:true to cancel and redirect it.",
|
|
240
|
+
description: "Send a follow-up message to an existing subagent thread. If it is still running, pass interrupt:true to cancel and redirect it. Restarting a finished child goes through the scheduler like any spawn.",
|
|
141
241
|
parameters: {
|
|
142
242
|
type: "object",
|
|
143
243
|
properties: {
|
|
@@ -168,6 +268,7 @@ export function createSendInputTool() {
|
|
|
168
268
|
`Sent input to ${snapshot.nickname} (${snapshot.agentName})`,
|
|
169
269
|
`agent_id: ${snapshot.agentId}`,
|
|
170
270
|
`status: ${snapshot.status}`,
|
|
271
|
+
...(snapshot.status === "queued" ? [queuedStatusLine(snapshot)] : []),
|
|
171
272
|
]);
|
|
172
273
|
}
|
|
173
274
|
catch (error) {
|
|
@@ -212,15 +313,196 @@ export function createCloseAgentTool() {
|
|
|
212
313
|
},
|
|
213
314
|
};
|
|
214
315
|
}
|
|
215
|
-
export function
|
|
316
|
+
export function createListAgentsTool() {
|
|
317
|
+
return {
|
|
318
|
+
name: "list_agents",
|
|
319
|
+
readOnly: true,
|
|
320
|
+
effect: "read",
|
|
321
|
+
description: "List this session's subagents with their current status. Use it to recall which children exist before spawning duplicates or narrating progress.",
|
|
322
|
+
parameters: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
status_filter: {
|
|
326
|
+
type: "array",
|
|
327
|
+
description: "Only include these statuses (queued, running, completed, failed, blocked, cancelled, closed).",
|
|
328
|
+
items: { type: "string" },
|
|
329
|
+
},
|
|
330
|
+
include_closed: { type: "boolean", description: "Include closed subagents. Defaults to false." },
|
|
331
|
+
},
|
|
332
|
+
additionalProperties: false,
|
|
333
|
+
},
|
|
334
|
+
async execute(args, ctx) {
|
|
335
|
+
if (!ctx.agent?.listSubAgents) {
|
|
336
|
+
return toolRuntimeMissing("list_agents");
|
|
337
|
+
}
|
|
338
|
+
const filter = Array.isArray(args.status_filter)
|
|
339
|
+
? new Set(args.status_filter.filter((item) => typeof item === "string"))
|
|
340
|
+
: undefined;
|
|
341
|
+
const includeClosed = args.include_closed === true;
|
|
342
|
+
const snapshots = ctx.agent.listSubAgents()
|
|
343
|
+
.filter((snapshot) => includeClosed || snapshot.status !== "closed")
|
|
344
|
+
.filter((snapshot) => !filter || filter.size === 0 || filter.has(snapshot.status));
|
|
345
|
+
if (snapshots.length === 0) {
|
|
346
|
+
return {
|
|
347
|
+
content: "No subagents match. Spawn one with spawn_agent when delegation helps.",
|
|
348
|
+
metadata: { kind: "subagent", mode: "lifecycle", subagents: [] },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const lines = [
|
|
352
|
+
`${snapshots.length} subagent${snapshots.length === 1 ? "" : "s"}:`,
|
|
353
|
+
...snapshots.map((snapshot) => {
|
|
354
|
+
const usage = snapshot.usage ? ` tokens=${snapshot.usage.totalTokens}` : "";
|
|
355
|
+
const queued = snapshot.status === "queued" && snapshot.queuePosition !== undefined
|
|
356
|
+
? ` queue_position=${snapshot.queuePosition}`
|
|
357
|
+
: "";
|
|
358
|
+
return `- ${snapshot.nickname} (${formatSnapshotRole(snapshot)}) agent_id=${snapshot.agentId} status=${snapshot.status}${queued}${usage} task=${truncateText(snapshot.task, 80)}`;
|
|
359
|
+
}),
|
|
360
|
+
];
|
|
361
|
+
return {
|
|
362
|
+
content: lines.join("\n"),
|
|
363
|
+
metadata: {
|
|
364
|
+
kind: "subagent",
|
|
365
|
+
mode: "lifecycle",
|
|
366
|
+
subagents: snapshots.map(snapshotToMetadata),
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/** Items bound for one agent_team call (design §1.2). */
|
|
373
|
+
export const AGENT_TEAM_MIN_ITEMS = 2;
|
|
374
|
+
export const AGENT_TEAM_MAX_ITEMS = 32;
|
|
375
|
+
export function createAgentTeamTool(options = {}, sharedTrust) {
|
|
376
|
+
const trust = sharedTrust ?? new ProjectProfileTrust(options.approval);
|
|
377
|
+
return {
|
|
378
|
+
name: "agent_team",
|
|
379
|
+
readOnly: true,
|
|
380
|
+
effect: "read",
|
|
381
|
+
description: [
|
|
382
|
+
"Run the same task template over many items as parallel subagents (homogeneous map fan-out).",
|
|
383
|
+
"Proactively use this when a task naturally splits into the same read-only operation over several independent items.",
|
|
384
|
+
"Each item becomes one child with its own agent_id; the call blocks until every member reaches a final state and returns results in item order.",
|
|
385
|
+
"Failed members can be resumed individually with send_input afterwards.",
|
|
386
|
+
`Use ${AGENT_TEAM_MIN_ITEMS}-${AGENT_TEAM_MAX_ITEMS} items. agent_team must be the ONLY tool call in your response; run other tools or further teams after it returns.`,
|
|
387
|
+
"Scoping rule: split items so members never overlap or conflict with each other.",
|
|
388
|
+
].join(" "),
|
|
389
|
+
parameters: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: {
|
|
392
|
+
description: { type: "string", description: "Short (3-5 word) description of the team, shown in the UI." },
|
|
393
|
+
agent_type: { type: "string", description: "Subagent profile for every member. Defaults to default." },
|
|
394
|
+
category: { type: "string", description: "Optional semantic category for model/thinking routing." },
|
|
395
|
+
prompt_template: { type: "string", description: "Task template applied to each item. Must contain the literal placeholder {{item}}." },
|
|
396
|
+
items: {
|
|
397
|
+
type: "array",
|
|
398
|
+
description: `Items to fan out over (${AGENT_TEAM_MIN_ITEMS}-${AGENT_TEAM_MAX_ITEMS} unique strings); each becomes one subagent.`,
|
|
399
|
+
items: { type: "string" },
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
required: ["description", "prompt_template", "items"],
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
},
|
|
405
|
+
async execute(args, ctx) {
|
|
406
|
+
if (!ctx.agent?.runAgentTeam) {
|
|
407
|
+
return toolRuntimeMissing("agent_team");
|
|
408
|
+
}
|
|
409
|
+
const template = stringArg(args.prompt_template);
|
|
410
|
+
if (!template) {
|
|
411
|
+
return { content: "Error: agent_team requires prompt_template.", isError: true };
|
|
412
|
+
}
|
|
413
|
+
if (!template.includes("{{item}}")) {
|
|
414
|
+
return {
|
|
415
|
+
content: "Error: prompt_template must contain the literal placeholder {{item}} — it is replaced with each item. Example: \"Review {{item}} for risks.\"",
|
|
416
|
+
isError: true,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const rawItems = Array.isArray(args.items)
|
|
420
|
+
? args.items.filter((item) => typeof item === "string" && !!item.trim()).map((item) => item.trim())
|
|
421
|
+
: [];
|
|
422
|
+
const items = [...new Set(rawItems)];
|
|
423
|
+
if (items.length < AGENT_TEAM_MIN_ITEMS) {
|
|
424
|
+
return {
|
|
425
|
+
content: `Error: agent_team needs at least ${AGENT_TEAM_MIN_ITEMS} unique items after deduplication (got ${items.length}). For a single task use spawn_agent instead.`,
|
|
426
|
+
isError: true,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (items.length > AGENT_TEAM_MAX_ITEMS) {
|
|
430
|
+
return {
|
|
431
|
+
content: `Error: agent_team accepts at most ${AGENT_TEAM_MAX_ITEMS} items (got ${items.length}). Split the work into sequential teams.`,
|
|
432
|
+
isError: true,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const profileName = stringArg(args.agent_type) ?? "default";
|
|
436
|
+
const resolved = resolveProfile(ctx.cwd, profileName, "both");
|
|
437
|
+
if ("error" in resolved)
|
|
438
|
+
return resolved.error;
|
|
439
|
+
const modeBlock = unsupportedProfile(resolved.profile);
|
|
440
|
+
if (modeBlock)
|
|
441
|
+
return modeBlock;
|
|
442
|
+
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
443
|
+
if (trustBlock)
|
|
444
|
+
return trustBlock;
|
|
445
|
+
try {
|
|
446
|
+
const snapshots = await ctx.agent.runAgentTeam(ctx.cwd, {
|
|
447
|
+
profile: resolved.profile,
|
|
448
|
+
category: stringArg(args.category),
|
|
449
|
+
promptTemplate: template,
|
|
450
|
+
items,
|
|
451
|
+
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
452
|
+
emitUpdate: ctx.emitUpdate,
|
|
453
|
+
abortSignal: ctx.abortSignal,
|
|
454
|
+
});
|
|
455
|
+
const counts = teamStatusCounts(snapshots);
|
|
456
|
+
const lines = [
|
|
457
|
+
`agent_team "${stringArg(args.description) ?? "team"}": ${snapshots.length} members — ${counts}`,
|
|
458
|
+
"Failed or cancelled members can be resumed individually with send_input (see per-member guidance below).",
|
|
459
|
+
"",
|
|
460
|
+
...snapshots.flatMap((snapshot, index) => [
|
|
461
|
+
`### item ${index + 1}: ${truncateText(items[index] ?? "", 100)}`,
|
|
462
|
+
...formatSnapshot(snapshot),
|
|
463
|
+
"",
|
|
464
|
+
]),
|
|
465
|
+
];
|
|
466
|
+
return {
|
|
467
|
+
content: lines.join("\n").trim(),
|
|
468
|
+
status: snapshots.every((snapshot) => snapshot.status === "completed")
|
|
469
|
+
? "success"
|
|
470
|
+
: snapshots.some((snapshot) => snapshot.status === "completed")
|
|
471
|
+
? "partial"
|
|
472
|
+
: "blocked",
|
|
473
|
+
isError: snapshots.length > 0 && snapshots.every((snapshot) => snapshot.status !== "completed"),
|
|
474
|
+
metadata: {
|
|
475
|
+
kind: "subagent",
|
|
476
|
+
mode: "team",
|
|
477
|
+
subagents: snapshots.map(snapshotToMetadata),
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
return toolError("agent_team", error);
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function teamStatusCounts(snapshots) {
|
|
488
|
+
const counts = new Map();
|
|
489
|
+
for (const snapshot of snapshots) {
|
|
490
|
+
counts.set(snapshot.status, (counts.get(snapshot.status) ?? 0) + 1);
|
|
491
|
+
}
|
|
492
|
+
return [...counts.entries()].map(([status, count]) => `${status} ${count}`).join(" / ");
|
|
493
|
+
}
|
|
494
|
+
export function createAgentLifecycleTools(options = {}) {
|
|
495
|
+
const trust = new ProjectProfileTrust(options.approval);
|
|
216
496
|
return [
|
|
217
|
-
createSpawnAgentTool(),
|
|
497
|
+
createSpawnAgentTool(options, trust),
|
|
218
498
|
createWaitAgentTool(),
|
|
219
499
|
createSendInputTool(),
|
|
220
500
|
createCloseAgentTool(),
|
|
501
|
+
createListAgentsTool(),
|
|
502
|
+
createAgentTeamTool(options, trust),
|
|
221
503
|
];
|
|
222
504
|
}
|
|
223
|
-
function resolveProfile(cwd, name, scope
|
|
505
|
+
function resolveProfile(cwd, name, scope) {
|
|
224
506
|
const discovered = discoverAgentProfiles(cwd, scope);
|
|
225
507
|
const profile = findAgentProfile(discovered.profiles, name);
|
|
226
508
|
if (!profile) {
|
|
@@ -232,20 +514,38 @@ function resolveProfile(cwd, name, scope, allowProjectAgents) {
|
|
|
232
514
|
},
|
|
233
515
|
};
|
|
234
516
|
}
|
|
235
|
-
if (profile.source === "project" && !allowProjectAgents) {
|
|
236
|
-
return {
|
|
237
|
-
error: {
|
|
238
|
-
content: [
|
|
239
|
-
`Blocked: subagent profile "${profile.name}" was loaded from project-local .bubble/agents.`,
|
|
240
|
-
"Pass allowProjectAgents: true only when you trust this repository's agent profile prompts.",
|
|
241
|
-
].join("\n"),
|
|
242
|
-
isError: true,
|
|
243
|
-
status: "blocked",
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
517
|
return { profile };
|
|
248
518
|
}
|
|
519
|
+
function spawnNextSteps(snapshot) {
|
|
520
|
+
if (snapshot.status === "queued" && snapshot.queuePosition !== undefined && snapshot.queuePosition > 0) {
|
|
521
|
+
return [
|
|
522
|
+
queuedStatusLine(snapshot),
|
|
523
|
+
"next: continue other non-overlapping work, then call wait_agent to collect the result",
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
return [
|
|
527
|
+
`next: call wait_agent for ${snapshot.agentId} before reporting this subagent's current status or final result`,
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
function queuedStatusLine(snapshot) {
|
|
531
|
+
const behind = snapshot.queuePosition !== undefined && snapshot.queuePosition > 1
|
|
532
|
+
? ` behind ${snapshot.queuePosition - 1} child${snapshot.queuePosition - 1 === 1 ? "" : "ren"}`
|
|
533
|
+
: "";
|
|
534
|
+
return `queued: waiting for a concurrency slot${behind}; it starts automatically — no action needed`;
|
|
535
|
+
}
|
|
536
|
+
function waitTimeoutGuidance(snapshots) {
|
|
537
|
+
const lines = [];
|
|
538
|
+
const queued = snapshots.filter((snapshot) => snapshot.status === "queued");
|
|
539
|
+
const running = snapshots.filter((snapshot) => snapshot.status === "running");
|
|
540
|
+
if (queued.length > 0) {
|
|
541
|
+
lines.push(`${queued.length} child${queued.length === 1 ? " is" : "ren are"} queued for a concurrency slot and will start automatically.`);
|
|
542
|
+
}
|
|
543
|
+
if (running.length > 0) {
|
|
544
|
+
lines.push(`${running.length} child${running.length === 1 ? " is" : "ren are"} still running.`);
|
|
545
|
+
}
|
|
546
|
+
lines.push("Call wait_agent again with a longer timeout instead of duplicating the same work locally.");
|
|
547
|
+
return lines;
|
|
548
|
+
}
|
|
249
549
|
function formatLifecycleResult(toolName, snapshots, header) {
|
|
250
550
|
const lines = header ?? [`${toolName}: ${snapshots.length} subagent${snapshots.length === 1 ? "" : "s"}`];
|
|
251
551
|
if (!header)
|
|
@@ -267,7 +567,7 @@ function formatLifecycleResult(toolName, snapshots, header) {
|
|
|
267
567
|
};
|
|
268
568
|
}
|
|
269
569
|
function lifecycleStatus(toolName, snapshots) {
|
|
270
|
-
if (toolName === "spawn_agent" || toolName === "send_input" || toolName === "close_agent") {
|
|
570
|
+
if (toolName === "spawn_agent" || toolName === "send_input" || toolName === "close_agent" || toolName === "list_agents") {
|
|
271
571
|
return "success";
|
|
272
572
|
}
|
|
273
573
|
if (snapshots.some((snapshot) => !isFinalSnapshotStatus(snapshot.status))) {
|
|
@@ -294,20 +594,66 @@ function formatSnapshot(snapshot) {
|
|
|
294
594
|
}
|
|
295
595
|
lines.push(...formatRouteLines(snapshot));
|
|
296
596
|
lines.push(`task: ${snapshot.task}`);
|
|
597
|
+
if (snapshot.status === "queued") {
|
|
598
|
+
lines.push(queuedStatusLine(snapshot));
|
|
599
|
+
}
|
|
297
600
|
if (snapshot.summary) {
|
|
298
601
|
lines.push("", "Summary:", snapshot.summary);
|
|
299
602
|
}
|
|
300
603
|
else if (snapshot.status === "completed") {
|
|
301
604
|
lines.push("", "Summary: (no final text summary was produced)");
|
|
302
605
|
}
|
|
606
|
+
if (snapshot.worktree && isFinalSnapshotStatus(snapshot.status)) {
|
|
607
|
+
if (snapshot.worktree.changed) {
|
|
608
|
+
lines.push("", `Worktree with changes: ${snapshot.worktree.path}`, "Review the diff there and apply / cherry-pick / discard; the parent working tree was never touched.", ...(snapshot.worktree.diffStat ? ["Diff stat:", snapshot.worktree.diffStat] : []));
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
lines.push("", "Worktree: no changes were left behind (removed automatically).");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
303
614
|
if (snapshot.toolNotes.length > 0) {
|
|
304
615
|
lines.push("", "Recent tool notes:", ...snapshot.toolNotes.slice(-8).map((note) => `- ${note}`));
|
|
305
616
|
}
|
|
306
617
|
if (snapshot.error) {
|
|
307
618
|
lines.push("", `Error: ${snapshot.error}`);
|
|
308
619
|
}
|
|
620
|
+
lines.push(...finalGuidanceLines(snapshot));
|
|
309
621
|
return lines;
|
|
310
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Per-reason guidance (design §3.1): a resume hint is rendered iff the
|
|
625
|
+
* runtime judged the run resumable. Wait timeouts are not final states and
|
|
626
|
+
* never reach this function with a finalReason.
|
|
627
|
+
*/
|
|
628
|
+
function finalGuidanceLines(snapshot) {
|
|
629
|
+
if (!isFinalSnapshotStatus(snapshot.status) || snapshot.status === "completed" || snapshot.status === "closed") {
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
switch (snapshot.finalReason) {
|
|
633
|
+
case "rate_limited_exhausted":
|
|
634
|
+
return [
|
|
635
|
+
"",
|
|
636
|
+
resumeLine(snapshot.agentId),
|
|
637
|
+
"note: the provider was rate limited; prefer resuming later or running fewer children at once",
|
|
638
|
+
];
|
|
639
|
+
case "failed_transient":
|
|
640
|
+
case "cancelled_interrupt":
|
|
641
|
+
case "cancelled_user":
|
|
642
|
+
case "cancelled_parent_abort":
|
|
643
|
+
return ["", resumeLine(snapshot.agentId)];
|
|
644
|
+
case "cancelled_budget":
|
|
645
|
+
return ["", "budget exhausted — do not resume this child; integrate what it already reported and narrow the task if more is needed"];
|
|
646
|
+
case "blocked":
|
|
647
|
+
return ["", "blocked: re-spawn with an adjusted profile or approval setting — resuming as-is would hit the same block"];
|
|
648
|
+
case "failed_fatal":
|
|
649
|
+
return [];
|
|
650
|
+
default:
|
|
651
|
+
return snapshot.resumable ? ["", resumeLine(snapshot.agentId)] : [];
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function resumeLine(agentId) {
|
|
655
|
+
return `resume: call send_input with agent_id ${agentId} to continue this child with its context intact`;
|
|
656
|
+
}
|
|
311
657
|
function formatSnapshotRole(snapshot) {
|
|
312
658
|
return [snapshot.agentName, snapshot.category ? `/${snapshot.category}` : ""].join("") || "default";
|
|
313
659
|
}
|
|
@@ -321,6 +667,8 @@ function snapshotToMetadata(snapshot) {
|
|
|
321
667
|
agentName: snapshot.agentName,
|
|
322
668
|
nickname: snapshot.nickname,
|
|
323
669
|
status: snapshot.status === "closed" ? "cancelled" : snapshot.status,
|
|
670
|
+
finalReason: snapshot.finalReason,
|
|
671
|
+
resumable: snapshot.resumable,
|
|
324
672
|
profileSource: snapshot.profileSource,
|
|
325
673
|
category: snapshot.category,
|
|
326
674
|
route: snapshot.route,
|
|
@@ -332,14 +680,16 @@ function snapshotToMetadata(snapshot) {
|
|
|
332
680
|
};
|
|
333
681
|
}
|
|
334
682
|
function unsupportedProfile(profile) {
|
|
683
|
+
if (profile.mode === "readonly" || profile.mode === "write_worktree")
|
|
684
|
+
return undefined;
|
|
335
685
|
return {
|
|
336
|
-
content: `Error: subagent profile "${profile.name}" uses mode "${profile.mode}",
|
|
686
|
+
content: `Error: subagent profile "${profile.name}" uses mode "${profile.mode}", which is not supported. Use "readonly" for investigation or "write_worktree" for isolated write work.`,
|
|
337
687
|
isError: true,
|
|
338
688
|
status: "blocked",
|
|
339
689
|
};
|
|
340
690
|
}
|
|
341
691
|
function parseScope(value) {
|
|
342
|
-
return value === "project" || value === "
|
|
692
|
+
return value === "project" || value === "user" ? value : "both";
|
|
343
693
|
}
|
|
344
694
|
function parseApproval(value) {
|
|
345
695
|
return value === "fail" || value === "disabled" ? value : undefined;
|
|
@@ -359,6 +709,10 @@ function normalizeAgentIds(value, single) {
|
|
|
359
709
|
function stringArg(value) {
|
|
360
710
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
361
711
|
}
|
|
712
|
+
function truncateText(value, max) {
|
|
713
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
714
|
+
return oneLine.length <= max ? oneLine : `${oneLine.slice(0, max - 3)}...`;
|
|
715
|
+
}
|
|
362
716
|
function snapshotFallbackId() {
|
|
363
717
|
return `spawn_${Date.now().toString(36)}`;
|
|
364
718
|
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -21,6 +21,10 @@ export function createBashTool(cwd, approval, _fileState) {
|
|
|
21
21
|
type: "object",
|
|
22
22
|
properties: {
|
|
23
23
|
command: { type: "string", description: "Bash command to execute" },
|
|
24
|
+
description: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "One short sentence (5-10 words) describing what this command does, shown to the user in the UI. Write it in the same language the user is conversing in.",
|
|
27
|
+
},
|
|
24
28
|
timeout: { type: "number", description: "Timeout in seconds (optional)" },
|
|
25
29
|
},
|
|
26
30
|
required: ["command"],
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-child tool factory for write_worktree subagents (design doc §8).
|
|
3
|
+
*
|
|
4
|
+
* Parent tools close over the parent cwd at creation, so a write child needs
|
|
5
|
+
* fresh instances bound to its worktree — with their own FileStateTracker —
|
|
6
|
+
* plus a worktree-scoped approval policy: file operations are runtime-checked
|
|
7
|
+
* to stay under the worktree root (the tools' own workspace fence does this
|
|
8
|
+
* structurally), bash auto-approves inside the worktree when the command
|
|
9
|
+
* passes a deny-list of escaping operations, and everything else fails fast.
|
|
10
|
+
*/
|
|
11
|
+
import type { ApprovalController, ApprovalDecision, ApprovalRequest } from "../approval/types.js";
|
|
12
|
+
import type { PermissionCheckResult } from "../permissions/types.js";
|
|
13
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
14
|
+
export declare function isPathInsideWorktree(worktreeRoot: string, candidate: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Approval policy for a worktree child: containment is enforced by code
|
|
17
|
+
* (path checks, deny-list), never by prompt text. There is no interactive
|
|
18
|
+
* fallback — anything outside the policy fails fast (design §11).
|
|
19
|
+
*/
|
|
20
|
+
export declare class WorktreeApprovalController implements ApprovalController {
|
|
21
|
+
private readonly worktreeRoot;
|
|
22
|
+
constructor(worktreeRoot: string);
|
|
23
|
+
request(req: ApprovalRequest): Promise<ApprovalDecision>;
|
|
24
|
+
checkRules(): PermissionCheckResult;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Builds the write child's toolset bound to its worktree: fresh instances
|
|
28
|
+
* with their own FileStateTracker and the worktree approval policy. A
|
|
29
|
+
* profile's tools list can narrow the set but never widen it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createWorktreeChildTools(worktreeCwd: string, include?: string[]): ToolRegistryEntry[];
|