klyro 1.0.1 → 1.0.3

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 (63) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +13 -5
  2. package/dist/agent/anthropic-adapter.js +19 -2
  3. package/dist/agent/capabilities.js +7 -1
  4. package/dist/agent/orchestrator.d.ts +44 -4
  5. package/dist/agent/orchestrator.js +102 -8
  6. package/dist/agent/retry.js +52 -10
  7. package/dist/agent/runtime.d.ts +34 -0
  8. package/dist/agent/runtime.js +169 -15
  9. package/dist/agent/stream-budget.d.ts +36 -0
  10. package/dist/agent/stream-budget.js +121 -0
  11. package/dist/checkpoints/store.d.ts +9 -0
  12. package/dist/checkpoints/store.js +26 -0
  13. package/dist/cli/commit.d.ts +31 -0
  14. package/dist/cli/commit.js +142 -0
  15. package/dist/cli/config.d.ts +45 -0
  16. package/dist/cli/config.js +82 -0
  17. package/dist/cli/doctor.d.ts +1 -0
  18. package/dist/cli/doctor.js +71 -6
  19. package/dist/cli/hooks.d.ts +47 -0
  20. package/dist/cli/hooks.js +181 -0
  21. package/dist/cli/markdown.js +29 -1
  22. package/dist/cli/repl.js +41 -1
  23. package/dist/cli/run.d.ts +6 -0
  24. package/dist/cli/run.js +76 -3
  25. package/dist/context/tokenizer.d.ts +18 -0
  26. package/dist/context/tokenizer.js +50 -1
  27. package/dist/events/bus.d.ts +5 -0
  28. package/dist/events/bus.js +10 -0
  29. package/dist/events/catalog.d.ts +9 -0
  30. package/dist/events/catalog.js +9 -0
  31. package/dist/index.js +89 -5
  32. package/dist/mcp/client.js +1 -1
  33. package/dist/mcp/registry.d.ts +0 -18
  34. package/dist/mcp/registry.js +49 -2
  35. package/dist/policy/engine.d.ts +16 -0
  36. package/dist/policy/engine.js +74 -1
  37. package/dist/policy/path-guard.d.ts +24 -0
  38. package/dist/policy/path-guard.js +46 -0
  39. package/dist/providers/model-info.d.ts +6 -0
  40. package/dist/providers/model-info.js +8 -0
  41. package/dist/tools/fs/apply-patch.js +6 -1
  42. package/dist/tools/fs/edit-file.js +4 -1
  43. package/dist/tools/fs/multi-edit.js +4 -1
  44. package/dist/tools/fs/write-file.js +16 -6
  45. package/dist/tools/plan/todo-write.js +1 -1
  46. package/dist/tools/shell/sandbox.d.ts +4 -3
  47. package/dist/tools/shell/sandbox.js +23 -3
  48. package/dist/tools/shell/shell-exec.d.ts +28 -0
  49. package/dist/tools/shell/shell-exec.js +87 -1
  50. package/dist/trace/writer.d.ts +7 -0
  51. package/dist/trace/writer.js +7 -0
  52. package/dist/tui/app.js +20 -1
  53. package/dist/tui/app.test.js +18 -12
  54. package/dist/tui/approval.test.js +20 -3
  55. package/dist/tui/markdown.d.ts +13 -0
  56. package/dist/tui/markdown.js +169 -2
  57. package/dist/tui/scroll-flow.test.js +3 -1
  58. package/dist/verification/classify.js +4 -3
  59. package/dist/verification/engine.d.ts +8 -0
  60. package/dist/verification/engine.js +25 -0
  61. package/dist/verification/registry.js +16 -5
  62. package/dist/verification/scoped.js +36 -5
  63. package/package.json +1 -1
@@ -72,6 +72,9 @@ interface AnthropicRequest {
72
72
  name: string;
73
73
  description: string;
74
74
  input_schema: unknown;
75
+ cache_control?: {
76
+ type: 'ephemeral';
77
+ };
75
78
  }>;
