klyro 1.0.16 → 1.0.18
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.js +26 -1
- package/dist/agent/provider-adapter.d.ts +7 -0
- package/dist/agent/provider-adapter.js +26 -1
- package/dist/agent/runtime.d.ts +14 -0
- package/dist/agent/runtime.js +59 -22
- package/dist/chat.js +16 -56
- package/dist/checkpoints/store.d.ts +15 -1
- package/dist/checkpoints/store.js +7 -2
- package/dist/cli/args.d.ts +7 -0
- package/dist/cli/args.js +10 -0
- package/dist/cli/completion.js +0 -3
- package/dist/cli/doctor.d.ts +13 -0
- package/dist/cli/doctor.js +133 -1
- package/dist/cli/errors.js +1 -1
- package/dist/cli/eval.d.ts +32 -0
- package/dist/cli/eval.js +83 -6
- package/dist/cli/markdown.js +1 -1
- package/dist/cli/repl.js +13 -24
- package/dist/cli/run.d.ts +37 -0
- package/dist/cli/run.js +81 -9
- package/dist/cli/update.js +7 -1
- package/dist/context/accounting.js +1 -1
- package/dist/context/klyro-md.d.ts +11 -0
- package/dist/context/klyro-md.js +59 -4
- package/dist/context/level7.d.ts +4 -0
- package/dist/context/level7.js +20 -1
- package/dist/context/project-map.js +14 -13
- package/dist/eval/baseline.d.ts +47 -0
- package/dist/eval/baseline.js +55 -0
- package/dist/eval/harness.d.ts +14 -2
- package/dist/eval/harness.js +42 -4
- package/dist/index.js +57 -3
- package/dist/persistence/store.js +2 -2
- package/dist/policy/approval.js +3 -0
- package/dist/policy/path-guard.d.ts +9 -4
- package/dist/policy/path-guard.js +47 -23
- package/dist/providers/endpoints.js +8 -0
- package/dist/providers/model-info.d.ts +1 -0
- package/dist/providers/model-info.js +19 -0
- package/dist/providers.js +6 -2
- package/dist/renderers/terminal.d.ts +7 -0
- package/dist/renderers/terminal.js +52 -0
- package/dist/repl.d.ts +9 -0
- package/dist/repl.js +24 -5
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/proxy.d.ts +37 -0
- package/dist/shared/proxy.js +360 -0
- package/dist/tools/agent/task-list.d.ts +1 -1
- package/dist/tools/fs/apply-patch.js +4 -4
- package/dist/tools/fs/edit-file.js +2 -4
- package/dist/tools/fs/list-dir.js +1 -1
- package/dist/tools/fs/multi-edit.js +2 -2
- package/dist/tools/fs/read-file.js +1 -1
- package/dist/tools/fs/write-file.js +2 -2
- package/dist/tools/git/git-blame.d.ts +5 -0
- package/dist/tools/git/git-blame.js +22 -0
- package/dist/tools/git/git-diff.js +3 -20
- package/dist/tools/git/git-log.js +5 -19
- package/dist/tools/git/git-show.d.ts +5 -0
- package/dist/tools/git/git-show.js +22 -0
- package/dist/tools/git/git-status.js +4 -21
- package/dist/tools/git/run-git.d.ts +5 -0
- package/dist/tools/git/run-git.js +18 -0
- package/dist/tools/lsp/diagnostics.js +2 -3
- package/dist/tools/plan/todo-write.d.ts +1 -1
- package/dist/tools/registry.js +4 -0
- package/dist/tools/search/dependencies.d.ts +4 -4
- package/dist/tools/search/dependencies.js +1 -1
- package/dist/tools/search/glob.js +9 -4
- package/dist/tools/search/grep.js +11 -4
- package/dist/tools/search/ignore.d.ts +8 -0
- package/dist/tools/search/ignore.js +49 -0
- package/dist/tools/search/recent-files.js +1 -1
- package/dist/tools/search/search-files.js +11 -4
- package/dist/tools/web/web-fetch.js +2 -1
- package/dist/tools/web/web-search.js +3 -1
- package/dist/tui/app.js +117 -10
- package/dist/tui/app.test.js +65 -4
- package/dist/tui/measure.js +1 -0
- package/dist/tui/mouse.d.ts +7 -5
- package/dist/tui/mouse.js +9 -6
- package/dist/tui/scroll-flow.test.js +0 -1
- package/dist/tui/transcript-commands.d.ts +14 -0
- package/dist/tui/transcript-commands.js +31 -0
- package/dist/util/log.d.ts +19 -10
- package/dist/util/log.js +87 -52
- package/dist/verification/classify.d.ts +1 -1
- package/dist/verification/classify.js +2 -2
- package/dist/verification/engine.js +0 -6
- package/dist/verification/scoped.js +6 -4
- package/package.json +6 -1
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { assertSafeBaseURL } from '../chat.js';
|
|
19
19
|
import { parseRetryAfterMs } from './provider-adapter.js';
|
|
20
|
+
import { estimateTokens } from '../context/tokenizer.js';
|
|
21
|
+
import { proxiedFetch } from '../shared/proxy.js';
|
|
20
22
|
const DEFAULT_VERSION = '2023-06-01';
|
|
21
23
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
22
24
|
export const PROMPT_CACHING_BETA = 'prompt-caching-2024-07-31';
|
|
@@ -40,7 +42,7 @@ export function anthropicAdapter(opts) {
|
|
|
40
42
|
const betas = [...(opts.betas ?? [])];
|
|
41
43
|
if (promptCache && !betas.includes(PROMPT_CACHING_BETA))
|
|
42
44
|
betas.push(PROMPT_CACHING_BETA);
|
|
43
|
-
const fetchImpl = opts.fetchImpl ??
|
|
45
|
+
const fetchImpl = opts.fetchImpl ?? proxiedFetch;
|
|
44
46
|
if (!fetchImpl) {
|
|
45
47
|
throw new Error('anthropicAdapter: no fetch available — pass opts.fetchImpl or run on Node 18+');
|
|
46
48
|
}
|
|
@@ -52,6 +54,29 @@ export function anthropicAdapter(opts) {
|
|
|
52
54
|
fetchImpl, version, authHeader, betas, promptCache,
|
|
53
55
|
});
|
|
54
56
|
},
|
|
57
|
+
// 2.1 — capability discovery via the Anthropic Models API.
|
|
58
|
+
// Returns [] when unavailable (callers fall back to configured ids).
|
|
59
|
+
async listModels() {
|
|
60
|
+
try {
|
|
61
|
+
const headers = { 'anthropic-version': version };
|
|
62
|
+
if (authHeader === 'x-api-key')
|
|
63
|
+
headers['x-api-key'] = opts.apiKey;
|
|
64
|
+
else
|
|
65
|
+
headers['Authorization'] = `Bearer ${opts.apiKey}`;
|
|
66
|
+
const res = await fetchImpl(`${baseURL}/v1/models`, { headers });
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
return [];
|
|
69
|
+
const json = (await res.json());
|
|
70
|
+
const ids = Array.isArray(json.data) ? json.data.map((m) => m.id).filter((id) => typeof id === 'string' && id.length > 0) : [];
|
|
71
|
+
return [...new Set(ids)];
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
countTokens(text) {
|
|
78
|
+
return estimateTokens(text);
|
|
79
|
+
},
|
|
55
80
|
};
|
|
56
81
|
}
|
|
57
82
|
/**
|
|
@@ -68,11 +68,17 @@ export interface CallRequest {
|
|
|
68
68
|
tools: ToolDefinition[];
|
|
69
69
|
maxTokens?: number;
|
|
70
70
|
temperature?: number;
|
|
71
|
+
/** Reasoning effort for reasoning models ( OpenAI `reasoning_effort` ); adapters that lack the concept ignore it. */
|
|
72
|
+
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
71
73
|
signal?: AbortSignal;
|
|
72
74
|
}
|
|
73
75
|
export interface ProviderAdapter {
|
|
74
76
|
readonly id: string;
|
|
75
77
|
stream(req: CallRequest): AsyncIterable<StreamEvent>;
|
|
78
|
+
/** Optional capability discovery: model ids (2.1). Absent = unknown, use configured ids. */
|
|
79
|
+
listModels?(): Promise<string[]>;
|
|
80
|
+
/** Optional local token estimate (2.1). Absent = caller estimates. */
|
|
81
|
+
countTokens?(text: string): number | Promise<number>;
|
|
76
82
|
}
|
|
77
83
|
export interface HttpAdapterOptions {
|
|
78
84
|
baseURL: string;
|
|
@@ -110,6 +116,7 @@ interface ChatCompletionsRequest {
|
|
|
110
116
|
}>;
|
|
111
117
|
max_tokens?: number;
|
|
112
118
|
temperature?: number;
|
|
119
|
+
reasoning_effort?: 'low' | 'medium' | 'high';
|
|
113
120
|
stream: true;
|
|
114
121
|
}
|
|
115
122
|
/**
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* runtime loop sees one shape regardless of provider quirks.
|
|
12
12
|
*/
|
|
13
13
|
import { redact } from '../policy/secret-redactor.js';
|
|
14
|
+
import { estimateTokens } from '../context/tokenizer.js';
|
|
15
|
+
import { proxiedFetch } from '../shared/proxy.js';
|
|
14
16
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
15
17
|
/**
|
|
16
18
|
* Parse a `Retry-After` response header value into milliseconds.
|
|
@@ -157,6 +159,8 @@ export function buildChatCompletionsBody(req) {
|
|
|
157
159
|
body.max_tokens = req.maxTokens;
|
|
158
160
|
if (typeof req.temperature === 'number')
|
|
159
161
|
body.temperature = req.temperature;
|
|
162
|
+
if (req.reasoningEffort)
|
|
163
|
+
body.reasoning_effort = req.reasoningEffort;
|
|
160
164
|
if (req.tools.length) {
|
|
161
165
|
body.tools = req.tools.map((t) => ({
|
|
162
166
|
type: 'function',
|
|
@@ -166,13 +170,34 @@ export function buildChatCompletionsBody(req) {
|
|
|
166
170
|
return body;
|
|
167
171
|
}
|
|
168
172
|
export function httpChatAdapter(opts) {
|
|
169
|
-
const fetchImpl = opts.fetchImpl ??
|
|
173
|
+
const fetchImpl = opts.fetchImpl ?? proxiedFetch;
|
|
170
174
|
const url = `${opts.baseURL.replace(/\/+$/, '')}/chat/completions`;
|
|
171
175
|
return {
|
|
172
176
|
id: 'http-chat',
|
|
173
177
|
stream(req) {
|
|
174
178
|
return streamChatCompletions(url, opts, req, fetchImpl);
|
|
175
179
|
},
|
|
180
|
+
// 2.1 — capability discovery: list model ids via OpenAI /models.
|
|
181
|
+
// Returns [] when the endpoint doesn't serve a model list (not an
|
|
182
|
+
// error: callers fall back to configured ids).
|
|
183
|
+
async listModels() {
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetchImpl(`${opts.baseURL.replace(/\/+$/, '')}/models`, {
|
|
186
|
+
headers: opts.apiKey ? { Authorization: `Bearer ${opts.apiKey}` } : {},
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok)
|
|
189
|
+
return [];
|
|
190
|
+
const json = (await res.json());
|
|
191
|
+
const ids = Array.isArray(json.data) ? json.data.map((m) => m.id).filter((id) => typeof id === 'string' && id.length > 0) : [];
|
|
192
|
+
return [...new Set(ids)];
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
countTokens(text) {
|
|
199
|
+
return estimateTokens(text);
|
|
200
|
+
},
|
|
176
201
|
};
|
|
177
202
|
}
|
|
178
203
|
async function* streamChatCompletions(url, opts, req, fetchImpl) {
|
package/dist/agent/runtime.d.ts
CHANGED
|
@@ -83,6 +83,7 @@ export interface RunOptions {
|
|
|
83
83
|
maxTimeMs?: number;
|
|
84
84
|
maxTokens?: number;
|
|
85
85
|
temperature?: number;
|
|
86
|
+
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
86
87
|
signal?: AbortSignal;
|
|
87
88
|
nonInteractive: boolean;
|
|
88
89
|
/**
|
|
@@ -157,6 +158,12 @@ export interface RunOptions {
|
|
|
157
158
|
allowedPaths?: readonly string[];
|
|
158
159
|
model?: string;
|
|
159
160
|
};
|
|
161
|
+
/**
|
|
162
|
+
* Root sandbox extensions (--add-dir): extra resolvable roots for file
|
|
163
|
+
* tools, merged with cwd. Child scopes (parentContext.allowedPaths) still
|
|
164
|
+
* narrow further when present.
|
|
165
|
+
*/
|
|
166
|
+
allowedPaths?: readonly string[];
|
|
160
167
|
/**
|
|
161
168
|
* Delegation bridge (P0). Present on the root run so the model can call
|
|
162
169
|
* spawn_agent / task_list / task_get. The tool layer reads it from the
|
|
@@ -264,6 +271,13 @@ export type RuntimeEvent = {
|
|
|
264
271
|
kind: 'model_override';
|
|
265
272
|
requested: string;
|
|
266
273
|
effective: string;
|
|
274
|
+
} | {
|
|
275
|
+
kind: 'turn.summary';
|
|
276
|
+
turn: number;
|
|
277
|
+
toolCalls: number;
|
|
278
|
+
inputTokens: number;
|
|
279
|
+
outputTokens: number;
|
|
280
|
+
durationMs: number;
|
|
267
281
|
};
|
|
268
282
|
export interface RunResult {
|
|
269
283
|
status: 'complete' | 'max_steps' | 'aborted' | 'no_final' | 'verify_failed' | 'limit' | 'blocked' | 'stuck';
|
package/dist/agent/runtime.js
CHANGED
|
@@ -119,6 +119,8 @@ export async function run(opts, deps) {
|
|
|
119
119
|
}
|
|
120
120
|
};
|
|
121
121
|
let steps = 0;
|
|
122
|
+
// 4.5c — per-turn start time for turn.summary durationMs.
|
|
123
|
+
let turnStartMs = Date.now();
|
|
122
124
|
let lastRemindTurn = 0;
|
|
123
125
|
let toolCallCount = 0;
|
|
124
126
|
let finalText = '';
|
|
@@ -223,18 +225,6 @@ export async function run(opts, deps) {
|
|
|
223
225
|
const sessionId = opts.persist?.sessionId;
|
|
224
226
|
// 6.1 baseline cache per HEAD — capture before first edit
|
|
225
227
|
let baselinePrimed = false;
|
|
226
|
-
async function primeBaseline() {
|
|
227
|
-
if (baselinePrimed)
|
|
228
|
-
return;
|
|
229
|
-
baselinePrimed = true;
|
|
230
|
-
const cmd = opts.verify?.command ?? detectVerifyCommand(opts.cwd);
|
|
231
|
-
if (!cmd)
|
|
232
|
-
return;
|
|
233
|
-
try {
|
|
234
|
-
await ensureBaseline(opts.cwd, cmd);
|
|
235
|
-
}
|
|
236
|
-
catch { /* ignore */ }
|
|
237
|
-
}
|
|
238
228
|
async function checkpoint(msg, obs) {
|
|
239
229
|
if (!store || !sessionId)
|
|
240
230
|
return;
|
|
@@ -259,6 +249,23 @@ export async function run(opts, deps) {
|
|
|
259
249
|
// best-effort — don't crash runtime on persistence failure
|
|
260
250
|
}
|
|
261
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* 4.5c — end-of-turn summary: emit a turn.summary event (cumulative
|
|
254
|
+
* toolCalls + token counts, per-turn durationMs) and record a compact
|
|
255
|
+
* one-line summary in the transcript.
|
|
256
|
+
*/
|
|
257
|
+
async function emitTurnSummary() {
|
|
258
|
+
const durationMs = Date.now() - turnStartMs;
|
|
259
|
+
emit?.({ kind: 'turn.summary', turn: steps, toolCalls: toolCallCount, inputTokens: usage.input, outputTokens: usage.output, durationMs });
|
|
260
|
+
const msg = {
|
|
261
|
+
role: 'user',
|
|
262
|
+
content: [text(`[turn ${steps} summary] ${toolCallCount} tool call(s), ${usage.input} in / ${usage.output} out tokens, ${durationMs}ms`)],
|
|
263
|
+
};
|
|
264
|
+
transcript.push(msg);
|
|
265
|
+
await checkpoint(msg);
|
|
266
|
+
// Invalidate token cache since transcript changed
|
|
267
|
+
tokenCache = { lastRef: null, lastSystem: undefined, lastCount: 0 };
|
|
268
|
+
}
|
|
262
269
|
// Persist initial user message
|
|
263
270
|
if (store && sessionId && transcript.length > 0) {
|
|
264
271
|
// Fire-and-forget initial checkpoint (don't await to block loop start)
|
|
@@ -319,6 +326,11 @@ export async function run(opts, deps) {
|
|
|
319
326
|
// Abort cascade (fix: background shells must not outlive the run).
|
|
320
327
|
const killed = killAllJobs();
|
|
321
328
|
emitKlyro({ type: 'abort', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', reason: killed.length > 0 ? `aborted by operator (${killed.length} background job(s) killed)` : 'aborted by operator' });
|
|
329
|
+
// 2.4 — explicit interrupted marker: partial output stays labeled in
|
|
330
|
+
// the transcript so resume/replay never mistakes it for final text.
|
|
331
|
+
const interruptedNote = { role: 'user', content: [text('[system note] Interrupted by user — output above is partial and preserved.')] };
|
|
332
|
+
transcript.push(interruptedNote);
|
|
333
|
+
await checkpoint(interruptedNote);
|
|
322
334
|
if (store && sessionId) {
|
|
323
335
|
try {
|
|
324
336
|
await store.setStatus(sessionId, 'aborted', finalText);
|
|
@@ -329,6 +341,7 @@ export async function run(opts, deps) {
|
|
|
329
341
|
return { status: 'aborted', steps, toolCalls: toolCallCount, finalText, transcript, hasEdits, usage, repairs, verification: hasEdits ? withRepairTokens({ ok: false, attempts: verificationAttempts }) : undefined, phase: 'blocked' };
|
|
330
342
|
}
|
|
331
343
|
steps++;
|
|
344
|
+
turnStartMs = Date.now();
|
|
332
345
|
// 8.4 — stale-todo reminder: every 20 turns, re-inject pending plan
|
|
333
346
|
// items from `.klyro/plans/todos.json` (written by todo_write) so a
|
|
334
347
|
// long run cannot silently drop its checklist. Best-effort + tiny.
|
|
@@ -399,23 +412,22 @@ export async function run(opts, deps) {
|
|
|
399
412
|
tools: toolDefinitions(deps.registry),
|
|
400
413
|
...(opts.maxTokens ? { maxTokens: opts.maxTokens } : {}),
|
|
401
414
|
...(typeof opts.temperature === 'number' ? { temperature: opts.temperature } : {}),
|
|
415
|
+
...(opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {}),
|
|
402
416
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
403
417
|
};
|
|
404
418
|
const events = activeAdapter.stream(req);
|
|
405
419
|
let textBuf = '';
|
|
406
420
|
// Thinking is ephemeral: streamed to the UI live, never stored in the
|
|
407
421
|
// transcript, and cleared when the turn's answer completes.
|
|
408
|
-
let thinkingBuf = '';
|
|
409
422
|
const pendingToolCalls = new Map();
|
|
410
|
-
let lastFinishReason;
|
|
411
423
|
// Set when this step's request must be re-issued after overflow recovery.
|
|
412
424
|
let overflowRetryPending = false;
|
|
413
425
|
// Set when a terminal provider error consumed a failover adapter — the
|
|
414
426
|
// step is re-issued against the next adapter without consuming budget.
|
|
415
427
|
let failoverPending = false;
|
|
416
|
-
let failoverFrom
|
|
417
|
-
let failoverTo
|
|
418
|
-
let failoverReason
|
|
428
|
+
let failoverFrom;
|
|
429
|
+
let failoverTo;
|
|
430
|
+
let failoverReason;
|
|
419
431
|
for await (const ev of events) {
|
|
420
432
|
if (opts.signal?.aborted)
|
|
421
433
|
break outer;
|
|
@@ -424,7 +436,6 @@ export async function run(opts, deps) {
|
|
|
424
436
|
emit?.({ kind: 'text_delta', text: ev.text });
|
|
425
437
|
}
|
|
426
438
|
else if (ev.kind === 'thinking_delta') {
|
|
427
|
-
thinkingBuf += ev.text;
|
|
428
439
|
emit?.({ kind: 'thinking_delta', text: ev.text });
|
|
429
440
|
}
|
|
430
441
|
else if (ev.kind === 'tool_call_start') {
|
|
@@ -441,7 +452,6 @@ export async function run(opts, deps) {
|
|
|
441
452
|
// tool_calls are accumulated; finalization happens after stream.
|
|
442
453
|
}
|
|
443
454
|
else if (ev.kind === 'message_end') {
|
|
444
|
-
lastFinishReason = ev.finishReason;
|
|
445
455
|
if (ev.usage) {
|
|
446
456
|
usage.input += ev.usage.input;
|
|
447
457
|
usage.output += ev.usage.output;
|
|
@@ -553,9 +563,7 @@ export async function run(opts, deps) {
|
|
|
553
563
|
if (failoverPending) {
|
|
554
564
|
failoverPending = false;
|
|
555
565
|
textBuf = '';
|
|
556
|
-
thinkingBuf = '';
|
|
557
566
|
pendingToolCalls.clear();
|
|
558
|
-
lastFinishReason = undefined;
|
|
559
567
|
steps--;
|
|
560
568
|
emit?.({ kind: 'step_end', step: steps + 1 });
|
|
561
569
|
continue outer;
|
|
@@ -613,6 +621,10 @@ export async function run(opts, deps) {
|
|
|
613
621
|
emit?.({ kind: 'aborted' });
|
|
614
622
|
const killed = killAllJobs();
|
|
615
623
|
emitKlyro({ type: 'abort', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', reason: killed.length > 0 ? `aborted by operator (${killed.length} background job(s) killed)` : 'aborted by operator' });
|
|
624
|
+
// 2.4 — explicit interrupted marker (see loop-top abort).
|
|
625
|
+
const interruptedNote = { role: 'user', content: [text('[system note] Interrupted by user — output above is partial and preserved.')] };
|
|
626
|
+
transcript.push(interruptedNote);
|
|
627
|
+
await checkpoint(interruptedNote);
|
|
616
628
|
if (store && sessionId) {
|
|
617
629
|
try {
|
|
618
630
|
await store.setStatus(sessionId, 'aborted', finalText);
|
|
@@ -623,6 +635,9 @@ export async function run(opts, deps) {
|
|
|
623
635
|
return { status: 'aborted', steps, toolCalls: toolCallCount, finalText, transcript, hasEdits, usage, repairs, verification: hasEdits ? withRepairTokens({ ok: false, attempts: verificationAttempts }) : undefined };
|
|
624
636
|
}
|
|
625
637
|
if (finalizedCalls.length === 0) {
|
|
638
|
+
// 4.5c — the turn completed (final text, malformed-only, or
|
|
639
|
+
// stop-hook continuation): summarize before the verify branching.
|
|
640
|
+
await emitTurnSummary();
|
|
626
641
|
// Steerable stop: a stop hook asked for one more turn instead of
|
|
627
642
|
// completing. Consumed once per verdict, max 3 per run.
|
|
628
643
|
if (stopCont !== null && stopContUsed < 3) {
|
|
@@ -907,6 +922,7 @@ export async function run(opts, deps) {
|
|
|
907
922
|
: {}),
|
|
908
923
|
agentDepth: opts.parentContext?.depth ?? 0,
|
|
909
924
|
agentMaxDepth: opts.parentContext?.maxDepth ?? 1,
|
|
925
|
+
...(opts.allowedPaths ? { agentAllowedPaths: opts.allowedPaths } : {}),
|
|
910
926
|
...(opts.parentContext?.allowedTools ? { agentAllowedTools: opts.parentContext.allowedTools } : {}),
|
|
911
927
|
...(opts.parentContext?.allowedPaths ? { agentAllowedPaths: opts.parentContext.allowedPaths } : {}),
|
|
912
928
|
...(opts.parentContext?.model ?? opts.model ? { agentModel: opts.parentContext?.model ?? opts.model } : {}),
|
|
@@ -1058,6 +1074,18 @@ export async function run(opts, deps) {
|
|
|
1058
1074
|
}
|
|
1059
1075
|
}
|
|
1060
1076
|
}
|
|
1077
|
+
// 4.5a — shell/git mutations bypass file-change inference: take a
|
|
1078
|
+
// pre-execution snapshot so the turn stays restorable. Best-effort.
|
|
1079
|
+
if (call.name === 'shell_exec' || call.name.startsWith('git_')) {
|
|
1080
|
+
try {
|
|
1081
|
+
const { snapshot } = await import('../checkpoints/store.js');
|
|
1082
|
+
await snapshot(opts.cwd, [...fileEditCounts.keys()].slice(-20), {
|
|
1083
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1084
|
+
eventId: call.id,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
catch { /* ignore */ }
|
|
1088
|
+
}
|
|
1061
1089
|
let obs;
|
|
1062
1090
|
try {
|
|
1063
1091
|
obs = await deps.registry.execute(call.name, call.input, toolCtx);
|
|
@@ -1145,7 +1173,10 @@ export async function run(opts, deps) {
|
|
|
1145
1173
|
// 4.5 — checkpoint snapshot after each mutation
|
|
1146
1174
|
try {
|
|
1147
1175
|
const { snapshot } = await import('../checkpoints/store.js');
|
|
1148
|
-
await snapshot(opts.cwd, [fileChanged.path]
|
|
1176
|
+
await snapshot(opts.cwd, [fileChanged.path], {
|
|
1177
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1178
|
+
eventId: call.id,
|
|
1179
|
+
});
|
|
1149
1180
|
}
|
|
1150
1181
|
catch { /* ignore */ }
|
|
1151
1182
|
// 5.2 file edit count
|
|
@@ -1258,6 +1289,8 @@ export async function run(opts, deps) {
|
|
|
1258
1289
|
break;
|
|
1259
1290
|
}
|
|
1260
1291
|
}
|
|
1292
|
+
// 4.5c — the tool turn completed: summarize (cumulative counts).
|
|
1293
|
+
await emitTurnSummary();
|
|
1261
1294
|
// P0 — drain finished sub-agent completions into parent visibility.
|
|
1262
1295
|
// drainCompletions is OPTIONAL on the bridge — guarded with `?.` so
|
|
1263
1296
|
// older bridges without it simply yield nothing.
|
|
@@ -1308,6 +1341,10 @@ export async function run(opts, deps) {
|
|
|
1308
1341
|
}
|
|
1309
1342
|
if (opts.signal?.aborted) {
|
|
1310
1343
|
emit?.({ kind: 'aborted' });
|
|
1344
|
+
// 2.4 — explicit interrupted marker (see loop-top abort).
|
|
1345
|
+
const interruptedNote = { role: 'user', content: [text('[system note] Interrupted by user — output above is partial and preserved.')] };
|
|
1346
|
+
transcript.push(interruptedNote);
|
|
1347
|
+
await checkpoint(interruptedNote);
|
|
1311
1348
|
if (store && sessionId) {
|
|
1312
1349
|
try {
|
|
1313
1350
|
await store.setStatus(sessionId, 'aborted', finalText);
|
package/dist/chat.js
CHANGED
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
*/
|
|
21
21
|
/** Default request timeout: 60s. */
|
|
22
22
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
23
|
+
/**
|
|
24
|
+
* Legacy output flows through the shared terminal renderer (3.1): all
|
|
25
|
+
* human output leaves via the renderer module, which also sanitizes
|
|
26
|
+
* control sequences out of model text.
|
|
27
|
+
*/
|
|
28
|
+
import { TerminalRenderer, writeStdoutDrained } from './renderers/terminal.js';
|
|
29
|
+
const renderer = new TerminalRenderer();
|
|
30
|
+
function frame(text) {
|
|
31
|
+
renderer.handle({ type: 'stream.delta', ts: Date.now(), sessionId: 'legacy-chat', text });
|
|
32
|
+
}
|
|
23
33
|
/** Max bytes of an error response body we will print. */
|
|
24
34
|
const MAX_ERROR_BODY_BYTES = 4_000;
|
|
25
35
|
/** Strip a trailing slash so we can append /chat/completions cleanly. */
|
|
@@ -187,7 +197,7 @@ export async function streamToStdout(body, signal) {
|
|
|
187
197
|
const reader = body.getReader();
|
|
188
198
|
const decoder = new TextDecoder('utf-8');
|
|
189
199
|
let buf = '';
|
|
190
|
-
|
|
200
|
+
frame('\n');
|
|
191
201
|
try {
|
|
192
202
|
while (true) {
|
|
193
203
|
if (signal.aborted)
|
|
@@ -209,7 +219,7 @@ export async function streamToStdout(body, signal) {
|
|
|
209
219
|
continue;
|
|
210
220
|
const data = line.slice(5).trim();
|
|
211
221
|
if (data === '[DONE]') {
|
|
212
|
-
|
|
222
|
+
frame('\n');
|
|
213
223
|
return;
|
|
214
224
|
}
|
|
215
225
|
// Handle case where data was split across chunks and reassembled as event
|
|
@@ -224,7 +234,7 @@ export async function streamToStdout(body, signal) {
|
|
|
224
234
|
}
|
|
225
235
|
const text = parsed.choices?.[0]?.delta?.content;
|
|
226
236
|
if (text) {
|
|
227
|
-
if (!await
|
|
237
|
+
if (!await writeStdoutDrained(text, signal)) {
|
|
228
238
|
try {
|
|
229
239
|
await reader.cancel();
|
|
230
240
|
}
|
|
@@ -246,7 +256,7 @@ export async function streamToStdout(body, signal) {
|
|
|
246
256
|
continue;
|
|
247
257
|
const data = line.slice(5).trim();
|
|
248
258
|
if (data === '[DONE]') {
|
|
249
|
-
|
|
259
|
+
frame('\n');
|
|
250
260
|
return;
|
|
251
261
|
}
|
|
252
262
|
if (data === '')
|
|
@@ -255,7 +265,7 @@ export async function streamToStdout(body, signal) {
|
|
|
255
265
|
const parsed = JSON.parse(data);
|
|
256
266
|
const text = parsed.choices?.[0]?.delta?.content;
|
|
257
267
|
if (text) {
|
|
258
|
-
if (!await
|
|
268
|
+
if (!await writeStdoutDrained(text, signal)) {
|
|
259
269
|
try {
|
|
260
270
|
await reader.cancel();
|
|
261
271
|
}
|
|
@@ -270,7 +280,7 @@ export async function streamToStdout(body, signal) {
|
|
|
270
280
|
}
|
|
271
281
|
}
|
|
272
282
|
}
|
|
273
|
-
|
|
283
|
+
frame('\n');
|
|
274
284
|
}
|
|
275
285
|
finally {
|
|
276
286
|
try {
|
|
@@ -281,56 +291,6 @@ export async function streamToStdout(body, signal) {
|
|
|
281
291
|
}
|
|
282
292
|
}
|
|
283
293
|
}
|
|
284
|
-
/**
|
|
285
|
-
* Write to stdout and wait for the drain event if the buffer is full.
|
|
286
|
-
* Returns false if stdout has been closed (e.g. piped to `head`).
|
|
287
|
-
* Respects abort signal — resolves false if aborted while waiting.
|
|
288
|
-
*/
|
|
289
|
-
function writeWithBackpressure(chunk, signal) {
|
|
290
|
-
return new Promise((resolve) => {
|
|
291
|
-
if (signal?.aborted) {
|
|
292
|
-
resolve(false);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (!process.stdout.write(chunk)) {
|
|
296
|
-
let settled = false;
|
|
297
|
-
const cleanup = () => {
|
|
298
|
-
process.stdout.off('drain', onDrain);
|
|
299
|
-
process.stdout.off('error', onError);
|
|
300
|
-
if (signal)
|
|
301
|
-
signal.removeEventListener('abort', onAbort);
|
|
302
|
-
};
|
|
303
|
-
const onDrain = () => {
|
|
304
|
-
if (settled)
|
|
305
|
-
return;
|
|
306
|
-
settled = true;
|
|
307
|
-
cleanup();
|
|
308
|
-
resolve(true);
|
|
309
|
-
};
|
|
310
|
-
const onError = () => {
|
|
311
|
-
if (settled)
|
|
312
|
-
return;
|
|
313
|
-
settled = true;
|
|
314
|
-
cleanup();
|
|
315
|
-
resolve(false);
|
|
316
|
-
};
|
|
317
|
-
const onAbort = () => {
|
|
318
|
-
if (settled)
|
|
319
|
-
return;
|
|
320
|
-
settled = true;
|
|
321
|
-
cleanup();
|
|
322
|
-
resolve(false);
|
|
323
|
-
};
|
|
324
|
-
process.stdout.once('drain', onDrain);
|
|
325
|
-
process.stdout.once('error', onError);
|
|
326
|
-
if (signal)
|
|
327
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
resolve(true);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
294
|
/**
|
|
335
295
|
* Read up to `max` bytes from a response body. Used for error responses where
|
|
336
296
|
* we want to surface the cause without risking OOM on a misbehaving server.
|
|
@@ -10,7 +10,21 @@
|
|
|
10
10
|
* enforced at the trace/persist boundaries (TraceWriter, SessionStore)
|
|
11
11
|
* instead of here.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
/** Optional provenance recorded into a checkpoint's .meta.json. */
|
|
14
|
+
export interface SnapshotOptions {
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
eventId?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Checkpoint meta on disk — sessionId/eventId are absent on old metas. */
|
|
19
|
+
export interface CheckpointMeta {
|
|
20
|
+
id: string;
|
|
21
|
+
files: string[];
|
|
22
|
+
missing: string[];
|
|
23
|
+
ts: number;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
eventId?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function snapshot(cwd: string, files: string[], opts?: SnapshotOptions): Promise<string>;
|
|
14
28
|
export declare function listCheckpoints(cwd: string): Promise<string[]>;
|
|
15
29
|
export interface CheckpointInfo {
|
|
16
30
|
/** 1-based index from the latest (1 = newest, like `undo(n)`). */
|
|
@@ -55,7 +55,7 @@ function containedPath(cwd, base, rel) {
|
|
|
55
55
|
return null;
|
|
56
56
|
return out;
|
|
57
57
|
}
|
|
58
|
-
export async function snapshot(cwd, files) {
|
|
58
|
+
export async function snapshot(cwd, files, opts) {
|
|
59
59
|
const dir = ckptDir(cwd);
|
|
60
60
|
await fs.mkdir(dir, { recursive: true });
|
|
61
61
|
lockDown(dir, 0o700);
|
|
@@ -92,7 +92,12 @@ export async function snapshot(cwd, files) {
|
|
|
92
92
|
// leaves a truncated .meta.json that undo() then trusts).
|
|
93
93
|
const metaPath = path.join(dest, '.meta.json');
|
|
94
94
|
const metaTmp = `${metaPath}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
95
|
-
|
|
95
|
+
const meta = { id, files: kept, missing, ts: Date.now() };
|
|
96
|
+
if (opts?.sessionId !== undefined)
|
|
97
|
+
meta.sessionId = opts.sessionId;
|
|
98
|
+
if (opts?.eventId !== undefined)
|
|
99
|
+
meta.eventId = opts.eventId;
|
|
100
|
+
await fs.writeFile(metaTmp, JSON.stringify(meta, null, 2));
|
|
96
101
|
lockDown(metaTmp, 0o600);
|
|
97
102
|
await fsyncFile(metaTmp);
|
|
98
103
|
try {
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
1
|
export declare function parsePositiveInt(name: string, v: string): number;
|
|
2
|
+
/**
|
|
3
|
+
* 5.3 — `--auto-answer <text>` wiring. `ask_user` honors
|
|
4
|
+
* `KLYRO_AUTO_ANSWER` in headless runs; `klyro run` / `klyro eval` set it
|
|
5
|
+
* from the flag via this helper before the run starts (explicit env wins
|
|
6
|
+
* when the flag is omitted). Exported for tests.
|
|
7
|
+
*/
|
|
8
|
+
export declare function applyAutoAnswer(value: string | undefined): void;
|
package/dist/cli/args.js
CHANGED
|
@@ -12,3 +12,13 @@ export function parsePositiveInt(name, v) {
|
|
|
12
12
|
}
|
|
13
13
|
return n;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* 5.3 — `--auto-answer <text>` wiring. `ask_user` honors
|
|
17
|
+
* `KLYRO_AUTO_ANSWER` in headless runs; `klyro run` / `klyro eval` set it
|
|
18
|
+
* from the flag via this helper before the run starts (explicit env wins
|
|
19
|
+
* when the flag is omitted). Exported for tests.
|
|
20
|
+
*/
|
|
21
|
+
export function applyAutoAnswer(value) {
|
|
22
|
+
if (value !== undefined)
|
|
23
|
+
process.env.KLYRO_AUTO_ANSWER = value;
|
|
24
|
+
}
|
package/dist/cli/completion.js
CHANGED
|
@@ -19,9 +19,6 @@ const COMMAND_FLAGS = {
|
|
|
19
19
|
completion: ['bash', 'zsh', 'fish', 'powershell'],
|
|
20
20
|
resume: ['-m', '--model', '--max-steps'],
|
|
21
21
|
};
|
|
22
|
-
function flagsFor(cmd) {
|
|
23
|
-
return [...GLOBAL_FLAGS, ...(COMMAND_FLAGS[cmd] ?? [])];
|
|
24
|
-
}
|
|
25
22
|
function bashScript() {
|
|
26
23
|
const cmdCases = Object.entries(COMMAND_FLAGS)
|
|
27
24
|
.map(([c, fs]) => ` ${c}) opts="${fs.join(' ')}" ;;`)
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
* Runs quick diagnostics: node version, config, provider reachability,
|
|
4
4
|
* persistence dir, git, tools.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Pure byte interpreter for `doctor --keys` (unit-tested): names what a
|
|
8
|
+
* terminal delivered so broken arrow/paste/scroll input becomes provable
|
|
9
|
+
* fact instead of guesswork. Covers arrows, paging/home/end, Ctrl codes,
|
|
10
|
+
* bracketed-paste markers, and SGR/X10 mouse sequences.
|
|
11
|
+
*/
|
|
12
|
+
export declare function describeKeyBytes(chunk: Buffer): string;
|
|
13
|
+
/**
|
|
14
|
+
* Interactive key probe: raw-mode stdin echo of every chunk with its
|
|
15
|
+
* meaning. Proves what THIS terminal delivers for arrows, Ctrl+P/N,
|
|
16
|
+
* paste, and wheel — run it when TUI input misbehaves.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runKeysProbe(): Promise<number>;
|
|
6
19
|
export declare function runDoctor(opts?: {
|
|
7
20
|
json?: boolean;
|
|
8
21
|
cwd?: string;
|