@bubblebrain-ai/bubble 0.0.46 → 0.0.48

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 (95) hide show
  1. package/dist/agent/child-runner.js +11 -0
  2. package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
  3. package/dist/agent/internal-reminder-sanitizer.js +13 -0
  4. package/dist/agent/subagent/router.d.ts +65 -0
  5. package/dist/agent/subagent/router.js +198 -0
  6. package/dist/agent/subagent/runtime.d.ts +203 -0
  7. package/dist/agent/subagent/runtime.js +789 -0
  8. package/dist/agent/subagent-control.d.ts +5 -0
  9. package/dist/agent/subagent-store.d.ts +18 -2
  10. package/dist/agent/subagent-store.js +32 -1
  11. package/dist/agent/tool-intent.d.ts +10 -0
  12. package/dist/agent/tool-intent.js +18 -0
  13. package/dist/agent/tool-visibility.d.ts +10 -0
  14. package/dist/agent/tool-visibility.js +6 -0
  15. package/dist/agent/workflow/runs.d.ts +72 -0
  16. package/dist/agent/workflow/runs.js +157 -0
  17. package/dist/agent.d.ts +78 -96
  18. package/dist/agent.js +266 -1072
  19. package/dist/context/compact-llm.js +14 -5
  20. package/dist/context/compact.d.ts +9 -0
  21. package/dist/context/compact.js +36 -10
  22. package/dist/context/compaction-files.d.ts +44 -0
  23. package/dist/context/compaction-files.js +148 -0
  24. package/dist/context/llm-compactor.js +24 -5
  25. package/dist/feishu/agent-host/run-driver.js +3 -0
  26. package/dist/feishu/serve.js +13 -9
  27. package/dist/goal/tools.js +12 -3
  28. package/dist/main.js +24 -19
  29. package/dist/model-catalog.js +36 -2
  30. package/dist/model-config.d.ts +2 -0
  31. package/dist/model-data.generated.d.ts +16 -0
  32. package/dist/model-data.generated.js +275 -0
  33. package/dist/model-pricing.d.ts +11 -7
  34. package/dist/model-pricing.js +0 -0
  35. package/dist/oauth/grok.d.ts +1 -1
  36. package/dist/oauth/grok.js +5 -2
  37. package/dist/oauth/storage.d.ts +9 -2
  38. package/dist/oauth/storage.js +16 -9
  39. package/dist/orchestrator/default-hooks.d.ts +20 -0
  40. package/dist/orchestrator/default-hooks.js +47 -295
  41. package/dist/orchestrator/hooks.d.ts +6 -39
  42. package/dist/permission/mode.js +1 -1
  43. package/dist/prompt/reminders.d.ts +9 -35
  44. package/dist/prompt/reminders.js +30 -136
  45. package/dist/prompt/task-reminders.d.ts +0 -31
  46. package/dist/prompt/task-reminders.js +0 -87
  47. package/dist/provider-ai-sdk.d.ts +2 -0
  48. package/dist/provider-ai-sdk.js +1 -0
  49. package/dist/provider-anthropic.d.ts +2 -0
  50. package/dist/provider-anthropic.js +3 -1
  51. package/dist/provider-ark-responses.d.ts +2 -0
  52. package/dist/provider-ark-responses.js +1 -0
  53. package/dist/provider-grok.d.ts +26 -0
  54. package/dist/provider-grok.js +39 -0
  55. package/dist/provider-registry.d.ts +14 -1
  56. package/dist/provider-registry.js +131 -3
  57. package/dist/provider-transform.js +37 -1
  58. package/dist/provider.d.ts +7 -0
  59. package/dist/provider.js +13 -1
  60. package/dist/sdk/index.d.ts +152 -0
  61. package/dist/sdk/index.js +400 -0
  62. package/dist/session.js +9 -1
  63. package/dist/slash-commands/commands.js +33 -19
  64. package/dist/tools/exit-plan-mode.js +8 -0
  65. package/dist/tui/detect-theme.d.ts +1 -0
  66. package/dist/tui/detect-theme.js +11 -5
  67. package/dist/tui-ink/app.d.ts +2 -6
  68. package/dist/tui-ink/app.js +11 -18
  69. package/dist/tui-ink/display-history.d.ts +0 -2
  70. package/dist/tui-ink/display-history.js +0 -52
  71. package/dist/tui-ink/footer.js +2 -2
  72. package/dist/tui-ink/input-box.js +18 -6
  73. package/dist/tui-ink/message-list.js +62 -23
  74. package/dist/tui-ink/run.js +10 -5
  75. package/dist/tui-ink/sync-output.d.ts +17 -0
  76. package/dist/tui-ink/sync-output.js +35 -0
  77. package/dist/tui-ink/theme.d.ts +0 -1
  78. package/dist/tui-ink/theme.js +0 -2
  79. package/dist/tui-ink/trace-groups.js +1 -1
  80. package/dist/types.d.ts +21 -1
  81. package/package.json +16 -2
  82. package/dist/agent/discovery-barrier.d.ts +0 -21
  83. package/dist/agent/discovery-barrier.js +0 -173
  84. package/dist/agent/evidence-tracker.d.ts +0 -15
  85. package/dist/agent/evidence-tracker.js +0 -93
  86. package/dist/agent/execution-governor.d.ts +0 -36
  87. package/dist/agent/execution-governor.js +0 -218
  88. package/dist/agent/task-classifier.d.ts +0 -3
  89. package/dist/agent/task-classifier.js +0 -114
  90. package/dist/agent/task-size.d.ts +0 -9
  91. package/dist/agent/task-size.js +0 -33
  92. package/dist/agent/tool-arbiter.d.ts +0 -7
  93. package/dist/agent/tool-arbiter.js +0 -33
  94. package/dist/orchestrator/workflow.d.ts +0 -10
  95. package/dist/orchestrator/workflow.js +0 -22