76
79
  max_tokens: number;
77
80
  temperature?: number;
@@ -94,6 +97,14 @@ export declare function anthropicAdapter(opts: AnthropicAdapterOptions): Provide
94
97
  * Exported via _internal for testing.
95
98
  */
96
99
  export declare function buildAnthropicSystem(system: string | undefined, suffix: string | undefined, promptCache: boolean): AnthropicRequest['system'];
100
+ /**
101
+ * Build the Anthropic `tools` array. When prompt caching is enabled, the
102
+ * last tool carries a `cache_control: {type:'ephemeral'}` breakpoint so
103
+ * the (usually stable) tool definitions join the cacheable prefix —
104
+ * mirroring the system-text breakpoint. OpenAI path untouched.
105
+ * Exported via _internal for testing.
106
+ */
107
+ export declare function buildAnthropicTools(tools: ToolDefinition[], promptCache: boolean): AnthropicRequest['tools'];
97
108
  /**
98
109
  * Mutable per-stream assembly state. Blocks are keyed by content_block
99
110
  * index; the tool id is carried inside the block entry. There is no global
@@ -118,15 +129,12 @@ interface AnthropicStreamState {
118
129
  }
119
130
  declare function translateSse(event: string, parsed: AnthropicSseEvent, state: AnthropicStreamState): StreamEvent[];
120
131
  declare function toAnthropicMessages(messages: Message[]): AnthropicMessage[];
121
- declare function toAnthropicTool(t: ToolDefinition): {
122
- name: string;
123
- description: string;
124
- input_schema: unknown;
125
- };
132
+ declare function toAnthropicTool(t: ToolDefinition): NonNullable<AnthropicRequest['tools']>[number];
126
133
  export declare const _internal: {
127
134
  toAnthropicMessages: typeof toAnthropicMessages;
128
135
  toAnthropicTool: typeof toAnthropicTool;
129
136
  translateSse: typeof translateSse;
130
137
  buildAnthropicSystem: typeof buildAnthropicSystem;
138
+ buildAnthropicTools: typeof buildAnthropicTools;
131
139
  };
132
140
  export {};
@@ -75,12 +75,29 @@ export function buildAnthropicSystem(system, suffix, promptCache) {
75
75
  return undefined;
76
76
  return promptCache ? [{ type: 'text', text: system, ...breakpoint }] : system;
77
77
  }
78
+ /**
79
+ * Build the Anthropic `tools` array. When prompt caching is enabled, the
80
+ * last tool carries a `cache_control: {type:'ephemeral'}` breakpoint so
81
+ * the (usually stable) tool definitions join the cacheable prefix —
82
+ * mirroring the system-text breakpoint. OpenAI path untouched.
83
+ * Exported via _internal for testing.
84
+ */
85
+ export function buildAnthropicTools(tools, promptCache) {
86
+ if (tools.length === 0)
87
+ return undefined;
88
+ const out = tools.map(toAnthropicTool);
89
+ if (promptCache) {
90
+ const last = out[out.length - 1];
91
+ last.cache_control = { type: 'ephemeral' };
92
+ }
93
+ return out;
94
+ }
78
95
  async function* streamAnthropic(req, opts) {
79
96
  const body = {
80
97
  model: req.model,
81
98
  system: buildAnthropicSystem(req.system, req.systemSuffix, opts.promptCache),
82
99
  messages: toAnthropicMessages(req.messages),
83
- tools: req.tools.length > 0 ? req.tools.map(toAnthropicTool) : undefined,
100
+ tools: buildAnthropicTools(req.tools, opts.promptCache),
84
101
  max_tokens: req.maxTokens ?? 4096,
85
102
  temperature: req.temperature,
86
103
  stream: true,
@@ -430,4 +447,4 @@ function toAnthropicTool(t) {
430
447
  };
431
448
  }
432
449
  // Re-export for testability.
433
- export const _internal = { toAnthropicMessages, toAnthropicTool, translateSse, buildAnthropicSystem };
450
+ export const _internal = { toAnthropicMessages, toAnthropicTool, translateSse, buildAnthropicSystem, buildAnthropicTools };
@@ -187,5 +187,11 @@ export const DEFAULT_SPAWN_TOOLS = new Set([
187
187
  ]);
188
188
  /** Default deny-list — these are NEVER allowed, even if explicitly requested. */
189
189
  export const DEFAULT_DENIED_TOOLS = new Set([
190
- // Add dangerous tools here. Empty by default — extend as policy matures.
190
+ // Intentionally empty. Deny happens per-pattern (shellDenyRule /
191
+ // DANGEROUS_PATTERNS, .env guards, repair-guard), not per-tool: a
192
+ // tool-granularity deny-all entry (e.g. banning `shell_exec` outright)
193
+ // would break legitimate flows that rely on the allowlist + approval
194
+ // path. Seed candidates considered and rejected: `shell_exec` (needed
195
+ // for tests/builds via approval), `run_verify` (needed by tester/
196
+ // implementer agents), `write_file`/`edit_file` (core agent function).
191
197
  ]);
@@ -12,7 +12,7 @@
12
12
  * The compact result is a `ChildSummary` — a `ToolResult` the parent model
13
13
  * can act on — never the full child transcript.
14
14
  */
15
- import type { RuntimeDeps } from './runtime.js';
15
+ import type { RuntimeDeps, RunOptions, RuntimeEvent } from './runtime.js';
16
16
  import type { ToolResult } from '../tools/types.js';
17
17
  import { TaskManager, type TaskRecord, type TaskStatus, type TaskSummary } from './task-manager.js';
18
18
  import { WorkerSpawner } from './worker-spawner.js';
@@ -151,12 +151,31 @@ export interface OrchestratorOpts {
151
151
  taskManager?: TaskManager;
152
152
  workerSpawner?: WorkerSpawner;
153
153
  /**
154
- * True when the parent is the interactive TUI. TUI children stay in-process
155
- * (V1 limitation — the Ink approval bridge is tied to the parent terminal),
156
- * while headless/CLI children run process-isolated. Defaults to false.
154
+ * True when the parent is the interactive TUI. TUI children run
155
+ * process-isolated *unless* they may need to surface an approval prompt to
156
+ * the operator (see childCanIsolate) — the Ink bridge is tied to the parent
157
+ * terminal, so a prompting child must stay in-process. Headless/CLI children
158
+ * always isolate. Defaults to false.
157
159
  */
158
160
  isTui?: boolean;
159
161
  }
162
+ /**
163
+ * Build a `subtask.progress` note for one finished tool call.
164
+ * Pure — unit-tested directly (see agent-tools.test.ts).
165
+ */
166
+ export declare function progressNote(step: number, tool: string, isError: boolean): string;
167
+ /**
168
+ * Build the `RunOptions.onEvent` handler the orchestrator passes into each
169
+ * child's run options. Emits at most one `subtask.progress` per tool call:
170
+ * a `tool_result` is only mirrored when its `tool_call_end` was observed
171
+ * first, so duplicate/late results can never double-emit. (The note needs
172
+ * the ok/ERR outcome, which only `tool_result` carries — `tool_call_end`
173
+ * alone cannot build it — hence the end-gated result throttle.)
174
+ */
175
+ export declare function createSubtaskProgressEmitter(opts: {
176
+ taskId: string;
177
+ sessionId: string;
178
+ }): (ev: RuntimeEvent) => void;
160
179
  export declare class AgentOrchestrator {
161
180
  readonly sessionId: string;
162
181
  readonly deps: RuntimeDeps;
@@ -172,6 +191,27 @@ export declare class AgentOrchestrator {
172
191
  getAgent(id: string): AgentDefinition | undefined;
173
192
  /** Build the bridge the parent's runtime hands to tools. */
174
193
  bridgeFor(parent: ParentContextRef): AgentSpawnBridge;
194
+ /**
195
+ * R2 — decide whether a child may run process-isolated.
196
+ *
197
+ * The only thing that forces a child to stay in-process is the possibility
198
+ * of an interactive approval prompt: the Ink bridge lives in the parent
199
+ * terminal, so a subprocess could never ask. A child that cannot prompt is
200
+ * therefore free to isolate.
201
+ *
202
+ * A child cannot prompt when any of these hold:
203
+ * - it is readonly (no write/execute tools → no `ask` on those),
204
+ * - the parent is not a TUI (no bridge to reach in the first place), or
205
+ * - the child's toolset contains no tool the policy can put in `ask`.
206
+ *
207
+ * Isolation is deliberately conservative here: when in doubt we keep the
208
+ * child in-process, because a stranded prompt is a hang and a hang is worse
209
+ * than lost isolation. Public for direct unit testing.
210
+ */
211
+ childCanIsolate(resolved: {
212
+ allowed: ReadonlySet<string>;
213
+ readonly: boolean;
214
+ }, def: AgentDefinition, childOptions: RunOptions): boolean;
175
215
  /** Compute a child's effective capabilities from the parent's own. */
176
216
  private resolveChild;
177
217
  /**
@@ -87,6 +87,45 @@ function mapResultStatus(status) {
87
87
  return 'failed';
88
88
  }
89
89
  }
90
+ /**
91
+ * Build a `subtask.progress` note for one finished tool call.
92
+ * Pure — unit-tested directly (see agent-tools.test.ts).
93
+ */
94
+ export function progressNote(step, tool, isError) {
95
+ return `step ${step}: ${tool} ${isError ? 'ERR' : 'ok'}`;
96
+ }
97
+ /**
98
+ * Build the `RunOptions.onEvent` handler the orchestrator passes into each
99
+ * child's run options. Emits at most one `subtask.progress` per tool call:
100
+ * a `tool_result` is only mirrored when its `tool_call_end` was observed
101
+ * first, so duplicate/late results can never double-emit. (The note needs
102
+ * the ok/ERR outcome, which only `tool_result` carries — `tool_call_end`
103
+ * alone cannot build it — hence the end-gated result throttle.)
104
+ */
105
+ export function createSubtaskProgressEmitter(opts) {
106
+ let step = 0;
107
+ const ended = new Set();
108
+ return (ev) => {
109
+ if (ev.kind === 'step_start') {
110
+ step = ev.step;
111
+ }
112
+ else if (ev.kind === 'tool_call_end') {
113
+ ended.add(ev.id);
114
+ }
115
+ else if (ev.kind === 'tool_result') {
116
+ if (!ended.has(ev.id))
117
+ return;
118
+ ended.delete(ev.id);
119
+ globalBus.emit({
120
+ type: 'subtask.progress',
121
+ ts: Date.now(),
122
+ sessionId: opts.sessionId,
123
+ taskId: opts.taskId,
124
+ note: progressNote(step, ev.name, ev.isError),
125
+ });
126
+ }
127
+ };
128
+ }
90
129
  export class AgentOrchestrator {
91
130
  sessionId;
92
131
  deps;
@@ -131,6 +170,45 @@ export class AgentOrchestrator {
131
170
  applyTask: (taskId) => this.applyTask(taskId),
132
171
  };
133
172
  }
173
+ /**
174
+ * R2 — decide whether a child may run process-isolated.
175
+ *
176
+ * The only thing that forces a child to stay in-process is the possibility
177
+ * of an interactive approval prompt: the Ink bridge lives in the parent
178
+ * terminal, so a subprocess could never ask. A child that cannot prompt is
179
+ * therefore free to isolate.
180
+ *
181
+ * A child cannot prompt when any of these hold:
182
+ * - it is readonly (no write/execute tools → no `ask` on those),
183
+ * - the parent is not a TUI (no bridge to reach in the first place), or
184
+ * - the child's toolset contains no tool the policy can put in `ask`.
185
+ *
186
+ * Isolation is deliberately conservative here: when in doubt we keep the
187
+ * child in-process, because a stranded prompt is a hang and a hang is worse
188
+ * than lost isolation. Public for direct unit testing.
189
+ */
190
+ childCanIsolate(resolved, def, childOptions) {
191
+ // Headless parents have no approval bridge — isolation is always safe.
192
+ if (!this.isTui)
193
+ return true;
194
+ // Readonly agents never write/execute, so never prompt.
195
+ if (resolved.readonly)
196
+ return true;
197
+ // A child with an inherited bridge (grandchildren possible) must stay
198
+ // in-process: its own children need the bridge chain.
199
+ if (childOptions.agentBridge)
200
+ return false;
201
+ // Any tool in the child's set that the policy can escalate to `ask`
202
+ // pins it in-process. `execute` is the class that most commonly prompts
203
+ // (shell_exec), so its presence is the deciding signal alongside writes.
204
+ const prompting = new Set(['shell_exec', 'write_file', 'edit_file', 'multi_edit', 'apply_patch', 'run_verify']);
205
+ for (const t of resolved.allowed) {
206
+ if (prompting.has(t))
207
+ return false;
208
+ }
209
+ void def;
210
+ return true;
211
+ }
134
212
  /** Compute a child's effective capabilities from the parent's own. */
135
213
  resolveChild(def, parent, registryTools) {
136
214
  const input = {
@@ -212,14 +290,17 @@ export class AgentOrchestrator {
212
290
  const registryTools = new Set(this.deps.registry.list().map((t) => t.name));
213
291
  const resolved = this.resolveChild(def, parent, registryTools);
214
292
  const childModel = input.model ?? resolved.model ?? parent.model;
215
- // Worktree isolation: write-capable children without an explicit cwd get
216
- // their own worktree; readonly agents keep the parent cwd. A
217
- // write-capable spawn outside a git repo is rejected outright.
293
+ // Worktree isolation: write-capable children get their own worktree —
294
+ // including when the spawn carries an explicit cwd (the worktree is
295
+ // then rooted at the resolved explicit cwd, which containment above
296
+ // already pinned inside the parent). Readonly agents keep the resolved
297
+ // cwd with no worktree. A write-capable spawn outside a git repo is
298
+ // rejected outright.
218
299
  const writeCapable = [...resolved.allowed].some((t) => DEFAULT_WRITE_TOOLS.has(t));
219
300
  let childCwd = baseCwd;
220
301
  let worktree;
221
302
  let repoCwd;
222
- if (!resolved.readonly && writeCapable && !input.cwd) {
303
+ if (!resolved.readonly && writeCapable) {
223
304
  const isRepo = await ensureGitRepo(baseCwd).catch(() => false);
224
305
  if (!isRepo) {
225
306
  return {
@@ -285,6 +366,11 @@ export class AgentOrchestrator {
285
366
  maxTimeMs: def.maxTimeMs ?? input.timeoutMs,
286
367
  signal: record.abortController.signal,
287
368
  nonInteractive: true,
369
+ // Mid-life progress: mirror each finished tool call as one
370
+ // `subtask.progress` bus event (see createSubtaskProgressEmitter).
371
+ // Process-isolated children don't run this closure — only the
372
+ // in-process path reports mid-life progress.
373
+ onEvent: createSubtaskProgressEmitter({ taskId: record.id, sessionId: this.sessionId }),
288
374
  // Grandchildren: only children that canSpawn receive the bridge —
289
375
  // otherwise tools see NO_ORCHESTRATOR as before.
290
376
  ...(resolved.canSpawn ? { agentBridge: this.bridgeFor(childRef) } : {}),
@@ -301,10 +387,18 @@ export class AgentOrchestrator {
301
387
  depth: childDepth,
302
388
  ...(typeof childModel === 'string' ? { model: childModel } : {}),
303
389
  });
304
- // G2 — process isolation for headless sub-agents. In-process is the
305
- // fallback (and mandatory for TUI children — see OrchestratorOpts.isTui),
306
- // and opt-out via KLYRO_WORKER=0 for tests/dev.
307
- const useProcessIsolation = !this.isTui && process.env.KLYRO_WORKER !== '0';
390
+ // G2/R2 — process isolation for sub-agents. A child can be spawned as a
391
+ // real OS process whenever it will never need to surface an interactive
392
+ // approval prompt to the operator. Every child that might prompt must stay
393
+ // in-process so the TUI approval bridge (tied to the parent terminal) can
394
+ // reach the operator. Headless/readonly/DenyAll children — which is the
395
+ // ordinary case — isolate into a subprocess. The blanket exclusion of ALL
396
+ // TUI children (V1) is replaced by this capability-aware rule, so a TUI
397
+ // session with readonly or non-interactive children gets real isolation
398
+ // too. Explicitly opt out with KLYRO_WORKER=0.
399
+ // See orchestratorOpts.isTui, capabilities.resolveCapabilities, and
400
+ // child-worker.buildChildDeps (DenyAll approval).
401
+ const useProcessIsolation = process.env.KLYRO_WORKER !== '0' && this.childCanIsolate(resolved, def, childOptions);
308
402
  this.workerSpawner.spawn(async (signal) => {
309
403
  // Both the in-process path and the forked child resolve to the same
310
404
  // minimal outcome shape the settle tail needs.
@@ -17,6 +17,7 @@
17
17
  *
18
18
  * The default policy matches the L6 plan: 5 attempts, 500ms base, 8s cap.
19
19
  */
20
+ import { acquireStreamSlot, noteRateLimited } from './stream-budget.js';
20
21
  export const DEFAULT_RETRY = {
21
22
  maxAttempts: 5,
22
23
  baseMs: 500,
@@ -88,6 +89,28 @@ export function computeBackoff(attempt, baseMs, maxMs) {
88
89
  const jitter = exp * 0.25 * (Math.random() * 2 - 1);
89
90
  return Math.max(0, Math.floor(exp + jitter));
90
91
  }
92
+ /**
93
+ * True when a retryable error event carries a 429 rate-limit signal.
94
+ * Checks the `status` field first, then `code`; accepts numeric values
95
+ * and `'429'` substrings (e.g. `'429'`, `'HTTP_429'`).
96
+ */
97
+ function isRateLimitedError(ev) {
98
+ if (ev.kind !== 'error')
99
+ return false;
100
+ const rec = ev;
101
+ for (const key of ['status', 'code']) {
102
+ const value = rec[key];
103
+ if (typeof value === 'string' && value.includes('429'))
104
+ return true;
105
+ if (typeof value === 'number' && String(value).includes('429'))
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ function rateLimitDelayMs(ev) {
111
+ const raw = ev['retryAfterMs'];
112
+ return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : undefined;
113
+ }
91
114
  export function retryingAdapter(inner, opts = {}) {
92
115
  const cfg = { ...DEFAULT_RETRY, ...opts };
93
116
  const sleep = opts.sleep ?? defaultSleep;
@@ -129,20 +152,39 @@ export function retryingAdapter(inner, opts = {}) {
129
152
  opts.onAttempt?.(attempt);
130
153
  if (effectiveSignal?.aborted)
131
154
  return;
155
+ // Rate-limit scheduler: hold one stream slot for the duration of
156
+ // this attempt's inner.stream consumption. Abort while queued ends
157
+ // the stream promptly with no inner call.
158
+ let release;
132
159
  let sawRetryable = false;
133
160
  let lastError = null;
134
- for await (const ev of streamWithAbort(inner.stream(attemptReq), effectiveSignal)) {
135
- if (effectiveSignal?.aborted)
161
+ try {
162
+ try {
163
+ release = await acquireStreamSlot(effectiveSignal);
164
+ }
165
+ catch {
136
166
  return;
137
- if (ev.kind === 'error' && ev.retryable) {
138
- // Buffer the retryable error; don't yield it yet. We'll either
139
- // re-issue (and the caller will never see the error) or, on
140
- // final attempt, yield it as the terminal error.
141
- sawRetryable = true;
142
- lastError = ev;
143
- break; // stop consuming; the stream is dead on retryable errors.
144
167
  }
145
- yield ev;
168
+ for await (const ev of streamWithAbort(inner.stream(attemptReq), effectiveSignal)) {
169
+ if (effectiveSignal?.aborted)
170
+ return;
171
+ if (ev.kind === 'error' && ev.retryable) {
172
+ // Adaptive throttling: a 429 collapses the global stream cap
173
+ // to 1 for retryAfterMs (or 60s) — see stream-budget.ts.
174
+ if (isRateLimitedError(ev))
175
+ noteRateLimited(rateLimitDelayMs(ev));
176
+ // Buffer the retryable error; don't yield it yet. We'll either
177
+ // re-issue (and the caller will never see the error) or, on
178
+ // final attempt, yield it as the terminal error.
179
+ sawRetryable = true;
180
+ lastError = ev;
181
+ break; // stop consuming; the stream is dead on retryable errors.
182
+ }
183
+ yield ev;
184
+ }
185
+ }
186
+ finally {
187
+ release?.();
146
188
  }
147
189
  if (!sawRetryable)
148
190
  return; // success or non-retryable error — done.
@@ -31,6 +31,13 @@ export type VerifyMode = typeof import('../verification/engine.js') extends {
31
31
  } ? V : 'strict' | 'advisory' | 'off';
32
32
  export interface RuntimeDeps {
33
33
  adapter: ProviderAdapter;
34
+ /**
35
+ * Ordered failover adapters (L15). When the active adapter ends a step
36
+ * with a terminal provider error, the runtime swaps to the next entry
37
+ * and re-issues the step (bounded by chain length, never loops).
38
+ * Optional — single-adapter callers behave exactly as before.
39
+ */
40
+ failoverAdapters?: ProviderAdapter[];
34
41
  registry: ToolRegistry;
35
42
  policy: PolicyEngine;
36
43
  approval: ApprovalPrompt;
@@ -226,6 +233,22 @@ export type RuntimeEvent = {
226
233
  } | {
227
234
  kind: 'checkpoint_saved';
228
235
  sessionId: string;
236
+ } | {
237
+ kind: 'status';
238
+ message: string;
239
+ } | {
240
+ kind: 'budget_warning';
241
+ ratio: number;
242
+ threshold: number;
243
+ } | {
244
+ kind: 'provider_failover';
245
+ from: string;
246
+ to: string;
247
+ reason: string;
248
+ } | {
249
+ kind: 'model_override';
250
+ requested: string;
251
+ effective: string;
229
252
  };
230
253
  export interface RunResult {
231
254
  status: 'complete' | 'max_steps' | 'aborted' | 'no_final' | 'verify_failed' | 'limit' | 'blocked' | 'stuck';
@@ -264,7 +287,18 @@ export declare function toolDefinitions(registry: ToolRegistry): ToolDefinition[
264
287
  export declare function estimateCost(model: string, usage: {
265
288
  input: number;
266
289
  output: number;
290
+ cacheRead?: number;
291
+ cacheWrite?: number;
267
292
  }): number;
293
+ /**
294
+ * Parallel fan-out cap: approved concurrencySafe tool calls execute in
295
+ * sequential chunks of at most this size. Commit order stays identical
296
+ * (commits run sequentially after execution), so the transcript reads as
297
+ * if the calls ran in order.
298
+ */
299
+ export declare const MAX_PARALLEL_TOOLS = 8;
300
+ /** Progressive budget-warning thresholds (fraction of maxCost), fired once each per run. */
301
+ export declare const BUDGET_WARNING_THRESHOLDS: readonly [0.4, 0.7, 0.9];
268
302
  /** Run the autonomous loop. */
269
303
  export declare function run(opts: RunOptions, deps: RuntimeDeps): Promise<RunResult>;
270
304
  export declare function defaultSystemPrompt(ctx: {