klyro 0.1.1
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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/agent/anthropic-adapter.d.ts +75 -0
- package/dist/agent/anthropic-adapter.js +297 -0
- package/dist/agent/message.d.ts +40 -0
- package/dist/agent/message.js +21 -0
- package/dist/agent/observation.d.ts +47 -0
- package/dist/agent/observation.js +53 -0
- package/dist/agent/provider-adapter.d.ts +101 -0
- package/dist/agent/provider-adapter.js +254 -0
- package/dist/agent/registry.d.ts +42 -0
- package/dist/agent/registry.js +86 -0
- package/dist/agent/retry.d.ts +34 -0
- package/dist/agent/retry.js +91 -0
- package/dist/agent/runtime.d.ts +130 -0
- package/dist/agent/runtime.js +221 -0
- package/dist/agent/worker-spawner.d.ts +41 -0
- package/dist/agent/worker-spawner.js +76 -0
- package/dist/chat.d.ts +39 -0
- package/dist/chat.js +235 -0
- package/dist/chat.js.map +1 -0
- package/dist/cli/eval.d.ts +75 -0
- package/dist/cli/eval.js +190 -0
- package/dist/cli/repl.d.ts +15 -0
- package/dist/cli/repl.js +194 -0
- package/dist/cli/run.d.ts +67 -0
- package/dist/cli/run.js +198 -0
- package/dist/cli/slash/parser.d.ts +39 -0
- package/dist/cli/slash/parser.js +47 -0
- package/dist/context/level6.d.ts +45 -0
- package/dist/context/level6.js +231 -0
- package/dist/context/level7.d.ts +84 -0
- package/dist/context/level7.js +156 -0
- package/dist/context/project-map.d.ts +48 -0
- package/dist/context/project-map.js +438 -0
- package/dist/context/repo-map.d.ts +30 -0
- package/dist/context/repo-map.js +168 -0
- package/dist/context/selector.d.ts +22 -0
- package/dist/context/selector.js +37 -0
- package/dist/context/snippets.d.ts +17 -0
- package/dist/context/snippets.js +15 -0
- package/dist/context/tokenizer.d.ts +49 -0
- package/dist/context/tokenizer.js +97 -0
- package/dist/eval/harness.d.ts +48 -0
- package/dist/eval/harness.js +149 -0
- package/dist/eval/tasks.d.ts +6 -0
- package/dist/eval/tasks.js +98 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/audit.d.ts +75 -0
- package/dist/persistence/audit.js +15 -0
- package/dist/persistence/store.d.ts +67 -0
- package/dist/persistence/store.js +106 -0
- package/dist/policy/approval.d.ts +35 -0
- package/dist/policy/approval.js +54 -0
- package/dist/policy/engine.d.ts +76 -0
- package/dist/policy/engine.js +163 -0
- package/dist/policy/path-guard.d.ts +30 -0
- package/dist/policy/path-guard.js +100 -0
- package/dist/policy/secret-redactor.d.ts +19 -0
- package/dist/policy/secret-redactor.js +52 -0
- package/dist/providers.d.ts +23 -0
- package/dist/providers.js +83 -0
- package/dist/repl.d.ts +9 -0
- package/dist/repl.js +170 -0
- package/dist/repl.js.map +1 -0
- package/dist/tools/fs/edit-file.d.ts +48 -0
- package/dist/tools/fs/edit-file.js +84 -0
- package/dist/tools/fs/list-dir.d.ts +40 -0
- package/dist/tools/fs/list-dir.js +72 -0
- package/dist/tools/fs/read-file.d.ts +66 -0
- package/dist/tools/fs/read-file.js +75 -0
- package/dist/tools/fs/write-file.d.ts +24 -0
- package/dist/tools/fs/write-file.js +48 -0
- package/dist/tools/git/git-diff.d.ts +35 -0
- package/dist/tools/git/git-diff.js +68 -0
- package/dist/tools/git/git-status.d.ts +29 -0
- package/dist/tools/git/git-status.js +57 -0
- package/dist/tools/normalize.d.ts +31 -0
- package/dist/tools/normalize.js +88 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +87 -0
- package/dist/tools/schema.d.ts +29 -0
- package/dist/tools/schema.js +183 -0
- package/dist/tools/search/dependencies.d.ts +44 -0
- package/dist/tools/search/dependencies.js +188 -0
- package/dist/tools/search/glob.d.ts +26 -0
- package/dist/tools/search/glob.js +88 -0
- package/dist/tools/search/grep.d.ts +52 -0
- package/dist/tools/search/grep.js +125 -0
- package/dist/tools/search/recent-files.d.ts +33 -0
- package/dist/tools/search/recent-files.js +100 -0
- package/dist/tools/search/search-files.d.ts +39 -0
- package/dist/tools/search/search-files.js +110 -0
- package/dist/tools/shell/shell-exec.d.ts +60 -0
- package/dist/tools/shell/shell-exec.js +133 -0
- package/dist/tools/types.d.ts +54 -0
- package/dist/tools/types.js +15 -0
- package/dist/tools/verify/run-verify.d.ts +60 -0
- package/dist/tools/verify/run-verify.js +115 -0
- package/dist/tui/app.d.ts +27 -0
- package/dist/tui/app.js +79 -0
- package/dist/tui/app.test.d.ts +1 -0
- package/dist/tui/app.test.js +77 -0
- package/dist/tui/status.d.ts +21 -0
- package/dist/tui/status.js +13 -0
- package/dist/tui/status.test.d.ts +1 -0
- package/dist/tui/status.test.js +39 -0
- package/dist/tui/transcript.d.ts +40 -0
- package/dist/tui/transcript.js +28 -0
- package/dist/tui/transcript.test.d.ts +1 -0
- package/dist/tui/transcript.test.js +71 -0
- package/dist/verification/detect.d.ts +29 -0
- package/dist/verification/detect.js +85 -0
- package/dist/verification/engine.d.ts +25 -0
- package/dist/verification/engine.js +57 -0
- package/package.json +58 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level-6 context: project map + repo map + recent files + dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Computed once per run, formatted compactly, and prepended to the system
|
|
5
|
+
* prompt. Each sub-block is independently optional — if a sub-step errors
|
|
6
|
+
* (e.g. unreadable package.json), we omit that block rather than failing
|
|
7
|
+
* the whole context load.
|
|
8
|
+
*
|
|
9
|
+
* Token budget: we cap each sub-block so the total added context is bounded
|
|
10
|
+
* (≤ ~6 KB). The exact budget is exposed via Level6Options.
|
|
11
|
+
*
|
|
12
|
+
* Static counterpart to Level-7 (`./level7.ts`), which injects a *live*
|
|
13
|
+
* runtime-telemetry snapshot (step count, recent tool calls, errors) into
|
|
14
|
+
* every model call. Together they give the model a static picture of the
|
|
15
|
+
* project (L6) and a live picture of its own run (L7).
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'node:fs/promises';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { buildProjectMap, formatProjectMap } from './project-map.js';
|
|
20
|
+
import { buildRepoMap, formatRepoMap } from './repo-map.js';
|
|
21
|
+
const DEFAULT_MAX_TOTAL_CHARS = 12_000;
|
|
22
|
+
const RECENT_LIMIT = 15;
|
|
23
|
+
const RECENT_GLOB = 'src/**';
|
|
24
|
+
async function buildRecentFilesBlock(cwd, hours) {
|
|
25
|
+
const sinceMs = Date.now() - hours * 60 * 60 * 1000;
|
|
26
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.klyro']);
|
|
27
|
+
const SKIP_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.woff', '.woff2', '.ttf', '.otf', '.map']);
|
|
28
|
+
async function walk(dir, onFile) {
|
|
29
|
+
let entries = [];
|
|
30
|
+
try {
|
|
31
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
if (SKIP_DIRS.has(e.name))
|
|
38
|
+
continue;
|
|
39
|
+
const full = path.join(dir, e.name);
|
|
40
|
+
let isDir = false;
|
|
41
|
+
try {
|
|
42
|
+
const s = await fs.lstat(full);
|
|
43
|
+
if (s.isSymbolicLink())
|
|
44
|
+
continue;
|
|
45
|
+
isDir = s.isDirectory();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const rel = path.relative(cwd, full).split(path.sep).join('/');
|
|
51
|
+
if (isDir) {
|
|
52
|
+
if (rel.startsWith(RECENT_GLOB.replace('/**', '/')))
|
|
53
|
+
await walk(full, onFile);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const ext = path.extname(full).toLowerCase();
|
|
57
|
+
if (SKIP_EXT.has(ext))
|
|
58
|
+
continue;
|
|
59
|
+
if (!rel.startsWith(RECENT_GLOB.replace('/**', '/')))
|
|
60
|
+
continue;
|
|
61
|
+
await onFile(full, rel);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const recent = [];
|
|
66
|
+
await walk(cwd, async (full, rel) => {
|
|
67
|
+
try {
|
|
68
|
+
const mtimeMs = (await fs.stat(full)).mtimeMs;
|
|
69
|
+
if (mtimeMs >= sinceMs)
|
|
70
|
+
recent.push({ rel, mtimeMs });
|
|
71
|
+
}
|
|
72
|
+
catch { /* noop */ }
|
|
73
|
+
});
|
|
74
|
+
recent.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
75
|
+
const top = recent.slice(0, RECENT_LIMIT);
|
|
76
|
+
if (!top.length)
|
|
77
|
+
return '';
|
|
78
|
+
const lines = ['# Recently changed (in src/, last ' + hours + 'h)'];
|
|
79
|
+
for (const f of top) {
|
|
80
|
+
const ageH = ((Date.now() - f.mtimeMs) / (1000 * 60 * 60)).toFixed(1);
|
|
81
|
+
lines.push(`- ${f.rel} (${ageH}h ago)`);
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
async function buildDepsBlock(cwd) {
|
|
86
|
+
// Quick-and-dirty: only package.json for now (Python/Go/Rust fall back
|
|
87
|
+
// to the dedicated `dependencies` tool the model can call).
|
|
88
|
+
let pkgText = null;
|
|
89
|
+
try {
|
|
90
|
+
const s = await fs.stat(path.join(cwd, 'package.json'));
|
|
91
|
+
if (s.isFile() && s.size <= 64 * 1024) {
|
|
92
|
+
pkgText = await fs.readFile(path.join(cwd, 'package.json'), 'utf-8');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch { /* no package.json */ }
|
|
96
|
+
if (!pkgText)
|
|
97
|
+
return '';
|
|
98
|
+
let pkg;
|
|
99
|
+
try {
|
|
100
|
+
pkg = JSON.parse(pkgText);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
const sections = {};
|
|
106
|
+
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
107
|
+
const d = pkg[k];
|
|
108
|
+
if (!d || typeof d !== 'object')
|
|
109
|
+
continue;
|
|
110
|
+
sections[k] = d;
|
|
111
|
+
}
|
|
112
|
+
if (!Object.keys(sections).length)
|
|
113
|
+
return '';
|
|
114
|
+
const lines = ['# Direct dependencies (npm)'];
|
|
115
|
+
for (const [k, obj] of Object.entries(sections)) {
|
|
116
|
+
const entries = Object.entries(obj);
|
|
117
|
+
if (!entries.length)
|
|
118
|
+
continue;
|
|
119
|
+
lines.push(`## ${k}`);
|
|
120
|
+
for (const [n, v] of entries)
|
|
121
|
+
lines.push(`- ${n} ${v}`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
/** Build a Level-6 context block for the given working directory. */
|
|
126
|
+
export async function buildLevel6Context(opts) {
|
|
127
|
+
const maxChars = opts.maxTotalChars ?? DEFAULT_MAX_TOTAL_CHARS;
|
|
128
|
+
const recentHours = opts.recentHours ?? 24;
|
|
129
|
+
const blocks = [];
|
|
130
|
+
const flags = { hasProjectMap: false, hasRepoMap: false, hasRecentFiles: false, hasDeps: false };
|
|
131
|
+
if (!opts.skipProjectMap) {
|
|
132
|
+
try {
|
|
133
|
+
const m = await buildProjectMap(opts.cwd);
|
|
134
|
+
const text = formatProjectMap(m);
|
|
135
|
+
if (text) {
|
|
136
|
+
blocks.push(text);
|
|
137
|
+
flags.hasProjectMap = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch { /* swallow — project map is a soft signal */ }
|
|
141
|
+
}
|
|
142
|
+
if (!opts.skipRepoMap) {
|
|
143
|
+
try {
|
|
144
|
+
const files = await buildRepoMap({ cwd: opts.cwd, maxFiles: 80, maxFileBytes: 16 * 1024 });
|
|
145
|
+
// Rank: prefer files with more named symbols + first-party paths.
|
|
146
|
+
const ranked = files
|
|
147
|
+
.map((f) => ({ f, score: f.symbols.length * 3 + (f.path.startsWith('src/') || f.path.startsWith('lib/') ? 5 : 0) }))
|
|
148
|
+
.sort((a, b) => b.score - a.score)
|
|
149
|
+
.slice(0, 40)
|
|
150
|
+
.map((x) => x.f);
|
|
151
|
+
const text = formatRepoMap(ranked);
|
|
152
|
+
if (text) {
|
|
153
|
+
blocks.push(text);
|
|
154
|
+
flags.hasRepoMap = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch { /* swallow */ }
|
|
158
|
+
}
|
|
159
|
+
if (!opts.skipRecentFiles) {
|
|
160
|
+
try {
|
|
161
|
+
const text = await buildRecentFilesBlock(opts.cwd, recentHours);
|
|
162
|
+
if (text) {
|
|
163
|
+
blocks.push(text);
|
|
164
|
+
flags.hasRecentFiles = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { /* swallow */ }
|
|
168
|
+
}
|
|
169
|
+
if (!opts.skipDeps) {
|
|
170
|
+
try {
|
|
171
|
+
const text = await buildDepsBlock(opts.cwd);
|
|
172
|
+
if (text) {
|
|
173
|
+
blocks.push(text);
|
|
174
|
+
flags.hasDeps = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* swallow */ }
|
|
178
|
+
}
|
|
179
|
+
let formatted = blocks.join('\n\n');
|
|
180
|
+
if (formatted.length > maxChars) {
|
|
181
|
+
// Truncate at the last newline before maxChars, then append a marker.
|
|
182
|
+
const cut = formatted.lastIndexOf('\n', maxChars);
|
|
183
|
+
formatted = formatted.slice(0, cut > 0 ? cut : maxChars) + '\n[truncated]';
|
|
184
|
+
}
|
|
185
|
+
return { formatted, ...flags };
|
|
186
|
+
}
|
|
187
|
+
/** Persist the Level-6 context to a JSON cache under .klyro/. */
|
|
188
|
+
export async function writeLevel6Cache(cwd, ctx) {
|
|
189
|
+
if (!ctx.formatted)
|
|
190
|
+
return null;
|
|
191
|
+
const dir = path.join(cwd, '.klyro');
|
|
192
|
+
try {
|
|
193
|
+
await fs.mkdir(dir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const file = path.join(dir, 'context.json');
|
|
199
|
+
const payload = {
|
|
200
|
+
generatedAt: new Date().toISOString(),
|
|
201
|
+
formatted: ctx.formatted,
|
|
202
|
+
hasProjectMap: ctx.hasProjectMap,
|
|
203
|
+
hasRepoMap: ctx.hasRepoMap,
|
|
204
|
+
hasRecentFiles: ctx.hasRecentFiles,
|
|
205
|
+
hasDeps: ctx.hasDeps,
|
|
206
|
+
};
|
|
207
|
+
try {
|
|
208
|
+
await fs.writeFile(file, JSON.stringify(payload, null, 2), 'utf-8');
|
|
209
|
+
return file;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function readLevel6Cache(cwd) {
|
|
216
|
+
const file = path.join(cwd, '.klyro', 'context.json');
|
|
217
|
+
try {
|
|
218
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
219
|
+
const data = JSON.parse(raw);
|
|
220
|
+
return {
|
|
221
|
+
formatted: typeof data.formatted === 'string' ? data.formatted : '',
|
|
222
|
+
hasProjectMap: !!data.hasProjectMap,
|
|
223
|
+
hasRepoMap: !!data.hasRepoMap,
|
|
224
|
+
hasRecentFiles: !!data.hasRecentFiles,
|
|
225
|
+
hasDeps: !!data.hasDeps,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level-7 context: runtime telemetry injected into every model call.
|
|
3
|
+
*
|
|
4
|
+
* Level-6 gives the model a precomputed, file-system-shaped picture of the
|
|
5
|
+
* project (file map, deps, recent files). Level-7 gives it a live picture
|
|
6
|
+
* of *itself mid-run* — the run so far is a feedback channel, not a blind
|
|
7
|
+
* loop.
|
|
8
|
+
*
|
|
9
|
+
* What gets surfaced (per step, before the next model call):
|
|
10
|
+
* - current step number, max steps
|
|
11
|
+
* - cumulative tool call count
|
|
12
|
+
* - approximate token usage
|
|
13
|
+
* - the last N tool calls (name, brief arg, latency, error?)
|
|
14
|
+
* - the last error, if any
|
|
15
|
+
* - how many times we've hit "max_steps" / abort / etc.
|
|
16
|
+
*
|
|
17
|
+
* Budget: hard-capped at `maxChars` (default 1.2 KB) so the telemetry
|
|
18
|
+
* block can never bloat the system prompt. Each sub-block is independently
|
|
19
|
+
* optional and bounded.
|
|
20
|
+
*
|
|
21
|
+
* This is in-memory only. No cache, no .klyro/ write — telemetry dies
|
|
22
|
+
* with the process. (We never want to leak one run's state into another.)
|
|
23
|
+
*/
|
|
24
|
+
import type { ToolUseBlock } from '../agent/message.js';
|
|
25
|
+
export interface TelemetryToolCall {
|
|
26
|
+
name: string;
|
|
27
|
+
/** One-line summary (path for fs tools, command for shell). */
|
|
28
|
+
brief: string;
|
|
29
|
+
latencyMs: number;
|
|
30
|
+
isError: boolean;
|
|
31
|
+
step: number;
|
|
32
|
+
}
|
|
33
|
+
export interface TelemetrySnapshot {
|
|
34
|
+
step: number;
|
|
35
|
+
maxSteps: number;
|
|
36
|
+
toolCallCount: number;
|
|
37
|
+
inputTokens: number;
|
|
38
|
+
outputTokens: number;
|
|
39
|
+
lastError: string | null;
|
|
40
|
+
lastToolCalls: TelemetryToolCall[];
|
|
41
|
+
/** How many tool calls failed in the entire run so far. */
|
|
42
|
+
errorCount: number;
|
|
43
|
+
/** When the run started (ISO string). */
|
|
44
|
+
startedAt: string;
|
|
45
|
+
}
|
|
46
|
+
export interface Level7Options {
|
|
47
|
+
maxRecentCalls?: number;
|
|
48
|
+
/** Cap on the formatted string (characters). */
|
|
49
|
+
maxChars?: number;
|
|
50
|
+
}
|
|
51
|
+
/** Mutable accumulator the runtime mutates each step. */
|
|
52
|
+
export declare class RuntimeTelemetry {
|
|
53
|
+
private readonly opts;
|
|
54
|
+
private startMs;
|
|
55
|
+
private step;
|
|
56
|
+
private maxSteps;
|
|
57
|
+
private toolCallCount;
|
|
58
|
+
private inputTokens;
|
|
59
|
+
private outputTokens;
|
|
60
|
+
private lastError;
|
|
61
|
+
private errorCount;
|
|
62
|
+
private recent;
|
|
63
|
+
constructor(opts?: Level7Options);
|
|
64
|
+
setMaxSteps(n: number): void;
|
|
65
|
+
recordStepStart(step: number): void;
|
|
66
|
+
recordUsage(input: number, output: number): void;
|
|
67
|
+
recordToolCall(call: ToolUseBlock, latencyMs: number, isError: boolean): void;
|
|
68
|
+
recordError(msg: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* One-shot helper for "this call didn't run / failed before executing".
|
|
71
|
+
* Records the call as an error (counted in `errorCount`, surfaced in
|
|
72
|
+
* `recentToolCalls`) AND sets the `lastError` message — both in one
|
|
73
|
+
* call so a future contributor can't update one without the other.
|
|
74
|
+
*/
|
|
75
|
+
recordToolError(call: ToolUseBlock, kind: string): void;
|
|
76
|
+
snapshot(): TelemetrySnapshot;
|
|
77
|
+
format(): string;
|
|
78
|
+
}
|
|
79
|
+
/** Exported so the runtime can reuse this exact one-liner for the user-facing
|
|
80
|
+
* approval prompt — keeps the L7 telemetry line and the approval summary
|
|
81
|
+
* in sync. */
|
|
82
|
+
export declare function summarizeToolCall(call: ToolUseBlock): string;
|
|
83
|
+
/** Convenience: the empty "no telemetry yet" block. Used at step 0. */
|
|
84
|
+
export declare function emptyTelemetryBlock(): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level-7 context: runtime telemetry injected into every model call.
|
|
3
|
+
*
|
|
4
|
+
* Level-6 gives the model a precomputed, file-system-shaped picture of the
|
|
5
|
+
* project (file map, deps, recent files). Level-7 gives it a live picture
|
|
6
|
+
* of *itself mid-run* — the run so far is a feedback channel, not a blind
|
|
7
|
+
* loop.
|
|
8
|
+
*
|
|
9
|
+
* What gets surfaced (per step, before the next model call):
|
|
10
|
+
* - current step number, max steps
|
|
11
|
+
* - cumulative tool call count
|
|
12
|
+
* - approximate token usage
|
|
13
|
+
* - the last N tool calls (name, brief arg, latency, error?)
|
|
14
|
+
* - the last error, if any
|
|
15
|
+
* - how many times we've hit "max_steps" / abort / etc.
|
|
16
|
+
*
|
|
17
|
+
* Budget: hard-capped at `maxChars` (default 1.2 KB) so the telemetry
|
|
18
|
+
* block can never bloat the system prompt. Each sub-block is independently
|
|
19
|
+
* optional and bounded.
|
|
20
|
+
*
|
|
21
|
+
* This is in-memory only. No cache, no .klyro/ write — telemetry dies
|
|
22
|
+
* with the process. (We never want to leak one run's state into another.)
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_MAX_RECENT = 6;
|
|
25
|
+
const DEFAULT_MAX_CHARS = 1200;
|
|
26
|
+
/** Mutable accumulator the runtime mutates each step. */
|
|
27
|
+
export class RuntimeTelemetry {
|
|
28
|
+
opts;
|
|
29
|
+
startMs = Date.now();
|
|
30
|
+
step = 0;
|
|
31
|
+
maxSteps = 30;
|
|
32
|
+
toolCallCount = 0;
|
|
33
|
+
inputTokens = 0;
|
|
34
|
+
outputTokens = 0;
|
|
35
|
+
lastError = null;
|
|
36
|
+
errorCount = 0;
|
|
37
|
+
recent = [];
|
|
38
|
+
constructor(opts = {}) {
|
|
39
|
+
this.opts = {
|
|
40
|
+
maxRecentCalls: opts.maxRecentCalls ?? DEFAULT_MAX_RECENT,
|
|
41
|
+
maxChars: opts.maxChars ?? DEFAULT_MAX_CHARS,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
setMaxSteps(n) { this.maxSteps = n; }
|
|
45
|
+
recordStepStart(step) { this.step = step; }
|
|
46
|
+
recordUsage(input, output) {
|
|
47
|
+
this.inputTokens += input;
|
|
48
|
+
this.outputTokens += output;
|
|
49
|
+
}
|
|
50
|
+
recordToolCall(call, latencyMs, isError) {
|
|
51
|
+
this.toolCallCount++;
|
|
52
|
+
if (isError)
|
|
53
|
+
this.errorCount++;
|
|
54
|
+
this.recent.push({
|
|
55
|
+
name: call.name,
|
|
56
|
+
brief: summarize(call),
|
|
57
|
+
latencyMs,
|
|
58
|
+
isError,
|
|
59
|
+
step: this.step,
|
|
60
|
+
});
|
|
61
|
+
if (this.recent.length > this.opts.maxRecentCalls) {
|
|
62
|
+
this.recent.splice(0, this.recent.length - this.opts.maxRecentCalls);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
recordError(msg) {
|
|
66
|
+
this.lastError = msg;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One-shot helper for "this call didn't run / failed before executing".
|
|
70
|
+
* Records the call as an error (counted in `errorCount`, surfaced in
|
|
71
|
+
* `recentToolCalls`) AND sets the `lastError` message — both in one
|
|
72
|
+
* call so a future contributor can't update one without the other.
|
|
73
|
+
*/
|
|
74
|
+
recordToolError(call, kind) {
|
|
75
|
+
this.recordToolCall(call, 0, true);
|
|
76
|
+
this.recordError(`${kind}: ${call.name}`);
|
|
77
|
+
}
|
|
78
|
+
snapshot() {
|
|
79
|
+
return {
|
|
80
|
+
step: this.step,
|
|
81
|
+
maxSteps: this.maxSteps,
|
|
82
|
+
toolCallCount: this.toolCallCount,
|
|
83
|
+
inputTokens: this.inputTokens,
|
|
84
|
+
outputTokens: this.outputTokens,
|
|
85
|
+
lastError: this.lastError,
|
|
86
|
+
lastToolCalls: [...this.recent],
|
|
87
|
+
errorCount: this.errorCount,
|
|
88
|
+
startedAt: new Date(this.startMs).toISOString(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
format() {
|
|
92
|
+
const s = this.snapshot();
|
|
93
|
+
const lines = [];
|
|
94
|
+
lines.push('# Runtime telemetry');
|
|
95
|
+
const elapsedMs = Date.now() - this.startMs;
|
|
96
|
+
const elapsed = formatElapsed(elapsedMs);
|
|
97
|
+
lines.push(`Step ${s.step}/${s.maxSteps} | tool calls: ${s.toolCallCount} | errors: ${s.errorCount} | tokens: ${s.inputTokens} in / ${s.outputTokens} out | elapsed: ${elapsed}`);
|
|
98
|
+
if (s.lastError) {
|
|
99
|
+
lines.push(`Last error: ${truncate(s.lastError, 200)}`);
|
|
100
|
+
}
|
|
101
|
+
if (s.lastToolCalls.length) {
|
|
102
|
+
lines.push('Recent calls (newest last):');
|
|
103
|
+
for (const c of s.lastToolCalls) {
|
|
104
|
+
const tag = c.isError ? 'ERR' : 'ok ';
|
|
105
|
+
const lat = c.latencyMs < 1000 ? `${c.latencyMs}ms` : `${(c.latencyMs / 1000).toFixed(1)}s`;
|
|
106
|
+
lines.push(` [${tag}] step=${c.step} ${c.name} ${truncate(c.brief, 80)} (${lat})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let out = lines.join('\n');
|
|
110
|
+
if (out.length > this.opts.maxChars) {
|
|
111
|
+
const cut = out.lastIndexOf('\n', this.opts.maxChars);
|
|
112
|
+
out = out.slice(0, cut > 0 ? cut : this.opts.maxChars) + '\n[truncated]';
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function summarize(call) {
|
|
118
|
+
const i = call.input;
|
|
119
|
+
if (call.name === 'shell_exec' || call.name === 'run_verify') {
|
|
120
|
+
return String(i.command ?? '');
|
|
121
|
+
}
|
|
122
|
+
if (call.name === 'read_file' || call.name === 'write_file' || call.name === 'edit_file') {
|
|
123
|
+
return String(i.path ?? '');
|
|
124
|
+
}
|
|
125
|
+
if (call.name === 'grep' || call.name === 'search_files' || call.name === 'glob') {
|
|
126
|
+
return String(i.pattern ?? i.query ?? i.path ?? '');
|
|
127
|
+
}
|
|
128
|
+
if (call.name === 'list_directory') {
|
|
129
|
+
return String(i.path ?? '.');
|
|
130
|
+
}
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
/** Exported so the runtime can reuse this exact one-liner for the user-facing
|
|
134
|
+
* approval prompt — keeps the L7 telemetry line and the approval summary
|
|
135
|
+
* in sync. */
|
|
136
|
+
export function summarizeToolCall(call) {
|
|
137
|
+
return summarize(call);
|
|
138
|
+
}
|
|
139
|
+
function truncate(s, n) {
|
|
140
|
+
if (s.length <= n)
|
|
141
|
+
return s;
|
|
142
|
+
return s.slice(0, Math.max(0, n - 1)) + '…';
|
|
143
|
+
}
|
|
144
|
+
function formatElapsed(ms) {
|
|
145
|
+
if (ms < 1000)
|
|
146
|
+
return `${ms}ms`;
|
|
147
|
+
const s = Math.floor(ms / 1000);
|
|
148
|
+
if (s < 60)
|
|
149
|
+
return `${s}s`;
|
|
150
|
+
const m = Math.floor(s / 60);
|
|
151
|
+
return `${m}m${s % 60}s`;
|
|
152
|
+
}
|
|
153
|
+
/** Convenience: the empty "no telemetry yet" block. Used at step 0. */
|
|
154
|
+
export function emptyTelemetryBlock() {
|
|
155
|
+
return '# Runtime telemetry\n(no telemetry yet — this is step 0)';
|
|
156
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project map — high-level description of a repository.
|
|
3
|
+
*
|
|
4
|
+
* Goal: when an agent opens a repo for the first time, it should learn
|
|
5
|
+
* "what kind of project is this" in one read. We detect:
|
|
6
|
+
* - language(s) from file extensions
|
|
7
|
+
* - framework from package.json / requirements.txt / go.mod
|
|
8
|
+
* - package manager from lockfiles + scripts
|
|
9
|
+
* - test framework from package.json scripts + devDependencies
|
|
10
|
+
* - build commands from package.json scripts + Makefile
|
|
11
|
+
* - source directories from convention + first-party markers
|
|
12
|
+
* - important config files from a curated allow-list
|
|
13
|
+
*
|
|
14
|
+
* Output: ProjectMap (machine-readable) + formatProjectMap() (model-readable).
|
|
15
|
+
*
|
|
16
|
+
* Design constraints:
|
|
17
|
+
* - Single root-dir read, no subprocess calls (fast + hermetic).
|
|
18
|
+
* - Bounded output: at most MAX_SOURCE_DIRS source dirs, MAX_CONFIG_FILES
|
|
19
|
+
* config files, top-3 languages, top-3 frameworks.
|
|
20
|
+
* - Reuses IGNORED_DIRS from repo-map.ts so the two scanners agree on
|
|
21
|
+
* what to walk past.
|
|
22
|
+
*/
|
|
23
|
+
export declare const IGNORED_DIRS: Set<string>;
|
|
24
|
+
export interface Dependency {
|
|
25
|
+
name: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ProjectMap {
|
|
29
|
+
root: string;
|
|
30
|
+
language: string[];
|
|
31
|
+
framework: string[];
|
|
32
|
+
packageManager?: string;
|
|
33
|
+
testFramework?: string;
|
|
34
|
+
buildCommands: string[];
|
|
35
|
+
sourceDirs: string[];
|
|
36
|
+
configFiles: string[];
|
|
37
|
+
dependencies: Dependency[];
|
|
38
|
+
/** True when the repo has a package.json (Node/JS) anywhere up the tree. */
|
|
39
|
+
hasPackageJson: boolean;
|
|
40
|
+
/** True when the repo has a git working tree. */
|
|
41
|
+
hasGit: boolean;
|
|
42
|
+
/** Time the map was built. */
|
|
43
|
+
generatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
/** Build a project map for the repo rooted at `root`. */
|
|
46
|
+
export declare function buildProjectMap(root: string): Promise<ProjectMap>;
|
|
47
|
+
/** Render a ProjectMap as a compact, model-friendly block. */
|
|
48
|
+
export declare function formatProjectMap(m: ProjectMap): string;
|