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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/agent/anthropic-adapter.d.ts +75 -0
  4. package/dist/agent/anthropic-adapter.js +297 -0
  5. package/dist/agent/message.d.ts +40 -0
  6. package/dist/agent/message.js +21 -0
  7. package/dist/agent/observation.d.ts +47 -0
  8. package/dist/agent/observation.js +53 -0
  9. package/dist/agent/provider-adapter.d.ts +101 -0
  10. package/dist/agent/provider-adapter.js +254 -0
  11. package/dist/agent/registry.d.ts +42 -0
  12. package/dist/agent/registry.js +86 -0
  13. package/dist/agent/retry.d.ts +34 -0
  14. package/dist/agent/retry.js +91 -0
  15. package/dist/agent/runtime.d.ts +130 -0
  16. package/dist/agent/runtime.js +221 -0
  17. package/dist/agent/worker-spawner.d.ts +41 -0
  18. package/dist/agent/worker-spawner.js +76 -0
  19. package/dist/chat.d.ts +39 -0
  20. package/dist/chat.js +235 -0
  21. package/dist/chat.js.map +1 -0
  22. package/dist/cli/eval.d.ts +75 -0
  23. package/dist/cli/eval.js +190 -0
  24. package/dist/cli/repl.d.ts +15 -0
  25. package/dist/cli/repl.js +194 -0
  26. package/dist/cli/run.d.ts +67 -0
  27. package/dist/cli/run.js +198 -0
  28. package/dist/cli/slash/parser.d.ts +39 -0
  29. package/dist/cli/slash/parser.js +47 -0
  30. package/dist/context/level6.d.ts +45 -0
  31. package/dist/context/level6.js +231 -0
  32. package/dist/context/level7.d.ts +84 -0
  33. package/dist/context/level7.js +156 -0
  34. package/dist/context/project-map.d.ts +48 -0
  35. package/dist/context/project-map.js +438 -0
  36. package/dist/context/repo-map.d.ts +30 -0
  37. package/dist/context/repo-map.js +168 -0
  38. package/dist/context/selector.d.ts +22 -0
  39. package/dist/context/selector.js +37 -0
  40. package/dist/context/snippets.d.ts +17 -0
  41. package/dist/context/snippets.js +15 -0
  42. package/dist/context/tokenizer.d.ts +49 -0
  43. package/dist/context/tokenizer.js +97 -0
  44. package/dist/eval/harness.d.ts +48 -0
  45. package/dist/eval/harness.js +149 -0
  46. package/dist/eval/tasks.d.ts +6 -0
  47. package/dist/eval/tasks.js +98 -0
  48. package/dist/index.d.ts +14 -0
  49. package/dist/index.js +128 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/persistence/audit.d.ts +75 -0
  52. package/dist/persistence/audit.js +15 -0
  53. package/dist/persistence/store.d.ts +67 -0
  54. package/dist/persistence/store.js +106 -0
  55. package/dist/policy/approval.d.ts +35 -0
  56. package/dist/policy/approval.js +54 -0
  57. package/dist/policy/engine.d.ts +76 -0
  58. package/dist/policy/engine.js +163 -0
  59. package/dist/policy/path-guard.d.ts +30 -0
  60. package/dist/policy/path-guard.js +100 -0
  61. package/dist/policy/secret-redactor.d.ts +19 -0
  62. package/dist/policy/secret-redactor.js +52 -0
  63. package/dist/providers.d.ts +23 -0
  64. package/dist/providers.js +83 -0
  65. package/dist/repl.d.ts +9 -0
  66. package/dist/repl.js +170 -0
  67. package/dist/repl.js.map +1 -0
  68. package/dist/tools/fs/edit-file.d.ts +48 -0
  69. package/dist/tools/fs/edit-file.js +84 -0
  70. package/dist/tools/fs/list-dir.d.ts +40 -0
  71. package/dist/tools/fs/list-dir.js +72 -0
  72. package/dist/tools/fs/read-file.d.ts +66 -0
  73. package/dist/tools/fs/read-file.js +75 -0
  74. package/dist/tools/fs/write-file.d.ts +24 -0
  75. package/dist/tools/fs/write-file.js +48 -0
  76. package/dist/tools/git/git-diff.d.ts +35 -0
  77. package/dist/tools/git/git-diff.js +68 -0
  78. package/dist/tools/git/git-status.d.ts +29 -0
  79. package/dist/tools/git/git-status.js +57 -0
  80. package/dist/tools/normalize.d.ts +31 -0
  81. package/dist/tools/normalize.js +88 -0
  82. package/dist/tools/registry.d.ts +23 -0
  83. package/dist/tools/registry.js +87 -0
  84. package/dist/tools/schema.d.ts +29 -0
  85. package/dist/tools/schema.js +183 -0
  86. package/dist/tools/search/dependencies.d.ts +44 -0
  87. package/dist/tools/search/dependencies.js +188 -0
  88. package/dist/tools/search/glob.d.ts +26 -0
  89. package/dist/tools/search/glob.js +88 -0
  90. package/dist/tools/search/grep.d.ts +52 -0
  91. package/dist/tools/search/grep.js +125 -0
  92. package/dist/tools/search/recent-files.d.ts +33 -0
  93. package/dist/tools/search/recent-files.js +100 -0
  94. package/dist/tools/search/search-files.d.ts +39 -0
  95. package/dist/tools/search/search-files.js +110 -0
  96. package/dist/tools/shell/shell-exec.d.ts +60 -0
  97. package/dist/tools/shell/shell-exec.js +133 -0
  98. package/dist/tools/types.d.ts +54 -0
  99. package/dist/tools/types.js +15 -0
  100. package/dist/tools/verify/run-verify.d.ts +60 -0
  101. package/dist/tools/verify/run-verify.js +115 -0
  102. package/dist/tui/app.d.ts +27 -0
  103. package/dist/tui/app.js +79 -0
  104. package/dist/tui/app.test.d.ts +1 -0
  105. package/dist/tui/app.test.js +77 -0
  106. package/dist/tui/status.d.ts +21 -0
  107. package/dist/tui/status.js +13 -0
  108. package/dist/tui/status.test.d.ts +1 -0
  109. package/dist/tui/status.test.js +39 -0
  110. package/dist/tui/transcript.d.ts +40 -0
  111. package/dist/tui/transcript.js +28 -0
  112. package/dist/tui/transcript.test.d.ts +1 -0
  113. package/dist/tui/transcript.test.js +71 -0
  114. package/dist/verification/detect.d.ts +29 -0
  115. package/dist/verification/detect.js +85 -0
  116. package/dist/verification/engine.d.ts +25 -0
  117. package/dist/verification/engine.js +57 -0
  118. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 siddu-lingampelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Klyro
