@bubblebrain-ai/bubble 0.0.37 → 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 +4 -0
- package/dist/agent/profiles.d.ts +6 -0
- package/dist/agent/profiles.js +32 -0
- package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
- package/dist/agent/task-lifecycle-reminder.js +59 -0
- package/dist/agent.d.ts +43 -0
- package/dist/agent.js +70 -0
- package/dist/approval/controller.d.ts +3 -1
- package/dist/approval/controller.js +5 -2
- package/dist/approval/tool-helper.js +10 -1
- package/dist/approval/types.d.ts +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -0
- package/dist/loop/engine.d.ts +29 -0
- package/dist/loop/engine.js +63 -0
- package/dist/main.js +92 -24
- package/dist/orchestrator/default-hooks.js +94 -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/dist/session-types.d.ts +1 -1
- package/dist/tasks/manager.d.ts +136 -0
- package/dist/tasks/manager.js +530 -0
- package/dist/tasks/process-utils.d.ts +10 -0
- package/dist/tasks/process-utils.js +49 -0
- package/dist/tasks/promotion.d.ts +17 -0
- package/dist/tasks/promotion.js +30 -0
- package/dist/tasks/wake.d.ts +53 -0
- package/dist/tasks/wake.js +125 -0
- package/dist/tools/bash.d.ts +15 -1
- package/dist/tools/bash.js +103 -3
- package/dist/tools/child-tools.js +3 -1
- package/dist/tools/edit.js +4 -12
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +8 -1
- package/dist/tools/server-manager.d.ts +6 -36
- package/dist/tools/server-manager.js +6 -234
- package/dist/tools/task-tools.d.ts +10 -0
- package/dist/tools/task-tools.js +129 -0
- package/dist/tools/write.js +6 -12
- package/dist/tui-ink/app.d.ts +20 -2
- package/dist/tui-ink/app.js +307 -19
- package/dist/tui-ink/approval/approval-dialog.js +4 -4
- package/dist/tui-ink/display-history.d.ts +1 -1
- package/dist/tui-ink/footer.d.ts +3 -3
- package/dist/tui-ink/footer.js +3 -3
- package/dist/tui-ink/input-box.d.ts +6 -0
- package/dist/tui-ink/markdown.d.ts +10 -2
- package/dist/tui-ink/markdown.js +13 -12
- package/dist/tui-ink/message-list.js +14 -3
- package/dist/tui-ink/run.d.ts +6 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-inspector.d.ts +5 -1
- package/dist/tui-ink/subagent-inspector.js +23 -7
- package/dist/tui-ink/welcome.js +4 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -18,6 +18,9 @@ import { createRoutableModelIndex, createRoutingSnapshotAccessor } from "./agent
|
|
|
18
18
|
import { buildModelRoutingPrompt } from "./prompt/routing.js";
|
|
19
19
|
import { SkillRegistry } from "./skills/registry.js";
|
|
20
20
|
import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
|
|
21
|
+
import { getProcessManager } from "./tasks/manager.js";
|
|
22
|
+
import { PromotionChannel } from "./tasks/promotion.js";
|
|
23
|
+
import { PrintRunCollector, formatPrintJson, formatPrintJsonError } from "./print-output.js";
|
|
21
24
|
import { FileStateTracker } from "./tools/file-state.js";
|
|
22
25
|
import { GoalStore } from "./goal/store.js";
|
|
23
26
|
import { PermissionAwareApprovalController } from "./approval/controller.js";
|
|
@@ -165,6 +168,13 @@ async function main() {
|
|
|
165
168
|
// Shared between the goal tool (model-facing update_goal) and the
|
|
166
169
|
// TUI's auto-continuation engine / status-line indicator.
|
|
167
170
|
const goalStore = new GoalStore();
|
|
171
|
+
// Background tasks are TUI-only (design §2.0): print mode exits (and reaps)
|
|
172
|
+
// right after the single run, so a backgrounded command would be killed
|
|
173
|
+
// immediately after the model was told it started. Feishu/desktop hosts
|
|
174
|
+
// never reach this call site and stay off until they wire wake seams.
|
|
175
|
+
const processManager = getProcessManager();
|
|
176
|
+
const allowBackgroundTasks = !printMode;
|
|
177
|
+
const promotionChannel = new PromotionChannel();
|
|
168
178
|
const tools = createAllTools(args.cwd, skillRegistry, {
|
|
169
179
|
todoStore,
|
|
170
180
|
planController,
|
|
@@ -174,6 +184,9 @@ async function main() {
|
|
|
174
184
|
lspService,
|
|
175
185
|
fileStateTracker,
|
|
176
186
|
goalStore,
|
|
187
|
+
processManager,
|
|
188
|
+
allowBackgroundTasks,
|
|
189
|
+
promotionChannel,
|
|
177
190
|
// Lazy: sessionManager is resolved after tools are created.
|
|
178
191
|
checkpoints: () => sessionManager?.getCheckpoints(),
|
|
179
192
|
});
|
|
@@ -222,6 +235,9 @@ async function main() {
|
|
|
222
235
|
}
|
|
223
236
|
};
|
|
224
237
|
const shutdownSidecars = async () => {
|
|
238
|
+
// Signal path (design §2.2b layer 2): kill-only, no escalation wait —
|
|
239
|
+
// the process is about to exit and shutdownRuntime never runs here.
|
|
240
|
+
processManager.reapTasksSync();
|
|
225
241
|
await Promise.allSettled([shutdownMcp(), shutdownExternalRuntime()]);
|
|
226
242
|
};
|
|
227
243
|
process.once("SIGINT", () => { void shutdownSidecars().then(() => process.exit(130)); });
|
|
@@ -433,6 +449,15 @@ async function main() {
|
|
|
433
449
|
externalHooks: hookController,
|
|
434
450
|
});
|
|
435
451
|
agentRef = agent;
|
|
452
|
+
if (allowBackgroundTasks) {
|
|
453
|
+
// Reminder bridge (design §2.3a): list() is owner-filtered at call time so
|
|
454
|
+
// the reminder never leaks another session's tasks after a switch.
|
|
455
|
+
agent.backgroundTasks = {
|
|
456
|
+
list: () => processManager.listTasks(agent.getSessionID()),
|
|
457
|
+
version: () => processManager.getTaskStateVersion(),
|
|
458
|
+
outputTail: (id) => processManager.taskOutputTail(id, 2000),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
436
461
|
if (sessionManager) {
|
|
437
462
|
sessionTitleUpdater = createSessionTitleUpdater({
|
|
438
463
|
sessionManager,
|
|
@@ -486,6 +511,8 @@ async function main() {
|
|
|
486
511
|
shutdownMcp(),
|
|
487
512
|
shutdownExternalRuntime(),
|
|
488
513
|
lspService.shutdown(),
|
|
514
|
+
// Graceful task reaping (design §2.2b layer 1): SIGTERM → SIGKILL.
|
|
515
|
+
processManager.shutdownTasks(),
|
|
489
516
|
]);
|
|
490
517
|
for (const result of results) {
|
|
491
518
|
if (result.status === "rejected") {
|
|
@@ -526,7 +553,8 @@ async function main() {
|
|
|
526
553
|
agent.injectModeReminder();
|
|
527
554
|
}
|
|
528
555
|
agent.injectDeferredToolsReminder();
|
|
529
|
-
|
|
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()}`));
|
|
530
558
|
}
|
|
531
559
|
}
|
|
532
560
|
try {
|
|
@@ -542,33 +570,70 @@ async function main() {
|
|
|
542
570
|
console.error(chalk.red("Error: No prompt provided."));
|
|
543
571
|
process.exit(1);
|
|
544
572
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
+
}
|
|
562
614
|
}
|
|
563
|
-
|
|
564
|
-
|
|
615
|
+
if (jsonOutput) {
|
|
616
|
+
process.stdout.write(formatPrintJson({
|
|
617
|
+
summary: collector.summary(),
|
|
618
|
+
sessionId: printSessionId,
|
|
619
|
+
}) + "\n");
|
|
565
620
|
}
|
|
566
|
-
else
|
|
567
|
-
|
|
568
|
-
console.log(color(`[Result: ${event.result.content.slice(0, 200)}${event.result.content.length > 200 ? "..." : ""}]`));
|
|
621
|
+
else {
|
|
622
|
+
console.log();
|
|
569
623
|
}
|
|
570
624
|
}
|
|
571
|
-
|
|
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
|
+
}
|
|
572
637
|
return;
|
|
573
638
|
}
|
|
574
639
|
const themeConfig = userConfig.getTheme();
|
|
@@ -653,6 +718,9 @@ async function main() {
|
|
|
653
718
|
runMemoryCompaction,
|
|
654
719
|
runMemorySummary,
|
|
655
720
|
runMemoryRefresh,
|
|
721
|
+
processManager,
|
|
722
|
+
tasksAutoResume: userConfig.getTasksAutoResume(),
|
|
723
|
+
promotionChannel,
|
|
656
724
|
};
|
|
657
725
|
// Grok Subscription is a first-class interactive provider target. Manager
|
|
658
726
|
// construction is deliberately lazy: it does not inspect, spawn, or reach
|
|
@@ -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
|
|
@@ -53,6 +59,14 @@ export function createDefaultHooks() {
|
|
|
53
59
|
ctx.state.workflowPhase = "investigate";
|
|
54
60
|
ctx.state.workflowKey = "";
|
|
55
61
|
}
|
|
62
|
+
// Background-task truth at turn start: completions that landed while
|
|
63
|
+
// the agent was idle (or during another session's turn) reach the
|
|
64
|
+
// model here; the same state-change gate prevents duplicates with the
|
|
65
|
+
// beforeContinuation emission (background-tasks design §2.3a).
|
|
66
|
+
const backgroundTaskReminder = ctx.agent.consumeBackgroundTaskReminder?.();
|
|
67
|
+
if (backgroundTaskReminder) {
|
|
68
|
+
ctx.queueReminder(backgroundTaskReminder);
|
|
69
|
+
}
|
|
56
70
|
for (const reminder of ctx.state.governor.consumePendingReminders()) {
|
|
57
71
|
ctx.queueReminder(reminder);
|
|
58
72
|
}
|
|
@@ -89,6 +103,12 @@ export function createDefaultHooks() {
|
|
|
89
103
|
const toolCall = { ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) };
|
|
90
104
|
ctx.replaceToolCall(toolCall);
|
|
91
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
|
+
}
|
|
92
112
|
const blockedResult = ctx.state.discoveryBarrier?.beforeToolCall(toolCall);
|
|
93
113
|
if (blockedResult)
|
|
94
114
|
ctx.blockToolCall(blockedResult);
|
|
@@ -145,6 +165,18 @@ export function createDefaultHooks() {
|
|
|
145
165
|
// dedup do the work.
|
|
146
166
|
if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
|
|
147
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
|
+
}
|
|
148
180
|
}
|
|
149
181
|
// Removed: active verification tracking. The previous design nagged the
|
|
150
182
|
// model every turn until it ran a recognised verification command, and
|
|
@@ -167,6 +199,14 @@ export function createDefaultHooks() {
|
|
|
167
199
|
ctx.queueReminder(reminder);
|
|
168
200
|
}
|
|
169
201
|
}
|
|
202
|
+
// Background-task truth: state-change gated inside the agent (a task
|
|
203
|
+
// started/finished/killed since the last emission), so a mid-turn
|
|
204
|
+
// completion reaches the model without stacking per-call duplicates
|
|
205
|
+
// (background-tasks design §2.3a).
|
|
206
|
+
const taskReminder = ctx.agent.consumeBackgroundTaskReminder?.();
|
|
207
|
+
if (taskReminder) {
|
|
208
|
+
ctx.queueReminder(taskReminder);
|
|
209
|
+
}
|
|
170
210
|
// Routing detector note (model-routing design §6): rides the same
|
|
171
211
|
// channel as the lifecycle reminder, once per session. Consuming also
|
|
172
212
|
// closes the per-fan-out counting window.
|
|
@@ -190,6 +230,59 @@ export function createDefaultHooks() {
|
|
|
190
230
|
function markCodeChanged(state) {
|
|
191
231
|
state.codeChanged = true;
|
|
192
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
|
+
}
|
|
193
286
|
function isCodeWriteResult(_toolCall, result) {
|
|
194
287
|
if (result.isError || result.status === "blocked" || result.status === "cancelled" || result.status === "command_error") {
|
|
195
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)
|
package/dist/session-types.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface SessionMetadata {
|
|
|
31
31
|
*/
|
|
32
32
|
externalRuntime?: ExternalRuntimeSessionMetadata;
|
|
33
33
|
}
|
|
34
|
-
export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch" | "runtime_switch" | "conversation_clear";
|
|
34
|
+
export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch" | "runtime_switch" | "conversation_clear" | "task_started" | "task_finished" | "task_killed";
|
|
35
35
|
interface BaseSessionLogEntry {
|
|
36
36
|
id: string;
|
|
37
37
|
timestamp: number;
|