klyro 0.1.4 → 0.1.5

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 (78) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +1 -1
  2. package/dist/agent/anthropic-adapter.js +22 -20
  3. package/dist/agent/provider-adapter.js +30 -7
  4. package/dist/agent/registry.js +24 -7
  5. package/dist/agent/retry.js +54 -9
  6. package/dist/agent/runtime.d.ts +45 -1
  7. package/dist/agent/runtime.js +236 -23
  8. package/dist/chat.d.ts +3 -3
  9. package/dist/chat.js +137 -39
  10. package/dist/cli/auth.d.ts +9 -0
  11. package/dist/cli/auth.js +84 -0
  12. package/dist/cli/completion.d.ts +6 -0
  13. package/dist/cli/completion.js +66 -0
  14. package/dist/cli/config.d.ts +44 -0
  15. package/dist/cli/config.js +428 -0
  16. package/dist/cli/doctor.d.ts +8 -0
  17. package/dist/cli/doctor.js +136 -0
  18. package/dist/cli/errors.d.ts +5 -0
  19. package/dist/cli/errors.js +37 -0
  20. package/dist/cli/markdown.d.ts +9 -0
  21. package/dist/cli/markdown.js +77 -0
  22. package/dist/cli/repl.js +113 -5
  23. package/dist/cli/run.d.ts +9 -0
  24. package/dist/cli/run.js +97 -2
  25. package/dist/cli/slash/parser.d.ts +10 -0
  26. package/dist/cli/slash/parser.js +6 -1
  27. package/dist/cli/trace.d.ts +7 -0
  28. package/dist/cli/trace.js +63 -0
  29. package/dist/cli/update.d.ts +6 -0
  30. package/dist/cli/update.js +70 -0
  31. package/dist/context/system-prompt.d.ts +14 -0
  32. package/dist/context/system-prompt.js +47 -0
  33. package/dist/events/bus.d.ts +16 -0
  34. package/dist/events/bus.js +28 -0
  35. package/dist/events/catalog.d.ts +136 -0
  36. package/dist/events/catalog.js +5 -0
  37. package/dist/index.js +335 -22
  38. package/dist/persistence/session.d.ts +12 -0
  39. package/dist/persistence/session.js +45 -0
  40. package/dist/policy/engine.d.ts +11 -1
  41. package/dist/policy/engine.js +85 -2
  42. package/dist/providers/model-info.d.ts +18 -0
  43. package/dist/providers/model-info.js +18 -0
  44. package/dist/providers.js +3 -2
  45. package/dist/renderers/json.d.ts +11 -0
  46. package/dist/renderers/json.js +14 -0
  47. package/dist/renderers/terminal.d.ts +9 -0
  48. package/dist/renderers/terminal.js +41 -0
  49. package/dist/repl.js +52 -15
  50. package/dist/shared/errors.d.ts +18 -0
  51. package/dist/shared/errors.js +33 -0
  52. package/dist/shared/index.d.ts +2 -0
  53. package/dist/shared/index.js +2 -0
  54. package/dist/shared/types.d.ts +19 -0
  55. package/dist/shared/types.js +11 -0
  56. package/dist/tools/fs/read-file.d.ts +1 -31
  57. package/dist/tools/fs/read-file.js +45 -17
  58. package/dist/tools/fs/read-history.d.ts +3 -0
  59. package/dist/tools/fs/read-history.js +13 -0
  60. package/dist/tools/fs/write-file.d.ts +1 -4
  61. package/dist/tools/fs/write-file.js +35 -1
  62. package/dist/tools/normalize.d.ts +1 -1
  63. package/dist/tools/shell/shell-exec.d.ts +1 -26
  64. package/dist/tools/shell/shell-exec.js +121 -17
  65. package/dist/tools/types.d.ts +25 -0
  66. package/dist/trace/writer.d.ts +13 -0
  67. package/dist/trace/writer.js +53 -0
  68. package/dist/tui/app.d.ts +1 -15
  69. package/dist/tui/app.js +203 -22
  70. package/dist/tui/approval.js +25 -4
  71. package/dist/tui/approval.test.js +1 -1
  72. package/dist/tui/snapshot.test.js +2 -2
  73. package/dist/tui/status.js +5 -1
  74. package/dist/util/log.d.ts +12 -0
  75. package/dist/util/log.js +75 -0
  76. package/dist/verification/auto.d.ts +8 -0
  77. package/dist/verification/auto.js +52 -0
  78. package/package.json +3 -2