2
+
3
+ Minimal streaming CLI for any OpenAI-compatible LLM endpoint. **Foundation piece** of the Klyro harness project.
4
+
5
+ ## What works today
6
+
7
+ - Streams from `https://<host>/v1/chat/completions`
8
+ - HTTPS-only (with localhost exemption for local LLMs)
9
+ - Per-request timeout
10
+ - Interactive REPL with multi-turn history
11
+ - Bounded error reads
12
+ - Strict TypeScript, zero dependencies beyond `commander`
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ npm run build
18
+ export KLYRO_BASE_URL="https://api.groq.com/openai/v1"
19
+ export KLYRO_API_KEY="gsk_..."
20
+ export KLYRO_MODEL="openai/gpt-oss-20b"
21
+
22
+ # One-shot
23
+ node dist/index.js chat "Explain TypeScript in 2 sentences"
24
+
25
+ # Interactive
26
+ node dist/index.js chat
27
+ ```
28
+
29
+ ## Documentation
30
+
31
+ | Doc | Purpose |
32
+ |---|---|
33
+ | [`docs/done.md`](docs/done.md) | **Status** — what's built, what's verified, what isn't |
34
+ | [`docs/plan.md`](docs/plan.md) | **Roadmap** — 20-level plan from bare CLI to super-harness |
35
+ | [`docs/PRD.md`](docs/PRD.md) | (authoritative) product vision |
36
+ | [`docs/HarnessFlow.md`](docs/HarnessFlow.md) | (authoritative) system flow |
37
+ | [`docs/MVP.md`](docs/MVP.md) | (authoritative) MVP scope |
38
+
39
+ ## Code structure
40
+
41
+ ```
42
+ src/
43
+ ├── index.ts # commander entry — two commands (chat, REPL)
44
+ ├── chat.ts # single-turn streaming chat (251 LOC)
45
+ └── repl.ts # multi-turn REPL (168 LOC)
46
+ ```
47
+
48
+ ## License
49
+
50
+ MIT
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Anthropic Messages API provider adapter.
3
+ *
4
+ * Translates the normalized ProviderAdapter surface to the Anthropic
5
+ * /v1/messages streaming endpoint. Distinct from httpChatAdapter (which
6
+ * speaks the OpenAI chat-completions protocol) in three ways:
7
+ *
8
+ * 1. `system` is a top-level field, not a message with role=system.
9
+ * 2. Tool definitions use `input_schema` not `parameters`, and have no
10
+ * `type: 'function'` wrapper.
11
+ * 3. `tool_use_id` becomes our `id`; the tool input is sent as a single
12
+ * `input_json_delta` block.
13
+ *
14
+ * Auth: `x-api-key: <key>`. Version header is sent as `anthropic-version`.
15
+ * Auth can be a Bearer token (for proxies) — the adapter accepts either.
16
+ */
17
+ import type { Message } from './message.js';
18
+ import type { ProviderAdapter, ToolDefinition } from './provider-adapter.js';
19
+ export interface AnthropicAdapterOptions {
20
+ baseURL?: string;
21
+ apiKey: string;
22
+ /** Per-request timeout in ms (default 120_000). */
23
+ timeoutMs?: number;
24
+ /** Override fetch (e.g. for tests). */
25
+ fetchImpl?: typeof fetch;
26
+ /** Override the Anthropic API version. Default '2023-06-01'. */
27
+ anthropicVersion?: string;
28
+ /** Override the auth header. Default 'x-api-key'. Set to 'Authorization' for proxy compat. */
29
+ authHeader?: 'x-api-key' | 'Authorization';
30
+ /** Beta features (e.g. ['prompt-caching-2024-07-31', 'tools-2024-04-04']). */
31
+ betas?: string[];
32
+ }
33
+ interface AnthropicMessage {
34
+ role: 'user' | 'assistant';
35
+ content: Array<{
36
+ type: 'text';
37
+ text: string;
38
+ } | {
39
+ type: 'tool_use';
40
+ id: string;
41
+ name: string;
42
+ input: unknown;
43
+ } | {
44
+ type: 'tool_result';
45
+ tool_use_id: string;
46
+ content: string | Array<{
47
+ type: 'text';
48
+ text: string;
49
+ }>;
50
+ is_error?: boolean;
51
+ }>;
52
+ }
53
+ export declare class AnthropicApiError extends Error {
54
+ readonly status: number;
55
+ readonly body: string;
56
+ constructor(status: number, body: string);
57
+ }
58
+ export declare function anthropicAdapter(opts: AnthropicAdapterOptions): ProviderAdapter;
59
+ /** Match an Anthropic content_block index to the tool_use id we emitted. */
60
+ declare function findToolIdByIndex(index: number | undefined, buffers: Map<string, {
61
+ name: string;
62
+ argsJson: string;
63
+ }>): string | undefined;
64
+ declare function toAnthropicMessages(messages: Message[]): AnthropicMessage[];
65
+ declare function toAnthropicTool(t: ToolDefinition): {
66
+ name: string;
67
+ description: string;
68
+ input_schema: unknown;
69
+ };
70
+ export declare const _internal: {
71
+ toAnthropicMessages: typeof toAnthropicMessages;
72
+ toAnthropicTool: typeof toAnthropicTool;
73
+ findToolIdByIndex: typeof findToolIdByIndex;
74
+ };
75
+ export {};
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Anthropic Messages API provider adapter.
3
+ *
4
+ * Translates the normalized ProviderAdapter surface to the Anthropic
5
+ * /v1/messages streaming endpoint. Distinct from httpChatAdapter (which
6
+ * speaks the OpenAI chat-completions protocol) in three ways:
7
+ *
8
+ * 1. `system` is a top-level field, not a message with role=system.
9
+ * 2. Tool definitions use `input_schema` not `parameters`, and have no
10
+ * `type: 'function'` wrapper.
11
+ * 3. `tool_use_id` becomes our `id`; the tool input is sent as a single
12
+ * `input_json_delta` block.
13
+ *
14
+ * Auth: `x-api-key: <key>`. Version header is sent as `anthropic-version`.
15
+ * Auth can be a Bearer token (for proxies) — the adapter accepts either.
16
+ */
17
+ const DEFAULT_VERSION = '2023-06-01';
18
+ const DEFAULT_TIMEOUT_MS = 120_000;
19
+ export class AnthropicApiError extends Error {
20
+ status;
21
+ body;
22
+ constructor(status, body) {
23
+ super(`Anthropic API error ${status}: ${body.slice(0, 500)}`);
24
+ this.name = 'AnthropicApiError';
25
+ this.status = status;
26
+ this.body = body;
27
+ }
28
+ }
29
+ export function anthropicAdapter(opts) {
30
+ const baseURL = (opts.baseURL ?? 'https://api.anthropic.com').replace(/\/+$/, '');
31
+ const version = opts.anthropicVersion ?? DEFAULT_VERSION;
32
+ const authHeader = opts.authHeader ?? 'x-api-key';
33
+ const betas = opts.betas ?? [];
34
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
35
+ if (!fetchImpl) {
36
+ throw new Error('anthropicAdapter: no fetch available — pass opts.fetchImpl or run on Node 18+');
37
+ }
38
+ return {
39
+ id: 'anthropic',
40
+ stream(req) {
41
+ return streamAnthropic(req, {
42
+ baseURL, apiKey: opts.apiKey, timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
43
+ fetchImpl, version, authHeader, betas,
44
+ });
45
+ },
46
+ };
47
+ }
48
+ async function* streamAnthropic(req, opts) {
49
+ const body = {
50
+ model: req.model,
51
+ system: req.system,
52
+ messages: toAnthropicMessages(req.messages),
53
+ tools: req.tools.length > 0 ? req.tools.map(toAnthropicTool) : undefined,
54
+ max_tokens: req.maxTokens ?? 4096,
55
+ temperature: req.temperature,
56
+ stream: true,
57
+ };
58
+ const url = `${opts.baseURL}/v1/messages`;
59
+ const ac = new AbortController();
60
+ const timer = setTimeout(() => ac.abort(new Error('anthropicAdapter: timeout')), opts.timeoutMs);
61
+ if (req.signal) {
62
+ if (req.signal.aborted)
63
+ ac.abort(req.signal.reason);
64
+ else
65
+ req.signal.addEventListener('abort', () => ac.abort(req.signal?.reason), { once: true });
66
+ }
67
+ const headers = {
68
+ 'content-type': 'application/json',
69
+ 'anthropic-version': opts.version,
70
+ };
71
+ if (opts.authHeader === 'x-api-key')
72
+ headers['x-api-key'] = opts.apiKey;
73
+ else
74
+ headers['Authorization'] = `Bearer ${opts.apiKey}`;
75
+ if (opts.betas.length > 0)
76
+ headers['anthropic-beta'] = opts.betas.join(',');
77
+ let resp;
78
+ try {
79
+ resp = await opts.fetchImpl(url, {
80
+ method: 'POST',
81
+ headers,
82
+ body: JSON.stringify(body),
83
+ signal: ac.signal,
84
+ });
85
+ }
86
+ catch (err) {
87
+ clearTimeout(timer);
88
+ const message = err instanceof Error ? err.message : String(err);
89
+ const retryable = !/abort/i.test(message) || /timeout/i.test(message);
90
+ yield { kind: 'error', code: 'transport', message, retryable };
91
+ return;
92
+ }
93
+ clearTimeout(timer);
94
+ if (!resp.ok || !resp.body) {
95
+ const text = await resp.text().catch(() => '<unreadable>');
96
+ yield {
97
+ kind: 'error',
98
+ code: `http_${resp.status}`,
99
+ message: `Anthropic API returned ${resp.status}: ${text.slice(0, 500)}`,
100
+ retryable: resp.status >= 500 || resp.status === 429,
101
+ };
102
+ return;
103
+ }
104
+ yield { kind: 'message_start' };
105
+ // Stream SSE: lines are `event: <type>\ndata: <json>\n\n`.
106
+ // We use a simple incremental parser; Claude's API guarantees
107
+ // well-formed SSE.
108
+ const reader = resp.body.getReader();
109
+ const decoder = new TextDecoder('utf-8');
110
+ let buf = '';
111
+ // Track in-progress tool calls so we can emit start/delta/end.
112
+ const toolBuffers = new Map();
113
+ try {
114
+ while (true) {
115
+ const { value, done } = await reader.read();
116
+ if (done)
117
+ break;
118
+ buf += decoder.decode(value, { stream: true });
119
+ // Split on SSE event boundary.
120
+ const events = [];
121
+ let idx = 0;
122
+ while (true) {
123
+ const start = idx;
124
+ const sep = buf.indexOf('\n\n', start);
125
+ if (sep === -1)
126
+ break;
127
+ const chunk = buf.slice(start, sep);
128
+ idx = sep + 2;
129
+ let event = 'message';
130
+ let data = '';
131
+ for (const line of chunk.split('\n')) {
132
+ if (line.startsWith('event:'))
133
+ event = line.slice(6).trim();
134
+ else if (line.startsWith('data:'))
135
+ data += line.slice(5).trim();
136
+ }
137
+ if (data)
138
+ events.push({ event, data });
139
+ }
140
+ buf = buf.slice(idx);
141
+ for (const e of events) {
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(e.data);
145
+ }
146
+ catch {
147
+ continue;
148
+ }
149
+ const out = translateSse(e.event, parsed, toolBuffers);
150
+ for (const ev of out)
151
+ yield ev;
152
+ }
153
+ }
154
+ }
155
+ catch (err) {
156
+ const message = err instanceof Error ? err.message : String(err);
157
+ yield { kind: 'error', code: 'stream', message, retryable: true };
158
+ return;
159
+ }
160
+ finally {
161
+ reader.releaseLock();
162
+ }
163
+ yield { kind: 'message_end', finishReason: 'stop' };
164
+ }
165
+ function translateSse(event, parsed, toolBuffers) {
166
+ const out = [];
167
+ switch (event) {
168
+ case 'content_block_start': {
169
+ const block = parsed.content_block;
170
+ if (block?.type === 'tool_use' && block.id && block.name) {
171
+ toolBuffers.set(block.id, { name: block.name, argsJson: '' });
172
+ out.push({ kind: 'tool_call_start', id: block.id, name: block.name });
173
+ }
174
+ return out;
175
+ }
176
+ case 'content_block_delta': {
177
+ const delta = parsed.delta;
178
+ 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
+ if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
182
+ out.push({ kind: 'text_delta', text: delta.text });
183
+ }
184
+ else if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
185
+ // Find the tool buffer whose index matches this delta.
186
+ // We map by index order: the i-th tool_use block corresponds to the
187
+ // i-th tool_start we've emitted. Track that counter separately.
188
+ const id = findToolIdByIndex(index, toolBuffers);
189
+ if (id) {
190
+ const buf = toolBuffers.get(id);
191
+ if (buf) {
192
+ buf.argsJson += delta.partial_json;
193
+ out.push({ kind: 'tool_call_delta', id, argsJson: delta.partial_json });
194
+ }
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+ 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
+ const index = parsed.index;
203
+ const id = findToolIdByIndex(index, toolBuffers);
204
+ if (id) {
205
+ toolBuffers.delete(id);
206
+ out.push({ kind: 'tool_call_end', id });
207
+ }
208
+ return out;
209
+ }
210
+ case 'message_stop': {
211
+ out.push({ kind: 'message_end', finishReason: 'stop' });
212
+ return out;
213
+ }
214
+ case 'error': {
215
+ const err = parsed.error;
216
+ out.push({
217
+ kind: 'error',
218
+ code: err?.type ?? 'anthropic_error',
219
+ message: err?.message ?? 'unknown Anthropic error',
220
+ retryable: false,
221
+ });
222
+ return out;
223
+ }
224
+ default:
225
+ return out;
226
+ }
227
+ }
228
+ /** Match an Anthropic content_block index to the tool_use id we emitted. */
229
+ function findToolIdByIndex(index, buffers) {
230
+ if (index === undefined)
231
+ return undefined;
232
+ // buffers is a Map preserving insertion order. The index of a tool_use
233
+ // block is its position among all content blocks, not just tool_use
234
+ // blocks. We don't know about non-tool blocks, so we use a simpler
235
+ // heuristic: the index matches the i-th tool_use that has been started
236
+ // since message_start. This is good enough for streamed responses where
237
+ // tool blocks are typically 0,1,2...
238
+ let i = 0;
239
+ for (const id of buffers.keys()) {
240
+ if (i === index)
241
+ return id;
242
+ i++;
243
+ }
244
+ // Fallback: if there's exactly one tool in flight, it's almost certainly it.
245
+ if (buffers.size === 1)
246
+ return buffers.keys().next().value;
247
+ return undefined;
248
+ }
249
+ function toAnthropicMessages(messages) {
250
+ return messages.map((m) => {
251
+ if (m.role === 'user') {
252
+ return {
253
+ role: 'user',
254
+ content: m.content.map((b) => {
255
+ if (b.kind === 'text')
256
+ return { type: 'text', text: b.text };
257
+ if (b.kind === 'tool_result') {
258
+ const content = typeof b.output === 'string'
259
+ ? (b.isError ? `Error: ${b.output}` : b.output)
260
+ : JSON.stringify(b.output);
261
+ return {
262
+ type: 'tool_result',
263
+ tool_use_id: b.toolCallId,
264
+ content,
265
+ is_error: b.isError,
266
+ };
267
+ }
268
+ return { type: 'text', text: '' };
269
+ }),
270
+ };
271
+ }
272
+ if (m.role === 'assistant') {
273
+ return {
274
+ role: 'assistant',
275
+ content: m.content.map((b) => {
276
+ if (b.kind === 'text')
277
+ return { type: 'text', text: b.text };
278
+ if (b.kind === 'tool_use')
279
+ return { type: 'tool_use', id: b.id, name: b.name, input: b.input };
280
+ return { type: 'text', text: '' };
281
+ }),
282
+ };
283
+ }
284
+ // 'system' is hoisted to the top-level `system` field; never appears
285
+ // in the messages array passed to the adapter.
286
+ return { role: 'user', content: [{ type: 'text', text: '' }] };
287
+ });
288
+ }
289
+ function toAnthropicTool(t) {
290
+ return {
291
+ name: t.name,
292
+ description: t.description,
293
+ input_schema: t.inputSchema,
294
+ };
295
+ }
296
+ // Re-export for testability.
297
+ export const _internal = { toAnthropicMessages, toAnthropicTool, findToolIdByIndex };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Message types shared between provider adapters and the runtime loop.
3
+ *
4
+ * Modeled after OpenAI/Anthropic chat-message shapes but kept simple:
5
+ * a Message has a role and an array of ContentBlocks. ContentBlocks
6
+ * cover text, tool_use (model asks to call a tool), and tool_result
7
+ * (the harness feeds back the tool's observation).
8
+ */
9
+ export type Role = 'system' | 'user' | 'assistant' | 'tool';
10
+ export interface TextBlock {
11
+ kind: 'text';
12
+ text: string;
13
+ }
14
+ export interface ToolUseBlock {
15
+ kind: 'tool_use';
16
+ /** Provider-assigned id; used to match a tool_result. */
17
+ id: string;
18
+ name: string;
19
+ input: Record<string, unknown>;
20
+ }
21
+ export interface ToolResultBlock {
22
+ kind: 'tool_result';
23
+ /** Matches the ToolUseBlock.id. */
24
+ toolCallId: string;
25
+ /** Tool name (for routing on resume and debugging). */
26
+ name: string;
27
+ output: unknown;
28
+ /** True if the tool itself failed (vs returning a normal value). */
29
+ isError?: boolean;
30
+ }
31
+ export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;
32
+ export interface Message {
33
+ role: Role;
34
+ content: ContentBlock[];
35
+ }
36
+ export declare function text(s: string): TextBlock;
37
+ export declare function toolUse(id: string, name: string, input: Record<string, unknown>): ToolUseBlock;
38
+ export declare function toolResult(toolCallId: string, name: string, output: unknown, isError?: boolean): ToolResultBlock;
39
+ /** Convenience: extract every ToolUseBlock from an assistant message. */
40
+ export declare function toolUses(m: Message): ToolUseBlock[];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Message types shared between provider adapters and the runtime loop.
3
+ *
4
+ * Modeled after OpenAI/Anthropic chat-message shapes but kept simple:
5
+ * a Message has a role and an array of ContentBlocks. ContentBlocks
6
+ * cover text, tool_use (model asks to call a tool), and tool_result
7
+ * (the harness feeds back the tool's observation).
8
+ */
9
+ export function text(s) {
10
+ return { kind: 'text', text: s };
11
+ }
12
+ export function toolUse(id, name, input) {
13
+ return { kind: 'tool_use', id, name, input };
14
+ }
15
+ export function toolResult(toolCallId, name, output, isError = false) {
16
+ return { kind: 'tool_result', toolCallId, name, output, isError };
17
+ }
18
+ /** Convenience: extract every ToolUseBlock from an assistant message. */
19
+ export function toolUses(m) {
20
+ return m.content.filter((b) => b.kind === 'tool_use');
21
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * In-memory ObservationStore — records every tool call + result during a
3
+ * single agent run so the runtime can recall "what happened so far" when
4
+ * deciding the next step.
5
+ *
6
+ * MVP: in-memory only (no persistence). Persistence is a later level
7
+ * (per `docs/plan.md` Level 14: durable session).
8
+ *
9
+ * Thread-safety: single agent loop = single owner. No locking needed.
10
+ */
11
+ import type { ToolResult } from '../tools/types.js';
12
+ /** One recorded observation: a tool invocation + its outcome. */
13
+ export interface Observation {
14
+ /** Monotonic id assigned at record-time. */
15
+ readonly id: number;
16
+ /** Name of the tool that was called. */
17
+ readonly tool: string;
18
+ /** Validated input that was passed to the tool. */
19
+ readonly input: unknown;
20
+ /** Tool's result — success value or structured error. */
21
+ readonly result: ToolResult<unknown>;
22
+ /** Wall-clock ms when execution started. */
23
+ readonly startedAt: number;
24
+ /** Wall-clock ms when execution finished. */
25
+ readonly finishedAt: number;
26
+ /** Duration in ms. */
27
+ readonly durationMs: number;
28
+ }
29
+ export declare class ObservationStore {
30
+ private readonly entries;
31
+ private nextId;
32
+ /** Record one completed tool call. Returns the assigned observation. */
33
+ record(entry: Omit<Observation, 'id'>): Observation;
34
+ /** All observations in insertion order. */
35
+ all(): readonly Observation[];
36
+ /** Most recent N observations. */
37
+ tail(n: number): readonly Observation[];
38
+ /** Count of observations recorded so far. */
39
+ size(): number;
40
+ /** Clear all observations (e.g. after /clear or compact). */
41
+ clear(): void;
42
+ /**
43
+ * Serialize for use as model context. Returns a compact, model-readable
44
+ * summary — not the full input/result blobs (those can be huge).
45
+ */
46
+ toContextSummary(): string;
47
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * In-memory ObservationStore — records every tool call + result during a
3
+ * single agent run so the runtime can recall "what happened so far" when
4
+ * deciding the next step.
5
+ *
6
+ * MVP: in-memory only (no persistence). Persistence is a later level
7
+ * (per `docs/plan.md` Level 14: durable session).
8
+ *
9
+ * Thread-safety: single agent loop = single owner. No locking needed.
10
+ */
11
+ export class ObservationStore {
12
+ entries = [];
13
+ nextId = 0;
14
+ /** Record one completed tool call. Returns the assigned observation. */
15
+ record(entry) {
16
+ const full = { ...entry, id: this.nextId++ };
17
+ this.entries.push(full);
18
+ return full;
19
+ }
20
+ /** All observations in insertion order. */
21
+ all() {
22
+ return this.entries;
23
+ }
24
+ /** Most recent N observations. */
25
+ tail(n) {
26
+ if (n <= 0)
27
+ return [];
28
+ return this.entries.slice(-n);
29
+ }
30
+ /** Count of observations recorded so far. */
31
+ size() {
32
+ return this.entries.length;
33
+ }
34
+ /** Clear all observations (e.g. after /clear or compact). */
35
+ clear() {
36
+ this.entries.length = 0;
37
+ }
38
+ /**
39
+ * Serialize for use as model context. Returns a compact, model-readable
40
+ * summary — not the full input/result blobs (those can be huge).
41
+ */
42
+ toContextSummary() {
43
+ if (this.entries.length === 0)
44
+ return '(no tool calls yet)';
45
+ const lines = [];
46
+ for (const o of this.entries) {
47
+ const status = o.result.ok ? 'OK' : `ERR(${o.result.error.code})`;
48
+ const dur = `${o.durationMs}ms`;
49
+ lines.push(`#${o.id} ${o.tool} ${status} ${dur}`);
50
+ }
51
+ return lines.join('\n');
52
+ }
53
+ }