@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.
Files changed (61) hide show
  1. package/dist/agent/internal-reminder-sanitizer.js +4 -0
  2. package/dist/agent/profiles.d.ts +6 -0
  3. package/dist/agent/profiles.js +32 -0
  4. package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
  5. package/dist/agent/task-lifecycle-reminder.js +59 -0
  6. package/dist/agent.d.ts +43 -0
  7. package/dist/agent.js +70 -0
  8. package/dist/approval/controller.d.ts +3 -1
  9. package/dist/approval/controller.js +5 -2
  10. package/dist/approval/tool-helper.js +10 -1
  11. package/dist/approval/types.d.ts +15 -0
  12. package/dist/cli.d.ts +2 -0
  13. package/dist/cli.js +11 -0
  14. package/dist/config.d.ts +6 -0
  15. package/dist/config.js +4 -0
  16. package/dist/loop/engine.d.ts +29 -0
  17. package/dist/loop/engine.js +63 -0
  18. package/dist/main.js +92 -24
  19. package/dist/orchestrator/default-hooks.js +94 -1
  20. package/dist/orchestrator/hooks.d.ts +7 -0
  21. package/dist/print-output.d.ts +54 -0
  22. package/dist/print-output.js +103 -0
  23. package/dist/prompt/delegation.js +6 -3
  24. package/dist/prompt/task-reminders.d.ts +17 -0
  25. package/dist/prompt/task-reminders.js +15 -0
  26. package/dist/session-types.d.ts +1 -1
  27. package/dist/tasks/manager.d.ts +136 -0
  28. package/dist/tasks/manager.js +530 -0
  29. package/dist/tasks/process-utils.d.ts +10 -0
  30. package/dist/tasks/process-utils.js +49 -0
  31. package/dist/tasks/promotion.d.ts +17 -0
  32. package/dist/tasks/promotion.js +30 -0
  33. package/dist/tasks/wake.d.ts +53 -0
  34. package/dist/tasks/wake.js +125 -0
  35. package/dist/tools/bash.d.ts +15 -1
  36. package/dist/tools/bash.js +103 -3
  37. package/dist/tools/child-tools.js +3 -1
  38. package/dist/tools/edit.js +4 -12
  39. package/dist/tools/index.d.ts +13 -0
  40. package/dist/tools/index.js +8 -1
  41. package/dist/tools/server-manager.d.ts +6 -36
  42. package/dist/tools/server-manager.js +6 -234
  43. package/dist/tools/task-tools.d.ts +10 -0
  44. package/dist/tools/task-tools.js +129 -0
  45. package/dist/tools/write.js +6 -12
  46. package/dist/tui-ink/app.d.ts +20 -2
  47. package/dist/tui-ink/app.js +307 -19
  48. package/dist/tui-ink/approval/approval-dialog.js +4 -4
  49. package/dist/tui-ink/display-history.d.ts +1 -1
  50. package/dist/tui-ink/footer.d.ts +3 -3
  51. package/dist/tui-ink/footer.js +3 -3
  52. package/dist/tui-ink/input-box.d.ts +6 -0
  53. package/dist/tui-ink/markdown.d.ts +10 -2
  54. package/dist/tui-ink/markdown.js +13 -12
  55. package/dist/tui-ink/message-list.js +14 -3
  56. package/dist/tui-ink/run.d.ts +6 -0
  57. package/dist/tui-ink/run.js +1 -1
  58. package/dist/tui-ink/subagent-inspector.d.ts +5 -1
  59. package/dist/tui-ink/subagent-inspector.js +23 -7
  60. package/dist/tui-ink/welcome.js +4 -0
  61. 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
- console.log(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
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
- let printedTurnText = false;
546
- for await (const event of agent.run(prompt, args.cwd)) {
547
- traceEvent("print_agent_event", summarizeAgentEventForTrace(event));
548
- if (event.type === "turn_start") {
549
- printedTurnText = false;
550
- }
551
- else if (event.type === "provider_retry") {
552
- // The stream died mid-response and the agent re-issues the whole
553
- // request. Text already on stdout cannot be un-printed, so at least
554
- // separate the retried response and say what happened.
555
- if (printedTurnText)
556
- process.stdout.write("\n");
557
- console.error(chalk.yellow(`[Stream interrupted; retrying (${event.attempt}/${event.maxAttempts}) — the partial text above is superseded by the retried response]`));
558
- }
559
- else if (event.type === "text_delta") {
560
- printedTurnText = true;
561
- process.stdout.write(event.content);
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
- else if (event.type === "tool_start") {
564
- console.log(chalk.cyan(`\n[Tool: ${event.name}]`));
615
+ if (jsonOutput) {
616
+ process.stdout.write(formatPrintJson({
617
+ summary: collector.summary(),
618
+ sessionId: printSessionId,
619
+ }) + "\n");
565
620
  }
566
- else if (event.type === "tool_end") {
567
- const color = event.result.isError ? chalk.red : chalk.dim;
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
- console.log();
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 task requires editing files or running state-changing commands.
55
- Built-in subagents are read-only; do edits and writes yourself unless a
56
- write-capable (write_worktree) profile is explicitly available.
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)
@@ -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;