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.
- package/dist/agent/anthropic-adapter.d.ts +1 -1
- package/dist/agent/anthropic-adapter.js +22 -20
- package/dist/agent/provider-adapter.js +30 -7
- package/dist/agent/registry.js +24 -7
- package/dist/agent/retry.js +54 -9
- package/dist/agent/runtime.d.ts +45 -1
- package/dist/agent/runtime.js +236 -23
- package/dist/chat.d.ts +3 -3
- package/dist/chat.js +137 -39
- package/dist/cli/auth.d.ts +9 -0
- package/dist/cli/auth.js +84 -0
- package/dist/cli/completion.d.ts +6 -0
- package/dist/cli/completion.js +66 -0
- package/dist/cli/config.d.ts +44 -0
- package/dist/cli/config.js +428 -0
- package/dist/cli/doctor.d.ts +8 -0
- package/dist/cli/doctor.js +136 -0
- package/dist/cli/errors.d.ts +5 -0
- package/dist/cli/errors.js +37 -0
- package/dist/cli/markdown.d.ts +9 -0
- package/dist/cli/markdown.js +77 -0
- package/dist/cli/repl.js +113 -5
- package/dist/cli/run.d.ts +9 -0
- package/dist/cli/run.js +97 -2
- package/dist/cli/slash/parser.d.ts +10 -0
- package/dist/cli/slash/parser.js +6 -1
- package/dist/cli/trace.d.ts +7 -0
- package/dist/cli/trace.js +63 -0
- package/dist/cli/update.d.ts +6 -0
- package/dist/cli/update.js +70 -0
- package/dist/context/system-prompt.d.ts +14 -0
- package/dist/context/system-prompt.js +47 -0
- package/dist/events/bus.d.ts +16 -0
- package/dist/events/bus.js +28 -0
- package/dist/events/catalog.d.ts +136 -0
- package/dist/events/catalog.js +5 -0
- package/dist/index.js +335 -22
- package/dist/persistence/session.d.ts +12 -0
- package/dist/persistence/session.js +45 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +85 -2
- package/dist/providers/model-info.d.ts +18 -0
- package/dist/providers/model-info.js +18 -0
- package/dist/providers.js +3 -2
- package/dist/renderers/json.d.ts +11 -0
- package/dist/renderers/json.js +14 -0
- package/dist/renderers/terminal.d.ts +9 -0
- package/dist/renderers/terminal.js +41 -0
- package/dist/repl.js +52 -15
- package/dist/shared/errors.d.ts +18 -0
- package/dist/shared/errors.js +33 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/types.d.ts +19 -0
- package/dist/shared/types.js +11 -0
- package/dist/tools/fs/read-file.d.ts +1 -31
- package/dist/tools/fs/read-file.js +45 -17
- package/dist/tools/fs/read-history.d.ts +3 -0
- package/dist/tools/fs/read-history.js +13 -0
- package/dist/tools/fs/write-file.d.ts +1 -4
- package/dist/tools/fs/write-file.js +35 -1
- package/dist/tools/normalize.d.ts +1 -1
- package/dist/tools/shell/shell-exec.d.ts +1 -26
- package/dist/tools/shell/shell-exec.js +121 -17
- package/dist/tools/types.d.ts +25 -0
- package/dist/trace/writer.d.ts +13 -0
- package/dist/trace/writer.js +53 -0
- package/dist/tui/app.d.ts +1 -15
- package/dist/tui/app.js +203 -22
- package/dist/tui/approval.js +25 -4
- package/dist/tui/approval.test.js +1 -1
- package/dist/tui/snapshot.test.js +2 -2
- package/dist/tui/status.js +5 -1
- package/dist/util/log.d.ts +12 -0
- package/dist/util/log.js +75 -0
- package/dist/verification/auto.d.ts +8 -0
- package/dist/verification/auto.js +52 -0
- 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
|
|
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
|
-
|
|
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
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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' };
|
package/dist/agent/registry.js
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
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
|
|
61
|
-
throw new Error('
|
|
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(`
|
|
95
|
+
throw new Error(`klyro: invalid --provider: ${provider} (expected openai|anthropic)`);
|
|
79
96
|
}
|
|
80
97
|
return buildProvider({
|
|
81
98
|
provider,
|
package/dist/agent/retry.js
CHANGED
|
@@ -33,13 +33,28 @@ async function* streamWithAbort(source, signal) {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const it = source[Symbol.asyncIterator]();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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(
|
|
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) {
|
package/dist/agent/runtime.d.ts
CHANGED
|
@@ -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[];
|