@@ -61,6 +61,11 @@ export interface SubagentThreadRecord {
61
61
  /** True for agents spawned inside a run_workflow: kept out of list_agents /
62
62
  * wait_agent and not persisted (design v2 appendix; option C). */
63
63
  workflowInternal?: boolean;
64
+ /** A SubagentStart hook fired for the current logical run and its paired
65
+ * SubagentStop has not fired yet (design §9). Lets terminal paths that
66
+ * assume "the run never started" — cancel during a retry backoff — close
67
+ * the pair instead of leaking an unmatched Start. */
68
+ hookStopPending?: boolean;
64
69
  abortController: AbortController;
65
70
  waiters: Set<() => void>;
66
71
  agent?: {
@@ -13,9 +13,25 @@
13
13
  */
14
14
  import { type SubagentThreadRecord, type SubagentThreadStatus } from "./subagent-control.js";
15
15
  export declare class SubagentStore {
16
- private readonly persistDir?;
17
16
  private readonly threads;
18
- constructor(persistDir?: string | undefined);
17
+ private persistDir?;
18
+ constructor(persistDir?: string);
19
+ /**
20
+ * Repoints persistence at a different session's directory (the TUI reuses
21
+ * one Agent across session switches; see Agent.setSessionID).
22
+ *
23
+ * Final non-workflow records are evicted first: they are already persisted
24
+ * in the OLD directory and reloadable by switching back, and keeping them
25
+ * would leak one session's children into another's list_agents — growing
26
+ * monotonically with every switch. Live records stay (their runs are still
27
+ * attached to this process; their eventual persist lands in the new
28
+ * directory — an accepted tradeoff, preferable to losing them). Workflow-
29
+ * internal records stay because a running workflow's schema-correction
30
+ * retry looks its members up in this store by agentId.
31
+ *
32
+ * `undefined` disables persistence and loads nothing; threads are kept.
33
+ */
34
+ repoint(persistDir: string | undefined): void;
19
35
  get(agentId: string): SubagentThreadRecord | undefined;
20
36
  set(record: SubagentThreadRecord): void;
21
37
  values(): SubagentThreadRecord[];
@@ -16,11 +16,37 @@ import { join } from "node:path";
16
16
  import { isFinalSubagentThreadStatus, } from "./subagent-control.js";
17
17
  const PERSIST_SCHEMA_VERSION = 1;
18
18
  export class SubagentStore {
19
- persistDir;
20
19
  threads = new Map();
20
+ persistDir;
21
21
  constructor(persistDir) {
22
22
  this.persistDir = persistDir;
23
23
  }
24
+ /**
25
+ * Repoints persistence at a different session's directory (the TUI reuses
26
+ * one Agent across session switches; see Agent.setSessionID).
27
+ *
28
+ * Final non-workflow records are evicted first: they are already persisted
29
+ * in the OLD directory and reloadable by switching back, and keeping them
30
+ * would leak one session's children into another's list_agents — growing
31
+ * monotonically with every switch. Live records stay (their runs are still
32
+ * attached to this process; their eventual persist lands in the new
33
+ * directory — an accepted tradeoff, preferable to losing them). Workflow-
34
+ * internal records stay because a running workflow's schema-correction
35
+ * retry looks its members up in this store by agentId.
36
+ *
37
+ * `undefined` disables persistence and loads nothing; threads are kept.
38
+ */
39
+ repoint(persistDir) {
40
+ if (persistDir === this.persistDir)
41
+ return;
42
+ for (const [agentId, record] of this.threads) {
43
+ if (isFinalSubagentThreadStatus(record.status) && !record.workflowInternal) {
44
+ this.threads.delete(agentId);
45
+ }
46
+ }
47
+ this.persistDir = persistDir;
48
+ this.loadPersisted();
49
+ }
24
50
  get(agentId) {
25
51
  return this.threads.get(agentId);
26
52
  }
@@ -62,6 +88,11 @@ export class SubagentStore {
62
88
  return;
63
89
  if (!isFinalSubagentThreadStatus(record.status))
64
90
  return;
91
+ // Workflow-internal members never persist (design: they never re-import
92
+ // on restart). The onFinal gate alone was not enough — markDelivered
93
+ // reaches persist too (known-defects #5) — so the store enforces it.
94
+ if (record.workflowInternal)
95
+ return;
65
96
  try {
66
97
  mkdirSync(this.persistDir, { recursive: true });
67
98
  const payload = {
@@ -24,6 +24,16 @@ export interface ParsedSearchCommand {
24
24
  pattern: string;
25
25
  path?: string;
26
26
  include?: string;
27
+ /**
28
+ * True only when a structured-grep rewrite would execute EXACTLY what the
29
+ * model asked for. False whenever anything was dropped or reinterpreted:
30
+ * unknown flags (-i, -A, -w, ...), --iglob (case-insensitivity lost),
31
+ * --include under rg (not an rg flag - a faithful run would error), more
32
+ * than one path, or the `grep` binary itself (BRE dialect vs the tool's
33
+ * rg engine). Observation (intent classification) tolerates lossy parses;
34
+ * execution rewriting must not (tool-arbiter).
35
+ */
36
+ lossless: boolean;
27
37
  }
28
38
  export interface ParsedReadCommand {
29
39
  path: string;
@@ -79,6 +79,10 @@ export function parseSearchBashCommand(command) {
79
79
  if (!["grep", "rg", "ripgrep"].includes(binary)) {
80
80
  return undefined;
81
81
  }
82
+ // The structured grep tool shells out to rg, so only rg-origin commands
83
+ // can be dialect-identical; GNU grep patterns are BRE and must not be
84
+ // silently re-executed under rg's Rust regex engine.
85
+ let lossless = binary !== "grep";
82
86
  const positional = [];
83
87
  let include;
84
88
  for (let index = 1; index < tokens.length; index++) {
@@ -87,14 +91,24 @@ export function parseSearchBashCommand(command) {
87
91
  continue;
88
92
  if (token === "--glob" || token === "--iglob" || token === "--include") {
89
93
  include = tokens[index + 1];
94
+ // --iglob loses case-insensitivity in translation; --include is not an
95
+ // rg flag at all (a faithful run would error, and hiding that from the
96
+ // model masks its mistake). Only --glob maps 1:1 onto the tool.
97
+ if (token !== "--glob")
98
+ lossless = false;
90
99
  index += 1;
91
100
  continue;
92
101
  }
93
102
  if (token.startsWith("--glob=") || token.startsWith("--iglob=") || token.startsWith("--include=")) {
94
103
  include = token.slice(token.indexOf("=") + 1);
104
+ if (!token.startsWith("--glob="))
105
+ lossless = false;
95
106
  continue;
96
107
  }
97
108
  if (token.startsWith("-")) {
109
+ // Any other flag (-i, -A 3, -w, -l, ...) has no representation in the
110
+ // structured tool; dropping it would change the search's meaning.
111
+ lossless = false;
98
112
  continue;
99
113
  }
100
114
  positional.push(token);
@@ -102,11 +116,15 @@ export function parseSearchBashCommand(command) {
102
116
  if (positional.length === 0) {
103
117
  return undefined;
104
118
  }
119
+ // A third positional (multiple search roots) cannot be expressed either.
120
+ if (positional.length > 2)
121
+ lossless = false;
105
122
  const [pattern, maybePath] = positional;
106
123
  return {
107
124
  pattern,
108
125
  path: maybePath,
109
126
  include,
127
+ lossless,
110
128
  };
111
129
  }
112
130
  export function parseReadBashCommand(command) {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Transcript visibility of tool results.
3
+ *
4
+ * `hiddenFromTranscript` is a general mechanism: a hook may block a tool call
5
+ * and mark the exchange hidden so it never reaches the model's transcript or
6
+ * the UI. These helpers are the canonical way to test for it.
7
+ */
8
+ import type { ToolResult, ToolResultMetadata } from "../types.js";
9
+ export declare function isHiddenToolResult(result: ToolResult | undefined): result is ToolResult;
10
+ export declare function isHiddenToolMetadata(metadata: ToolResultMetadata | undefined): boolean;
@@ -0,0 +1,6 @@
1
+ export function isHiddenToolResult(result) {
2
+ return result?.metadata?.hiddenFromTranscript === true;
3
+ }
4
+ export function isHiddenToolMetadata(metadata) {
5
+ return metadata?.hiddenFromTranscript === true;
6
+ }
@@ -0,0 +1,72 @@
1
+ import { type WorkflowRunSnapshot } from "./control.js";
2
+ import type { AgentProfile } from "../profiles.js";
3
+ import type { SubagentThreadSnapshot } from "../subagent-control.js";
4
+ import type { ToolUpdate } from "../../types.js";
5
+ export interface WorkflowExecuteOptions {
6
+ script: string;
7
+ args?: unknown;
8
+ parentToolCallId: string;
9
+ abortSignal?: AbortSignal;
10
+ directEmit?: (update: ToolUpdate) => void;
11
+ queueUpdates?: boolean;
12
+ ensureProfileTrusted?: (profile: AgentProfile) => Promise<{
13
+ content: string | unknown;
14
+ } | undefined>;
15
+ }
16
+ export interface WorkflowExecuteOutcome {
17
+ result: {
18
+ ok: true;
19
+ value: unknown;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ };
24
+ agentCount: number;
25
+ logs: string[];
26
+ snapshots: SubagentThreadSnapshot[];
27
+ }
28
+ export interface WorkflowLedgerDeps {
29
+ /** Runs the script. Supplied by Agent — the ledger never executes anything itself. */
30
+ execute(cwd: string, options: WorkflowExecuteOptions): Promise<WorkflowExecuteOutcome>;
31
+ }
32
+ export declare class WorkflowLedger {
33
+ private readonly deps;
34
+ private readonly runs;
35
+ /** Completed runs whose result has not yet been announced to the parent. */
36
+ private readonly pendingDeliveries;
37
+ constructor(deps: WorkflowLedgerDeps);
38
+ hasRunning(): boolean;
39
+ /**
40
+ * Starts a workflow in the BACKGROUND: returns a runId immediately; the
41
+ * script runs detached, its agents stream progress through the queued
42
+ * channel (drained at turn boundaries like spawn_agent), and its result is
43
+ * ingested at the next turn. Collect explicitly with wait().
44
+ */
45
+ start(cwd: string, options: {
46
+ script: string;
47
+ args?: unknown;
48
+ title?: string;
49
+ parentToolCallId: string;
50
+ abortSignal?: AbortSignal;
51
+ ensureProfileTrusted?: (profile: AgentProfile) => Promise<{
52
+ content: string | unknown;
53
+ } | undefined>;
54
+ }): {
55
+ runId: string;
56
+ title: string;
57
+ };
58
+ /**
59
+ * Blocks until a background workflow reaches a final state (or times out).
60
+ * `timeoutMs` must already be normalized by the caller.
61
+ */
62
+ wait(runId: string, timeoutMs: number): Promise<WorkflowRunSnapshot | undefined>;
63
+ /** Cancels a running background workflow. */
64
+ close(runId: string): WorkflowRunSnapshot | undefined;
65
+ list(): WorkflowRunSnapshot[];
66
+ /**
67
+ * Completed background-workflow results to announce before the next turn
68
+ * (§5 analog). Marks each record delivered, so a notice is emitted once.
69
+ */
70
+ drainDeliveryNotices(): string[];
71
+ private snapshot;
72
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Background workflow run ledger (option C Phase 4).
3
+ *
4
+ * Owns run records, waiters, cancellation and the pending-delivery set —
5
+ * everything about tracking a detached workflow EXCEPT actually executing the
6
+ * script, which stays on Agent (it needs subagent dispatch, profiles and the
7
+ * concurrency gate) and arrives here as the single injected `execute` callback.
8
+ *
9
+ * Delivery notices are PULLED (`drainDeliveryNotices`) rather than pushed into
10
+ * the parent, matching ResultIntegrator.drainNotices: the ledger never holds a
11
+ * reference back to the agent.
12
+ */
13
+ import { mkdirSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { randomUUID } from "node:crypto";
16
+ import { getBubbleHome } from "../../bubble-home.js";
17
+ import { composeAbortSignals } from "../budget-ledger.js";
18
+ import { buildWorkflowDeliveryNotice, renderWorkflowResultValue } from "./control.js";
19
+ export class WorkflowLedger {
20
+ deps;
21
+ runs = new Map();
22
+ /** Completed runs whose result has not yet been announced to the parent. */
23
+ pendingDeliveries = new Set();
24
+ constructor(deps) {
25
+ this.deps = deps;
26
+ }
27
+ hasRunning() {
28
+ return [...this.runs.values()].some((record) => record.status === "running");
29
+ }
30
+ /**
31
+ * Starts a workflow in the BACKGROUND: returns a runId immediately; the
32
+ * script runs detached, its agents stream progress through the queued
33
+ * channel (drained at turn boundaries like spawn_agent), and its result is
34
+ * ingested at the next turn. Collect explicitly with wait().
35
+ */
36
+ start(cwd, options) {
37
+ const runId = randomUUID();
38
+ const abortController = new AbortController();
39
+ const composed = composeAbortSignals([options.abortSignal, abortController.signal]);
40
+ if (composed) {
41
+ composed.addEventListener("abort", () => abortController.abort(composed.reason), { once: true });
42
+ }
43
+ const record = {
44
+ runId,
45
+ title: options.title ?? "workflow",
46
+ status: "running",
47
+ agentCount: 0,
48
+ snapshots: [],
49
+ logs: [],
50
+ abortController,
51
+ waiters: new Set(),
52
+ createdAt: Date.now(),
53
+ parentToolCallId: options.parentToolCallId,
54
+ };
55
+ this.runs.set(runId, record);
56
+ record.promise = this.deps.execute(cwd, {
57
+ script: options.script,
58
+ args: options.args,
59
+ parentToolCallId: options.parentToolCallId,
60
+ abortSignal: abortController.signal,
61
+ queueUpdates: true,
62
+ ensureProfileTrusted: options.ensureProfileTrusted,
63
+ }).then((out) => {
64
+ record.agentCount = out.agentCount;
65
+ record.snapshots = out.snapshots;
66
+ record.logs = out.logs;
67
+ record.result = out.result;
68
+ record.status = out.result.ok ? "completed" : (abortController.signal.aborted ? "cancelled" : "failed");
69
+ if (out.result.ok)
70
+ record.resultPath = persistWorkflowResult(runId, out.result.value);
71
+ }, (error) => {
72
+ record.result = { ok: false, error: error?.message || String(error) };
73
+ record.status = "failed";
74
+ }).finally(() => {
75
+ record.updatedAt = Date.now();
76
+ this.pendingDeliveries.add(runId);
77
+ for (const waiter of record.waiters)
78
+ waiter();
79
+ record.waiters.clear();
80
+ });
81
+ return { runId, title: record.title };
82
+ }
83
+ /**
84
+ * Blocks until a background workflow reaches a final state (or times out).
85
+ * `timeoutMs` must already be normalized by the caller.
86
+ */
87
+ async wait(runId, timeoutMs) {
88
+ const record = this.runs.get(runId);
89
+ if (!record)
90
+ return undefined;
91
+ if (record.status === "running") {
92
+ let waiter;
93
+ await Promise.race([
94
+ new Promise((resolve) => { waiter = resolve; record.waiters.add(resolve); }),
95
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
96
+ ]).finally(() => { if (waiter)
97
+ record.waiters.delete(waiter); });
98
+ }
99
+ if (record.status !== "running")
100
+ this.pendingDeliveries.delete(runId);
101
+ return this.snapshot(record);
102
+ }
103
+ /** Cancels a running background workflow. */
104
+ close(runId) {
105
+ const record = this.runs.get(runId);
106
+ if (!record)
107
+ return undefined;
108
+ if (record.status === "running")
109
+ record.abortController.abort(new Error("workflow cancelled"));
110
+ return this.snapshot(record);
111
+ }
112
+ list() {
113
+ return [...this.runs.values()].map((record) => this.snapshot(record));
114
+ }
115
+ /**
116
+ * Completed background-workflow results to announce before the next turn
117
+ * (§5 analog). Marks each record delivered, so a notice is emitted once.
118
+ */
119
+ drainDeliveryNotices() {
120
+ if (this.pendingDeliveries.size === 0)
121
+ return [];
122
+ const notices = [];
123
+ for (const runId of [...this.pendingDeliveries]) {
124
+ this.pendingDeliveries.delete(runId);
125
+ const record = this.runs.get(runId);
126
+ if (!record || record.status === "running" || record.deliveredAt)
127
+ continue;
128
+ record.deliveredAt = Date.now();
129
+ notices.push(buildWorkflowDeliveryNotice(this.snapshot(record)));
130
+ }
131
+ return notices;
132
+ }
133
+ snapshot(record) {
134
+ return {
135
+ runId: record.runId,
136
+ title: record.title,
137
+ status: record.status,
138
+ agentCount: record.agentCount,
139
+ result: record.result,
140
+ resultPath: record.resultPath,
141
+ logs: record.logs,
142
+ snapshots: record.snapshots,
143
+ };
144
+ }
145
+ }
146
+ function persistWorkflowResult(runId, value) {
147
+ try {
148
+ const dir = join(getBubbleHome(), "workflows");
149
+ mkdirSync(dir, { recursive: true });
150
+ const path = join(dir, `${runId}.result.txt`);
151
+ writeFileSync(path, renderWorkflowResultValue(value));
152
+ return path;
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
package/dist/agent.d.ts CHANGED
@@ -8,6 +8,7 @@ import { type TurnHooks } from "./orchestrator/hooks.js";
8
8
  import type { ExternalHookController } from "./hooks/controller.js";
9
9
  import { type AgentCategoriesConfig, type ResolvedSubagentRoute } from "./agent/categories.js";
10
10
  import { type AgentRoutingConfig, type RoutableModelEntry, type RoutableModelIndex, type RoutingSnapshotAccessor } from "./agent/routing-catalog.js";
11
+ import { SubagentRuntime } from "./agent/subagent/runtime.js";
11
12
  import { type WorkflowRunSnapshot } from "./agent/workflow/control.js";
12
13
  import { BudgetLedger } from "./agent/budget-ledger.js";
13
14
  import { type AgentProfile, type SubagentRunResult } from "./agent/profiles.js";
@@ -52,6 +53,15 @@ export interface AgentOptions {
52
53
  onToolResult?: (toolName: string, result: ToolResult) => void;
53
54
  onTodosUpdate?: (todos: Todo[]) => void;
54
55
  onModeUpdate?: (mode: PermissionMode) => void;
56
+ /**
57
+ * Fired when MULTI-TURN compaction rewrites resident history, with the
58
+ * summary text. Hosts persist it (SessionManager.applyLLMCompaction) so the
59
+ * compacted state — including the cumulative file blocks — survives resume;
60
+ * without this, meta summaries live only in memory (session-log drops meta).
61
+ * Sub-turn summaries never fire it: they cover only the current turn, and a
62
+ * session-level summary entry would make resume skip earlier history.
63
+ */
64
+ onCompactionApplied?: (summary: string) => void;
55
65
  hooks?: TurnHooks[];
56
66
  externalHooks?: ExternalHookController;
57
67
  agentRole?: "parent" | "subagent";
@@ -119,7 +129,6 @@ export declare class Agent {
119
129
  * If the model read the nudge once and chose not to delegate, repeating
120
130
  * it is noise.
121
131
  */
122
- largeTaskNudgeConsumed: boolean;
123
132
  private _providerId;
124
133
  private _model;
125
134
  private tools;
@@ -129,6 +138,7 @@ export declare class Agent {
129
138
  private _mode;
130
139
  private _modeVersion;
131
140
  private onModeUpdate?;
141
+ private onCompactionApplied?;
132
142
  private _todos;
133
143
  private _todosVersion;
134
144
  private onTodosUpdate?;
@@ -147,32 +157,21 @@ export declare class Agent {
147
157
  private fileStateTracker?;
148
158
  private agentCategories;
149
159
  private agentRouting;
150
- private routingSnapshotAccessor?;
151
- private routableModelIndex?;
152
- /** Detector state (design §6): once per session, absolute defaulted count. */
153
- private routingReminderFired;
154
- private defaultedRoutingStreak;
155
- private pendingRoutingReminder?;
160
+ /** Owns the routing decision chain, the §6 detector state and the §4 menu. */
161
+ private readonly router;
156
162
  private providerFactory?;
157
- private readonly subagentStore;
158
- private readonly subagentScheduler;
159
- private readonly childRunner;
160
- private readonly resultIntegrator;
161
- /** Background dynamic-workflow runs (option C Phase 4), keyed by runId. */
162
- private readonly workflowRuns;
163
- /** runIds whose completed result should be ingested at the next turn. */
164
- private readonly pendingWorkflowDeliveries;
165
- private subagentsConfig;
166
- private readonly rateLimitPolicy?;
167
- private pendingSubagentUpdates;
168
163
  /**
169
- * Wakers for tool-execution update loops currently awaiting updates. A
170
- * blocking lifecycle tool (wait_workflow / wait_agent) produces no updates of
171
- * its own, so without this wake the background children's queued lifecycle
172
- * updates sit undrained until the tool settles — the UI would only show
173
- * subagent traces after the whole team finished.
164
+ * Subagent thread lifecycle + workflow script execution: the store,
165
+ * scheduler, ChildRunner and their dispatch/ingestion plumbing.
174
166
  */
175
- private readonly subagentUpdateWakers;
167
+ private readonly subagents;
168
+ /**
169
+ * Background dynamic-workflow runs (option C Phase 4): run records, waiters,
170
+ * cancellation and the pending-delivery set. Execution stays on Agent.
171
+ */
172
+ private readonly workflowLedger;
173
+ private subagentsConfig;
174
+ private readonly rateLimitPolicy?;
176
175
  private lastInputTokens;
177
176
  private lastAnchorMessageCount;
178
177
  private compactionStats;
@@ -191,6 +190,13 @@ export declare class Agent {
191
190
  * accurate (background-tasks design §2.2c).
192
191
  */
193
192
  setSessionID(sessionID: string | undefined): void;
193
+ /**
194
+ * Children persist next to the session file so a later process can resume
195
+ * them via send_input (design §7). An explicit subagents.persistDir config
196
+ * always wins over the sessionID derivation. Child agents themselves
197
+ * (agentRole "subagent") never persist children — no recursion exists.
198
+ */
199
+ private deriveSubagentPersistDir;
194
200
  getSessionID(): string | undefined;
195
201
  /**
196
202
  * Background-task reminder, state-change gated (design §2.3a): returns a
@@ -227,6 +233,20 @@ export declare class Agent {
227
233
  private getActiveToolEntries;
228
234
  injectSystemReminder(content: string): void;
229
235
  injectModeReminder(): void;
236
+ /**
237
+ * Mode reminders are enter-announcements with no cancel semantics, so a stale
238
+ * one keeps describing a mode the session left long ago. Shift+Tab cycles
239
+ * through plan on the way to bypassPermissions, which used to leave a live
240
+ * "Plan mode is now ACTIVE" (plus its "call exit_plan_mode" rule) in history
241
+ * for the rest of the session — the model then planned instead of building,
242
+ * and hunted for a tool that is filtered out outside plan mode.
243
+ *
244
+ * Hidden rather than spliced: keeping the array shape intact leaves message
245
+ * indices (usage anchors, compaction bookkeeping) valid, and the transcript
246
+ * still shows what happened. Cache cost is one invalidated prefix per real
247
+ * mode switch — the no-op path above keeps repeat injections free.
248
+ */
249
+ private retireStaleModeReminders;
230
250
  get role(): "parent" | "subagent";
231
251
  get model(): string;
232
252
  set model(value: string);
@@ -278,32 +298,8 @@ export declare class Agent {
278
298
  * if the provider stream itself errors.
279
299
  */
280
300
  summarizeForCompaction(oldMessages: Message[], onDelta?: (full: string, delta: string) => void, abortSignal?: AbortSignal): Promise<string>;
281
- runSubAgent(input: string | ContentPart[], cwd: string, options: {
282
- profile: AgentProfile;
283
- runId: string;
284
- subAgentId: string;
285
- parentToolCallId: string;
286
- category?: string;
287
- route?: ResolvedSubagentRoute;
288
- approval?: "fail" | "disabled";
289
- emitUpdate?: (update: ToolUpdate) => void;
290
- description?: string;
291
- abortSignal?: AbortSignal;
292
- nickname?: string;
293
- forkContext?: boolean;
294
- }): Promise<SubagentRunResult>;
295
- spawnSubAgent(input: string | ContentPart[], cwd: string, options: {
296
- profile: AgentProfile;
297
- parentToolCallId: string;
298
- category?: string;
299
- model?: string;
300
- effort?: ThinkingLevel;
301
- route?: ResolvedSubagentRoute;
302
- approval?: "fail" | "disabled";
303
- description?: string;
304
- abortSignal?: AbortSignal;
305
- forkContext?: boolean;
306
- }): Promise<SubagentThreadSnapshot>;
301
+ runSubAgent(input: string | ContentPart[], cwd: string, options: Parameters<SubagentRuntime["runSubAgent"]>[2]): Promise<SubagentRunResult>;
302
+ spawnSubAgent(input: string | ContentPart[], cwd: string, options: Parameters<SubagentRuntime["spawnSubAgent"]>[2]): Promise<SubagentThreadSnapshot>;
307
303
  waitSubAgents(options?: {
308
304
  agentIds?: string[];
309
305
  timeoutMs?: number;
@@ -321,9 +317,18 @@ export declare class Agent {
321
317
  * never be used to answer "am I currently delegating?".
322
318
  */
323
319
  activeSubAgentCount(): number;
320
+ listSubAgents(): SubagentThreadSnapshot[];
321
+ /** Marks a child's full summary as delivered to parent context (design §3.3). */
322
+ markSubagentDelivered(agentId: string): void;
323
+ /**
324
+ * The child store, still reachable at its old name. Several tests inspect a
325
+ * spawned child's record (and its live child Agent) through
326
+ * `(agent as any).subagentStore`; keeping this forward means the extraction
327
+ * changed no caller, test or otherwise.
328
+ */
329
+ private get subagentStore();
324
330
  /** run_workflow children are workflowInternal and invisible to listSubAgents. */
325
331
  hasRunningWorkflow(): boolean;
326
- listSubAgents(): SubagentThreadSnapshot[];
327
332
  /**
328
333
  * Dynamic workflow (option C): runs an LLM-authored JS orchestration script in
329
334
  * a QuickJS sandbox. Each agent() call in the script becomes a real scheduled
@@ -379,68 +384,45 @@ export declare class Agent {
379
384
  /** Cancels a running background workflow. */
380
385
  closeWorkflow(runId: string): WorkflowRunSnapshot | undefined;
381
386
  listWorkflows(): WorkflowRunSnapshot[];
382
- private snapshotWorkflow;
383
387
  /** Injects completed background-workflow results before the next turn (§5 analog). */
384
388
  private flushWorkflowDeliveries;
385
- private executeWorkflow;
386
- /** Marks a child's full summary as delivered to parent context (design §3.3). */
387
- markSubagentDelivered(agentId: string): void;
388
- private snapshotSubagent;
389
- /** Returns the blocking diagnostic message when the profile cannot run, else undefined. */
390
- private admitSubagentProfile;
391
- /**
392
- * Background children (queueUpdates) get their results ingested before the
393
- * parent's next inference turn (design §5); foreground children (team,
394
- * legacy task) deliver through their tool result instead.
395
- */
396
- private maybeEnqueueIngestion;
397
- private flushSubagentIngestions;
398
- private finalizeSubagentBlocked;
399
- private dispatchSubagentRun;
400
- private emitSubagentLifecycle;
401
- /** Lets a blocked tool-execution loop drain freshly queued subagent updates. */
402
- private wakeSubagentUpdateWaiters;
403
- private runSubagentLifecycleHookFor;
404
389
  /**
405
390
  * Resolves a child's model route. Priority, highest first (design v2 §1.1):
406
391
  * call-site override (model/effort) > profile.model > category > inherit parent.
407
392
  * The call-site override is what lets the model say "opus for this reviewer,
408
393
  * haiku for these twenty scouts" per spawn/batch member at request time.
409
394
  */
395
+ /** Kept as a thin delegator: routing decisions now live in SubagentRouter. */
410
396
  private resolveRouteForSubagent;
411
- /** Live snapshot for the CURRENT parent route; undefined when no accessor is wired. */
412
- private currentRoutingSnapshot;
413
- /**
414
- * Early route validation at dispatch time (design §7). Throws with an
415
- * actionable message so the model self-corrects in the same turn. The
416
- * cross-provider lock (§7.2.1) is snapshot-independent; catalog checks
417
- * degrade silently when no snapshot is available (§7.0).
418
- */
419
- private validateRouteForDispatch;
420
397
  /** Routable catalog across runnable providers (design v3.6); undefined when unwired. */
421
398
  listRoutableModels(): RoutableModelEntry[] | undefined;
422
399
  /**
423
- * Decision-point detector (design §6): counts dispatches whose model was
424
- * decided by NO routing layer (modelSource "inherit") under a strong-tier
425
- * parent. Fires once per session at the Nth qualifying dispatch; the
426
- * reminder rides the same channel as the lifecycle reminder.
400
+ * The ONE place a child Agent is constructed. The runtime hands over what it
401
+ * resolved (provider/route/tools/prompt); everything inherited from the
402
+ * parent is filled in here, which is also what keeps the deliberately
403
+ * ABSENT arguments absent — `onMessageAppend` (a child must not append to
404
+ * the parent's session log), `sessionID` (a child must not stop the
405
+ * parent's auto servers) and the routing catalog (children route by
406
+ * default, not from the parent's menu).
427
407
  */
428
- private noteRoutingDispatch;
429
- /** Consumed by the turn hooks; also closes the counting window (§6). */
430
- consumePendingRoutingReminder(): string | undefined;
431
- private createSubagentThreadRecord;
432
- private runSubagentThread;
433
- private createSubAgentInstance;
434
- private resolveProviderForRoute;
408
+ private createChildAgent;
435
409
  private forkMessagesForSubagent;
436
- private buildSubagentUpdate;
437
- private queueSubagentUpdate;
438
- private drainSubagentToolUpdates;
439
- private activeSubagentNicknames;
440
- private resolveSubagentTargets;
441
410
  private maybeCompactResidentHistory;
411
+ /**
412
+ * Hand a multi-turn compaction summary to the host for session persistence.
413
+ * Failure is swallowed: losing persistence must never break the run itself.
414
+ */
415
+ private persistCompactionSummary;
442
416
  private appendMessage;
443
417
  private appendInterruptedAssistantBoundary;
444
418
  private clearTodosAfterInterruptedRun;
445
419
  private executeTool;
446
420
  }
421
+ /** Exported for tests: the wake latch below is easiest to pin directly. */
422
+ export declare function createUpdateQueue<T>(): {
423
+ push(item: T): void;
424
+ drain(): T[];
425
+ hasItems(): boolean;
426
+ wait(signal?: AbortSignal): Promise<"woken" | "aborted">;
427
+ wake(): void;
428
+ };