@twinklerg/coden 0.1.0

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/index.js +21269 -0
  4. package/package.json +48 -0
  5. package/src/cli/agent-command.ts +497 -0
  6. package/src/cli/format.ts +42 -0
  7. package/src/cli/index.ts +149 -0
  8. package/src/cli/plugin-command.ts +217 -0
  9. package/src/config/config.ts +96 -0
  10. package/src/config/trust.ts +35 -0
  11. package/src/context/manager.ts +186 -0
  12. package/src/context/truncate.ts +9 -0
  13. package/src/core/events.ts +32 -0
  14. package/src/core/runtime.ts +402 -0
  15. package/src/core/types.ts +97 -0
  16. package/src/index.ts +14 -0
  17. package/src/observability/terminal.ts +201 -0
  18. package/src/observability/trace.ts +30 -0
  19. package/src/permissions/policy.ts +56 -0
  20. package/src/permissions/workspace.ts +139 -0
  21. package/src/plugins/api.ts +68 -0
  22. package/src/plugins/bun-package-manager.ts +35 -0
  23. package/src/plugins/installed-loader.ts +144 -0
  24. package/src/plugins/installer.ts +314 -0
  25. package/src/plugins/manifest.ts +89 -0
  26. package/src/plugins/package-manager.ts +10 -0
  27. package/src/plugins/package-metadata.ts +95 -0
  28. package/src/plugins/paths.ts +43 -0
  29. package/src/plugins/specifier.ts +63 -0
  30. package/src/plugins/transaction.ts +403 -0
  31. package/src/process/runner.ts +134 -0
  32. package/src/providers/anthropic.ts +117 -0
  33. package/src/providers/openai.ts +96 -0
  34. package/src/providers/scripted.ts +28 -0
  35. package/src/sessions/store.ts +278 -0
  36. package/src/tools/builtin/bash.ts +56 -0
  37. package/src/tools/builtin/edit.ts +42 -0
  38. package/src/tools/builtin/index.ts +9 -0
  39. package/src/tools/builtin/read.ts +91 -0
  40. package/src/tools/builtin/write.ts +34 -0
  41. package/src/tools/executor.ts +90 -0
  42. package/src/tools/plugin-loader.ts +122 -0
  43. package/src/tools/registry.ts +97 -0
