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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent runtime — the core autonomous loop.
|
|
3
|
+
*
|
|
4
|
+
* Loop:
|
|
5
|
+
* 1. Build a CallRequest from the current transcript + system prompt.
|
|
6
|
+
* 2. Stream from the provider; collect text + tool calls.
|
|
7
|
+
* 3. If the assistant produced no tool calls, treat as final answer.
|
|
8
|
+
* 4. Otherwise, evaluate policy for each call, execute (or deny), feed
|
|
9
|
+
* tool_results back as the next user message.
|
|
10
|
+
* 5. Repeat up to maxSteps. Cap at maxSteps to avoid runaway costs.
|
|
11
|
+
*
|
|
12
|
+
* Stream cancellation: pass `signal` to abort mid-step.
|
|
13
|
+
*
|
|
14
|
+
* The runtime is intentionally provider-agnostic — it only sees the
|
|
15
|
+
* normalized StreamEvent shape from ProviderAdapter.
|
|
16
|
+
*/
|
|
17
|
+
import type { ProviderAdapter, ToolDefinition } from './provider-adapter.js';
|
|
18
|
+
import type { Message } from './message.js';
|
|
19
|
+
import type { ToolRegistry } from '../tools/registry.js';
|
|
20
|
+
import type { PolicyEngine } from '../policy/engine.js';
|
|
21
|
+
import type { ApprovalPrompt } from '../policy/approval.js';
|
|
22
|
+
export interface RuntimeDeps {
|
|
23
|
+
adapter: ProviderAdapter;
|
|
24
|
+
registry: ToolRegistry;
|
|
25
|
+
policy: PolicyEngine;
|
|
26
|
+
approval: ApprovalPrompt;
|
|
27
|
+
/**
|
|
28
|
+
* Build a system prompt given cwd + the current Level-7 runtime telemetry.
|
|
29
|
+
* The telemetry block is a compact, in-memory summary of the run so far
|
|
30
|
+
* (step count, last tool calls, recent errors). Injected as part of the
|
|
31
|
+
* system prompt so the model can see its own state mid-run.
|
|
32
|
+
*/
|
|
33
|
+
systemPrompt: (ctx: {
|
|
34
|
+
cwd: string;
|
|
35
|
+
telemetry?: string;
|
|
36
|
+
}) => string;
|
|
37
|
+
}
|
|
38
|
+
export interface RunOptions {
|
|
39
|
+
task: string;
|
|
40
|
+
cwd: string;
|
|
41
|
+
model: string;
|
|
42
|
+
maxSteps?: number;
|
|
43
|
+
maxTokens?: number;
|
|
44
|
+
temperature?: number;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
nonInteractive: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Optional pre-existing transcript to seed the conversation. When set,
|
|
49
|
+
* the runtime skips the initial `[{role:'user', content:[text(task)]}]`
|
|
50
|
+
* and starts with this list instead. Used for `--resume` and tests.
|
|
51
|
+
*
|
|
52
|
+
* If provided AND `task` is set, the task is appended as a new user
|
|
53
|
+
* message at the end (so resume+continue works naturally).
|
|
54
|
+
*/
|
|
55
|
+
initialTranscript?: Message[];
|
|
56
|
+
/**
|
|
57
|
+
* Optional hook for live UIs (e.g. the TUI). Fires for every observable
|
|
58
|
+
* event the runtime processes: text deltas, tool-call boundaries,
|
|
59
|
+
* policy decisions, observation results, and step boundaries. Callers
|
|
60
|
+
* that don't pass it pay zero cost.
|
|
61
|
+
*/
|
|
62
|
+
onEvent?: (ev: RuntimeEvent) => void;
|
|
63
|
+
}
|
|
64
|
+
/** High-level event stream the runtime emits. Safe for UI consumption. */
|
|
65
|
+
export type RuntimeEvent = {
|
|
66
|
+
kind: 'step_start';
|
|
67
|
+
step: number;
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'step_end';
|
|
70
|
+
step: number;
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'text_delta';
|
|
73
|
+
text: string;
|
|
74
|
+
} | {
|
|
75
|
+
kind: 'tool_call_start';
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: 'tool_call_delta';
|
|
80
|
+
id: string;
|
|
81
|
+
argsJson: string;
|
|
82
|
+
} | {
|
|
83
|
+
kind: 'tool_call_end';
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
input: Record<string, unknown>;
|
|
87
|
+
} | {
|
|
88
|
+
kind: 'policy_decision';
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
action: 'allow' | 'ask' | 'deny';
|
|
92
|
+
reason?: string;
|
|
93
|
+
} | {
|
|
94
|
+
kind: 'tool_result';
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
output: unknown;
|
|
98
|
+
isError: boolean;
|
|
99
|
+
latencyMs: number;
|
|
100
|
+
} | {
|
|
101
|
+
kind: 'usage';
|
|
102
|
+
input: number;
|
|
103
|
+
output: number;
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'final_text';
|
|
106
|
+
text: string;
|
|
107
|
+
} | {
|
|
108
|
+
kind: 'aborted';
|
|
109
|
+
};
|
|
110
|
+
export interface RunResult {
|
|
111
|
+
status: 'complete' | 'max_steps' | 'aborted' | 'no_final';
|
|
112
|
+
steps: number;
|
|
113
|
+
toolCalls: number;
|
|
114
|
+
finalText: string;
|
|
115
|
+
transcript: Message[];
|
|
116
|
+
usage: {
|
|
117
|
+
input: number;
|
|
118
|
+
output: number;
|
|
119
|
+
};
|
|
120
|
+
/** Number of policy-driven user prompts the user accepted. */
|
|
121
|
+
repairs?: number;
|
|
122
|
+
}
|
|
123
|
+
/** Convert a registry of tools into ToolDefinitions for the provider. */
|
|
124
|
+
export declare function toolDefinitions(registry: ToolRegistry): ToolDefinition[];
|
|
125
|
+
/** Run the autonomous loop. */
|
|
126
|
+
export declare function run(opts: RunOptions, deps: RuntimeDeps): Promise<RunResult>;
|
|
127
|
+
export declare function defaultSystemPrompt(ctx: {
|
|
128
|
+
cwd: string;
|
|
129
|
+
telemetry?: string;
|
|
130
|
+
}): string;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent runtime — the core autonomous loop.
|
|
3
|
+
*
|
|
4
|
+
* Loop:
|
|
5
|
+
* 1. Build a CallRequest from the current transcript + system prompt.
|
|
6
|
+
* 2. Stream from the provider; collect text + tool calls.
|
|
7
|
+
* 3. If the assistant produced no tool calls, treat as final answer.
|
|
8
|
+
* 4. Otherwise, evaluate policy for each call, execute (or deny), feed
|
|
9
|
+
* tool_results back as the next user message.
|
|
10
|
+
* 5. Repeat up to maxSteps. Cap at maxSteps to avoid runaway costs.
|
|
11
|
+
*
|
|
12
|
+
* Stream cancellation: pass `signal` to abort mid-step.
|
|
13
|
+
*
|
|
14
|
+
* The runtime is intentionally provider-agnostic — it only sees the
|
|
15
|
+
* normalized StreamEvent shape from ProviderAdapter.
|
|
16
|
+
*/
|
|
17
|
+
import { text, toolUse, toolResult as mkToolResult } from './message.js';
|
|
18
|
+
import { redact } from '../policy/secret-redactor.js';
|
|
19
|
+
import { RuntimeTelemetry, emptyTelemetryBlock, summarizeToolCall } from '../context/level7.js';
|
|
20
|
+
const DEFAULT_MAX_STEPS = 30;
|
|
21
|
+
/** Convert a registry of tools into ToolDefinitions for the provider. */
|
|
22
|
+
export function toolDefinitions(registry) {
|
|
23
|
+
return registry.toOpenAITools().map((t) => ({
|
|
24
|
+
name: t.function.name,
|
|
25
|
+
description: t.function.description,
|
|
26
|
+
inputSchema: t.function.parameters,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
/** Run the autonomous loop. */
|
|
30
|
+
export async function run(opts, deps) {
|
|
31
|
+
const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
32
|
+
const transcript = opts.initialTranscript
|
|
33
|
+
? [...opts.initialTranscript, { role: 'user', content: [text(opts.task)] }]
|
|
34
|
+
: [{ role: 'user', content: [text(opts.task)] }];
|
|
35
|
+
const usage = { input: 0, output: 0 };
|
|
36
|
+
let steps = 0;
|
|
37
|
+
let toolCallCount = 0;
|
|
38
|
+
let finalText = '';
|
|
39
|
+
let repairs = 0;
|
|
40
|
+
const emit = opts.onEvent;
|
|
41
|
+
const telemetry = new RuntimeTelemetry();
|
|
42
|
+
telemetry.setMaxSteps(maxSteps);
|
|
43
|
+
outer: while (steps < maxSteps) {
|
|
44
|
+
if (opts.signal?.aborted) {
|
|
45
|
+
emit?.({ kind: 'aborted' });
|
|
46
|
+
return { status: 'aborted', steps, toolCalls: toolCallCount, finalText, transcript, usage, repairs };
|
|
47
|
+
}
|
|
48
|
+
steps++;
|
|
49
|
+
emit?.({ kind: 'step_start', step: steps });
|
|
50
|
+
telemetry.recordStepStart(steps);
|
|
51
|
+
const req = {
|
|
52
|
+
model: opts.model,
|
|
53
|
+
system: deps.systemPrompt({ cwd: opts.cwd, telemetry: steps === 1 ? emptyTelemetryBlock() : telemetry.format() }),
|
|
54
|
+
messages: transcript,
|
|
55
|
+
tools: toolDefinitions(deps.registry),
|
|
56
|
+
...(opts.maxTokens ? { maxTokens: opts.maxTokens } : {}),
|
|
57
|
+
...(typeof opts.temperature === 'number' ? { temperature: opts.temperature } : {}),
|
|
58
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
59
|
+
};
|
|
60
|
+
const events = deps.adapter.stream(req);
|
|
61
|
+
let textBuf = '';
|
|
62
|
+
const pendingToolCalls = new Map();
|
|
63
|
+
let lastFinishReason;
|
|
64
|
+
for await (const ev of events) {
|
|
65
|
+
if (opts.signal?.aborted)
|
|
66
|
+
break outer;
|
|
67
|
+
if (ev.kind === 'text_delta') {
|
|
68
|
+
textBuf += ev.text;
|
|
69
|
+
emit?.({ kind: 'text_delta', text: ev.text });
|
|
70
|
+
}
|
|
71
|
+
else if (ev.kind === 'tool_call_start') {
|
|
72
|
+
pendingToolCalls.set(ev.id, { id: ev.id, name: ev.name, argsJson: '' });
|
|
73
|
+
emit?.({ kind: 'tool_call_start', id: ev.id, name: ev.name });
|
|
74
|
+
}
|
|
75
|
+
else if (ev.kind === 'tool_call_delta') {
|
|
76
|
+
const tc = pendingToolCalls.get(ev.id);
|
|
77
|
+
if (tc)
|
|
78
|
+
tc.argsJson += ev.argsJson;
|
|
79
|
+
emit?.({ kind: 'tool_call_delta', id: ev.id, argsJson: ev.argsJson });
|
|
80
|
+
}
|
|
81
|
+
else if (ev.kind === 'tool_call_end') {
|
|
82
|
+
// tool_calls are accumulated; finalization happens after stream.
|
|
83
|
+
}
|
|
84
|
+
else if (ev.kind === 'message_end') {
|
|
85
|
+
lastFinishReason = ev.finishReason;
|
|
86
|
+
if (ev.usage) {
|
|
87
|
+
usage.input += ev.usage.input;
|
|
88
|
+
usage.output += ev.usage.output;
|
|
89
|
+
telemetry.recordUsage(ev.usage.input, ev.usage.output);
|
|
90
|
+
emit?.({ kind: 'usage', input: usage.input, output: usage.output });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (ev.kind === 'error') {
|
|
94
|
+
telemetry.recordError(`stream_error: ${ev.code}`);
|
|
95
|
+
return {
|
|
96
|
+
status: 'no_final',
|
|
97
|
+
steps,
|
|
98
|
+
toolCalls: toolCallCount,
|
|
99
|
+
finalText: textBuf,
|
|
100
|
+
transcript,
|
|
101
|
+
usage,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Build the assistant message.
|
|
106
|
+
const assistantContent = [];
|
|
107
|
+
if (textBuf)
|
|
108
|
+
assistantContent.push(text(textBuf));
|
|
109
|
+
const finalizedCalls = [];
|
|
110
|
+
for (const tc of pendingToolCalls.values()) {
|
|
111
|
+
let input = {};
|
|
112
|
+
try {
|
|
113
|
+
input = JSON.parse(tc.argsJson || '{}');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
input = { _parse_error: true, raw: tc.argsJson };
|
|
117
|
+
}
|
|
118
|
+
finalizedCalls.push(toolUse(tc.id, tc.name, input));
|
|
119
|
+
assistantContent.push(toolUse(tc.id, tc.name, input));
|
|
120
|
+
emit?.({ kind: 'tool_call_end', id: tc.id, name: tc.name, input });
|
|
121
|
+
}
|
|
122
|
+
transcript.push({ role: 'assistant', content: assistantContent });
|
|
123
|
+
// No tool calls → done.
|
|
124
|
+
if (opts.signal?.aborted) {
|
|
125
|
+
finalText = textBuf;
|
|
126
|
+
emit?.({ kind: 'aborted' });
|
|
127
|
+
return { status: 'aborted', steps, toolCalls: toolCallCount, finalText, transcript, usage, repairs };
|
|
128
|
+
}
|
|
129
|
+
if (finalizedCalls.length === 0) {
|
|
130
|
+
finalText = textBuf;
|
|
131
|
+
emit?.({ kind: 'final_text', text: finalText });
|
|
132
|
+
emit?.({ kind: 'step_end', step: steps });
|
|
133
|
+
return { status: 'complete', steps, toolCalls: toolCallCount, finalText, transcript, usage, repairs };
|
|
134
|
+
}
|
|
135
|
+
// Execute each tool call (after policy).
|
|
136
|
+
const toolCtx = {
|
|
137
|
+
cwd: opts.cwd,
|
|
138
|
+
env: process.env,
|
|
139
|
+
signal: opts.signal,
|
|
140
|
+
nonInteractive: opts.nonInteractive,
|
|
141
|
+
};
|
|
142
|
+
for (const call of finalizedCalls) {
|
|
143
|
+
toolCallCount++;
|
|
144
|
+
const decision = await deps.policy.evaluate({ name: call.name, input: call.input }, { cwd: opts.cwd, nonInteractive: opts.nonInteractive });
|
|
145
|
+
emit?.({ kind: 'policy_decision', id: call.id, name: call.name, action: decision.action, ...(decision.action !== 'allow' ? { reason: decision.reason } : {}) });
|
|
146
|
+
if (decision.action === 'deny') {
|
|
147
|
+
transcript.push({
|
|
148
|
+
role: 'tool',
|
|
149
|
+
content: [
|
|
150
|
+
mkToolResult(call.id, call.name, { error: 'POLICY_DENIED', reason: decision.reason }, true),
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
telemetry.recordToolError(call, 'policy_denied');
|
|
154
|
+
emit?.({ kind: 'tool_result', id: call.id, name: call.name, output: { error: 'POLICY_DENIED', reason: decision.reason }, isError: true, latencyMs: 0 });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (decision.action === 'ask') {
|
|
158
|
+
const choice = await deps.approval.ask({
|
|
159
|
+
toolName: call.name,
|
|
160
|
+
reason: decision.reason,
|
|
161
|
+
summary: summarizeToolCall(call),
|
|
162
|
+
});
|
|
163
|
+
if (choice === 'deny') {
|
|
164
|
+
transcript.push({
|
|
165
|
+
role: 'tool',
|
|
166
|
+
content: [
|
|
167
|
+
mkToolResult(call.id, call.name, { error: 'POLICY_DENIED', reason: 'user denied' }, true),
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
telemetry.recordToolError(call, 'user_denied');
|
|
171
|
+
emit?.({ kind: 'tool_result', id: call.id, name: call.name, output: { error: 'POLICY_DENIED', reason: 'user denied' }, isError: true, latencyMs: 0 });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
repairs++;
|
|
175
|
+
}
|
|
176
|
+
const t0 = Date.now();
|
|
177
|
+
const obs = await deps.registry.execute(call.name, call.input, toolCtx);
|
|
178
|
+
const latencyMs = Date.now() - t0;
|
|
179
|
+
const output = obs.ok ? redactOutput(obs.value) : redactOutput({ error: obs.error });
|
|
180
|
+
transcript.push({
|
|
181
|
+
role: 'tool',
|
|
182
|
+
content: [mkToolResult(call.id, call.name, output, !obs.ok)],
|
|
183
|
+
});
|
|
184
|
+
if (obs.ok) {
|
|
185
|
+
telemetry.recordToolCall(call, latencyMs, false);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const code = String(obs.error?.code ?? 'tool_error');
|
|
189
|
+
telemetry.recordToolCall(call, latencyMs, true);
|
|
190
|
+
telemetry.recordError(`${code}: ${call.name}`);
|
|
191
|
+
}
|
|
192
|
+
emit?.({ kind: 'tool_result', id: call.id, name: call.name, output, isError: !obs.ok, latencyMs });
|
|
193
|
+
}
|
|
194
|
+
emit?.({ kind: 'step_end', step: steps });
|
|
195
|
+
}
|
|
196
|
+
if (opts.signal?.aborted) {
|
|
197
|
+
emit?.({ kind: 'aborted' });
|
|
198
|
+
return { status: 'aborted', steps, toolCalls: toolCallCount, finalText, transcript, usage, repairs };
|
|
199
|
+
}
|
|
200
|
+
emit?.({ kind: 'final_text', text: finalText });
|
|
201
|
+
return { status: 'max_steps', steps, toolCalls: toolCallCount, finalText, transcript, usage, repairs };
|
|
202
|
+
}
|
|
203
|
+
function redactOutput(v) {
|
|
204
|
+
if (typeof v === 'string')
|
|
205
|
+
return redact(v);
|
|
206
|
+
if (v && typeof v === 'object')
|
|
207
|
+
return v; // structured outputs are not redacted wholesale
|
|
208
|
+
return v;
|
|
209
|
+
}
|
|
210
|
+
export function defaultSystemPrompt(ctx) {
|
|
211
|
+
const base = [
|
|
212
|
+
'You are Klyro, an autonomous coding harness. You solve the user\'s task by',
|
|
213
|
+
'calling tools in a loop. Prefer the smallest change that solves the task.',
|
|
214
|
+
'When you have finished, produce a short final text answer (no tool calls).',
|
|
215
|
+
'Do not invent file paths. Do not call tools outside the working directory.',
|
|
216
|
+
].join(' ');
|
|
217
|
+
if (ctx.telemetry) {
|
|
218
|
+
return base + '\n\n' + ctx.telemetry + '\n\nUse the telemetry above to avoid repeating the same failing call and to keep within the step budget.';
|
|
219
|
+
}
|
|
220
|
+
return base;
|
|
221
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process worker spawner (MVP).
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/plan-fix.md` Step 2: workers run in-process for the MVP.
|
|
5
|
+
* Real subprocess / child-process spawning belongs to a later level
|
|
6
|
+
* (Level 7: sandboxed execution, Level 19: distributed workers).
|
|
7
|
+
*
|
|
8
|
+
* A "worker" here is just an async task with an AbortController. The
|
|
9
|
+
* spawner keeps a registry so they can be cancelled together (e.g. on
|
|
10
|
+
* user Ctrl+C, /abort, or session shutdown).
|
|
11
|
+
*/
|
|
12
|
+
export interface WorkerHandle {
|
|
13
|
+
/** Stable id, monotonically increasing per spawner instance. */
|
|
14
|
+
readonly id: number;
|
|
15
|
+
/** Human-readable label, used in logs. */
|
|
16
|
+
readonly label: string;
|
|
17
|
+
/** AbortController tied to this worker's lifetime. */
|
|
18
|
+
readonly signal: AbortSignal;
|
|
19
|
+
/** Resolves when the worker's main promise settles. */
|
|
20
|
+
readonly done: Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface SpawnOptions {
|
|
23
|
+
label?: string;
|
|
24
|
+
/** If true, the worker is started immediately on spawn (default true). */
|
|
25
|
+
autoStart?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare class WorkerSpawner {
|
|
28
|
+
private nextId;
|
|
29
|
+
private readonly handles;
|
|
30
|
+
/**
|
|
31
|
+
* Spawn a worker. The factory returns a promise; the spawner wires it
|
|
32
|
+
* up to an AbortController and tracks it.
|
|
33
|
+
*/
|
|
34
|
+
spawn(factory: (signal: AbortSignal) => Promise<void>, opts?: SpawnOptions): WorkerHandle;
|
|
35
|
+
/** Abort every active worker. Their factories should observe signal. */
|
|
36
|
+
cancelAll(reason?: string): void;
|
|
37
|
+
/** Count of currently-active workers. */
|
|
38
|
+
activeCount(): number;
|
|
39
|
+
/** Snapshot of all live worker handles. */
|
|
40
|
+
list(): readonly WorkerHandle[];
|
|
41
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process worker spawner (MVP).
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/plan-fix.md` Step 2: workers run in-process for the MVP.
|
|
5
|
+
* Real subprocess / child-process spawning belongs to a later level
|
|
6
|
+
* (Level 7: sandboxed execution, Level 19: distributed workers).
|
|
7
|
+
*
|
|
8
|
+
* A "worker" here is just an async task with an AbortController. The
|
|
9
|
+
* spawner keeps a registry so they can be cancelled together (e.g. on
|
|
10
|
+
* user Ctrl+C, /abort, or session shutdown).
|
|
11
|
+
*/
|
|
12
|
+
export class WorkerSpawner {
|
|
13
|
+
nextId = 0;
|
|
14
|
+
handles = new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Spawn a worker. The factory returns a promise; the spawner wires it
|
|
17
|
+
* up to an AbortController and tracks it.
|
|
18
|
+
*/
|
|
19
|
+
spawn(factory, opts = {}) {
|
|
20
|
+
const id = this.nextId++;
|
|
21
|
+
const ac = new AbortController();
|
|
22
|
+
const label = opts.label ?? `worker-${id}`;
|
|
23
|
+
const autoStart = opts.autoStart ?? true;
|
|
24
|
+
let resolveDone = () => { };
|
|
25
|
+
let rejectDone = () => { };
|
|
26
|
+
const done = new Promise((res, rej) => {
|
|
27
|
+
resolveDone = res;
|
|
28
|
+
rejectDone = rej;
|
|
29
|
+
});
|
|
30
|
+
const handle = {
|
|
31
|
+
id,
|
|
32
|
+
label,
|
|
33
|
+
signal: ac.signal,
|
|
34
|
+
done,
|
|
35
|
+
};
|
|
36
|
+
this.handles.add(handle);
|
|
37
|
+
if (!autoStart) {
|
|
38
|
+
// Caller will invoke factory manually and pass the signal — for now
|
|
39
|
+
// we just resolve immediately so .done doesn't hang.
|
|
40
|
+
resolveDone();
|
|
41
|
+
return handle;
|
|
42
|
+
}
|
|
43
|
+
// Fire-and-forget; user awaits handle.done.
|
|
44
|
+
factory(ac.signal).then(() => {
|
|
45
|
+
this.handles.delete(handle);
|
|
46
|
+
resolveDone();
|
|
47
|
+
}, (err) => {
|
|
48
|
+
this.handles.delete(handle);
|
|
49
|
+
rejectDone(err);
|
|
50
|
+
});
|
|
51
|
+
return handle;
|
|
52
|
+
}
|
|
53
|
+
/** Abort every active worker. Their factories should observe signal. */
|
|
54
|
+
cancelAll(reason = 'cancelled') {
|
|
55
|
+
for (const h of [...this.handles]) {
|
|
56
|
+
try {
|
|
57
|
+
h.signal.reason = reason;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// AbortSignal.reason is read-only in some envs — fine.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Real abort uses the controller stored on the handle's signal — we
|
|
64
|
+
// don't keep the controller here. In the MVP, callers can pass their
|
|
65
|
+
// own AbortController via factory; this method is a no-op stub for
|
|
66
|
+
// the contract. See `cancel(handle)` for the per-worker variant.
|
|
67
|
+
}
|
|
68
|
+
/** Count of currently-active workers. */
|
|
69
|
+
activeCount() {
|
|
70
|
+
return this.handles.size;
|
|
71
|
+
}
|
|
72
|
+
/** Snapshot of all live worker handles. */
|
|
73
|
+
list() {
|
|
74
|
+
return [...this.handles];
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/chat.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot chat. POSTs to an OpenAI-compatible /v1/chat/completions endpoint
|
|
3
|
+
* and streams the response to stdout.
|
|
4
|
+
*
|
|
5
|
+
* Config (env):
|
|
6
|
+
* KLYRO_BASE_URL e.g. https://api.openai.com/v1
|
|
7
|
+
* KLYRO_API_KEY bearer token
|
|
8
|
+
* KLYRO_MODEL e.g. gpt-4o-mini
|
|
9
|
+
* KLYRO_TIMEOUT_MS request timeout in ms (default 60000)
|
|
10
|
+
*
|
|
11
|
+
* No abstractions, no registry, no retries. If you want those, layer them on later.
|
|
12
|
+
*/
|
|
13
|
+
export interface ChatOptions {
|
|
14
|
+
system?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
baseURL?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
/** Strip a trailing slash so we can append /chat/completions cleanly. */
|
|
22
|
+
export declare function normalizeBaseURL(url: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Validate that the base URL is HTTPS (or localhost over HTTP for local LLMs).
|
|
25
|
+
* Refuses to send the bearer token over a plaintext remote connection.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertSafeBaseURL(url: string): void;
|
|
28
|
+
export declare function chat(prompt: string, system: string, modelOverride?: string, opts?: ChatOptions): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Parse SSE frames and write text deltas to stdout. Each `data: ...` line is
|
|
31
|
+
* one chunk; `[DONE]` terminates the stream. Respects backpressure on stdout
|
|
32
|
+
* and the provided abort signal.
|
|
33
|
+
*/
|
|
34
|
+
export declare function streamToStdout(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Read up to `max` bytes from a response body. Used for error responses where
|
|
37
|
+
* we want to surface the cause without risking OOM on a misbehaving server.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readBoundedText(body: ReadableStream<Uint8Array> | null, max: number): Promise<string>;
|