@@ -60,7 +60,7 @@ export declare function anthropicAdapter(opts: AnthropicAdapterOptions): Provide
60
60
  declare function findToolIdByIndex(index: number | undefined, buffers: Map<string, {
61
61
  name: string;
62
62
  argsJson: string;
63
- }>): string | undefined;
63
+ }>, indexToToolId?: Map<number, string>): string | undefined;
64
64
  declare function toAnthropicMessages(messages: Message[]): AnthropicMessage[];
65
65
  declare function toAnthropicTool(t: ToolDefinition): {
66
66
  name: string;
@@ -14,6 +14,7 @@
14
14
  * Auth: `x-api-key: <key>`. Version header is sent as `anthropic-version`.
15
15
  * Auth can be a Bearer token (for proxies) — the adapter accepts either.
16
16
  */
17
+ import { assertSafeBaseURL } from '../chat.js';
17
18
  const DEFAULT_VERSION = '2023-06-01';
18
19
  const DEFAULT_TIMEOUT_MS = 120_000;
19
20
  export class AnthropicApiError extends Error {
@@ -27,7 +28,9 @@ export class AnthropicApiError extends Error {
27
28
  }
28
29
  }
29
30
  export function anthropicAdapter(opts) {
30
- const baseURL = (opts.baseURL ?? 'https://api.anthropic.com').replace(/\/+$/, '');
31
+ const rawBase = opts.baseURL ?? 'https://api.anthropic.com';
32
+ assertSafeBaseURL(rawBase);
33
+ const baseURL = rawBase.replace(/\/+$/, '');
31
34
  const version = opts.anthropicVersion ?? DEFAULT_VERSION;
32
35
  const authHeader = opts.authHeader ?? 'x-api-key';
33
36
  const betas = opts.betas ?? [];
@@ -110,6 +113,8 @@ async function* streamAnthropic(req, opts) {
110
113
  let buf = '';
111
114
  // Track in-progress tool calls so we can emit start/delta/end.
112
115
  const toolBuffers = new Map();
116
+ // Map content_block index → tool_use id (persists after tool completes to handle late deltas)
117
+ const indexToToolId = new Map();
113
118
  try {
114
119
  while (true) {
115
120
  const { value, done } = await reader.read();
@@ -146,7 +151,7 @@ async function* streamAnthropic(req, opts) {
146
151
  catch {
147
152
  continue;
148
153
  }
149
- const out = translateSse(e.event, parsed, toolBuffers);
154
+ const out = translateSse(e.event, parsed, toolBuffers, indexToToolId);
150
155
  for (const ev of out)
151
156
  yield ev;
152
157
  }
@@ -162,13 +167,16 @@ async function* streamAnthropic(req, opts) {
162
167
  }
163
168
  yield { kind: 'message_end', finishReason: 'stop' };
164
169
  }
165
- function translateSse(event, parsed, toolBuffers) {
170
+ function translateSse(event, parsed, toolBuffers, indexToToolId) {
166
171
  const out = [];
167
172
  switch (event) {
168
173
  case 'content_block_start': {
169
174
  const block = parsed.content_block;
175
+ const idx = parsed.index;
170
176
  if (block?.type === 'tool_use' && block.id && block.name) {
171
177
  toolBuffers.set(block.id, { name: block.name, argsJson: '' });
178
+ if (idx !== undefined)
179
+ indexToToolId.set(idx, block.id);
172
180
  out.push({ kind: 'tool_call_start', id: block.id, name: block.name });
173
181
  }
174
182
  return out;
@@ -176,16 +184,11 @@ function translateSse(event, parsed, toolBuffers) {
176
184
  case 'content_block_delta': {
177
185
  const delta = parsed.delta;
178
186
  const index = parsed.index;
179
- // We need the id to know which tool buffer to update. Anthropic sends
180
- // index but not id on delta events. Match by name+index pair.
181
187
  if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
182
188
  out.push({ kind: 'text_delta', text: delta.text });
183
189
  }
184
190
  else if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
185
- // Find the tool buffer whose index matches this delta.
186
- // We map by index order: the i-th tool_use block corresponds to the
187
- // i-th tool_start we've emitted. Track that counter separately.
188
- const id = findToolIdByIndex(index, toolBuffers);
191
+ const id = findToolIdByIndex(index, toolBuffers, indexToToolId);
189
192
  if (id) {
190
193
  const buf = toolBuffers.get(id);
191
194
  if (buf) {
@@ -197,12 +200,11 @@ function translateSse(event, parsed, toolBuffers) {
197
200
  return out;
198
201
  }
199
202
  case 'content_block_stop': {
200
- // The 'index' of the stopped block tells us which tool finished.
201
- // We track tool starts in order and match by index.
202
203
  const index = parsed.index;
203
- const id = findToolIdByIndex(index, toolBuffers);
204
+ const id = findToolIdByIndex(index, toolBuffers, indexToToolId);
204
205
  if (id) {
205
206
  toolBuffers.delete(id);
207
+ // Keep index mapping for late deltas that may arrive after stop (rare)
206
208
  out.push({ kind: 'tool_call_end', id });
207
209
  }
208
210
  return out;
@@ -226,22 +228,22 @@ function translateSse(event, parsed, toolBuffers) {
226
228
  }
227
229
  }
228
230
  /** Match an Anthropic content_block index to the tool_use id we emitted. */
229
- function findToolIdByIndex(index, buffers) {
231
+ function findToolIdByIndex(index, buffers, indexToToolId) {
230
232
  if (index === undefined)
231
233
  return undefined;
232
- // buffers is a Map preserving insertion order. The index of a tool_use
233
- // block is its position among all content blocks, not just tool_use
234
- // blocks. We don't know about non-tool blocks, so we use a simpler
235
- // heuristic: the index matches the i-th tool_use that has been started
236
- // since message_start. This is good enough for streamed responses where
237
- // tool blocks are typically 0,1,2...
234
+ // Preferred: direct index id mapping from content_block_start
235
+ if (indexToToolId) {
236
+ const direct = indexToToolId.get(index);
237
+ if (direct)
238
+ return direct;
239
+ }
240
+ // Fallback heuristic for older streams without index on start
238
241
  let i = 0;
239
242
  for (const id of buffers.keys()) {
240
243
  if (i === index)
241
244
  return id;
242
245
  i++;
243
246
  }
244
- // Fallback: if there's exactly one tool in flight, it's almost certainly it.
245
247
  if (buffers.size === 1)
246
248
  return buffers.keys().next().value;
247
249
  return undefined;
@@ -10,6 +10,7 @@
10
10
  * The adapter exposes a single async generator of StreamEvents so the
11
11
  * runtime loop sees one shape regardless of provider quirks.
12
12
  */
13
+ import { redact } from '../policy/secret-redactor.js';
13
14
  const DEFAULT_TIMEOUT_MS = 120_000;
14
15
  /** Convert a Zod schema to a permissive JSON Schema object for tool defs. */
15
16
  export function zodToJsonSchema(schema) {
@@ -18,7 +19,7 @@ export function zodToJsonSchema(schema) {
18
19
  // inputs are flat objects with primitive types.
19
20
  const def = schema._def;
20
21
  if (!def)
21
- return { type: 'object', properties: {}, additionalProperties: true };
22
+ return { type: 'object', properties: {}, additionalProperties: false };
22
23
  if (def.typeName === 'ZodObject' && def.shape) {
23
24
  const props = {};
24
25
  const required = [];
@@ -33,7 +34,7 @@ export function zodToJsonSchema(schema) {
33
34
  out.additionalProperties = false;
34
35
  return out;
35
36
  }
36
- return { type: 'object', properties: {}, additionalProperties: true };
37
+ return { type: 'object', properties: {}, additionalProperties: false };
37
38
  }
38
39
  function zodFieldSchema(s) {
39
40
  const def = s._def;
@@ -124,7 +125,7 @@ export function buildChatCompletionsBody(req) {
124
125
  }
125
126
  export function httpChatAdapter(opts) {
126
127
  const fetchImpl = opts.fetchImpl ?? fetch;
127
- const url = `${opts.baseURL.replace(/\/$/, '')}/chat/completions`;
128
+ const url = `${opts.baseURL.replace(/\/+$/, '')}/chat/completions`;
128
129
  return {
129
130
  id: 'http-chat',
130
131
  stream(req) {
@@ -161,11 +162,12 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
161
162
  if (!res.ok || !res.body) {
162
163
  clearTimeout(timer);
163
164
  req.signal?.removeEventListener('abort', onAbort);
164
- const errText = await res.text().catch(() => '');
165
+ const rawErr = await res.text().catch(() => '');
166
+ const errText = redact(rawErr).slice(0, 500);
165
167
  yield {
166
168
  kind: 'error',
167
169
  code: `HTTP_${res.status}`,
168
- message: `provider returned ${res.status}: ${errText.slice(0, 500)}`,
170
+ message: `provider returned ${res.status}: ${errText}`,
169
171
  retryable: res.status >= 500 || res.status === 429,
170
172
  };
171
173
  return;
@@ -177,6 +179,7 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
177
179
  // Track per-tool-call id by index.
178
180
  const toolIds = new Map();
179
181
  const toolNames = new Map();
182
+ let pendingUsage;
180
183
  try {
181
184
  while (true) {
182
185
  const { value, done } = await reader.read();
@@ -203,6 +206,10 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
203
206
  catch {
204
207
  continue;
205
208
  }
209
+ // Capture usage even when finish_reason is null (Ollama/vLLM style)
210
+ if (chunk.usage) {
211
+ pendingUsage = { input: chunk.usage.prompt_tokens, output: chunk.usage.completion_tokens };
212
+ }
206
213
  for (const choice of chunk.choices) {
207
214
  if (choice.delta.content) {
208
215
  yield { kind: 'text_delta', text: choice.delta.content };
@@ -222,9 +229,16 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
222
229
  }
223
230
  }
224
231
  if (choice.finish_reason) {
225
- const usage = chunk.usage
232
+ const usage = pendingUsage ?? (chunk.usage
226
233
  ? { input: chunk.usage.prompt_tokens, output: chunk.usage.completion_tokens }
227
- : undefined;
234
+ : undefined);
235
+ pendingUsage = undefined;
236
+ // Clear tool tracking per message to avoid stale ids on next turn
237
+ const ids = [...toolIds.values()];
238
+ toolIds.clear();
239
+ toolNames.clear();
240
+ for (const id of ids)
241
+ yield { kind: 'tool_call_end', id };
228
242
  yield { kind: 'message_end', finishReason: choice.finish_reason, usage };
229
243
  }
230
244
  }
@@ -234,6 +248,15 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
234
248
  if (toolIds.size) {
235
249
  for (const id of toolIds.values())
236
250
  yield { kind: 'tool_call_end', id };
251
+ if (pendingUsage) {
252
+ yield { kind: 'message_end', usage: pendingUsage };
253
+ }
254
+ else {
255
+ yield { kind: 'message_end' };
256
+ }
257
+ }
258
+ else if (pendingUsage) {
259
+ yield { kind: 'message_end', usage: pendingUsage };
237
260
  }
238
261
  else {
239
262
  yield { kind: 'message_end' };
@@ -19,9 +19,10 @@
19
19
  import { httpChatAdapter } from './provider-adapter.js';
20
20
  import { anthropicAdapter } from './anthropic-adapter.js';
21
21
  import { retryingAdapter } from './retry.js';
22
- /** Heuristic: hosts that look like Anthropic's API. */
22
+ /** Heuristic: hosts that look like Anthropic's API. Only matches anthropic.com and subdomains, not substring. */
23
23
  function looksLikeAnthropic(host) {
24
- return /(^|\.)anthropic\.com$/i.test(host) || /anthropic/i.test(host);
24
+ const h = host.toLowerCase();
25
+ return h === 'anthropic.com' || h.endsWith('.anthropic.com');
25
26
  }
26
27
  export function inferProviderFromBaseURL(baseURL) {
27
28
  if (!baseURL)
@@ -34,6 +35,18 @@ export function inferProviderFromBaseURL(baseURL) {
34
35
  return 'openai';
35
36
  }
36
37
  }
38
+ function isLoopbackBaseURL(url) {
39
+ if (!url)
40
+ return false;
41
+ try {
42
+ const u = new URL(url);
43
+ const h = u.hostname.toLowerCase();
44
+ return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0.0.0.0' || h === '::' || h.startsWith('127.');
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
37
50
  export function buildProvider(opts = {}) {
38
51
  const baseURL = opts.baseURL ?? process.env.KLYRO_BASE_URL;
39
52
  const apiKey = opts.apiKey ?? process.env.KLYRO_API_KEY;
@@ -52,15 +65,19 @@ export function buildProvider(opts = {}) {
52
65
  let inner;
53
66
  if (provider === 'anthropic') {
54
67
  if (!apiKey) {
55
- throw new Error('buildProvider: anthropic provider requires an apiKey (or KLYRO_API_KEY).');
68
+ throw new Error('klyro: invalid --provider: anthropic requires an API key (set KLYRO_API_KEY or pass --api-key)');
56
69
  }
57
70
  inner = anthropicAdapter({ baseURL, apiKey, timeoutMs });
58
71
  }
59
72
  else {
60
- if (!baseURL || !apiKey) {
61
- throw new Error('buildProvider: openai provider requires baseURL and apiKey (or KLYRO_BASE_URL + KLYRO_API_KEY).');
73
+ if (!baseURL) {
74
+ throw new Error('klyro: invalid --provider: openai requires --base-url or KLYRO_BASE_URL');
75
+ }
76
+ // Allow empty apiKey for loopback local servers (Ollama etc.)
77
+ if (!apiKey && !isLoopbackBaseURL(baseURL)) {
78
+ throw new Error('klyro: invalid --provider: openai requires --api-key or KLYRO_API_KEY (or use a localhost URL for local models)');
62
79
  }
63
- inner = httpChatAdapter({ baseURL, apiKey, timeoutMs });
80
+ inner = httpChatAdapter({ baseURL, apiKey: apiKey ?? '', timeoutMs });
64
81
  }
65
82
  if (opts.retry === false)
66
83
  return inner;
@@ -75,7 +92,7 @@ export function buildProvider(opts = {}) {
75
92
  export function buildProviderFromCli(args) {
76
93
  const provider = args.provider;
77
94
  if (provider && provider !== 'openai' && provider !== 'anthropic') {
78
- throw new Error(`unknown provider: ${provider}`);
95
+ throw new Error(`klyro: invalid --provider: ${provider} (expected openai|anthropic)`);
79
96
  }
80
97
  return buildProvider({
81
98
  provider,
@@ -33,13 +33,28 @@ async function* streamWithAbort(source, signal) {
33
33
  return;
34
34
  }
35
35
  const it = source[Symbol.asyncIterator]();
36
- while (true) {
37
- if (signal.aborted)
38
- return;
39
- const next = await it.next();
40
- if (next.done)
41
- return;
42
- yield next.value;
36
+ try {
37
+ while (true) {
38
+ if (signal.aborted) {
39
+ try {
40
+ await it.return?.();
41
+ }
42
+ catch { /* ignore */ }
43
+ return;
44
+ }
45
+ const next = await it.next();
46
+ if (next.done)
47
+ return;
48
+ yield next.value;
49
+ }
50
+ }
51
+ finally {
52
+ if (signal.aborted) {
53
+ try {
54
+ await it.return?.();
55
+ }
56
+ catch { /* ignore */ }
57
+ }
43
58
  }
44
59
  }
45
60
  export function computeBackoff(attempt, baseMs, maxMs) {
@@ -54,14 +69,44 @@ export function retryingAdapter(inner, opts = {}) {
54
69
  return {
55
70
  id: `${inner.id}+retry`,
56
71
  async *stream(req, signal) {
57
- const effectiveSignal = signal ?? opts.signal;
72
+ // Support both calling conventions: stream(req) where req.signal is set, and legacy stream(req, signal)
73
+ // Combine per-request signal (req.signal), legacy second-arg signal, and adapter-level opts.signal
74
+ const getEffectiveSignal = (legacySignal) => {
75
+ const sigs = [req.signal, legacySignal, opts.signal].filter(Boolean);
76
+ if (sigs.length === 0)
77
+ return undefined;
78
+ if (sigs.length === 1)
79
+ return sigs[0];
80
+ // If any aborts, effective aborts — create a combined controller
81
+ const ctrl = new AbortController();
82
+ const onAbort = () => {
83
+ const reason = sigs.find((s) => s.aborted)?.reason ?? sigs[0]?.reason;
84
+ try {
85
+ ctrl.abort(reason);
86
+ }
87
+ catch {
88
+ ctrl.abort();
89
+ }
90
+ };
91
+ if (sigs.some((s) => s.aborted)) {
92
+ onAbort();
93
+ }
94
+ else {
95
+ for (const s of sigs)
96
+ s.addEventListener('abort', onAbort, { once: true });
97
+ }
98
+ return ctrl.signal;
99
+ };
58
100
  for (let attempt = 0; attempt < cfg.maxAttempts; attempt++) {
101
+ const effectiveSignal = getEffectiveSignal(signal);
102
+ // Clone req per attempt to avoid reusing aborted signal
103
+ const attemptReq = effectiveSignal ? { ...req, signal: effectiveSignal } : req;
59
104
  opts.onAttempt?.(attempt);
60
105
  if (effectiveSignal?.aborted)
61
106
  return;
62
107
  let sawRetryable = false;
63
108
  let lastError = null;
64
- for await (const ev of streamWithAbort(inner.stream(req), effectiveSignal)) {
109
+ for await (const ev of streamWithAbort(inner.stream(attemptReq), effectiveSignal)) {
65
110
  if (effectiveSignal?.aborted)
66
111
  return;
67
112
  if (ev.kind === 'error' && ev.retryable) {
@@ -40,6 +40,8 @@ export interface RunOptions {
40
40
  cwd: string;
41
41
  model: string;
42
42
  maxSteps?: number;
43
+ /** Alias for maxSteps (3.5) */
44
+ maxTurns?: number;
43
45
  maxTokens?: number;
44
46
  temperature?: number;
45
47
  signal?: AbortSignal;
@@ -60,6 +62,27 @@ export interface RunOptions {
60
62
  * that don't pass it pay zero cost.
61
63
  */
62
64
  onEvent?: (ev: RuntimeEvent) => void;
65
+ /**
66
+ * Level 8 — verification config. When enabled and the agent has edited
67
+ * files, the runtime auto-runs a verification command after the agent
68
+ * claims completion, feeds failure diagnostics back, and retries up to
69
+ * maxRepairAttempts.
70
+ */
71
+ verify?: {
72
+ enabled?: boolean;
73
+ command?: string;
74
+ maxRepairAttempts?: number;
75
+ timeoutMs?: number;
76
+ };
77
+ /**
78
+ * Level 9 — persistence. When a SessionStore is provided, every message
79
+ * and observation is checkpointed through it. This enables resume after
80
+ * interrupt and history inspection.
81
+ */
82
+ persist?: {
83
+ store?: import('../persistence/store.js').SessionStore;
84
+ sessionId?: string;
85
+ };
63
86
  }
64
87
  /** A single plan step emitted by the agent. */
65
88
  export interface PlanStep {
@@ -125,9 +148,23 @@ export type RuntimeEvent = {
125
148
  kind: 'verification_failed';
126
149
  step: string;
127
150
  reason: string;
151
+ } | {
152
+ kind: 'verification_started';
153
+ command: string;
154
+ } | {
155
+ kind: 'verification_succeeded';
156
+ command: string;
157
+ } | {
158
+ kind: 'repair_started';
159
+ attempt: number;
160
+ maxAttempts: number;
161
+ reason: string;
162
+ } | {
163
+ kind: 'checkpoint_saved';
164
+ sessionId: string;
128
165
  };
129
166
  export interface RunResult {
130
- status: 'complete' | 'max_steps' | 'aborted' | 'no_final';
167
+ status: 'complete' | 'max_steps' | 'aborted' | 'no_final' | 'verify_failed';
131
168
  steps: number;
132
169
  toolCalls: number;
133
170
  finalText: string;
@@ -138,6 +175,13 @@ export interface RunResult {
138
175
  };
139
176
  /** Number of policy-driven user prompts the user accepted. */
140
177
  repairs?: number;
178
+ /** Verification outcome (Level 8) */
179
+ verification?: {
180
+ ok: boolean;
181
+ command?: string;
182
+ attempts: number;
183
+ failureType?: string;
184
+ };
141
185
  }
142
186
  /** Convert a registry of tools into ToolDefinitions for the provider. */
143
187
  export declare function toolDefinitions(registry: ToolRegistry): ToolDefinition[];