@@ -0,0 +1,403 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { CodeNError } from "../core/types.js";
6
+ import type { PluginPaths } from "./paths.js";
7
+
8
+ export type TransactionPoint = "after-backup" | "after-runtime-commit" | "after-manifest-commit";
9
+
10
+ export interface PluginTransactionCandidate {
11
+ manifestPath: string;
12
+ runtimeDir: string;
13
+ }
14
+
15
+ export interface PluginTransactionOptions {
16
+ fault?: (point: TransactionPoint) => void;
17
+ now?: () => number;
18
+ isProcessAlive?: (pid: number) => boolean;
19
+ }
20
+
21
+ interface TransactionMarker {
22
+ version: 1;
23
+ id: string;
24
+ phase: "prepared" | "backed-up" | "runtime-committed" | "manifest-committed";
25
+ stageDirectory: string;
26
+ backupManifestPath: string;
27
+ backupRuntimeDir: string;
28
+ hadManifest: boolean;
29
+ hadRuntime: boolean;
30
+ }
31
+
32
+ interface LockOwner {
33
+ pid: number;
34
+ createdAt: number;
35
+ }
36
+
37
+ class FaultInterruption extends Error {
38
+ constructor(cause: unknown) {
39
+ super(
40
+ cause instanceof Error ? cause.message : String(cause),
41
+ cause instanceof Error ? { cause } : undefined,
42
+ );
43
+ this.name = "FaultInterruption";
44
+ }
45
+ }
46
+
47
+ export class PluginTransaction {
48
+ constructor(
49
+ private readonly paths: PluginPaths,
50
+ private readonly options: PluginTransactionOptions = {},
51
+ ) {}
52
+
53
+ async run<T>(builder: (candidate: PluginTransactionCandidate) => Promise<T>): Promise<T> {
54
+ let ownsLock = false;
55
+ let marker: TransactionMarker | undefined;
56
+ try {
57
+ await this.acquireLock();
58
+ ownsLock = true;
59
+ await this.recoverLocked();
60
+
61
+ const id = randomUUID();
62
+ const stageDirectory = path.join(this.paths.root, `.transaction-${id}-stage`);
63
+ marker = {
64
+ version: 1,
65
+ id,
66
+ phase: "prepared",
67
+ stageDirectory,
68
+ backupManifestPath: path.join(this.paths.root, `.transaction-${id}-plugins.json.bak`),
69
+ backupRuntimeDir: path.join(this.paths.root, `.transaction-${id}-runtime.bak`),
70
+ hadManifest: await pathExists(this.paths.manifestPath),
71
+ hadRuntime: await pathExists(this.paths.runtimeDir),
72
+ };
73
+
74
+ await mkdir(stageDirectory, { recursive: true });
75
+ const candidate = {
76
+ manifestPath: path.join(stageDirectory, "plugins.json"),
77
+ runtimeDir: path.join(stageDirectory, "runtime"),
78
+ };
79
+ const result = await builder(candidate);
80
+ await this.verifyCandidate(candidate);
81
+
82
+ await this.writeMarker(marker);
83
+ await this.backupCurrent(marker);
84
+
85
+ const backedUpMarker: TransactionMarker = { ...marker, phase: "backed-up" };
86
+ await this.writeMarker(backedUpMarker);
87
+ marker = backedUpMarker;
88
+ this.fault("after-backup");
89
+
90
+ await rename(candidate.runtimeDir, this.paths.runtimeDir);
91
+ const runtimeCommittedMarker: TransactionMarker = {
92
+ ...marker,
93
+ phase: "runtime-committed",
94
+ };
95
+ await this.writeMarker(runtimeCommittedMarker);
96
+ marker = runtimeCommittedMarker;
97
+ this.fault("after-runtime-commit");
98
+
99
+ await rename(candidate.manifestPath, this.paths.manifestPath);
100
+ const manifestCommittedMarker: TransactionMarker = {
101
+ ...marker,
102
+ phase: "manifest-committed",
103
+ };
104
+ await this.writeMarker(manifestCommittedMarker);
105
+ marker = manifestCommittedMarker;
106
+ this.fault("after-manifest-commit");
107
+
108
+ await this.cleanupCommitted(marker);
109
+ return result;
110
+ } catch (error) {
111
+ if (error instanceof FaultInterruption) throw error.cause ?? error;
112
+ await this.rollbackAfterError(marker);
113
+ throw error;
114
+ } finally {
115
+ if (ownsLock) await this.releaseLock();
116
+ }
117
+ }
118
+
119
+ async recover(): Promise<void> {
120
+ let ownsLock = false;
121
+ try {
122
+ await this.acquireLock();
123
+ ownsLock = true;
124
+ await this.recoverLocked();
125
+ } finally {
126
+ if (ownsLock) await this.releaseLock();
127
+ }
128
+ }
129
+
130
+ private async acquireLock(): Promise<void> {
131
+ await mkdir(this.paths.root, { recursive: true });
132
+ for (let attempt = 0; attempt < 2; attempt++) {
133
+ try {
134
+ await mkdir(this.paths.lockPath);
135
+ } catch (error) {
136
+ if (!isFileSystemError(error, "EEXIST")) throw error;
137
+ const owner = await this.readLockOwner();
138
+ if (!owner || this.isProcessAlive(owner.pid)) {
139
+ throw new CodeNError(
140
+ "plugin",
141
+ "plugin.install_busy",
142
+ `plugin.install_busy: ${this.paths.scope} plugin installation is already running`,
143
+ true,
144
+ { scope: this.paths.scope },
145
+ );
146
+ }
147
+ if (attempt === 1) {
148
+ throw new CodeNError(
149
+ "plugin",
150
+ "plugin.install_busy",
151
+ `plugin.install_busy: could not acquire ${this.paths.scope} plugin lock`,
152
+ true,
153
+ { scope: this.paths.scope },
154
+ );
155
+ }
156
+ await rm(this.paths.lockPath, { recursive: true, force: true });
157
+ continue;
158
+ }
159
+
160
+ try {
161
+ await writeFile(
162
+ path.join(this.paths.lockPath, "owner.json"),
163
+ `${JSON.stringify({ pid: process.pid, createdAt: this.now() } satisfies LockOwner)}\n`,
164
+ { mode: 0o600 },
165
+ );
166
+ return;
167
+ } catch (error) {
168
+ await rm(this.paths.lockPath, { recursive: true, force: true });
169
+ throw error;
170
+ }
171
+ }
172
+ }
173
+
174
+ private async readLockOwner(): Promise<LockOwner | undefined> {
175
+ try {
176
+ const owner = JSON.parse(
177
+ await readFile(path.join(this.paths.lockPath, "owner.json"), "utf8"),
178
+ ) as Partial<LockOwner>;
179
+ if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0) {
180
+ return {
181
+ pid: owner.pid,
182
+ createdAt: typeof owner.createdAt === "number" ? owner.createdAt : 0,
183
+ };
184
+ }
185
+ return undefined;
186
+ } catch {
187
+ return undefined;
188
+ }
189
+ }
190
+
191
+ private async releaseLock(): Promise<void> {
192
+ await rm(this.paths.lockPath, { recursive: true, force: true });
193
+ }
194
+
195
+ private async verifyCandidate(candidate: PluginTransactionCandidate): Promise<void> {
196
+ if (!(await pathExists(candidate.manifestPath)) || !(await pathExists(candidate.runtimeDir))) {
197
+ throw new CodeNError(
198
+ "plugin",
199
+ "plugin.transaction_recovery_failed",
200
+ "plugin.transaction_recovery_failed: candidate manifest and runtime are required",
201
+ false,
202
+ { scope: this.paths.scope },
203
+ );
204
+ }
205
+ }
206
+
207
+ private async backupCurrent(marker: TransactionMarker): Promise<void> {
208
+ await rm(marker.backupManifestPath, { force: true });
209
+ await rm(marker.backupRuntimeDir, { recursive: true, force: true });
210
+ if (marker.hadManifest) await rename(this.paths.manifestPath, marker.backupManifestPath);
211
+ if (marker.hadRuntime) await rename(this.paths.runtimeDir, marker.backupRuntimeDir);
212
+ }
213
+
214
+ private async rollbackAfterError(marker: TransactionMarker | undefined): Promise<void> {
215
+ if (!marker) return;
216
+ try {
217
+ if (marker.phase === "manifest-committed") {
218
+ await this.cleanupCommitted(marker);
219
+ return;
220
+ }
221
+ await this.restoreBackupPairBestEffort(marker);
222
+ await this.cleanupRecoveryArtifacts(marker);
223
+ } catch (error) {
224
+ throw new CodeNError(
225
+ "plugin",
226
+ "plugin.transaction_recovery_failed",
227
+ `plugin.transaction_recovery_failed: could not roll back ${this.paths.scope} plugin transaction`,
228
+ false,
229
+ { scope: this.paths.scope },
230
+ error instanceof Error ? { cause: error } : undefined,
231
+ );
232
+ }
233
+ }
234
+
235
+ private async recoverLocked(): Promise<void> {
236
+ const marker = await this.readMarker();
237
+ if (!marker) return;
238
+
239
+ try {
240
+ if (marker.phase === "prepared") {
241
+ await rm(marker.stageDirectory, { recursive: true, force: true });
242
+ await rm(this.paths.transactionPath, { force: true });
243
+ return;
244
+ }
245
+
246
+ if (marker.phase === "backed-up" || marker.phase === "runtime-committed") {
247
+ await this.removeCurrentTargets();
248
+ await this.restoreBackups(marker);
249
+ await this.cleanupRecoveryArtifacts(marker);
250
+ return;
251
+ }
252
+
253
+ await rm(marker.backupManifestPath, { force: true });
254
+ await rm(marker.backupRuntimeDir, { recursive: true, force: true });
255
+ await this.cleanupRecoveryArtifacts(marker);
256
+ } catch (error) {
257
+ throw new CodeNError(
258
+ "plugin",
259
+ "plugin.transaction_recovery_failed",
260
+ `plugin.transaction_recovery_failed: could not recover ${this.paths.scope} plugin transaction`,
261
+ false,
262
+ { scope: this.paths.scope },
263
+ error instanceof Error ? { cause: error } : undefined,
264
+ );
265
+ }
266
+ }
267
+
268
+ private async readMarker(): Promise<TransactionMarker | undefined> {
269
+ try {
270
+ const marker = JSON.parse(
271
+ await readFile(this.paths.transactionPath, "utf8"),
272
+ ) as Partial<TransactionMarker>;
273
+ if (!isTransactionMarker(marker)) throw new Error("invalid transaction marker");
274
+ return marker;
275
+ } catch (error) {
276
+ if (isFileSystemError(error, "ENOENT")) return undefined;
277
+ throw new CodeNError(
278
+ "plugin",
279
+ "plugin.transaction_recovery_failed",
280
+ `plugin.transaction_recovery_failed: invalid ${this.paths.scope} plugin transaction marker`,
281
+ false,
282
+ { scope: this.paths.scope },
283
+ error instanceof Error ? { cause: error } : undefined,
284
+ );
285
+ }
286
+ }
287
+
288
+ private async writeMarker(marker: TransactionMarker): Promise<void> {
289
+ await mkdir(path.dirname(this.paths.transactionPath), { recursive: true });
290
+ const temporaryPath = `${this.paths.transactionPath}.tmp`;
291
+ await writeFile(temporaryPath, `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
292
+ await rename(temporaryPath, this.paths.transactionPath);
293
+ }
294
+
295
+ private async cleanupCommitted(marker: TransactionMarker): Promise<void> {
296
+ await rm(marker.backupManifestPath, { force: true });
297
+ await rm(marker.backupRuntimeDir, { recursive: true, force: true });
298
+ await rm(marker.stageDirectory, { recursive: true, force: true });
299
+ await rm(this.paths.transactionPath, { force: true });
300
+ await rm(`${this.paths.transactionPath}.tmp`, { recursive: true, force: true });
301
+ }
302
+
303
+ private async cleanupRecoveryArtifacts(marker: TransactionMarker): Promise<void> {
304
+ await rm(marker.stageDirectory, { recursive: true, force: true });
305
+ await rm(this.paths.transactionPath, { force: true });
306
+ await rm(`${this.paths.transactionPath}.tmp`, { recursive: true, force: true });
307
+ }
308
+
309
+ private async removeCurrentTargets(): Promise<void> {
310
+ await rm(this.paths.manifestPath, { force: true });
311
+ await rm(this.paths.runtimeDir, { recursive: true, force: true });
312
+ }
313
+
314
+ private async restoreBackups(marker: TransactionMarker): Promise<void> {
315
+ if (marker.hadManifest) await rename(marker.backupManifestPath, this.paths.manifestPath);
316
+ if (marker.hadRuntime) await rename(marker.backupRuntimeDir, this.paths.runtimeDir);
317
+ }
318
+
319
+ private async restoreBackupPairBestEffort(marker: TransactionMarker): Promise<void> {
320
+ await this.restoreSingleBackup({
321
+ hadTarget: marker.hadManifest,
322
+ backupPath: marker.backupManifestPath,
323
+ targetPath: this.paths.manifestPath,
324
+ directory: false,
325
+ });
326
+ await this.restoreSingleBackup({
327
+ hadTarget: marker.hadRuntime,
328
+ backupPath: marker.backupRuntimeDir,
329
+ targetPath: this.paths.runtimeDir,
330
+ directory: true,
331
+ });
332
+ }
333
+
334
+ private async restoreSingleBackup(options: {
335
+ hadTarget: boolean;
336
+ backupPath: string;
337
+ targetPath: string;
338
+ directory: boolean;
339
+ }): Promise<void> {
340
+ if (await pathExists(options.backupPath)) {
341
+ await rm(options.targetPath, { recursive: options.directory, force: true });
342
+ await rename(options.backupPath, options.targetPath);
343
+ return;
344
+ }
345
+ if (!options.hadTarget)
346
+ await rm(options.targetPath, { recursive: options.directory, force: true });
347
+ }
348
+
349
+ private fault(point: TransactionPoint): void {
350
+ try {
351
+ this.options.fault?.(point);
352
+ } catch (error) {
353
+ throw new FaultInterruption(error);
354
+ }
355
+ }
356
+
357
+ private now(): number {
358
+ return this.options.now?.() ?? Date.now();
359
+ }
360
+
361
+ private isProcessAlive(pid: number): boolean {
362
+ if (this.options.isProcessAlive) return this.options.isProcessAlive(pid);
363
+ try {
364
+ process.kill(pid, 0);
365
+ return true;
366
+ } catch (error) {
367
+ return !isFileSystemError(error, "ESRCH");
368
+ }
369
+ }
370
+ }
371
+
372
+ function isTransactionMarker(value: Partial<TransactionMarker>): value is TransactionMarker {
373
+ return (
374
+ value.version === 1 &&
375
+ typeof value.id === "string" &&
376
+ ["prepared", "backed-up", "runtime-committed", "manifest-committed"].includes(
377
+ String(value.phase),
378
+ ) &&
379
+ typeof value.stageDirectory === "string" &&
380
+ typeof value.backupManifestPath === "string" &&
381
+ typeof value.backupRuntimeDir === "string" &&
382
+ typeof value.hadManifest === "boolean" &&
383
+ typeof value.hadRuntime === "boolean"
384
+ );
385
+ }
386
+
387
+ async function pathExists(file: string): Promise<boolean> {
388
+ try {
389
+ await access(file, constants.F_OK);
390
+ return true;
391
+ } catch {
392
+ return false;
393
+ }
394
+ }
395
+
396
+ function isFileSystemError(error: unknown, code: string): boolean {
397
+ return (
398
+ typeof error === "object" &&
399
+ error !== null &&
400
+ "code" in error &&
401
+ (error as { code?: string }).code === code
402
+ );
403
+ }
@@ -0,0 +1,134 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ class BoundedCollector {
4
+ private readonly headLimit: number;
5
+ private readonly tailLimit: number;
6
+ private head = "";
7
+ private tail = "";
8
+ private total = 0;
9
+
10
+ constructor(maxChars: number) {
11
+ this.headLimit = Math.ceil(maxChars * 0.6);
12
+ this.tailLimit = Math.floor(maxChars * 0.4);
13
+ }
14
+
15
+ add(chunk: string): void {
16
+ this.total += chunk.length;
17
+ const needed = Math.max(0, this.headLimit - this.head.length);
18
+ this.head += chunk.slice(0, needed);
19
+ const remainder = chunk.slice(needed);
20
+ if (remainder) this.tail = `${this.tail}${remainder}`.slice(-this.tailLimit);
21
+ }
22
+
23
+ value(): string {
24
+ const omitted = this.total - this.head.length - this.tail.length;
25
+ return omitted > 0
26
+ ? `${this.head}\n... [${omitted} characters omitted while capturing] ...\n${this.tail}`
27
+ : `${this.head}${this.tail}`;
28
+ }
29
+ }
30
+
31
+ export interface ProcessRunOptions {
32
+ cwd: string;
33
+ env?: NodeJS.ProcessEnv;
34
+ signal?: AbortSignal;
35
+ timeoutMs: number;
36
+ maxOutputChars: number;
37
+ }
38
+
39
+ export interface ProcessRunResult {
40
+ ok: boolean;
41
+ stdout: string;
42
+ stderr: string;
43
+ exitCode: number | null;
44
+ signal: NodeJS.Signals | null;
45
+ timedOut: boolean;
46
+ cancelled: boolean;
47
+ }
48
+
49
+ export type ProcessRunner = (
50
+ command: string,
51
+ args: string[],
52
+ options: ProcessRunOptions,
53
+ ) => Promise<ProcessRunResult>;
54
+
55
+ export const runProcess: ProcessRunner = (command, args, options) =>
56
+ new Promise((resolve) => {
57
+ const grouped = process.platform !== "win32";
58
+ const child = spawn(command, args, {
59
+ cwd: options.cwd,
60
+ env: options.env,
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ detached: grouped,
63
+ });
64
+ const stdout = new BoundedCollector(options.maxOutputChars);
65
+ const stderr = new BoundedCollector(options.maxOutputChars);
66
+ let timedOut = false;
67
+ let cancelled = false;
68
+ let settled = false;
69
+ let escalation: NodeJS.Timeout | undefined;
70
+ let timer: NodeJS.Timeout;
71
+
72
+ const terminate = (signal: NodeJS.Signals) => {
73
+ if (grouped && child.pid) {
74
+ try {
75
+ process.kill(-child.pid, signal);
76
+ return;
77
+ } catch {
78
+ // The process group may already have exited; kill the child directly.
79
+ }
80
+ }
81
+ child.kill(signal);
82
+ };
83
+ const finish = (exitCode: number | null, signal: NodeJS.Signals | null) => {
84
+ if (settled) return;
85
+ if ((timedOut || cancelled) && grouped && child.pid) {
86
+ try {
87
+ process.kill(-child.pid, "SIGKILL");
88
+ } catch {
89
+ // The leader or group may already be gone; resolving below is still safe.
90
+ }
91
+ }
92
+ settled = true;
93
+ clearTimeout(timer);
94
+ if (escalation) clearTimeout(escalation);
95
+ options.signal?.removeEventListener("abort", cancel);
96
+ resolve({
97
+ ok: !timedOut && !cancelled && exitCode === 0,
98
+ stdout: stdout.value(),
99
+ stderr: stderr.value(),
100
+ exitCode,
101
+ signal,
102
+ timedOut,
103
+ cancelled,
104
+ });
105
+ };
106
+ const escalate = () => {
107
+ if (escalation) return;
108
+ escalation = setTimeout(() => terminate("SIGKILL"), 500);
109
+ escalation.unref();
110
+ };
111
+ const cancel = () => {
112
+ if (cancelled) return;
113
+ cancelled = true;
114
+ terminate("SIGTERM");
115
+ escalate();
116
+ };
117
+
118
+ child.stdout.setEncoding("utf8");
119
+ child.stderr.setEncoding("utf8");
120
+ child.stdout.on("data", (chunk: string) => stdout.add(chunk));
121
+ child.stderr.on("data", (chunk: string) => stderr.add(chunk));
122
+ child.once("error", (error) => {
123
+ stderr.add(error.message);
124
+ finish(null, null);
125
+ });
126
+ child.once("close", finish);
127
+ if (options.signal?.aborted) cancel();
128
+ else options.signal?.addEventListener("abort", cancel, { once: true });
129
+ timer = setTimeout(() => {
130
+ timedOut = true;
131
+ terminate("SIGTERM");
132
+ escalate();
133
+ }, options.timeoutMs);
134
+ });
@@ -0,0 +1,117 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type { Tool as AnthropicTool, MessageParam } from "@anthropic-ai/sdk/resources/messages";
3
+ import type { AgentMessage, ModelEvent, ModelProvider, ModelRequest } from "../core/types.js";
4
+
5
+ export interface AnthropicProviderOptions {
6
+ apiKey: string;
7
+ baseURL?: string;
8
+ }
9
+ export class AnthropicProvider implements ModelProvider {
10
+ private readonly client: Anthropic;
11
+ constructor(options: AnthropicProviderOptions) {
12
+ this.client = new Anthropic({ ...options, maxRetries: 0 });
13
+ }
14
+ async *stream(request: ModelRequest): AsyncIterable<ModelEvent> {
15
+ const { system, messages } = toAnthropicMessages(request.messages);
16
+ const stream = this.client.messages.stream(
17
+ {
18
+ model: request.model,
19
+ system,
20
+ messages,
21
+ max_tokens: request.maxOutputTokens,
22
+ ...(request.tools.length
23
+ ? {
24
+ tools: request.tools.map(
25
+ (tool): AnthropicTool => ({
26
+ name: tool.name,
27
+ description: tool.description,
28
+ input_schema: tool.inputSchema as AnthropicTool.InputSchema,
29
+ }),
30
+ ),
31
+ }
32
+ : {}),
33
+ },
34
+ request.signal ? { signal: request.signal } : undefined,
35
+ );
36
+ const inputByIndex = new Map<number, string>();
37
+ for await (const event of stream) {
38
+ if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
39
+ inputByIndex.set(event.index, "");
40
+ yield {
41
+ type: "tool_call_start",
42
+ index: event.index,
43
+ callId: event.content_block.id,
44
+ name: event.content_block.name,
45
+ };
46
+ } else if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
47
+ yield { type: "text_delta", text: event.delta.text };
48
+ } else if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
49
+ inputByIndex.set(
50
+ event.index,
51
+ (inputByIndex.get(event.index) ?? "") + event.delta.partial_json,
52
+ );
53
+ yield {
54
+ type: "tool_call_delta",
55
+ index: event.index,
56
+ argumentsDelta: event.delta.partial_json,
57
+ };
58
+ } else if (event.type === "content_block_stop" && inputByIndex.has(event.index)) {
59
+ yield { type: "tool_call_end", index: event.index };
60
+ } else if (event.type === "message_start") {
61
+ yield {
62
+ type: "usage",
63
+ usage: {
64
+ inputTokens: event.message.usage.input_tokens,
65
+ outputTokens: event.message.usage.output_tokens,
66
+ },
67
+ };
68
+ } else if (event.type === "message_delta") {
69
+ yield { type: "usage", usage: { inputTokens: 0, outputTokens: event.usage.output_tokens } };
70
+ }
71
+ }
72
+ yield { type: "done" };
73
+ }
74
+ }
75
+
76
+ export function toAnthropicMessages(messages: AgentMessage[]): {
77
+ system: string;
78
+ messages: MessageParam[];
79
+ } {
80
+ const system = messages
81
+ .filter((message) => message.role === "system")
82
+ .map((message) => message.content)
83
+ .join("\n\n");
84
+ const converted: MessageParam[] = [];
85
+ for (const message of messages) {
86
+ if (message.role === "system") continue;
87
+ if (message.role === "user") converted.push({ role: "user", content: message.content });
88
+ else if (message.role === "assistant")
89
+ converted.push({
90
+ role: "assistant",
91
+ content: [
92
+ ...(message.content ? [{ type: "text" as const, text: message.content }] : []),
93
+ ...message.toolCalls.map((call) => ({
94
+ type: "tool_use" as const,
95
+ id: call.callId,
96
+ name: call.name,
97
+ input: call.input,
98
+ })),
99
+ ],
100
+ });
101
+ else {
102
+ const block = {
103
+ type: "tool_result" as const,
104
+ tool_use_id: message.callId,
105
+ content: message.content,
106
+ is_error: message.isError,
107
+ };
108
+ const previous = converted.at(-1);
109
+ if (previous?.role === "user" && Array.isArray(previous.content)) {
110
+ previous.content = [...previous.content, block];
111
+ } else {
112
+ converted.push({ role: "user", content: [block] });
113
+ }
114
+ }
115
+ }
116
+ return { system, messages: converted };
117
+ }