@bubblebrain-ai/bubble 0.0.38 → 0.0.39
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/internal-reminder-sanitizer.js +2 -0
- package/dist/agent/profiles.d.ts +6 -0
- package/dist/agent/profiles.js +32 -0
- package/dist/agent.d.ts +17 -0
- package/dist/agent.js +31 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/main.js +63 -24
- package/dist/orchestrator/default-hooks.js +78 -1
- package/dist/orchestrator/hooks.d.ts +7 -0
- package/dist/print-output.d.ts +54 -0
- package/dist/print-output.js +103 -0
- package/dist/prompt/delegation.js +6 -3
- package/dist/prompt/task-reminders.d.ts +17 -0
- package/dist/prompt/task-reminders.js +15 -0
- package/package.json +1 -1
|
@@ -169,6 +169,8 @@ const INTERNAL_REASONING_REFERENCE_PATTERNS = [
|
|
|
169
169
|
/Do not describe a subagent as running or still working/i,
|
|
170
170
|
/Background task truth/i,
|
|
171
171
|
/Never re-run work a finished task already did/i,
|
|
172
|
+
/Large-change checkpoint/i,
|
|
173
|
+
/delegation only adds merge risk/i,
|
|
172
174
|
];
|
|
173
175
|
function consumeInternalBlockAtStart(text, final) {
|
|
174
176
|
if (text.startsWith(INTERNAL_TAG_PREFIX)) {
|
package/dist/agent/profiles.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export interface AgentProfileDiagnostic {
|
|
|
56
56
|
export type AgentProfileScope = "user" | "project" | "both";
|
|
57
57
|
export declare function discoverAgentProfiles(cwd: string, scope?: AgentProfileScope): DiscoverAgentProfilesResult;
|
|
58
58
|
export declare function builtinAgentProfiles(): AgentProfile[];
|
|
59
|
+
/**
|
|
60
|
+
* The delegation nudge only fires when a write-capable spawn surface exists
|
|
61
|
+
* (large-task-delegation design §3): defense in depth for hosts that strip
|
|
62
|
+
* or override the builtin profiles.
|
|
63
|
+
*/
|
|
64
|
+
export declare function hasWriteWorktreeProfile(profiles: AgentProfile[]): boolean;
|
|
59
65
|
export declare function findAgentProfile(profiles: AgentProfile[], name: string): AgentProfile | undefined;
|
|
60
66
|
export declare function assignAgentNickname(profile: AgentProfile, activeNicknames?: Iterable<string>): string;
|
|
61
67
|
/**
|
package/dist/agent/profiles.js
CHANGED
|
@@ -134,8 +134,40 @@ export function builtinAgentProfiles() {
|
|
|
134
134
|
toProfile("security_investigation"),
|
|
135
135
|
toProfile("evidence_correlation"),
|
|
136
136
|
toProfile("general_readonly"),
|
|
137
|
+
// The write-capable builtin (large-task-delegation design §3): before
|
|
138
|
+
// this, write_worktree was reachable only via user-authored profile
|
|
139
|
+
// frontmatter, so the delegation nudge would have recommended a surface
|
|
140
|
+
// that does not exist in a stock session.
|
|
141
|
+
{
|
|
142
|
+
name: "implementer",
|
|
143
|
+
description: "Write-capable implementation subagent (isolated git worktree)",
|
|
144
|
+
source: "builtin",
|
|
145
|
+
mode: "write_worktree",
|
|
146
|
+
model: "inherit",
|
|
147
|
+
tools: {
|
|
148
|
+
preset: "explicit",
|
|
149
|
+
include: ["read", "glob", "grep", "edit", "write", "bash", "lsp", "todo_write"],
|
|
150
|
+
exclude: [],
|
|
151
|
+
},
|
|
152
|
+
approval: "fail",
|
|
153
|
+
nicknameCandidates: DEFAULT_NICKNAME_CANDIDATES,
|
|
154
|
+
prompt: [
|
|
155
|
+
"You are an implementer subagent working in your own isolated git worktree — the parent tree is never touched.",
|
|
156
|
+
"Your worktree forks from the last commit; changes the parent made after that commit are NOT present unless included in your briefing.",
|
|
157
|
+
"Make the smallest coherent edits that complete your assigned group. Stay strictly inside the file set you were given; if a needed change falls outside it, report that instead of editing.",
|
|
158
|
+
"Verify your changes narrowly (targeted tests or typecheck) when possible, and end with a concise summary of files changed and verification results — the parent reviews and applies your worktree.",
|
|
159
|
+
].join("\n"),
|
|
160
|
+
},
|
|
137
161
|
];
|
|
138
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* The delegation nudge only fires when a write-capable spawn surface exists
|
|
165
|
+
* (large-task-delegation design §3): defense in depth for hosts that strip
|
|
166
|
+
* or override the builtin profiles.
|
|
167
|
+
*/
|
|
168
|
+
export function hasWriteWorktreeProfile(profiles) {
|
|
169
|
+
return profiles.some((profile) => profile.mode === "write_worktree");
|
|
170
|
+
}
|
|
139
171
|
export function findAgentProfile(profiles, name) {
|
|
140
172
|
return profiles.find((profile) => profile.name === name)
|
|
141
173
|
?? profiles.find((profile) => profile.name === `builtin:${name}`);
|
package/dist/agent.d.ts
CHANGED
|
@@ -112,6 +112,14 @@ export declare class Agent {
|
|
|
112
112
|
outputTail: (id: string) => string | undefined;
|
|
113
113
|
};
|
|
114
114
|
private lastTaskReminderVersion;
|
|
115
|
+
/**
|
|
116
|
+
* Once-per-session latch for the large-task delegation nudge
|
|
117
|
+
* (large-task-delegation design §2): goal-loop continuations rebuild
|
|
118
|
+
* TurnHookState every run, so a per-turn latch would nag on every turn.
|
|
119
|
+
* If the model read the nudge once and chose not to delegate, repeating
|
|
120
|
+
* it is noise.
|
|
121
|
+
*/
|
|
122
|
+
largeTaskNudgeConsumed: boolean;
|
|
115
123
|
private _providerId;
|
|
116
124
|
private _model;
|
|
117
125
|
private tools;
|
|
@@ -295,6 +303,15 @@ export declare class Agent {
|
|
|
295
303
|
abortSignal?: AbortSignal;
|
|
296
304
|
}): Promise<SubagentThreadSnapshot>;
|
|
297
305
|
closeSubAgent(agentId: string): Promise<SubagentThreadSnapshot>;
|
|
306
|
+
/**
|
|
307
|
+
* Live (non-final) children only — the delegation-nudge gate
|
|
308
|
+
* (large-task-delegation design §2). listSubAgents() is a grows-only
|
|
309
|
+
* session history (finished + resumed children stay forever), so it must
|
|
310
|
+
* never be used to answer "am I currently delegating?".
|
|
311
|
+
*/
|
|
312
|
+
activeSubAgentCount(): number;
|
|
313
|
+
/** run_workflow children are workflowInternal and invisible to listSubAgents. */
|
|
314
|
+
hasRunningWorkflow(): boolean;
|
|
298
315
|
listSubAgents(): SubagentThreadSnapshot[];
|
|
299
316
|
/**
|
|
300
317
|
* Dynamic workflow (option C): runs an LLM-authored JS orchestration script in
|
package/dist/agent.js
CHANGED
|
@@ -20,6 +20,7 @@ import { HookBus } from "./orchestrator/hooks.js";
|
|
|
20
20
|
import { normalizeHookInput, truncateHookText, } from "./hooks/index.js";
|
|
21
21
|
import { createDefaultHooks } from "./orchestrator/default-hooks.js";
|
|
22
22
|
import { buildTaskLifecycleReminder } from "./agent/task-lifecycle-reminder.js";
|
|
23
|
+
import { classifyTask } from "./agent/task-classifier.js";
|
|
23
24
|
import { mergeAgentCategories, parseThinkingLevel, resolveModelRoute, resolveSubagentRoute } from "./agent/categories.js";
|
|
24
25
|
import { DEFAULT_AGENT_ROUTING, nearModelMatches, sanitizeAgentRouting, tierContextFromSnapshot } from "./agent/routing-catalog.js";
|
|
25
26
|
import { buildModelRoutingPrompt } from "./prompt/routing.js";
|
|
@@ -108,6 +109,14 @@ export class Agent {
|
|
|
108
109
|
*/
|
|
109
110
|
backgroundTasks;
|
|
110
111
|
lastTaskReminderVersion = -1;
|
|
112
|
+
/**
|
|
113
|
+
* Once-per-session latch for the large-task delegation nudge
|
|
114
|
+
* (large-task-delegation design §2): goal-loop continuations rebuild
|
|
115
|
+
* TurnHookState every run, so a per-turn latch would nag on every turn.
|
|
116
|
+
* If the model read the nudge once and chose not to delegate, repeating
|
|
117
|
+
* it is noise.
|
|
118
|
+
*/
|
|
119
|
+
largeTaskNudgeConsumed = false;
|
|
111
120
|
_providerId;
|
|
112
121
|
_model;
|
|
113
122
|
tools = new Map();
|
|
@@ -548,6 +557,15 @@ export class Agent {
|
|
|
548
557
|
events.push(...hook.events);
|
|
549
558
|
this.injectHookModelContext(hook.result);
|
|
550
559
|
this.appendMessage({ role: "user", content: input.content });
|
|
560
|
+
// Large-task detector (large-task-delegation design §1): a steer
|
|
561
|
+
// redirects the task — stale breadth evidence must not fire a nudge
|
|
562
|
+
// about the pre-steer task, and the classifier should reason about
|
|
563
|
+
// the redirected one.
|
|
564
|
+
hookState.exploredFiles?.clear();
|
|
565
|
+
hookState.largeTaskCheckpointDone = false;
|
|
566
|
+
if (typeof input.content === "string" && input.content.trim()) {
|
|
567
|
+
hookState.taskType = classifyTask(input.content);
|
|
568
|
+
}
|
|
551
569
|
events.push({
|
|
552
570
|
type: "input_applied",
|
|
553
571
|
id: input.id,
|
|
@@ -1675,6 +1693,19 @@ export class Agent {
|
|
|
1675
1693
|
this.subagentStore.notifyWaiters(record);
|
|
1676
1694
|
return this.snapshotSubagent(record);
|
|
1677
1695
|
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Live (non-final) children only — the delegation-nudge gate
|
|
1698
|
+
* (large-task-delegation design §2). listSubAgents() is a grows-only
|
|
1699
|
+
* session history (finished + resumed children stay forever), so it must
|
|
1700
|
+
* never be used to answer "am I currently delegating?".
|
|
1701
|
+
*/
|
|
1702
|
+
activeSubAgentCount() {
|
|
1703
|
+
return this.subagentStore.activeCount();
|
|
1704
|
+
}
|
|
1705
|
+
/** run_workflow children are workflowInternal and invisible to listSubAgents. */
|
|
1706
|
+
hasRunningWorkflow() {
|
|
1707
|
+
return [...this.workflowRuns.values()].some((record) => record.status === "running");
|
|
1708
|
+
}
|
|
1678
1709
|
listSubAgents() {
|
|
1679
1710
|
return this.subagentStore.values()
|
|
1680
1711
|
.filter((record) => !record.workflowInternal)
|
package/dist/cli.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface CliArgs {
|
|
|
11
11
|
resume?: boolean;
|
|
12
12
|
sessionName?: string;
|
|
13
13
|
print?: boolean;
|
|
14
|
+
/** Print-mode output: plain (default) or a single JSON object on stdout. */
|
|
15
|
+
outputFormat?: "plain" | "json";
|
|
14
16
|
prompt?: string;
|
|
15
17
|
thinkingLevel?: ThinkingLevel;
|
|
16
18
|
mode?: PermissionMode;
|
package/dist/cli.js
CHANGED
|
@@ -55,6 +55,15 @@ export function parseArgs(argv) {
|
|
|
55
55
|
case "-p":
|
|
56
56
|
args.print = true;
|
|
57
57
|
break;
|
|
58
|
+
case "--output-format": {
|
|
59
|
+
const value = argv[++i];
|
|
60
|
+
if (value !== "plain" && value !== "json") {
|
|
61
|
+
console.error(`Invalid --output-format: ${value ?? "(missing)"}. Expected plain or json.`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
args.outputFormat = value;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
58
67
|
case "--plan":
|
|
59
68
|
args.mode = "plan";
|
|
60
69
|
break;
|
|
@@ -108,6 +117,8 @@ Options (default):
|
|
|
108
117
|
--dangerously-skip-permissions
|
|
109
118
|
Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
|
|
110
119
|
-p, --print Non-interactive mode (single prompt)
|
|
120
|
+
--output-format <fmt> Print-mode output: plain (default) or json
|
|
121
|
+
(one JSON object on stdout: text, usage, num_turns)
|
|
111
122
|
-v, --version Print the installed version and exit
|
|
112
123
|
-h, --help Show this help
|
|
113
124
|
|
package/dist/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import { SkillRegistry } from "./skills/registry.js";
|
|
|
20
20
|
import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
|
|
21
21
|
import { getProcessManager } from "./tasks/manager.js";
|
|
22
22
|
import { PromotionChannel } from "./tasks/promotion.js";
|
|
23
|
+
import { PrintRunCollector, formatPrintJson, formatPrintJsonError } from "./print-output.js";
|
|
23
24
|
import { FileStateTracker } from "./tools/file-state.js";
|
|
24
25
|
import { GoalStore } from "./goal/store.js";
|
|
25
26
|
import { PermissionAwareApprovalController } from "./approval/controller.js";
|
|
@@ -552,7 +553,8 @@ async function main() {
|
|
|
552
553
|
agent.injectModeReminder();
|
|
553
554
|
}
|
|
554
555
|
agent.injectDeferredToolsReminder();
|
|
555
|
-
|
|
556
|
+
// JSON print mode reserves stdout for the single result object.
|
|
557
|
+
(args.outputFormat === "json" ? console.error : console.log)(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
|
|
556
558
|
}
|
|
557
559
|
}
|
|
558
560
|
try {
|
|
@@ -568,33 +570,70 @@ async function main() {
|
|
|
568
570
|
console.error(chalk.red("Error: No prompt provided."));
|
|
569
571
|
process.exit(1);
|
|
570
572
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
573
|
+
// JSON mode (benchmark/CI adapters): stdout carries exactly ONE JSON
|
|
574
|
+
// object; streaming text and tool logs move to stderr-silence. Plain
|
|
575
|
+
// mode is byte-for-byte the previous behavior.
|
|
576
|
+
const jsonOutput = args.outputFormat === "json";
|
|
577
|
+
const collector = new PrintRunCollector();
|
|
578
|
+
const printSessionId = sessionManager
|
|
579
|
+
? basename(sessionManager.getSessionFile())
|
|
580
|
+
: undefined;
|
|
581
|
+
try {
|
|
582
|
+
let printedTurnText = false;
|
|
583
|
+
for await (const event of agent.run(prompt, args.cwd)) {
|
|
584
|
+
traceEvent("print_agent_event", summarizeAgentEventForTrace(event));
|
|
585
|
+
collector.onEvent(event);
|
|
586
|
+
if (jsonOutput) {
|
|
587
|
+
if (event.type === "provider_retry") {
|
|
588
|
+
console.error(chalk.yellow(`[Stream interrupted; retrying (${event.attempt}/${event.maxAttempts})]`));
|
|
589
|
+
}
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (event.type === "turn_start") {
|
|
593
|
+
printedTurnText = false;
|
|
594
|
+
}
|
|
595
|
+
else if (event.type === "provider_retry") {
|
|
596
|
+
// The stream died mid-response and the agent re-issues the whole
|
|
597
|
+
// request. Text already on stdout cannot be un-printed, so at least
|
|
598
|
+
// separate the retried response and say what happened.
|
|
599
|
+
if (printedTurnText)
|
|
600
|
+
process.stdout.write("\n");
|
|
601
|
+
console.error(chalk.yellow(`[Stream interrupted; retrying (${event.attempt}/${event.maxAttempts}) — the partial text above is superseded by the retried response]`));
|
|
602
|
+
}
|
|
603
|
+
else if (event.type === "text_delta") {
|
|
604
|
+
printedTurnText = true;
|
|
605
|
+
process.stdout.write(event.content);
|
|
606
|
+
}
|
|
607
|
+
else if (event.type === "tool_start") {
|
|
608
|
+
console.log(chalk.cyan(`\n[Tool: ${event.name}]`));
|
|
609
|
+
}
|
|
610
|
+
else if (event.type === "tool_end") {
|
|
611
|
+
const color = event.result.isError ? chalk.red : chalk.dim;
|
|
612
|
+
console.log(color(`[Result: ${event.result.content.slice(0, 200)}${event.result.content.length > 200 ? "..." : ""}]`));
|
|
613
|
+
}
|
|
588
614
|
}
|
|
589
|
-
|
|
590
|
-
|
|
615
|
+
if (jsonOutput) {
|
|
616
|
+
process.stdout.write(formatPrintJson({
|
|
617
|
+
summary: collector.summary(),
|
|
618
|
+
sessionId: printSessionId,
|
|
619
|
+
}) + "\n");
|
|
591
620
|
}
|
|
592
|
-
else
|
|
593
|
-
|
|
594
|
-
console.log(color(`[Result: ${event.result.content.slice(0, 200)}${event.result.content.length > 200 ? "..." : ""}]`));
|
|
621
|
+
else {
|
|
622
|
+
console.log();
|
|
595
623
|
}
|
|
596
624
|
}
|
|
597
|
-
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (!jsonOutput)
|
|
627
|
+
throw error;
|
|
628
|
+
// Structured failure: adapters distinguish agent errors from crashes
|
|
629
|
+
// by the JSON error object + non-zero exit.
|
|
630
|
+
process.stdout.write(formatPrintJsonError({
|
|
631
|
+
message: error instanceof Error ? error.message : String(error),
|
|
632
|
+
summary: collector.summary(),
|
|
633
|
+
sessionId: printSessionId,
|
|
634
|
+
}) + "\n");
|
|
635
|
+
process.exitCode = 1;
|
|
636
|
+
}
|
|
598
637
|
return;
|
|
599
638
|
}
|
|
600
639
|
const themeConfig = userConfig.getTheme();
|
|
@@ -5,8 +5,11 @@ import { ExecutionGovernor } from "../agent/execution-governor.js";
|
|
|
5
5
|
import { DiscoveryBarrier } from "../agent/discovery-barrier.js";
|
|
6
6
|
import { arbitrateToolCall } from "../agent/tool-arbiter.js";
|
|
7
7
|
import { buildEditRetryEscalationReminder, buildSmallTaskHint, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
|
|
8
|
-
import { orchestrationRequestReminder, reminderForTaskType, userNamedModelReminder } from "../prompt/task-reminders.js";
|
|
8
|
+
import { largeImplementationTaskReminder, orchestrationRequestReminder, reminderForTaskType, userNamedModelReminder } from "../prompt/task-reminders.js";
|
|
9
9
|
import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
|
|
10
|
+
import { resolve as resolvePath } from "node:path";
|
|
11
|
+
import { traceEvent } from "../debug-trace.js";
|
|
12
|
+
import { builtinAgentProfiles, discoverAgentProfiles, hasWriteWorktreeProfile } from "../agent/profiles.js";
|
|
10
13
|
import { buildSubagentLifecycleReminder } from "../agent/subagent-lifecycle-reminder.js";
|
|
11
14
|
export function createDefaultHooks() {
|
|
12
15
|
return [
|
|
@@ -33,6 +36,9 @@ export function createDefaultHooks() {
|
|
|
33
36
|
const orchestrationReminder = orchestrationRequestReminder(ctx.input, ctx.agent.hasToolAvailable("run_workflow"));
|
|
34
37
|
if (orchestrationReminder) {
|
|
35
38
|
ctx.queueReminder(orchestrationReminder);
|
|
39
|
+
// The large-change checkpoint must not re-offer spawn_agent in a
|
|
40
|
+
// turn where this reminder just forbade it (design §3).
|
|
41
|
+
ctx.state.orchestrationReminderSent = true;
|
|
36
42
|
}
|
|
37
43
|
// User named a configured model ("gpt 5.6 sol"): resolve it against
|
|
38
44
|
// the routable catalog and hand over the exact id at the decision
|
|
@@ -97,6 +103,12 @@ export function createDefaultHooks() {
|
|
|
97
103
|
const toolCall = { ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) };
|
|
98
104
|
ctx.replaceToolCall(toolCall);
|
|
99
105
|
ctx.state.governor?.beforeToolCall(toolCall);
|
|
106
|
+
// Large-change checkpoint (large-task-delegation design §1): the
|
|
107
|
+
// first mutation of the turn is the decision point — exploration is
|
|
108
|
+
// over, nothing has been edited yet.
|
|
109
|
+
if (isMutationTool(toolCall.name)) {
|
|
110
|
+
maybeQueueLargeTaskNudge(ctx);
|
|
111
|
+
}
|
|
100
112
|
const blockedResult = ctx.state.discoveryBarrier?.beforeToolCall(toolCall);
|
|
101
113
|
if (blockedResult)
|
|
102
114
|
ctx.blockToolCall(blockedResult);
|
|
@@ -153,6 +165,18 @@ export function createDefaultHooks() {
|
|
|
153
165
|
// dedup do the work.
|
|
154
166
|
if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
|
|
155
167
|
markCodeChanged(ctx.state);
|
|
168
|
+
ctx.state.appliedEditCount = (ctx.state.appliedEditCount ?? 0) + 1;
|
|
169
|
+
}
|
|
170
|
+
// Large-task breadth evidence (design §1): only files actually READ.
|
|
171
|
+
// Search results contribute nothing — one glob returns up to 100
|
|
172
|
+
// match paths and would saturate any threshold instantly (review
|
|
173
|
+
// blocker). Bash-parsed reads (cat/head/sed -n) carry relative
|
|
174
|
+
// paths, so resolve against cwd before dedup.
|
|
175
|
+
if (!ctx.result.isError && ctx.result.metadata?.kind === "read") {
|
|
176
|
+
const path = typeof ctx.result.metadata.path === "string" ? ctx.result.metadata.path : undefined;
|
|
177
|
+
if (path) {
|
|
178
|
+
(ctx.state.exploredFiles ??= new Set()).add(resolvePath(ctx.cwd, path));
|
|
179
|
+
}
|
|
156
180
|
}
|
|
157
181
|
// Removed: active verification tracking. The previous design nagged the
|
|
158
182
|
// model every turn until it ran a recognised verification command, and
|
|
@@ -206,6 +230,59 @@ export function createDefaultHooks() {
|
|
|
206
230
|
function markCodeChanged(state) {
|
|
207
231
|
state.codeChanged = true;
|
|
208
232
|
}
|
|
233
|
+
// Large-task delegation checkpoint (large-task-delegation design §1-3).
|
|
234
|
+
const BREADTH_FILES = 8;
|
|
235
|
+
const BREADTH_FILES_WITH_PLAN = 5;
|
|
236
|
+
const BREADTH_TODOS = 6;
|
|
237
|
+
function maybeQueueLargeTaskNudge(ctx) {
|
|
238
|
+
// Evaluated exactly once per turn, at the first mutation — fired or not
|
|
239
|
+
// (mid-implementation re-detection is out of scope by design §4).
|
|
240
|
+
if (ctx.state.largeTaskCheckpointDone)
|
|
241
|
+
return;
|
|
242
|
+
ctx.state.largeTaskCheckpointDone = true;
|
|
243
|
+
const agent = ctx.agent;
|
|
244
|
+
const taskType = ctx.state.taskType;
|
|
245
|
+
const exploredFiles = ctx.state.exploredFiles?.size ?? 0;
|
|
246
|
+
const pendingTodos = agent.getTodos?.()
|
|
247
|
+
?.filter((todo) => todo.status === "pending" || todo.status === "in_progress").length ?? 0;
|
|
248
|
+
const threshold = pendingTodos >= BREADTH_TODOS ? BREADTH_FILES_WITH_PLAN : BREADTH_FILES;
|
|
249
|
+
// Gate order: cheap checks first; the profile discovery (filesystem) runs
|
|
250
|
+
// only when everything else already passed.
|
|
251
|
+
const suppressedBy = agent.largeTaskNudgeConsumed ? "session_latch"
|
|
252
|
+
: (taskType === "code_review" || taskType === "security_investigation") ? "task_type"
|
|
253
|
+
: !agent.hasToolAvailable("spawn_agent") ? "no_spawn_tool"
|
|
254
|
+
: (agent.activeSubAgentCount?.() ?? 0) > 0 ? "active_children"
|
|
255
|
+
: agent.hasRunningWorkflow?.() ? "running_workflow"
|
|
256
|
+
: exploredFiles < threshold ? "below_threshold"
|
|
257
|
+
: !writeWorktreeSurfaceAvailable(ctx.cwd) ? "no_write_profile"
|
|
258
|
+
: undefined;
|
|
259
|
+
traceEvent("delegation_detector", {
|
|
260
|
+
fired: suppressedBy === undefined,
|
|
261
|
+
exploredFiles,
|
|
262
|
+
pendingTodos,
|
|
263
|
+
threshold,
|
|
264
|
+
taskType: taskType ?? "unknown",
|
|
265
|
+
...(suppressedBy ? { suppressedBy } : {}),
|
|
266
|
+
});
|
|
267
|
+
if (suppressedBy !== undefined)
|
|
268
|
+
return;
|
|
269
|
+
agent.largeTaskNudgeConsumed = true;
|
|
270
|
+
ctx.queueReminder(largeImplementationTaskReminder({
|
|
271
|
+
exploredFiles,
|
|
272
|
+
pendingTodos,
|
|
273
|
+
appliedEdits: ctx.state.appliedEditCount ?? 0,
|
|
274
|
+
orchestrationRequested: ctx.state.orchestrationReminderSent === true,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
function writeWorktreeSurfaceAvailable(cwd) {
|
|
278
|
+
try {
|
|
279
|
+
const discovered = discoverAgentProfiles(cwd, "both").profiles;
|
|
280
|
+
return hasWriteWorktreeProfile([...builtinAgentProfiles(), ...discovered]);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return hasWriteWorktreeProfile(builtinAgentProfiles());
|
|
284
|
+
}
|
|
285
|
+
}
|
|
209
286
|
function isCodeWriteResult(_toolCall, result) {
|
|
210
287
|
if (result.isError || result.status === "blocked" || result.status === "cancelled" || result.status === "command_error") {
|
|
211
288
|
return false;
|
|
@@ -26,6 +26,13 @@ export interface TurnHookState {
|
|
|
26
26
|
total: number;
|
|
27
27
|
spent: number;
|
|
28
28
|
};
|
|
29
|
+
exploredFiles?: Set<string>;
|
|
30
|
+
/** Checkpoint evaluated at the first mutation — once per turn, fired or not. */
|
|
31
|
+
largeTaskCheckpointDone?: boolean;
|
|
32
|
+
/** Successful mutations this turn; rendered as K in the nudge wording. */
|
|
33
|
+
appliedEditCount?: number;
|
|
34
|
+
/** orchestrationRequestReminder fired: the nudge offers only run_workflow. */
|
|
35
|
+
orchestrationReminderSent?: boolean;
|
|
29
36
|
}
|
|
30
37
|
export interface TurnHookContext {
|
|
31
38
|
agent: Agent;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured output for print mode (`-p --output-format json`).
|
|
3
|
+
*
|
|
4
|
+
* Built for benchmark/CI adapters (Pier, terminal-bench): stdout carries
|
|
5
|
+
* exactly one JSON object; all human-facing noise goes to stderr. Field
|
|
6
|
+
* names follow the grok-build/Claude Code convention (text, stopReason,
|
|
7
|
+
* sessionId, usage.{input_tokens,...}, num_turns) so existing harness
|
|
8
|
+
* adapters port with minimal changes.
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentEvent } from "./types.js";
|
|
11
|
+
export type PrintOutputFormat = "plain" | "json";
|
|
12
|
+
export declare function parseOutputFormat(raw: string | undefined): PrintOutputFormat | undefined;
|
|
13
|
+
export interface PrintUsageSummary {
|
|
14
|
+
input_tokens: number;
|
|
15
|
+
cache_read_input_tokens: number;
|
|
16
|
+
output_tokens: number;
|
|
17
|
+
reasoning_tokens: number;
|
|
18
|
+
total_tokens: number;
|
|
19
|
+
}
|
|
20
|
+
export interface PrintRunSummary {
|
|
21
|
+
/** Final answer: the last turn's streamed text. */
|
|
22
|
+
text: string;
|
|
23
|
+
num_turns: number;
|
|
24
|
+
num_tool_calls: number;
|
|
25
|
+
usage: PrintUsageSummary;
|
|
26
|
+
/**
|
|
27
|
+
* False when no turn carried provider usage (some endpoints — notably the
|
|
28
|
+
* Grok subscription proxy — omit it): zeros then mean "not reported",
|
|
29
|
+
* not "free". Mirrors the goal engine's markTokenUsageUnavailable and
|
|
30
|
+
* grok-build's usage_is_incomplete convention.
|
|
31
|
+
*/
|
|
32
|
+
usage_reported: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class PrintRunCollector {
|
|
35
|
+
private turnText;
|
|
36
|
+
private lastTurnText;
|
|
37
|
+
private turns;
|
|
38
|
+
private toolCalls;
|
|
39
|
+
private usageReported;
|
|
40
|
+
private readonly usage;
|
|
41
|
+
onEvent(event: AgentEvent): void;
|
|
42
|
+
private addUsage;
|
|
43
|
+
summary(): PrintRunSummary;
|
|
44
|
+
}
|
|
45
|
+
export declare function formatPrintJson(input: {
|
|
46
|
+
summary: PrintRunSummary;
|
|
47
|
+
sessionId?: string;
|
|
48
|
+
stopReason?: "end_turn" | "error" | "cancelled";
|
|
49
|
+
}): string;
|
|
50
|
+
export declare function formatPrintJsonError(input: {
|
|
51
|
+
message: string;
|
|
52
|
+
summary?: PrintRunSummary;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
}): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured output for print mode (`-p --output-format json`).
|
|
3
|
+
*
|
|
4
|
+
* Built for benchmark/CI adapters (Pier, terminal-bench): stdout carries
|
|
5
|
+
* exactly one JSON object; all human-facing noise goes to stderr. Field
|
|
6
|
+
* names follow the grok-build/Claude Code convention (text, stopReason,
|
|
7
|
+
* sessionId, usage.{input_tokens,...}, num_turns) so existing harness
|
|
8
|
+
* adapters port with minimal changes.
|
|
9
|
+
*/
|
|
10
|
+
export function parseOutputFormat(raw) {
|
|
11
|
+
if (raw === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (raw === "plain" || raw === "json")
|
|
14
|
+
return raw;
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export class PrintRunCollector {
|
|
18
|
+
turnText = "";
|
|
19
|
+
lastTurnText = "";
|
|
20
|
+
turns = 0;
|
|
21
|
+
toolCalls = 0;
|
|
22
|
+
usageReported = false;
|
|
23
|
+
usage = {
|
|
24
|
+
input_tokens: 0,
|
|
25
|
+
cache_read_input_tokens: 0,
|
|
26
|
+
output_tokens: 0,
|
|
27
|
+
reasoning_tokens: 0,
|
|
28
|
+
total_tokens: 0,
|
|
29
|
+
};
|
|
30
|
+
onEvent(event) {
|
|
31
|
+
switch (event.type) {
|
|
32
|
+
case "turn_start":
|
|
33
|
+
this.turns += 1;
|
|
34
|
+
this.turnText = "";
|
|
35
|
+
break;
|
|
36
|
+
case "text_delta":
|
|
37
|
+
this.turnText += event.content;
|
|
38
|
+
break;
|
|
39
|
+
case "provider_retry":
|
|
40
|
+
// The retried response supersedes the partial text.
|
|
41
|
+
this.turnText = "";
|
|
42
|
+
break;
|
|
43
|
+
case "tool_start":
|
|
44
|
+
this.toolCalls += 1;
|
|
45
|
+
break;
|
|
46
|
+
case "turn_end":
|
|
47
|
+
// Usage reaches AgentEvent only on turn_end (the stream-level
|
|
48
|
+
// "usage" chunk is aggregated into it by the agent loop).
|
|
49
|
+
if (this.turnText.trim())
|
|
50
|
+
this.lastTurnText = this.turnText;
|
|
51
|
+
if (event.usage)
|
|
52
|
+
this.addUsage(event.usage);
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
addUsage(usage) {
|
|
59
|
+
this.usageReported = true;
|
|
60
|
+
this.usage.input_tokens += usage.promptTokens ?? 0;
|
|
61
|
+
this.usage.cache_read_input_tokens += usage.promptCacheHitTokens ?? 0;
|
|
62
|
+
this.usage.output_tokens += usage.completionTokens ?? 0;
|
|
63
|
+
this.usage.reasoning_tokens += usage.reasoningTokens ?? 0;
|
|
64
|
+
this.usage.total_tokens += usage.totalTokens
|
|
65
|
+
?? ((usage.promptTokens ?? 0) + (usage.completionTokens ?? 0));
|
|
66
|
+
}
|
|
67
|
+
summary() {
|
|
68
|
+
return {
|
|
69
|
+
text: (this.lastTurnText || this.turnText).trim(),
|
|
70
|
+
num_turns: this.turns,
|
|
71
|
+
num_tool_calls: this.toolCalls,
|
|
72
|
+
usage: { ...this.usage },
|
|
73
|
+
usage_reported: this.usageReported,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function formatPrintJson(input) {
|
|
78
|
+
return JSON.stringify({
|
|
79
|
+
text: input.summary.text,
|
|
80
|
+
stopReason: input.stopReason ?? "end_turn",
|
|
81
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
82
|
+
num_turns: input.summary.num_turns,
|
|
83
|
+
num_tool_calls: input.summary.num_tool_calls,
|
|
84
|
+
usage: input.summary.usage,
|
|
85
|
+
usage_reported: input.summary.usage_reported,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function formatPrintJsonError(input) {
|
|
89
|
+
return JSON.stringify({
|
|
90
|
+
type: "error",
|
|
91
|
+
message: input.message,
|
|
92
|
+
stopReason: "error",
|
|
93
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
94
|
+
...(input.summary
|
|
95
|
+
? {
|
|
96
|
+
text: input.summary.text,
|
|
97
|
+
num_turns: input.summary.num_turns,
|
|
98
|
+
num_tool_calls: input.summary.num_tool_calls,
|
|
99
|
+
usage: input.summary.usage,
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -51,9 +51,12 @@ earlier work lives in an existing subagent, prefer send_input to resume it
|
|
|
51
51
|
over spawning a fresh one.
|
|
52
52
|
|
|
53
53
|
Do NOT delegate when:
|
|
54
|
-
- The
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
- The edits are ENTANGLED — shared state, one file feeding the next, order
|
|
55
|
+
matters. Write delegation exists (the builtin "implementer" profile runs
|
|
56
|
+
in an isolated git worktree), but it only pays off for INDEPENDENT edit
|
|
57
|
+
groups with disjoint file sets. Worktree children fork from the last
|
|
58
|
+
commit and the parent reviews/applies their changes afterward; entangled
|
|
59
|
+
work through that contract only adds merge risk — do it yourself.
|
|
57
60
|
- The task takes one or two tool calls (reading a single file, looking up one
|
|
58
61
|
definition). The handoff overhead costs more than the task.
|
|
59
62
|
- Doing it well depends on conversation context (preferences the user stated
|
|
@@ -23,4 +23,21 @@ export declare function reminderForTaskType(taskType: TaskType, options?: TaskRe
|
|
|
23
23
|
* this, 2026-07-12 live case). Silent on no match: zero standing noise.
|
|
24
24
|
*/
|
|
25
25
|
export declare function userNamedModelReminder(input: string | import("../types.js").ContentPart[], routableModels: RoutableModelEntry[] | undefined, canSpawn: boolean): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Large-change checkpoint (large-task-delegation design §3): injected once
|
|
28
|
+
* per session at the first mutation of a broad turn. Carries the routing
|
|
29
|
+
* decision criteria (independent vs entangled) and the real worktree
|
|
30
|
+
* mechanics (children fork from the last commit; disjoint file sets) —
|
|
31
|
+
* never a command. The "smallest coherent edit" clause is lifted verbatim
|
|
32
|
+
* from the implementation-workflow reminder so the two compose.
|
|
33
|
+
*/
|
|
34
|
+
export interface LargeTaskReminderInput {
|
|
35
|
+
exploredFiles: number;
|
|
36
|
+
pendingTodos: number;
|
|
37
|
+
/** Mutations already applied this turn (the current batch lands regardless). */
|
|
38
|
+
appliedEdits: number;
|
|
39
|
+
/** orchestrationRequestReminder fired this turn: only offer run_workflow. */
|
|
40
|
+
orchestrationRequested: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function largeImplementationTaskReminder(input: LargeTaskReminderInput): string;
|
|
26
43
|
export declare function orchestrationRequestReminder(input: string | import("../types.js").ContentPart[], canRunWorkflow: boolean): string | undefined;
|
|
@@ -119,6 +119,21 @@ export function userNamedModelReminder(input, routableModels, canSpawn) {
|
|
|
119
119
|
"Use these exact ids in spawn_agent / run_workflow `model` params; do not retype model ids from memory.",
|
|
120
120
|
].join("\n");
|
|
121
121
|
}
|
|
122
|
+
export function largeImplementationTaskReminder(input) {
|
|
123
|
+
const plan = input.pendingTodos > 0 ? ` (plan: ${input.pendingTodos} open todo items)` : "";
|
|
124
|
+
const applied = input.appliedEdits > 0
|
|
125
|
+
? ` ${input.appliedEdits} edit${input.appliedEdits === 1 ? "" : "s"} from your current batch will land regardless.`
|
|
126
|
+
: "";
|
|
127
|
+
const mechanism = input.orchestrationRequested
|
|
128
|
+
? "one run_workflow script (the user asked for a coordinated workflow — keep everything in it)"
|
|
129
|
+
: "write_worktree subagents (spawn_agent with the implementer profile) or one run_workflow script";
|
|
130
|
+
return wrapInSystemReminder(`
|
|
131
|
+
Large-change checkpoint: you have read ${input.exploredFiles} files this turn${plan} and are starting to edit.${applied}
|
|
132
|
+
- If the REMAINING edits form INDEPENDENT groups (per-module, per-file, same shape repeated), split them across ${mechanism}. Assign each child a DISJOINT file set; keep shared files (types, registries, exports) for yourself. Children fork from the last COMMIT — commit your applied edits first, or fold them into the child briefings. Each child still makes the smallest coherent edit for its group.
|
|
133
|
+
- If the edits are ENTANGLED (shared state, one file feeding the next, order matters), delegation only adds merge risk — proceed yourself.
|
|
134
|
+
- Work already small enough to finish directly: just finish it.
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
122
137
|
const ORCHESTRATION_REQUEST = /\b(?:workflows?|orchestrat\w*|agent[ -]?teams?|fan[ -]?out)\b|工作流|编排|(?:智能体|代理|agent)\s*(?:团队|小队)/i;
|
|
123
138
|
export function orchestrationRequestReminder(input, canRunWorkflow) {
|
|
124
139
|
if (!canRunWorkflow)
|