@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
|
@@ -49,7 +49,9 @@ export class PermissionAwareApprovalController {
|
|
|
49
49
|
if (mode === "default" && (req.type === "edit" || req.type === "write" || req.type === "patch")) {
|
|
50
50
|
return finalize({ action: "approve" });
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
// Project profile trust is a user decision, not a destructive action:
|
|
53
|
+
// spawn_agent is legal in plan mode, so the gate prompts there too.
|
|
54
|
+
if (mode === "plan" && req.type !== "agent_profile") {
|
|
53
55
|
return finalize({
|
|
54
56
|
action: "reject",
|
|
55
57
|
feedback: "Plan mode is active. Do not call destructive tools directly — propose your changes via exit_plan_mode and wait for user approval.",
|
|
@@ -83,6 +85,8 @@ export class PermissionAwareApprovalController {
|
|
|
83
85
|
return { tool: "Edit", path: req.path, cwd: this.options.cwd };
|
|
84
86
|
case "lsp":
|
|
85
87
|
return { tool: "Lsp", path: req.path, cwd: this.options.cwd };
|
|
88
|
+
case "agent_profile":
|
|
89
|
+
return { tool: "AgentProfile" };
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
checkRequestRules(req) {
|
|
@@ -170,6 +174,8 @@ function approvalTarget(req) {
|
|
|
170
174
|
return "Edit";
|
|
171
175
|
case "lsp":
|
|
172
176
|
return "Lsp";
|
|
177
|
+
case "agent_profile":
|
|
178
|
+
return "AgentProfile";
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
181
|
function summarizeApprovalRequest(req) {
|
|
@@ -184,5 +190,7 @@ function summarizeApprovalRequest(req) {
|
|
|
184
190
|
return { type: req.type, path: req.path, paths: req.paths, files: req.files, diffLength: req.diff.length };
|
|
185
191
|
case "lsp":
|
|
186
192
|
return { type: req.type, path: req.path, operation: req.operation };
|
|
193
|
+
case "agent_profile":
|
|
194
|
+
return { type: req.type, name: req.name, path: req.path, contentHash: req.contentHash };
|
|
187
195
|
}
|
|
188
196
|
}
|
package/dist/approval/types.d.ts
CHANGED
|
@@ -45,7 +45,23 @@ export interface LspApprovalRequest {
|
|
|
45
45
|
path: string;
|
|
46
46
|
operation: string;
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Trust gate for project-local agent profiles (.bubble/agents). The user —
|
|
50
|
+
* not the model — decides whether a repository's profile prompt may drive a
|
|
51
|
+
* subagent; approvals are remembered per content hash for the session.
|
|
52
|
+
*/
|
|
53
|
+
export interface AgentProfileApprovalRequest {
|
|
54
|
+
type: "agent_profile";
|
|
55
|
+
/** Profile name as referenced by spawn_agent. */
|
|
56
|
+
name: string;
|
|
57
|
+
/** Absolute path of the profile file inside the repository. */
|
|
58
|
+
path: string;
|
|
59
|
+
/** Content hash; a changed file re-prompts. */
|
|
60
|
+
contentHash: string;
|
|
61
|
+
/** First lines of the profile prompt so the user can judge it. */
|
|
62
|
+
promptPreview: string;
|
|
63
|
+
}
|
|
64
|
+
export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | PatchApprovalRequest | BashApprovalRequest | LspApprovalRequest | AgentProfileApprovalRequest;
|
|
49
65
|
export type ApprovalDecision = {
|
|
50
66
|
action: "approve";
|
|
51
67
|
feedback?: string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint store - pre-mutation file snapshots, keyed by conversation turn.
|
|
3
|
+
*
|
|
4
|
+
* Before the edit/write tools persist a change, they record the file's prior
|
|
5
|
+
* content here so /rewind can restore the workspace to the state it had just
|
|
6
|
+
* before a given user message. Changes made by bash commands are NOT tracked;
|
|
7
|
+
* checkpoints complement git, they do not replace it.
|
|
8
|
+
*
|
|
9
|
+
* On-disk layout (sibling of the session JSONL):
|
|
10
|
+
* <session>.checkpoints/
|
|
11
|
+
* blobs/<sha256> full file content, content-addressed (deduplicated)
|
|
12
|
+
* manifest.jsonl one {turn, path, blob, timestamp} line per first
|
|
13
|
+
* capture of a file within a turn
|
|
14
|
+
*
|
|
15
|
+
* blob === null means the file did not exist before that turn, so rewinding
|
|
16
|
+
* deletes it.
|
|
17
|
+
*/
|
|
18
|
+
export interface CheckpointManifestEntry {
|
|
19
|
+
turn: string;
|
|
20
|
+
path: string;
|
|
21
|
+
blob: string | null;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
export interface CheckpointRestoreResult {
|
|
25
|
+
/** Files whose pre-turn content was written back. */
|
|
26
|
+
restored: string[];
|
|
27
|
+
/** Files deleted because they were created during the rewound turns. */
|
|
28
|
+
deleted: string[];
|
|
29
|
+
/** Files that could not be restored (I/O errors). */
|
|
30
|
+
failed: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare class CheckpointStore {
|
|
33
|
+
private readonly dir;
|
|
34
|
+
private readonly currentTurn;
|
|
35
|
+
private seen?;
|
|
36
|
+
constructor(dir: string, currentTurn: () => string);
|
|
37
|
+
/**
|
|
38
|
+
* Record a file's content before it is mutated. `priorContent` is the
|
|
39
|
+
* current on-disk content, or null when the file does not exist yet.
|
|
40
|
+
* Capturing must never break the mutation itself, so all errors are
|
|
41
|
+
* swallowed.
|
|
42
|
+
*/
|
|
43
|
+
captureBefore(filePath: string, priorContent: string | null): Promise<void>;
|
|
44
|
+
listEntries(): CheckpointManifestEntry[];
|
|
45
|
+
/** Unique files first captured at or after the given turn. */
|
|
46
|
+
filesTouchedSince(turn: string): string[];
|
|
47
|
+
/** Unique files captured during exactly the given turn. */
|
|
48
|
+
filesTouchedAt(turn: string): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Restore every tracked file to its content from just before `turn`.
|
|
51
|
+
* For each file the earliest capture at-or-after the cutoff wins (it holds
|
|
52
|
+
* the oldest pre-mutation content). Consumed manifest entries are pruned so
|
|
53
|
+
* a later rewind over reused turn ids cannot resurrect stale state.
|
|
54
|
+
*/
|
|
55
|
+
restoreTo(turn: string): Promise<CheckpointRestoreResult>;
|
|
56
|
+
private loadSeen;
|
|
57
|
+
}
|
|
Binary file
|
package/dist/config.d.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface UserConfigData {
|
|
|
30
30
|
providers?: ProviderProfile[];
|
|
31
31
|
defaultProvider?: string;
|
|
32
32
|
agentCategories?: AgentCategoriesConfig;
|
|
33
|
+
subagents?: SubagentsUserConfig;
|
|
34
|
+
}
|
|
35
|
+
export interface SubagentsUserConfig {
|
|
36
|
+
/** Global cap on concurrently running children. Default 8. */
|
|
37
|
+
maxActiveSubagents?: number;
|
|
38
|
+
/** Absolute per-child soft token cap. Default 200000. */
|
|
39
|
+
childTokenCap?: number;
|
|
33
40
|
}
|
|
34
41
|
export declare class UserConfig {
|
|
35
42
|
private data;
|
|
@@ -56,6 +63,7 @@ export declare class UserConfig {
|
|
|
56
63
|
setThemeMode(mode: ThemeMode): void;
|
|
57
64
|
setThemeOverrides(overrides: Record<string, string>): void;
|
|
58
65
|
getAgentCategories(): AgentCategoriesConfig;
|
|
66
|
+
getSubagents(): SubagentsUserConfig;
|
|
59
67
|
}
|
|
60
68
|
/** Mask an API key for safe display. */
|
|
61
69
|
export declare function maskKey(key: string): string;
|
package/dist/config.js
CHANGED
|
@@ -37,6 +37,19 @@ function sanitizeDefaultModel(model) {
|
|
|
37
37
|
function sanitizeDefaultProvider(providerId) {
|
|
38
38
|
return isHiddenProviderId(providerId) ? undefined : providerId;
|
|
39
39
|
}
|
|
40
|
+
function sanitizeSubagentsConfig(value) {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
42
|
+
return undefined;
|
|
43
|
+
const raw = value;
|
|
44
|
+
const out = {};
|
|
45
|
+
if (typeof raw.maxActiveSubagents === "number" && Number.isFinite(raw.maxActiveSubagents)) {
|
|
46
|
+
out.maxActiveSubagents = Math.max(1, Math.floor(raw.maxActiveSubagents));
|
|
47
|
+
}
|
|
48
|
+
if (typeof raw.childTokenCap === "number" && Number.isFinite(raw.childTokenCap)) {
|
|
49
|
+
out.childTokenCap = Math.max(1_000, Math.floor(raw.childTokenCap));
|
|
50
|
+
}
|
|
51
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
52
|
+
}
|
|
40
53
|
function sanitizeTheme(value) {
|
|
41
54
|
if (value == null)
|
|
42
55
|
return undefined;
|
|
@@ -89,6 +102,7 @@ export class UserConfig {
|
|
|
89
102
|
providers: sanitizeProviders(parsed.providers),
|
|
90
103
|
defaultProvider: sanitizeDefaultProvider(parsed.defaultProvider),
|
|
91
104
|
agentCategories: sanitizeAgentCategories(parsed.agentCategories),
|
|
105
|
+
subagents: sanitizeSubagentsConfig(parsed.subagents),
|
|
92
106
|
theme: sanitizeTheme(parsed.theme),
|
|
93
107
|
};
|
|
94
108
|
}
|
|
@@ -188,6 +202,9 @@ export class UserConfig {
|
|
|
188
202
|
getAgentCategories() {
|
|
189
203
|
return sanitizeAgentCategories(this.data.agentCategories);
|
|
190
204
|
}
|
|
205
|
+
getSubagents() {
|
|
206
|
+
return sanitizeSubagentsConfig(this.data.subagents) ?? {};
|
|
207
|
+
}
|
|
191
208
|
}
|
|
192
209
|
/** Mask an API key for safe display. */
|
|
193
210
|
export function maskKey(key) {
|
|
@@ -46,6 +46,15 @@ export function formatApprovalRequest(req) {
|
|
|
46
46
|
title: `LSP 操作 (${req.operation})`,
|
|
47
47
|
body: `**path:** \`${truncate(req.path, PATH_PREVIEW_MAX)}\``,
|
|
48
48
|
};
|
|
49
|
+
case "agent_profile":
|
|
50
|
+
return {
|
|
51
|
+
title: `使用项目 agent profile "${req.name}"`,
|
|
52
|
+
body: [
|
|
53
|
+
`**path:** \`${truncate(req.path, PATH_PREVIEW_MAX)}\``,
|
|
54
|
+
`\n**prompt preview:**\n\`\`\`\n${truncate(req.promptPreview, CONTENT_PREVIEW_MAX)}\n\`\`\``,
|
|
55
|
+
"\n该 profile 来自仓库本地 `.bubble/agents`,其 prompt 会驱动一个子代理。仅在信任该仓库时批准。",
|
|
56
|
+
].join("\n"),
|
|
57
|
+
};
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
function truncate(s, max) {
|
|
@@ -78,6 +78,7 @@ export class RunDriver {
|
|
|
78
78
|
approvalController,
|
|
79
79
|
lspService,
|
|
80
80
|
fileStateTracker,
|
|
81
|
+
checkpoints: () => session.manager.getCheckpoints(),
|
|
81
82
|
// questionController intentionally omitted — Feishu v1 doesn't surface
|
|
82
83
|
// the question tool to the agent.
|
|
83
84
|
});
|
|
@@ -139,6 +140,7 @@ export class RunDriver {
|
|
|
139
140
|
memoryPrompt,
|
|
140
141
|
fileStateTracker,
|
|
141
142
|
agentCategories: this.opts.deps.userConfig.getAgentCategories(),
|
|
143
|
+
subagents: this.opts.deps.userConfig.getSubagents(),
|
|
142
144
|
providerFactory: (route) => this.opts.deps.createProviderForRoute(route, promptCacheKey),
|
|
143
145
|
externalHooks: hookController,
|
|
144
146
|
});
|
package/dist/main.js
CHANGED
|
@@ -30,6 +30,10 @@ import { basename } from "node:path";
|
|
|
30
30
|
import { normalizeSingleLine, truncateVisual } from "./text-display.js";
|
|
31
31
|
import { BUBBLE_WORDMARK } from "./tui/wordmark.js";
|
|
32
32
|
import { configureDebugTrace, summarizeAgentEventForTrace, summarizeTraceMessage, traceEvent, } from "./debug-trace.js";
|
|
33
|
+
// OpenTUI is the default renderer. The React Ink implementation (alt-screen
|
|
34
|
+
// viewport, src/tui-ink) is feature-complete but still maturing — opt in with
|
|
35
|
+
// BUBBLE_TUI=ink.
|
|
36
|
+
const USE_OPENTUI = process.env.BUBBLE_TUI !== "ink";
|
|
33
37
|
async function main() {
|
|
34
38
|
const args = parseArgs(process.argv.slice(2));
|
|
35
39
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
@@ -163,6 +167,8 @@ async function main() {
|
|
|
163
167
|
toolSearchController,
|
|
164
168
|
lspService,
|
|
165
169
|
fileStateTracker,
|
|
170
|
+
// Lazy: sessionManager is resolved after tools are created.
|
|
171
|
+
checkpoints: () => sessionManager?.getCheckpoints(),
|
|
166
172
|
});
|
|
167
173
|
// Bring up MCP servers (if any). Failures are captured per-server and never
|
|
168
174
|
// block the rest of startup; /mcp surfaces status at runtime.
|
|
@@ -225,7 +231,9 @@ async function main() {
|
|
|
225
231
|
else {
|
|
226
232
|
preResolvedTheme = themeConfig.mode;
|
|
227
233
|
}
|
|
228
|
-
const { runSessionPicker } =
|
|
234
|
+
const { runSessionPicker } = USE_OPENTUI
|
|
235
|
+
? await import("./tui-opentui/run-session-picker.js")
|
|
236
|
+
: await import("./tui-ink/run-session-picker.js");
|
|
229
237
|
const picked = await runSessionPicker({
|
|
230
238
|
currentCwd: args.cwd,
|
|
231
239
|
currentSessions,
|
|
@@ -304,7 +312,7 @@ async function main() {
|
|
|
304
312
|
sessionFile: sessionManager?.getSessionFile(),
|
|
305
313
|
provider: activeProviderId || "none",
|
|
306
314
|
model: activeModel || "none",
|
|
307
|
-
renderer: printMode ? "print" : "opentui-core",
|
|
315
|
+
renderer: printMode ? "print" : USE_OPENTUI ? "opentui-core" : "ink",
|
|
308
316
|
});
|
|
309
317
|
if (traceInfo.enabled) {
|
|
310
318
|
traceEvent("run_start", {
|
|
@@ -372,6 +380,7 @@ async function main() {
|
|
|
372
380
|
memoryPrompt,
|
|
373
381
|
fileStateTracker,
|
|
374
382
|
agentCategories: userConfig.getAgentCategories(),
|
|
383
|
+
subagents: userConfig.getSubagents(),
|
|
375
384
|
providerFactory: createProviderForRoute,
|
|
376
385
|
externalHooks: hookController,
|
|
377
386
|
});
|
|
@@ -503,8 +512,41 @@ async function main() {
|
|
|
503
512
|
else {
|
|
504
513
|
detectedTheme = themeConfig.mode;
|
|
505
514
|
}
|
|
515
|
+
// In-place session switch for the /session picker: rebind every closure
|
|
516
|
+
// that persists to the session (onMessageAppend, markers, title updater)
|
|
517
|
+
// by reassigning the outer `sessionManager`, then replace the agent's
|
|
518
|
+
// history the same way startup resume does.
|
|
519
|
+
const switchSession = (sessionFile) => {
|
|
520
|
+
try {
|
|
521
|
+
const next = new SessionManager(sessionFile);
|
|
522
|
+
const history = next.getMessages();
|
|
523
|
+
sessionManager = next;
|
|
524
|
+
sessionPromptCacheKey = next.getOrCreatePromptCacheKey();
|
|
525
|
+
sessionTitleUpdater = createSessionTitleUpdater({
|
|
526
|
+
sessionManager: next,
|
|
527
|
+
complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
|
|
528
|
+
});
|
|
529
|
+
next.updateMetadata({
|
|
530
|
+
...(agent.model ? { model: agent.model } : {}),
|
|
531
|
+
cwd: args.cwd,
|
|
532
|
+
thinkingLevel: agent.thinking,
|
|
533
|
+
reasoningEffort: agent.thinking,
|
|
534
|
+
});
|
|
535
|
+
// Keep the live system/meta head (mode reminders survive the switch),
|
|
536
|
+
// mirroring the /rewind history-replacement pattern.
|
|
537
|
+
const head = agent.messages.filter((m) => m.role === "system" || m.role === "meta");
|
|
538
|
+
agent.messages = [...head, ...history];
|
|
539
|
+
agent.setTodos(next.getTodos());
|
|
540
|
+
agent.resetContextUsageAnchor();
|
|
541
|
+
return { manager: next };
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
545
|
+
}
|
|
546
|
+
};
|
|
506
547
|
const commonOptions = {
|
|
507
548
|
sessionManager,
|
|
549
|
+
switchSession,
|
|
508
550
|
createProvider,
|
|
509
551
|
registry,
|
|
510
552
|
skillRegistry,
|
|
@@ -523,19 +565,37 @@ async function main() {
|
|
|
523
565
|
};
|
|
524
566
|
const { getStartupUpdateNotice } = await import("./update/index.js");
|
|
525
567
|
const updateNotice = await getStartupUpdateNotice();
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
568
|
+
// Two explicit branches (not a dynamic ternary import) so TypeScript
|
|
569
|
+
// checks each renderer's RunTuiOptions shape independently.
|
|
570
|
+
let exitWallMs;
|
|
571
|
+
if (USE_OPENTUI) {
|
|
572
|
+
const { runTui } = await import("./tui/run.js");
|
|
573
|
+
await runTui(agent, args, {
|
|
574
|
+
...commonOptions,
|
|
575
|
+
themeMode: themeConfig.mode,
|
|
576
|
+
themeOverrides: themeConfig.overrides,
|
|
577
|
+
detectedTheme,
|
|
578
|
+
onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
|
|
579
|
+
updateNotice: updateNotice ?? undefined,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
const { runTui } = await import("./tui-ink/run.js");
|
|
584
|
+
const summary = await runTui(agent, args, {
|
|
585
|
+
...commonOptions,
|
|
586
|
+
themeMode: themeConfig.mode,
|
|
587
|
+
themeOverrides: themeConfig.overrides,
|
|
588
|
+
detectedTheme,
|
|
589
|
+
onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
|
|
590
|
+
updateNotice: updateNotice ?? undefined,
|
|
591
|
+
});
|
|
592
|
+
exitWallMs = summary?.wallMs;
|
|
593
|
+
}
|
|
535
594
|
if (sessionManager) {
|
|
536
|
-
|
|
595
|
+
printExitSummary(sessionManager, {
|
|
537
596
|
resumed: resumedExistingSession,
|
|
538
597
|
theme: detectedTheme,
|
|
598
|
+
wallMs: exitWallMs,
|
|
539
599
|
});
|
|
540
600
|
}
|
|
541
601
|
}
|
|
@@ -545,7 +605,7 @@ async function main() {
|
|
|
545
605
|
traceEvent("run_shutdown_end");
|
|
546
606
|
}
|
|
547
607
|
}
|
|
548
|
-
function
|
|
608
|
+
function printExitSummary(sessionManager, options) {
|
|
549
609
|
if (!process.stdout.isTTY)
|
|
550
610
|
return;
|
|
551
611
|
const sessionName = basename(sessionManager.getSessionFile());
|
|
@@ -589,6 +649,21 @@ function printOpenTuiExitSummary(sessionManager, options) {
|
|
|
589
649
|
console.log();
|
|
590
650
|
console.log(`${label("Session")}${colors.value(sessionLabel)}`);
|
|
591
651
|
console.log(`${label("Continue")}${colors.value(continueCommand)}`);
|
|
652
|
+
if (options.wallMs !== undefined) {
|
|
653
|
+
console.log(`${label("Duration")}${colors.value(formatWallDuration(options.wallMs))}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function formatWallDuration(ms) {
|
|
657
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
658
|
+
if (totalSeconds < 60)
|
|
659
|
+
return `${totalSeconds}s`;
|
|
660
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
661
|
+
const seconds = totalSeconds % 60;
|
|
662
|
+
if (minutes < 60)
|
|
663
|
+
return `${minutes}m ${seconds}s`;
|
|
664
|
+
const hours = Math.floor(minutes / 60);
|
|
665
|
+
const minutesRest = minutes % 60;
|
|
666
|
+
return `${hours}h ${minutesRest}m ${seconds}s`;
|
|
592
667
|
}
|
|
593
668
|
async function readPipedStdin() {
|
|
594
669
|
if (process.stdin.isTTY)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed transport errors shared between providers and the subagent runtime.
|
|
3
|
+
*
|
|
4
|
+
* `RateLimitError` is the contract for 429 handling (design doc
|
|
5
|
+
* docs/subagent-runtime-design.md §4.5): under `rateLimitPolicy: "defer"` the
|
|
6
|
+
* transport performs no 429 backoff of its own and throws this error
|
|
7
|
+
* immediately so the subagent scheduler can be the single backoff layer.
|
|
8
|
+
*/
|
|
9
|
+
export declare class RateLimitError extends Error {
|
|
10
|
+
readonly isRateLimitError = true;
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly retryAfterMs?: number;
|
|
13
|
+
constructor(message: string, options?: {
|
|
14
|
+
status?: number;
|
|
15
|
+
retryAfterMs?: number;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export declare function isRateLimitError(error: unknown): error is RateLimitError;
|
|
20
|
+
/**
|
|
21
|
+
* How a provider transport should treat HTTP 429 responses.
|
|
22
|
+
*
|
|
23
|
+
* - "handle": retry inside the transport with backoff (parent traffic default).
|
|
24
|
+
* - "defer": do not retry 429 at all; throw RateLimitError immediately so the
|
|
25
|
+
* caller (subagent scheduler) owns the backoff. Other retryable
|
|
26
|
+
* statuses are unaffected.
|
|
27
|
+
*/
|
|
28
|
+
export type RateLimitPolicy = "handle" | "defer";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed transport errors shared between providers and the subagent runtime.
|
|
3
|
+
*
|
|
4
|
+
* `RateLimitError` is the contract for 429 handling (design doc
|
|
5
|
+
* docs/subagent-runtime-design.md §4.5): under `rateLimitPolicy: "defer"` the
|
|
6
|
+
* transport performs no 429 backoff of its own and throws this error
|
|
7
|
+
* immediately so the subagent scheduler can be the single backoff layer.
|
|
8
|
+
*/
|
|
9
|
+
export class RateLimitError extends Error {
|
|
10
|
+
isRateLimitError = true;
|
|
11
|
+
status;
|
|
12
|
+
retryAfterMs;
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
15
|
+
this.name = "RateLimitError";
|
|
16
|
+
this.status = options?.status ?? 429;
|
|
17
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isRateLimitError(error) {
|
|
21
|
+
return !!error
|
|
22
|
+
&& typeof error === "object"
|
|
23
|
+
&& error.isRateLimitError === true;
|
|
24
|
+
}
|
|
@@ -20,7 +20,11 @@ export function createDefaultHooks() {
|
|
|
20
20
|
input: ctx.input,
|
|
21
21
|
enabled: taskType === "repo_orientation",
|
|
22
22
|
});
|
|
23
|
-
const taskReminder = reminderForTaskType(taskType
|
|
23
|
+
const taskReminder = reminderForTaskType(taskType, {
|
|
24
|
+
// Only parent agents carry the delegation tools; the nudge must
|
|
25
|
+
// never reach a child that cannot spawn anything.
|
|
26
|
+
canDelegate: ctx.agent.hasToolAvailable("spawn_agent"),
|
|
27
|
+
});
|
|
24
28
|
if (taskReminder) {
|
|
25
29
|
ctx.queueReminder(taskReminder);
|
|
26
30
|
}
|
package/dist/prompt/compose.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildGeminiProviderPrompt } from "./provider-prompts/gemini.js";
|
|
|
6
6
|
import { buildGlmProviderPrompt } from "./provider-prompts/glm.js";
|
|
7
7
|
import { buildGptProviderPrompt } from "./provider-prompts/gpt.js";
|
|
8
8
|
import { buildKimiProviderPrompt } from "./provider-prompts/kimi.js";
|
|
9
|
+
import { buildDelegationPolicyPrompt } from "./delegation.js";
|
|
9
10
|
import { buildEnvironmentPrompt, defaultToolNames } from "./environment.js";
|
|
10
11
|
import { buildRuntimePrompt } from "./runtime.js";
|
|
11
12
|
export function composeSystemPrompt(options = {}) {
|
|
@@ -25,10 +26,12 @@ export function composeSystemPrompt(options = {}) {
|
|
|
25
26
|
mode: options.mode,
|
|
26
27
|
guidelines: buildGuidelines(options.tools ?? defaultToolNames, options.guidelines ?? []),
|
|
27
28
|
});
|
|
29
|
+
const delegationPrompt = buildDelegationPolicyPrompt(options.tools ?? defaultToolNames);
|
|
28
30
|
return [
|
|
29
31
|
providerPrompt,
|
|
30
32
|
environmentPrompt,
|
|
31
33
|
runtimePrompt,
|
|
34
|
+
delegationPrompt,
|
|
32
35
|
options.agentProfilePrompt,
|
|
33
36
|
options.memoryPrompt,
|
|
34
37
|
].filter(Boolean).join("\n\n");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation policy section for the parent agent's system prompt.
|
|
3
|
+
*
|
|
4
|
+
* Two-sided by design (review 2026-06-12): positive triggers are quantified
|
|
5
|
+
* and read-only-scoped, negative clauses get equal weight — the user's hard
|
|
6
|
+
* constraint is "proactive, but never delegate-everything". Gated on the
|
|
7
|
+
* delegation tools being present, so child agents (whose tool sets never
|
|
8
|
+
* include spawn_agent/agent_team) never see it.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Returns the delegation policy section when the agent actually has the
|
|
12
|
+
* delegation tools; child agents and stripped-down tool sets get nothing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDelegationPolicyPrompt(tools: string[]): string | undefined;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation policy section for the parent agent's system prompt.
|
|
3
|
+
*
|
|
4
|
+
* Two-sided by design (review 2026-06-12): positive triggers are quantified
|
|
5
|
+
* and read-only-scoped, negative clauses get equal weight — the user's hard
|
|
6
|
+
* constraint is "proactive, but never delegate-everything". Gated on the
|
|
7
|
+
* delegation tools being present, so child agents (whose tool sets never
|
|
8
|
+
* include spawn_agent/agent_team) never see it.
|
|
9
|
+
*/
|
|
10
|
+
const DELEGATION_POLICY = `## Delegation policy (subagents)
|
|
11
|
+
|
|
12
|
+
You can delegate work to background subagents (spawn_agent) and batch
|
|
13
|
+
fan-outs (agent_team). Delegate deliberately, not by default.
|
|
14
|
+
|
|
15
|
+
Delegate when:
|
|
16
|
+
- An investigation will clearly require more than four search or read
|
|
17
|
+
operations, or spans multiple files and patterns, and the conversation only
|
|
18
|
+
needs the conclusion — delegate to a subagent so the intermediate noise
|
|
19
|
+
stays out of the main context. Launch multiple subagents concurrently for
|
|
20
|
+
independent questions.
|
|
21
|
+
- The task naturally splits into the same read-only investigation or
|
|
22
|
+
analysis (review, audit, summarize) over several independent items (files,
|
|
23
|
+
modules, endpoints) — use agent_team.
|
|
24
|
+
- A side-investigation is independent of your current main-line work and can
|
|
25
|
+
run in the background while you continue.
|
|
26
|
+
|
|
27
|
+
Briefing a subagent: it starts with zero context, so the task message must be
|
|
28
|
+
a self-contained work order — state the goal, list everything you already
|
|
29
|
+
know, and write known file paths or commands directly into the task. Never
|
|
30
|
+
outsource knowledge you already hold: if the task hinges on a specific path
|
|
31
|
+
or line number, pin it down yourself first and put it in the briefing. When
|
|
32
|
+
earlier work lives in an existing subagent, prefer send_input to resume it
|
|
33
|
+
over spawning a fresh one.
|
|
34
|
+
|
|
35
|
+
Do NOT delegate when:
|
|
36
|
+
- The task requires editing files or running state-changing commands.
|
|
37
|
+
Built-in subagents are read-only; do edits and writes yourself unless a
|
|
38
|
+
write-capable (write_worktree) profile is explicitly available.
|
|
39
|
+
- The task takes one or two tool calls (reading a single file, looking up one
|
|
40
|
+
definition). The handoff overhead costs more than the task.
|
|
41
|
+
- Doing it well depends on conversation context (preferences the user stated
|
|
42
|
+
this session, decisions made in this discussion). Subagents start without
|
|
43
|
+
the conversation, and fork_context is not the fix: it copies only a recent
|
|
44
|
+
slice of the history, re-pays it as child tokens, and still loses earlier
|
|
45
|
+
decisions — do context-heavy work yourself.
|
|
46
|
+
- You already read the relevant files in this conversation; a subagent would
|
|
47
|
+
re-read everything from scratch.
|
|
48
|
+
- You already delegated it. Never redo delegated work locally, and never
|
|
49
|
+
re-spawn the same task to a second subagent.
|
|
50
|
+
|
|
51
|
+
When in doubt about a one-off task, do it yourself. When a task is clearly
|
|
52
|
+
the same read-only operation over three or more independent items — where
|
|
53
|
+
each item alone would take more than a couple of tool calls — prefer
|
|
54
|
+
agent_team over doing them sequentially yourself. For just two small items,
|
|
55
|
+
do them yourself with parallel tool calls.`;
|
|
56
|
+
/**
|
|
57
|
+
* Returns the delegation policy section when the agent actually has the
|
|
58
|
+
* delegation tools; child agents and stripped-down tool sets get nothing.
|
|
59
|
+
*/
|
|
60
|
+
export function buildDelegationPolicyPrompt(tools) {
|
|
61
|
+
if (!tools.includes("spawn_agent"))
|
|
62
|
+
return undefined;
|
|
63
|
+
return DELEGATION_POLICY;
|
|
64
|
+
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { TaskType } from "../agent/task-classifier.js";
|
|
2
|
-
export
|
|
2
|
+
export interface TaskReminderOptions {
|
|
3
|
+
/** Whether this agent has the delegation tools (parent agents only). */
|
|
4
|
+
canDelegate?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function reminderForTaskType(taskType: TaskType, options?: TaskReminderOptions): string | undefined;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { wrapInSystemReminder } from "./reminders.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Delegation nudge for exploration-shaped tasks: injected at the decision
|
|
4
|
+
* point (start of turn), where it carries far more weight for weakly
|
|
5
|
+
* delegating models than the session-start system prompt. Task-type gating
|
|
6
|
+
* keeps it away from ordinary implementation/debugging turns, so it cannot
|
|
7
|
+
* amplify over-delegation.
|
|
8
|
+
*/
|
|
9
|
+
const DELEGATION_NUDGE = "- If answering needs scanning many files and only the conclusion matters, delegate to a background subagent (spawn_agent); when it is the same read-only question over several independent items, fan out with agent_team.";
|
|
10
|
+
export function reminderForTaskType(taskType, options = {}) {
|
|
3
11
|
switch (taskType) {
|
|
4
12
|
case "debugging":
|
|
5
13
|
return wrapInSystemReminder(`
|
|
@@ -39,7 +47,7 @@ Repository orientation workflow:
|
|
|
39
47
|
- Start with the repo purpose and main execution paths.
|
|
40
48
|
- Inspect README/package metadata plus core runtime files before summarizing.
|
|
41
49
|
- Keep the first pass read-only unless the user asks for changes or runtime verification.
|
|
42
|
-
`);
|
|
50
|
+
${options.canDelegate ? `${DELEGATION_NUDGE}\n` : ""}`);
|
|
43
51
|
case "product_discussion":
|
|
44
52
|
return wrapInSystemReminder(`
|
|
45
53
|
Product discussion workflow:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
|
|
2
|
+
import { RateLimitError } from "./network/errors.js";
|
|
2
3
|
import { isProviderTransportError, normalizeProviderNetworkError, providerFetch } from "./network/provider-transport.js";
|
|
3
4
|
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, retryAfterMsFromResponse, sleepBeforeRetry, } from "./network/retry.js";
|
|
4
5
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
@@ -32,6 +33,7 @@ export function createAnthropicMessagesProvider(options) {
|
|
|
32
33
|
method: "POST",
|
|
33
34
|
body: JSON.stringify(body),
|
|
34
35
|
signal: chatOptions.abortSignal,
|
|
36
|
+
rateLimitPolicy: chatOptions.rateLimitPolicy,
|
|
35
37
|
});
|
|
36
38
|
yield* translateAnthropicStream(events);
|
|
37
39
|
yield { type: "done" };
|
|
@@ -428,6 +430,27 @@ async function fetchAnthropicResponseWithRetry(options, request) {
|
|
|
428
430
|
if (response.ok)
|
|
429
431
|
return response;
|
|
430
432
|
const detail = await readAnthropicErrorDetail(response);
|
|
433
|
+
// Rate-limit contract (design §4.5): under "defer" the transport performs
|
|
434
|
+
// no 429 backoff and throws the typed error immediately; under "handle"
|
|
435
|
+
// an exhausted 429 retry budget still surfaces as the typed error so the
|
|
436
|
+
// caller can recognize it without string matching.
|
|
437
|
+
if (response.status === 429) {
|
|
438
|
+
const retryAfterMs = retryAfterMsFromResponse(response);
|
|
439
|
+
if (request.rateLimitPolicy === "defer") {
|
|
440
|
+
throw new RateLimitError(`Anthropic Messages API rate limited (429): ${detail || response.statusText}`, {
|
|
441
|
+
status: 429,
|
|
442
|
+
retryAfterMs,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (request.signal?.aborted || attempt >= maxRetries) {
|
|
446
|
+
throw new RateLimitError(`Anthropic Messages API rate limited (429) after ${attempt + 1} attempts: ${detail || response.statusText}`, {
|
|
447
|
+
status: 429,
|
|
448
|
+
retryAfterMs,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1, { retryAfterMs }), request.signal);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
431
454
|
const error = new Error(`Anthropic Messages API error ${response.status}: ${detail || response.statusText}`);
|
|
432
455
|
if (request.signal?.aborted || attempt >= maxRetries || !isRetryableAnthropicHttpError(response.status, detail)) {
|
|
433
456
|
throw error;
|