@justin06lee/yagami 0.4.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.
@@ -0,0 +1,1821 @@
1
+ // src/core/types.ts
2
+ var ApiError = class extends Error {
3
+ constructor(status, type, message) {
4
+ super(message);
5
+ this.status = status;
6
+ this.type = type;
7
+ this.name = "ApiError";
8
+ }
9
+ status;
10
+ type;
11
+ toBody() {
12
+ return { type: "error", error: { type: this.type, message: this.message } };
13
+ }
14
+ };
15
+
16
+ // src/core/errors.ts
17
+ var YagamiError = class extends Error {
18
+ constructor(message, code) {
19
+ super(message);
20
+ this.code = code;
21
+ this.name = "YagamiError";
22
+ }
23
+ code;
24
+ };
25
+ var ProviderNotInstalledError = class extends YagamiError {
26
+ constructor(provider, installHint, detail) {
27
+ super(
28
+ `${provider}: CLI not found${detail ? ` (${detail})` : ""}. ${installHint}`,
29
+ "provider_not_installed"
30
+ );
31
+ this.provider = provider;
32
+ this.installHint = installHint;
33
+ this.name = "ProviderNotInstalledError";
34
+ }
35
+ provider;
36
+ installHint;
37
+ };
38
+ var AuthRequiredError = class extends YagamiError {
39
+ constructor(provider, loginCommand, detail) {
40
+ super(
41
+ `${provider}: not logged in${detail ? ` (${detail})` : ""}. Run: ${loginCommand}`,
42
+ "auth_required"
43
+ );
44
+ this.provider = provider;
45
+ this.loginCommand = loginCommand;
46
+ this.name = "AuthRequiredError";
47
+ }
48
+ provider;
49
+ loginCommand;
50
+ };
51
+ var ProviderError = class extends YagamiError {
52
+ constructor(provider, message) {
53
+ super(`${provider}: ${message}`, "provider_error");
54
+ this.provider = provider;
55
+ this.name = "ProviderError";
56
+ }
57
+ provider;
58
+ };
59
+ var AUTH_PATTERNS = [
60
+ /not logged in/i,
61
+ /login required/i,
62
+ /please (run|use) [`'"]?\/?login/i,
63
+ /invalid api key/i,
64
+ /authentication[_ ]error/i,
65
+ /auth(entication)? required/i,
66
+ /not authenticated/i,
67
+ /unauthorized/i,
68
+ /credentials? (are|is) (missing|invalid|expired)/i,
69
+ /token (has )?expired/i,
70
+ /no credentials/i
71
+ ];
72
+ function looksLikeAuthFailure(text) {
73
+ return AUTH_PATTERNS.some((re) => re.test(text));
74
+ }
75
+ function classifyProviderFailure(provider, loginCommand, err) {
76
+ if (err instanceof YagamiError) return err;
77
+ const message = err instanceof Error ? err.message : String(err);
78
+ if (looksLikeAuthFailure(message)) {
79
+ return new AuthRequiredError(provider, loginCommand, message.split("\n")[0]?.slice(0, 200));
80
+ }
81
+ return new ProviderError(provider, message);
82
+ }
83
+ function toApiError(err) {
84
+ if (err instanceof ApiError) return err;
85
+ if (err instanceof AuthRequiredError || err instanceof ProviderNotInstalledError) {
86
+ return new ApiError(503, "api_error", err.message);
87
+ }
88
+ if (err instanceof YagamiError) return new ApiError(500, "api_error", err.message);
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ return new ApiError(500, "api_error", `engine error: ${message}`);
91
+ }
92
+
93
+ // src/core/provider.ts
94
+ function parseModelRef(model, providerIds) {
95
+ if (!model) return {};
96
+ const ids = new Set(providerIds);
97
+ if (ids.has(model)) return { providerId: model };
98
+ const colon = model.indexOf(":");
99
+ if (colon > 0) {
100
+ const prefix = model.slice(0, colon);
101
+ if (ids.has(prefix)) {
102
+ const rest = model.slice(colon + 1);
103
+ return rest ? { providerId: prefix, model: rest } : { providerId: prefix };
104
+ }
105
+ }
106
+ return { model };
107
+ }
108
+ function qualifiedModel(providerId, model) {
109
+ return `${providerId}:${model}`;
110
+ }
111
+
112
+ // src/core/executable.ts
113
+ import * as fs from "fs";
114
+ import * as os from "os";
115
+ import * as path from "path";
116
+ function expandHome(p) {
117
+ if (p === "~") return os.homedir();
118
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
119
+ return p;
120
+ }
121
+ function isFile(p) {
122
+ try {
123
+ return fs.statSync(p).isFile();
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+ function commonBinDirs() {
129
+ const home = os.homedir();
130
+ return [
131
+ path.join(home, ".local", "bin"),
132
+ "/opt/homebrew/bin",
133
+ "/usr/local/bin",
134
+ path.join(home, ".npm-global", "bin"),
135
+ path.join(home, "bin"),
136
+ path.join(home, ".bun", "bin"),
137
+ path.join(home, ".cargo", "bin")
138
+ ];
139
+ }
140
+ function findExecutable(name, options = {}) {
141
+ if (options.explicit) {
142
+ const p = expandHome(options.explicit);
143
+ return isFile(p) ? p : void 0;
144
+ }
145
+ if (name.includes("/")) {
146
+ const p = expandHome(name);
147
+ return isFile(p) ? p : void 0;
148
+ }
149
+ for (const dir of (process.env["PATH"] ?? "").split(path.delimiter)) {
150
+ if (!dir) continue;
151
+ const candidate = path.join(dir, name);
152
+ if (isFile(candidate)) return candidate;
153
+ }
154
+ for (const dir of commonBinDirs()) {
155
+ const candidate = path.join(dir, name);
156
+ if (isFile(candidate)) return candidate;
157
+ }
158
+ for (const candidate of options.extraPaths ?? []) {
159
+ if (isFile(expandHome(candidate))) return expandHome(candidate);
160
+ }
161
+ return void 0;
162
+ }
163
+ function resolveExecutable(providerId, name, installHint, options = {}) {
164
+ const found = findExecutable(name, options);
165
+ if (found) return found;
166
+ const detail = options.explicit ? `configured path ${options.explicit} does not exist` : `\`${name}\` not on PATH`;
167
+ throw new ProviderNotInstalledError(providerId, installHint, detail);
168
+ }
169
+ function resolveClaudeExecutable(explicit) {
170
+ const configured = explicit ?? process.env["YAGAMI_CLAUDE_PATH"];
171
+ return resolveExecutable(
172
+ "claude",
173
+ "claude",
174
+ "Install Claude Code and sign in (run `claude`, then /login), or point yagami at it with YAGAMI_CLAUDE_PATH or providers.claude.path.",
175
+ {
176
+ ...configured ? { explicit: configured } : {},
177
+ extraPaths: [path.join(os.homedir(), ".claude", "local", "claude")]
178
+ }
179
+ );
180
+ }
181
+
182
+ // src/version.ts
183
+ var VERSION = "0.4.1";
184
+
185
+ // src/core/providers/acp.ts
186
+ import { spawn, spawnSync } from "child_process";
187
+ import * as fs2 from "fs";
188
+ import * as os2 from "os";
189
+ import * as path2 from "path";
190
+ import * as readline from "readline";
191
+ import { PassThrough, Readable, Writable } from "stream";
192
+ import {
193
+ ClientSideConnection,
194
+ ndJsonStream,
195
+ PROTOCOL_VERSION
196
+ } from "@agentclientprotocol/sdk";
197
+
198
+ // src/core/providers/queue.ts
199
+ var AsyncQueue = class {
200
+ buffer = [];
201
+ waiting = null;
202
+ ended = false;
203
+ error = void 0;
204
+ push(value) {
205
+ if (this.ended) return;
206
+ if (this.waiting) {
207
+ const w = this.waiting;
208
+ this.waiting = null;
209
+ w.resolve({ value, done: false });
210
+ } else {
211
+ this.buffer.push(value);
212
+ }
213
+ }
214
+ end() {
215
+ if (this.ended) return;
216
+ this.ended = true;
217
+ if (this.waiting) {
218
+ const w = this.waiting;
219
+ this.waiting = null;
220
+ w.resolve({ value: void 0, done: true });
221
+ }
222
+ }
223
+ fail(err) {
224
+ if (this.ended) return;
225
+ this.error = err;
226
+ this.ended = true;
227
+ if (this.waiting) {
228
+ const w = this.waiting;
229
+ this.waiting = null;
230
+ w.reject(err);
231
+ }
232
+ }
233
+ get closed() {
234
+ return this.ended;
235
+ }
236
+ [Symbol.asyncIterator]() {
237
+ return {
238
+ next: () => {
239
+ if (this.buffer.length > 0) return Promise.resolve({ value: this.buffer.shift(), done: false });
240
+ if (this.error !== void 0) {
241
+ const e = this.error;
242
+ this.error = void 0;
243
+ return Promise.reject(e);
244
+ }
245
+ if (this.ended) return Promise.resolve({ value: void 0, done: true });
246
+ return new Promise((resolve, reject) => {
247
+ this.waiting = { resolve, reject };
248
+ });
249
+ },
250
+ return: () => {
251
+ this.ended = true;
252
+ return Promise.resolve({ value: void 0, done: true });
253
+ }
254
+ };
255
+ }
256
+ };
257
+
258
+ // src/core/providers/acp.ts
259
+ function rejectOption(p) {
260
+ const pick = p.options.find((o) => o.kind === "reject_once") ?? p.options.find((o) => o.kind === "reject_always") ?? p.options[0];
261
+ if (!pick) return { outcome: { outcome: "cancelled" } };
262
+ return { outcome: { outcome: "selected", optionId: pick.optionId } };
263
+ }
264
+ var AcpProvider = class {
265
+ id;
266
+ label;
267
+ executable;
268
+ loginCommand;
269
+ capabilities = {
270
+ resume: true,
271
+ fork: false,
272
+ images: true,
273
+ documents: false,
274
+ systemPrompt: false,
275
+ thinking: false,
276
+ effort: false,
277
+ streaming: "tokens"
278
+ };
279
+ args;
280
+ env;
281
+ workDir;
282
+ appName;
283
+ modelConfigId;
284
+ connectImpl;
285
+ constructor(options) {
286
+ this.id = options.id;
287
+ this.label = options.label;
288
+ this.loginCommand = options.loginCommand ?? `${options.command} (sign in per its docs)`;
289
+ this.executable = options.connect ? options.path ?? options.command : resolveExecutable(options.id, options.command, options.installHint ?? `Install \`${options.command}\` and sign in.`, {
290
+ ...options.path ? { explicit: options.path } : {}
291
+ });
292
+ this.args = options.args ?? [];
293
+ this.env = { ...process.env, ...options.env };
294
+ this.workDir = options.workDir ?? path2.join(os2.tmpdir(), "yagami-workspace");
295
+ this.appName = options.appName ?? "yagami";
296
+ this.modelConfigId = options.modelConfigId ?? "model";
297
+ this.connectImpl = options.connect ?? ((cwd) => this.spawnConnection(cwd));
298
+ fs2.mkdirSync(this.workDir, { recursive: true });
299
+ }
300
+ spawnConnection(cwd) {
301
+ return new Promise((resolve, reject) => {
302
+ let child;
303
+ try {
304
+ child = spawn(this.executable, this.args, { cwd, env: this.env, stdio: ["pipe", "pipe", "pipe"] });
305
+ } catch (err) {
306
+ reject(classifyProviderFailure(this.id, this.loginCommand, err));
307
+ return;
308
+ }
309
+ let stderr = "";
310
+ const noteNoise = (d) => {
311
+ stderr += d;
312
+ if (stderr.length > 16e3) stderr = stderr.slice(-8e3);
313
+ };
314
+ child.stderr?.on("data", (d) => noteNoise(d.toString()));
315
+ let handlers = {};
316
+ const stream = ndJsonStream(
317
+ Writable.toWeb(child.stdin),
318
+ Readable.toWeb(jsonLinesOnly(child.stdout, noteNoise))
319
+ );
320
+ const agent = new ClientSideConnection(
321
+ () => ({
322
+ requestPermission: (p) => handlers.onPermission ? handlers.onPermission(p) : rejectOption(p),
323
+ sessionUpdate: (n) => {
324
+ handlers.onUpdate?.(n);
325
+ }
326
+ }),
327
+ stream
328
+ );
329
+ let settled = false;
330
+ child.on("error", (err) => {
331
+ if (settled) return;
332
+ settled = true;
333
+ reject(classifyProviderFailure(this.id, this.loginCommand, err));
334
+ });
335
+ child.on("exit", (code) => {
336
+ if (settled) return;
337
+ settled = true;
338
+ reject(classifyProviderFailure(this.id, this.loginCommand, new Error(`${this.executable} exited with code ${code}${stderr ? `: ${stderr.trim().slice(-400)}` : ""}`)));
339
+ });
340
+ agent.initialize({
341
+ protocolVersion: PROTOCOL_VERSION,
342
+ clientInfo: { name: this.appName, version: VERSION },
343
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
344
+ }).then((init) => {
345
+ if (settled) return;
346
+ settled = true;
347
+ resolve({
348
+ agent,
349
+ init,
350
+ setHandlers: (h) => {
351
+ handlers = h;
352
+ },
353
+ close: () => {
354
+ child.kill("SIGTERM");
355
+ }
356
+ });
357
+ }).catch((err) => {
358
+ if (settled) return;
359
+ settled = true;
360
+ child.kill("SIGTERM");
361
+ reject(this.classify(err, stderr));
362
+ });
363
+ });
364
+ }
365
+ classify(err, context = "") {
366
+ const message = err instanceof Error ? err.message : String(err);
367
+ const code = err?.code;
368
+ if (code === -32e3 || looksLikeAuthFailure(message) || looksLikeAuthFailure(context)) {
369
+ return new AuthRequiredError(this.id, this.loginCommand, message.slice(0, 200));
370
+ }
371
+ return classifyProviderFailure(this.id, this.loginCommand, err);
372
+ }
373
+ async *run(req) {
374
+ const conn = await this.connectImpl(this.workDir);
375
+ const queue = new AsyncQueue();
376
+ let sessionId;
377
+ let costUsd;
378
+ const onAbort = () => {
379
+ if (sessionId) void conn.agent.cancel({ sessionId }).catch(() => {
380
+ });
381
+ };
382
+ try {
383
+ let configOptions;
384
+ let modes;
385
+ if (req.resume) {
386
+ if (!supportsResume(conn.init)) {
387
+ throw new ProviderError(this.id, "this agent cannot resume sessions; replaying the transcript instead");
388
+ }
389
+ const resumed = await conn.agent.resumeSession({ sessionId: req.resume, cwd: this.workDir }).catch((err) => {
390
+ throw this.classify(err);
391
+ });
392
+ sessionId = req.resume;
393
+ configOptions = resumed.configOptions;
394
+ modes = resumed.modes;
395
+ } else {
396
+ const created = await conn.agent.newSession({ cwd: this.workDir, mcpServers: [] }).catch((err) => {
397
+ throw this.classify(err);
398
+ });
399
+ sessionId = created.sessionId;
400
+ configOptions = created.configOptions;
401
+ modes = created.modes;
402
+ }
403
+ yield { type: "session", sessionId };
404
+ const plan = modes?.availableModes?.find((m) => /^(plan|read[-_]?only|ask)$/i.test(m.id));
405
+ if (plan && modes?.currentModeId !== plan.id) {
406
+ await conn.agent.setSessionMode({ sessionId, modeId: plan.id }).catch(() => {
407
+ });
408
+ }
409
+ if (req.model) await this.selectModel(conn, sessionId, configOptions, req.model);
410
+ const sid = sessionId;
411
+ conn.setHandlers({
412
+ onPermission: async (p) => rejectOption(p),
413
+ onUpdate: (n) => {
414
+ if (n.sessionId !== sid) return;
415
+ const u = n.update;
416
+ if (u.sessionUpdate === "agent_message_chunk" && u.content.type === "text") {
417
+ queue.push({ type: "text", text: u.content.text });
418
+ } else if (u.sessionUpdate === "agent_thought_chunk" && u.content.type === "text") {
419
+ queue.push({ type: "thinking", text: u.content.text });
420
+ } else if (u.sessionUpdate === "usage_update") {
421
+ const cost = u.cost;
422
+ if (cost && typeof cost.amount === "number" && (cost.currency ?? "USD") === "USD") costUsd = cost.amount;
423
+ }
424
+ }
425
+ });
426
+ req.signal?.addEventListener("abort", onAbort, { once: true });
427
+ conn.agent.prompt({ sessionId, prompt: toAcpBlocks(req.prompt, req.media ?? [], this.id) }).then((res) => {
428
+ queue.push({
429
+ type: "done",
430
+ usage: mapAcpUsage(res.usage ?? void 0),
431
+ ...costUsd !== void 0 ? { costUsd } : {},
432
+ stopReason: mapStopReason(res.stopReason)
433
+ });
434
+ queue.end();
435
+ }).catch((err) => queue.fail(this.classify(err)));
436
+ for await (const ev of queue) yield ev;
437
+ } finally {
438
+ req.signal?.removeEventListener("abort", onAbort);
439
+ conn.close();
440
+ }
441
+ }
442
+ async selectModel(conn, sessionId, configOptions, model) {
443
+ const option = configOptions?.find((o) => o.id === this.modelConfigId) ?? configOptions?.find((o) => o.category === "model");
444
+ if (!option || option.type !== "select") {
445
+ throw new ProviderError(this.id, `cannot select model "${model}": the agent exposes no model option (omit the model to use its default)`);
446
+ }
447
+ if (option.currentValue === model) return;
448
+ await conn.agent.setSessionConfigOption({ sessionId, configId: option.id, value: model }).catch((err) => {
449
+ throw this.classify(err);
450
+ });
451
+ }
452
+ async listModels() {
453
+ const conn = await this.connectImpl(this.workDir);
454
+ try {
455
+ const created = await conn.agent.newSession({ cwd: this.workDir, mcpServers: [] }).catch((err) => {
456
+ throw this.classify(err);
457
+ });
458
+ const option = created.configOptions?.find((o) => o.id === this.modelConfigId) ?? created.configOptions?.find((o) => o.category === "model");
459
+ if (!option || option.type !== "select") return [];
460
+ return flattenSelectOptions(option).map((o) => ({
461
+ id: o.value,
462
+ display_name: o.name,
463
+ ...o.description ? { description: o.description } : {}
464
+ }));
465
+ } finally {
466
+ conn.close();
467
+ }
468
+ }
469
+ /**
470
+ * The agent's self-reported name/version from the ACP handshake. When the
471
+ * handshake fails (wrong binary, version too old for ACP), falls back to
472
+ * `--version` and says so, because that is exactly what `doctor` needs.
473
+ */
474
+ async version() {
475
+ let handshakeError;
476
+ try {
477
+ const conn = await this.connectImpl(this.workDir);
478
+ try {
479
+ const info = conn.init.agentInfo;
480
+ if (info?.version) return `${info.name ?? this.id} ${info.version}`;
481
+ return `${this.id} (ACP ok)`;
482
+ } finally {
483
+ conn.close();
484
+ }
485
+ } catch (err) {
486
+ handshakeError = (err instanceof Error ? err.message : String(err)).split("\n")[0]?.slice(0, 120);
487
+ }
488
+ let plain;
489
+ try {
490
+ const out = spawnSync(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
491
+ plain = out.stdout?.trim().split("\n")[0] || void 0;
492
+ } catch {
493
+ plain = void 0;
494
+ }
495
+ return `${plain ?? "unknown version"} \u26A0 no ACP handshake (${handshakeError ?? "unknown error"}) \u2014 too old, or a different program with the same name?`;
496
+ }
497
+ };
498
+ function supportsResume(init) {
499
+ const caps = init.agentCapabilities;
500
+ return caps?.sessionCapabilities?.resume !== void 0;
501
+ }
502
+ function flattenSelectOptions(option) {
503
+ const raw = option.options;
504
+ if (!Array.isArray(raw)) return [];
505
+ const out = [];
506
+ for (const entry of raw) {
507
+ if (typeof entry["value"] === "string") {
508
+ out.push({ value: entry["value"], name: String(entry["name"] ?? entry["value"]), description: entry["description"] });
509
+ } else if (Array.isArray(entry["options"])) {
510
+ for (const leaf of entry["options"]) {
511
+ if (typeof leaf["value"] === "string") {
512
+ out.push({ value: leaf["value"], name: String(leaf["name"] ?? leaf["value"]), description: leaf["description"] });
513
+ }
514
+ }
515
+ }
516
+ }
517
+ return out;
518
+ }
519
+ function toAcpBlocks(text, media, providerId) {
520
+ const blocks = [];
521
+ for (const block of media) {
522
+ if (block.type !== "image") continue;
523
+ const source = block["source"];
524
+ if (source?.type !== "base64" || typeof source.data !== "string") {
525
+ throw new ProviderError(providerId, "only base64 image sources are supported (URL images are not fetched)");
526
+ }
527
+ blocks.push({ type: "image", data: source.data, mimeType: source.media_type ?? "image/png" });
528
+ }
529
+ if (text.length > 0 || blocks.length === 0) blocks.push({ type: "text", text });
530
+ return blocks;
531
+ }
532
+ function mapAcpUsage(u) {
533
+ const num = (k) => typeof u?.[k] === "number" ? u[k] : 0;
534
+ return {
535
+ input_tokens: num("inputTokens"),
536
+ output_tokens: num("outputTokens"),
537
+ cache_read_input_tokens: num("cachedReadTokens") || num("cacheReadTokens"),
538
+ cache_creation_input_tokens: num("cachedWriteTokens") || num("cacheWriteTokens")
539
+ };
540
+ }
541
+ function mapStopReason(reason) {
542
+ switch (reason) {
543
+ case "max_tokens":
544
+ return "max_tokens";
545
+ case "refusal":
546
+ return "refusal";
547
+ default:
548
+ return "end_turn";
549
+ }
550
+ }
551
+ function jsonLinesOnly(input, onNoise) {
552
+ const out = new PassThrough();
553
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
554
+ rl.on("line", (line) => {
555
+ if (line.trimStart().startsWith("{")) out.write(`${line}
556
+ `);
557
+ else if (line.trim()) onNoise(`${line}
558
+ `);
559
+ });
560
+ rl.on("close", () => out.end());
561
+ return out;
562
+ }
563
+
564
+ // src/core/providers/claude.ts
565
+ import { createRequire } from "module";
566
+ import { spawnSync as spawnSync2 } from "child_process";
567
+ import * as fs3 from "fs";
568
+ import * as os3 from "os";
569
+ import * as path3 from "path";
570
+ import {
571
+ query
572
+ } from "@anthropic-ai/claude-agent-sdk";
573
+ var DENY_ALL_TOOLS = async (toolName) => ({
574
+ behavior: "deny",
575
+ message: `yagami is a completions-only endpoint; tool "${toolName}" is disabled.`,
576
+ interrupt: true
577
+ });
578
+ var ClaudeProvider = class {
579
+ id = "claude";
580
+ label = "Claude Code";
581
+ executable;
582
+ loginCommand = "claude (then /login)";
583
+ capabilities = {
584
+ resume: true,
585
+ fork: true,
586
+ images: true,
587
+ documents: true,
588
+ systemPrompt: true,
589
+ thinking: true,
590
+ effort: true,
591
+ streaming: "tokens"
592
+ };
593
+ configDir;
594
+ workDir;
595
+ appName;
596
+ constructor(options = {}) {
597
+ this.executable = resolveClaudeExecutable(options.path);
598
+ this.configDir = options.configDir;
599
+ this.appName = options.appName ?? "yagami";
600
+ this.workDir = options.workDir ?? path3.join(os3.tmpdir(), "yagami-workspace");
601
+ fs3.mkdirSync(this.workDir, { recursive: true });
602
+ }
603
+ /** Hardened options shared by every completion turn and probe. */
604
+ baseOptions() {
605
+ return {
606
+ pathToClaudeCodeExecutable: this.executable,
607
+ cwd: this.workDir,
608
+ // Pure completions: no built-in tools, no settings/CLAUDE.md/skills
609
+ // leaking in, exactly one assistant turn per request.
610
+ tools: [],
611
+ settingSources: [],
612
+ maxTurns: 1,
613
+ canUseTool: DENY_ALL_TOOLS,
614
+ env: {
615
+ ...process.env,
616
+ ...this.configDir ? { CLAUDE_CONFIG_DIR: this.configDir } : {},
617
+ CLAUDE_AGENT_SDK_CLIENT_APP: `${this.appName}/${VERSION}`
618
+ }
619
+ };
620
+ }
621
+ async *run(req) {
622
+ const abortController = new AbortController();
623
+ const onAbort = () => abortController.abort();
624
+ req.signal?.addEventListener("abort", onAbort, { once: true });
625
+ const options = { ...this.baseOptions(), abortController, includePartialMessages: true };
626
+ if (req.model) options.model = req.model;
627
+ if (req.system !== void 0) options.systemPrompt = req.system;
628
+ if (req.resume) {
629
+ options.resume = req.resume;
630
+ options.forkSession = true;
631
+ }
632
+ const thinking = mapThinking(req.thinking);
633
+ if (thinking) options.thinking = thinking;
634
+ if (req.effort) options.effort = req.effort;
635
+ const prompt = req.media && req.media.length > 0 ? mediaPrompt(req.prompt, req.media) : req.prompt;
636
+ let result;
637
+ let model;
638
+ let stopReason;
639
+ let sawText = false;
640
+ try {
641
+ for await (const msg of query({ prompt, options })) {
642
+ if (msg.type === "system" && msg.subtype === "init") {
643
+ yield { type: "session", sessionId: msg.session_id };
644
+ } else if (msg.type === "stream_event" && msg.parent_tool_use_id === null) {
645
+ const event = msg.event;
646
+ if (event.type !== "content_block_delta") continue;
647
+ const delta = event["delta"];
648
+ if (delta?.type === "text_delta" && typeof delta.text === "string") {
649
+ sawText = true;
650
+ yield { type: "text", text: delta.text };
651
+ } else if (delta?.type === "thinking_delta" && typeof delta.thinking === "string") {
652
+ yield { type: "thinking", text: delta.thinking };
653
+ }
654
+ } else if (msg.type === "assistant" && msg.parent_tool_use_id === null) {
655
+ const raw = msg.message;
656
+ if (typeof raw.model === "string") model = raw.model;
657
+ if (typeof raw.stop_reason === "string") stopReason = raw.stop_reason;
658
+ } else if (msg.type === "result") {
659
+ result = msg;
660
+ }
661
+ }
662
+ } catch (err) {
663
+ throw classifyProviderFailure(this.id, this.loginCommand, err);
664
+ } finally {
665
+ req.signal?.removeEventListener("abort", onAbort);
666
+ }
667
+ if (req.signal?.aborted) return;
668
+ if (!result) throw new ProviderError(this.id, "engine terminated without producing a result");
669
+ if (result.subtype !== "success") {
670
+ const detail = "errors" in result && result.errors.length > 0 ? result.errors.join("; ") : result.subtype;
671
+ throw classifyProviderFailure(this.id, this.loginCommand, new Error(`engine error: ${detail}`));
672
+ }
673
+ if (!sawText && result.result) yield { type: "text", text: result.result };
674
+ yield {
675
+ type: "done",
676
+ usage: mapUsage(result.usage),
677
+ ...result.total_cost_usd !== void 0 ? { costUsd: result.total_cost_usd } : {},
678
+ ...model ? { model } : {},
679
+ ...stopReason ? { stopReason } : {}
680
+ };
681
+ }
682
+ /** Ask the CLI which models it supports via a short-lived control session. */
683
+ async listModels() {
684
+ const abortController = new AbortController();
685
+ let release;
686
+ const gate = new Promise((resolve) => release = resolve);
687
+ const idle = (async function* () {
688
+ await gate;
689
+ })();
690
+ const q = query({ prompt: idle, options: { ...this.baseOptions(), abortController } });
691
+ let timer;
692
+ try {
693
+ const models = await Promise.race([
694
+ q.supportedModels(),
695
+ new Promise((_, reject) => {
696
+ timer = setTimeout(() => reject(new Error("timed out probing supported models")), 15e3);
697
+ timer.unref?.();
698
+ })
699
+ ]);
700
+ return models.map((m) => ({
701
+ id: m.value,
702
+ display_name: m.displayName,
703
+ ...m.description ? { description: m.description } : {},
704
+ ...m.resolvedModel ? { resolved_model: m.resolvedModel } : {}
705
+ }));
706
+ } catch (err) {
707
+ throw classifyProviderFailure(this.id, this.loginCommand, err);
708
+ } finally {
709
+ if (timer) clearTimeout(timer);
710
+ release();
711
+ abortController.abort();
712
+ }
713
+ }
714
+ async version() {
715
+ try {
716
+ const out = spawnSync2(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
717
+ return out.stdout?.trim().split("\n")[0] || void 0;
718
+ } catch {
719
+ return void 0;
720
+ }
721
+ }
722
+ /**
723
+ * Compare the bundled Agent SDK build against the installed CLI. Their
724
+ * last version components track the same build number when in sync.
725
+ */
726
+ async versionSkew() {
727
+ const binary = await this.version();
728
+ const sdk = sdkVersion();
729
+ if (!binary || !sdk) return void 0;
730
+ const binaryVersion = binary.match(/\d+\.\d+\.\d+/)?.[0];
731
+ if (!binaryVersion) return void 0;
732
+ const sdkBuild = sdk.split(".").pop();
733
+ const binBuild = binaryVersion.split(".").pop();
734
+ const inSync = sdkBuild === binBuild;
735
+ return {
736
+ sdkVersion: sdk,
737
+ binaryVersion,
738
+ inSync,
739
+ note: inSync ? "Agent SDK and claude binary are the same build" : `Agent SDK ${sdk} was built alongside claude x.y.${sdkBuild}; you run ${binaryVersion}. Usually fine \u2014 update whichever is older if something misbehaves.`
740
+ };
741
+ }
742
+ };
743
+ function sdkVersion() {
744
+ try {
745
+ const req = createRequire(import.meta.url);
746
+ const entry = req.resolve("@anthropic-ai/claude-agent-sdk");
747
+ let dir = path3.dirname(entry);
748
+ for (let i = 0; i < 4; i += 1) {
749
+ const pkg = path3.join(dir, "package.json");
750
+ if (fs3.existsSync(pkg)) {
751
+ const parsed = JSON.parse(fs3.readFileSync(pkg, "utf8"));
752
+ if (parsed.name === "@anthropic-ai/claude-agent-sdk") return parsed.version;
753
+ }
754
+ dir = path3.dirname(dir);
755
+ }
756
+ } catch {
757
+ }
758
+ return void 0;
759
+ }
760
+ function mapThinking(t) {
761
+ if (t == null) return void 0;
762
+ if (t.type === "enabled") {
763
+ return typeof t.budget_tokens === "number" ? { type: "enabled", budgetTokens: t.budget_tokens } : { type: "enabled" };
764
+ }
765
+ if (t.type === "disabled") return { type: "disabled" };
766
+ return { type: "adaptive" };
767
+ }
768
+ function mapUsage(usage) {
769
+ const num = (key) => typeof usage[key] === "number" ? usage[key] : void 0;
770
+ return {
771
+ input_tokens: num("input_tokens") ?? 0,
772
+ output_tokens: num("output_tokens") ?? 0,
773
+ cache_creation_input_tokens: num("cache_creation_input_tokens") ?? 0,
774
+ cache_read_input_tokens: num("cache_read_input_tokens") ?? 0
775
+ };
776
+ }
777
+ function mediaPrompt(text, media) {
778
+ const content = [...media, ...text.length > 0 ? [{ type: "text", text }] : []];
779
+ const message = {
780
+ type: "user",
781
+ message: { role: "user", content },
782
+ parent_tool_use_id: null
783
+ };
784
+ return (async function* () {
785
+ yield message;
786
+ })();
787
+ }
788
+
789
+ // src/core/providers/codex.ts
790
+ import { spawn as spawn3, spawnSync as spawnSync3 } from "child_process";
791
+ import * as fs4 from "fs";
792
+ import * as os4 from "os";
793
+ import * as path4 from "path";
794
+ import * as readline3 from "readline";
795
+
796
+ // src/core/providers/jsonl.ts
797
+ import { spawn as spawn2 } from "child_process";
798
+ import * as readline2 from "readline";
799
+ var ProcessExitError = class extends Error {
800
+ constructor(exitCode, stderr) {
801
+ super(`process exited with code ${exitCode}${stderr ? `: ${stderr.trim().split("\n").slice(-3).join(" | ")}` : ""}`);
802
+ this.exitCode = exitCode;
803
+ this.stderr = stderr;
804
+ this.name = "ProcessExitError";
805
+ }
806
+ exitCode;
807
+ stderr;
808
+ };
809
+ function spawnJsonl(options) {
810
+ const queue = new AsyncQueue();
811
+ const child = spawn2(options.command, options.args, {
812
+ cwd: options.cwd,
813
+ env: options.env,
814
+ stdio: [options.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
815
+ });
816
+ let stderr = "";
817
+ child.stderr?.on("data", (chunk) => {
818
+ stderr += chunk.toString();
819
+ if (stderr.length > 16e3) stderr = stderr.slice(-8e3);
820
+ });
821
+ const rl = readline2.createInterface({ input: child.stdout, crlfDelay: Infinity });
822
+ rl.on("line", (line) => {
823
+ const trimmed = line.trim();
824
+ if (!trimmed.startsWith("{")) return;
825
+ try {
826
+ queue.push(JSON.parse(trimmed));
827
+ } catch {
828
+ }
829
+ });
830
+ const onAbort = () => {
831
+ child.kill("SIGTERM");
832
+ queue.end();
833
+ };
834
+ options.signal?.addEventListener("abort", onAbort, { once: true });
835
+ child.on("error", (err) => queue.fail(err));
836
+ child.on("close", (code) => {
837
+ options.signal?.removeEventListener("abort", onAbort);
838
+ if (options.signal?.aborted) return queue.end();
839
+ if (code !== 0) queue.fail(new ProcessExitError(code, stderr));
840
+ else queue.end();
841
+ });
842
+ if (options.stdin !== void 0) {
843
+ child.stdin?.end(options.stdin);
844
+ }
845
+ return queue;
846
+ }
847
+
848
+ // src/core/providers/codex.ts
849
+ var INSTALL_HINT = "Install Codex CLI (npm i -g @openai/codex or brew install codex) and run `codex login`.";
850
+ var CodexProvider = class {
851
+ id = "codex";
852
+ label = "Codex CLI";
853
+ executable;
854
+ loginCommand = "codex login";
855
+ capabilities = {
856
+ resume: true,
857
+ fork: false,
858
+ images: true,
859
+ documents: false,
860
+ systemPrompt: false,
861
+ thinking: false,
862
+ effort: true,
863
+ streaming: "chunks"
864
+ };
865
+ workDir;
866
+ sandbox;
867
+ env;
868
+ constructor(options = {}) {
869
+ this.executable = resolveExecutable("codex", "codex", INSTALL_HINT, {
870
+ ...options.path ?? process.env["YAGAMI_CODEX_PATH"] ? { explicit: options.path ?? process.env["YAGAMI_CODEX_PATH"] } : {}
871
+ });
872
+ this.workDir = options.workDir ?? path4.join(os4.tmpdir(), "yagami-workspace");
873
+ this.sandbox = options.sandbox ?? "read-only";
874
+ this.env = { ...process.env, ...options.env };
875
+ fs4.mkdirSync(this.workDir, { recursive: true });
876
+ }
877
+ /** Build the `codex exec` argument list for a turn (exported for tests). */
878
+ buildArgs(req, imagePaths) {
879
+ const args = ["exec", "--json", "--skip-git-repo-check", "-C", this.workDir, "-s", this.sandbox, "--color", "never"];
880
+ if (req.model) args.push("-m", req.model);
881
+ if (req.effort) args.push("-c", `model_reasoning_effort="${req.effort}"`);
882
+ for (const p of imagePaths) args.push("-i", p);
883
+ if (req.resume) args.push("resume", req.resume);
884
+ args.push(req.prompt);
885
+ return args;
886
+ }
887
+ async *run(req) {
888
+ const { paths: imagePaths, cleanup } = writeTempImages(req.media ?? []);
889
+ const emitted = /* @__PURE__ */ new Map();
890
+ let done = false;
891
+ try {
892
+ for await (const raw of spawnJsonl({
893
+ command: this.executable,
894
+ args: this.buildArgs(req, imagePaths),
895
+ cwd: this.workDir,
896
+ env: this.env,
897
+ ...req.signal ? { signal: req.signal } : {}
898
+ })) {
899
+ const ev = raw;
900
+ switch (ev.type) {
901
+ case "thread.started":
902
+ if (ev.thread_id) yield { type: "session", sessionId: ev.thread_id };
903
+ break;
904
+ case "item.started":
905
+ case "item.updated":
906
+ case "item.completed": {
907
+ const item = ev.item;
908
+ if (!item || typeof item.text !== "string") break;
909
+ if (item.type !== "agent_message" && item.type !== "reasoning") break;
910
+ const seen = emitted.get(item.id) ?? 0;
911
+ const fresh = item.text.slice(seen);
912
+ emitted.set(item.id, item.text.length);
913
+ if (fresh) yield { type: item.type === "agent_message" ? "text" : "thinking", text: fresh };
914
+ break;
915
+ }
916
+ case "turn.completed":
917
+ done = true;
918
+ yield { type: "done", usage: mapCodexUsage(ev.usage ?? {}), stopReason: "end_turn" };
919
+ break;
920
+ case "turn.failed":
921
+ throw new Error(ev.error?.message ?? "turn failed");
922
+ case "error":
923
+ throw new Error(ev.message ?? "codex error");
924
+ default:
925
+ break;
926
+ }
927
+ }
928
+ } catch (err) {
929
+ throw classifyProviderFailure(this.id, this.loginCommand, err);
930
+ } finally {
931
+ cleanup();
932
+ }
933
+ if (req.signal?.aborted) return;
934
+ if (!done) throw new ProviderError(this.id, "codex exited without completing the turn");
935
+ }
936
+ /** Ask the app-server protocol for the model catalog (no tokens spent). */
937
+ listModels() {
938
+ return new Promise((resolve, reject) => {
939
+ const child = spawn3(this.executable, ["app-server"], { env: this.env, stdio: ["pipe", "pipe", "pipe"] });
940
+ let stderr = "";
941
+ let settled = false;
942
+ const finish = (fn) => {
943
+ if (settled) return;
944
+ settled = true;
945
+ clearTimeout(timer);
946
+ child.kill("SIGTERM");
947
+ fn();
948
+ };
949
+ const timer = setTimeout(() => finish(() => reject(new ProviderError(this.id, "timed out listing models via app-server"))), 15e3);
950
+ timer.unref?.();
951
+ child.stderr.on("data", (d) => stderr += d.toString());
952
+ child.on("error", (err) => finish(() => reject(classifyProviderFailure(this.id, this.loginCommand, err))));
953
+ child.on(
954
+ "close",
955
+ () => finish(() => reject(classifyProviderFailure(this.id, this.loginCommand, new Error(stderr.trim() || "app-server exited"))))
956
+ );
957
+ const rl = readline3.createInterface({ input: child.stdout });
958
+ rl.on("line", (line) => {
959
+ let msg;
960
+ try {
961
+ msg = JSON.parse(line);
962
+ } catch {
963
+ return;
964
+ }
965
+ if (msg.id === 1) {
966
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "initialized", params: {} })}
967
+ `);
968
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "model/list", params: {} })}
969
+ `);
970
+ } else if (msg.id === 2) {
971
+ if (msg.error) {
972
+ finish(() => reject(classifyProviderFailure(this.id, this.loginCommand, new Error(msg.error?.message ?? "model/list failed"))));
973
+ return;
974
+ }
975
+ const models = (msg.result?.data ?? []).filter((m) => m["hidden"] !== true).map((m) => ({
976
+ id: String(m["id"] ?? m["model"]),
977
+ display_name: String(m["displayName"] ?? m["id"] ?? m["model"]),
978
+ ...typeof m["description"] === "string" ? { description: m["description"] } : {}
979
+ }));
980
+ finish(() => resolve(models));
981
+ }
982
+ });
983
+ child.stdin.write(
984
+ `${JSON.stringify({
985
+ jsonrpc: "2.0",
986
+ id: 1,
987
+ method: "initialize",
988
+ params: { clientInfo: { name: "yagami", title: "yagami", version: VERSION } }
989
+ })}
990
+ `
991
+ );
992
+ });
993
+ }
994
+ async version() {
995
+ try {
996
+ const out = spawnSync3(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
997
+ return out.stdout?.trim().split("\n")[0] || void 0;
998
+ } catch {
999
+ return void 0;
1000
+ }
1001
+ }
1002
+ };
1003
+ function mapCodexUsage(u) {
1004
+ return {
1005
+ input_tokens: u["input_tokens"] ?? 0,
1006
+ output_tokens: u["output_tokens"] ?? 0,
1007
+ cache_read_input_tokens: u["cached_input_tokens"] ?? 0,
1008
+ cache_creation_input_tokens: u["cache_write_input_tokens"] ?? 0
1009
+ };
1010
+ }
1011
+ var EXT_BY_MIME = {
1012
+ "image/png": ".png",
1013
+ "image/jpeg": ".jpg",
1014
+ "image/gif": ".gif",
1015
+ "image/webp": ".webp"
1016
+ };
1017
+ function writeTempImages(media) {
1018
+ const images = media.filter((b) => b.type === "image");
1019
+ if (images.length === 0) return { paths: [], cleanup: () => {
1020
+ } };
1021
+ const dir = fs4.mkdtempSync(path4.join(os4.tmpdir(), "yagami-img-"));
1022
+ const paths = [];
1023
+ images.forEach((block, i) => {
1024
+ const source = block["source"];
1025
+ if (source?.type !== "base64" || typeof source.data !== "string") {
1026
+ fs4.rmSync(dir, { recursive: true, force: true });
1027
+ throw new ProviderError("codex", "only base64 image sources are supported (URL images are not fetched)");
1028
+ }
1029
+ const file = path4.join(dir, `image-${i}${EXT_BY_MIME[source.media_type ?? ""] ?? ".png"}`);
1030
+ fs4.writeFileSync(file, Buffer.from(source.data, "base64"));
1031
+ paths.push(file);
1032
+ });
1033
+ return { paths, cleanup: () => fs4.rmSync(dir, { recursive: true, force: true }) };
1034
+ }
1035
+
1036
+ // src/core/providers/registry.ts
1037
+ var PROVIDER_PRESETS = [
1038
+ { id: "claude", label: "Claude Code", kind: "claude", command: "claude", args: [], loginCommand: "claude (then /login)", installHint: "npm i -g @anthropic-ai/claude-code" },
1039
+ { id: "codex", label: "Codex CLI", kind: "codex", command: "codex", args: [], loginCommand: "codex login", installHint: "npm i -g @openai/codex" },
1040
+ { id: "opencode", label: "OpenCode", kind: "acp", command: "opencode", args: ["acp"], loginCommand: "opencode auth login", installHint: "curl -fsSL https://opencode.ai/install | bash" },
1041
+ { id: "gemini", label: "Gemini CLI", kind: "acp", command: "gemini", args: ["--acp"], loginCommand: "gemini (then /auth)", installHint: "npm i -g @google/gemini-cli" },
1042
+ { id: "copilot", label: "GitHub Copilot CLI", kind: "acp", command: "copilot", args: ["--acp"], loginCommand: "copilot (then /login)", installHint: "npm i -g @github/copilot" },
1043
+ { id: "qwen", label: "Qwen Code", kind: "acp", command: "qwen", args: ["--acp"], loginCommand: "qwen (then /auth)", installHint: "npm i -g @qwen-code/qwen-code" },
1044
+ { id: "cursor", label: "Cursor Agent", kind: "acp", command: "cursor-agent", args: ["acp"], loginCommand: "cursor-agent login", installHint: "curl https://cursor.com/install -fsS | bash" },
1045
+ { id: "goose", label: "Goose", kind: "acp", command: "goose", args: ["acp"], loginCommand: "goose configure", installHint: "brew install block-goose-cli" },
1046
+ { id: "kimi", label: "Kimi CLI", kind: "acp", command: "kimi", args: ["acp"], loginCommand: "kimi login", installHint: "see github.com/MoonshotAI/kimi-cli" },
1047
+ { id: "kilo", label: "Kilo", kind: "acp", command: "kilo", args: ["acp"], loginCommand: "kilo login", installHint: "npm i -g @kilocode/cli" },
1048
+ { id: "cline", label: "Cline", kind: "acp", command: "cline", args: ["--acp"], loginCommand: "cline auth", installHint: "npm i -g cline" },
1049
+ { id: "auggie", label: "Auggie", kind: "acp", command: "auggie", args: ["--acp"], loginCommand: "auggie login", installHint: "npm i -g @augmentcode/auggie" },
1050
+ { id: "amp", label: "Amp", kind: "acp", command: "amp-acp", args: [], loginCommand: "amp login", installHint: "see ampcode.com" },
1051
+ { id: "grok", label: "Grok Build", kind: "acp", command: "grok", args: ["agent", "stdio"], loginCommand: "grok login", installHint: "npm i -g @xai-official/grok" },
1052
+ { id: "droid", label: "Factory Droid", kind: "acp", command: "droid", args: ["exec", "--output-format", "acp-daemon"], loginCommand: "droid login", installHint: "curl -fsSL https://app.factory.ai/cli | sh" },
1053
+ { id: "codex-acp", label: "Codex (ACP adapter)", kind: "acp", command: "codex-acp", args: [], loginCommand: "codex login", installHint: "npm i -g @agentclientprotocol/codex-acp" },
1054
+ { id: "claude-acp", label: "Claude (ACP adapter)", kind: "acp", command: "claude-agent-acp", args: [], loginCommand: "claude (then /login)", installHint: "npm i -g @agentclientprotocol/claude-agent-acp" }
1055
+ ];
1056
+ function presetFor(id) {
1057
+ return PROVIDER_PRESETS.find((p) => p.id === id);
1058
+ }
1059
+ function createProvider(id, entry = {}, common = {}) {
1060
+ const preset = presetFor(id);
1061
+ const kind = preset?.kind ?? "acp";
1062
+ if (kind === "claude") {
1063
+ return new ClaudeProvider({
1064
+ ...entry.path ? { path: entry.path } : {},
1065
+ ...entry.configDir ? { configDir: entry.configDir } : {},
1066
+ ...common
1067
+ });
1068
+ }
1069
+ if (kind === "codex") {
1070
+ return new CodexProvider({
1071
+ ...entry.path ? { path: entry.path } : {},
1072
+ ...entry.sandbox ? { sandbox: entry.sandbox } : {},
1073
+ ...entry.env ? { env: entry.env } : {},
1074
+ ...common.workDir ? { workDir: common.workDir } : {}
1075
+ });
1076
+ }
1077
+ const command = entry.command ?? preset?.command;
1078
+ if (!command) {
1079
+ throw new ProviderNotInstalledError(id, `Add providers.${id}.command (an ACP agent launch command) to config.json.`, "unknown provider with no command");
1080
+ }
1081
+ return new AcpProvider({
1082
+ id,
1083
+ label: entry.label ?? preset?.label ?? id,
1084
+ command,
1085
+ args: entry.args ?? preset?.args ?? [],
1086
+ ...entry.path ? { path: entry.path } : {},
1087
+ ...entry.env ? { env: entry.env } : {},
1088
+ ...entry.modelConfigId ? { modelConfigId: entry.modelConfigId } : {},
1089
+ loginCommand: entry.loginCommand ?? preset?.loginCommand ?? `${command} (sign in per its docs)`,
1090
+ installHint: preset?.installHint ?? `Install \`${command}\`.`,
1091
+ ...common
1092
+ });
1093
+ }
1094
+ function loadProviders(config = {}, common = {}) {
1095
+ const ids = /* @__PURE__ */ new Set([...PROVIDER_PRESETS.map((p) => p.id), ...Object.keys(config)]);
1096
+ const providers = /* @__PURE__ */ new Map();
1097
+ const unavailable = /* @__PURE__ */ new Map();
1098
+ for (const id of ids) {
1099
+ const entry = config[id] ?? {};
1100
+ if (entry.enabled === false) {
1101
+ unavailable.set(id, "disabled in config");
1102
+ continue;
1103
+ }
1104
+ try {
1105
+ providers.set(id, createProvider(id, entry, common));
1106
+ } catch (err) {
1107
+ unavailable.set(id, err instanceof Error ? err.message : String(err));
1108
+ }
1109
+ }
1110
+ return { providers, unavailable };
1111
+ }
1112
+ function detectProviders(config = {}) {
1113
+ const ids = /* @__PURE__ */ new Set([...PROVIDER_PRESETS.map((p) => p.id), ...Object.keys(config)]);
1114
+ return [...ids].map((id) => {
1115
+ const preset = presetFor(id);
1116
+ const entry = config[id] ?? {};
1117
+ const command = entry.command ?? preset?.command ?? id;
1118
+ const path7 = findExecutable(command, entry.path ? { explicit: entry.path } : {});
1119
+ return {
1120
+ id,
1121
+ label: entry.label ?? preset?.label ?? id,
1122
+ kind: preset?.kind ?? "acp",
1123
+ installed: path7 !== void 0 && entry.enabled !== false,
1124
+ ...path7 ? { path: path7 } : {},
1125
+ loginCommand: entry.loginCommand ?? preset?.loginCommand ?? `${command} (sign in per its docs)`,
1126
+ installHint: preset?.installHint ?? `Install \`${command}\`.`
1127
+ };
1128
+ });
1129
+ }
1130
+
1131
+ // src/core/sessionCache.ts
1132
+ import * as fs5 from "fs";
1133
+ import * as path5 from "path";
1134
+ var SessionCache = class {
1135
+ map = /* @__PURE__ */ new Map();
1136
+ maxEntries;
1137
+ persistPath;
1138
+ persistTimer = null;
1139
+ constructor(options = {}) {
1140
+ this.maxEntries = options.maxEntries ?? 1e3;
1141
+ this.persistPath = options.persistPath;
1142
+ this.load();
1143
+ }
1144
+ get size() {
1145
+ return this.map.size;
1146
+ }
1147
+ get(key) {
1148
+ const value = this.map.get(key);
1149
+ if (value !== void 0) {
1150
+ this.map.delete(key);
1151
+ this.map.set(key, value);
1152
+ }
1153
+ return value;
1154
+ }
1155
+ /** Drop a mapping, e.g. when its session turns out to be gone. */
1156
+ delete(key) {
1157
+ if (this.map.delete(key)) this.schedulePersist();
1158
+ }
1159
+ set(key, sessionId) {
1160
+ if (this.map.has(key)) this.map.delete(key);
1161
+ this.map.set(key, sessionId);
1162
+ while (this.map.size > this.maxEntries) {
1163
+ const oldest = this.map.keys().next().value;
1164
+ if (oldest === void 0) break;
1165
+ this.map.delete(oldest);
1166
+ }
1167
+ this.schedulePersist();
1168
+ }
1169
+ load() {
1170
+ if (!this.persistPath) return;
1171
+ try {
1172
+ const raw = JSON.parse(fs5.readFileSync(this.persistPath, "utf8"));
1173
+ for (const [key, value] of raw.entries ?? []) {
1174
+ if (typeof key === "string" && typeof value === "string") this.map.set(key, value);
1175
+ }
1176
+ } catch {
1177
+ }
1178
+ }
1179
+ schedulePersist() {
1180
+ if (!this.persistPath) return;
1181
+ if (this.persistTimer) clearTimeout(this.persistTimer);
1182
+ this.persistTimer = setTimeout(() => this.persistNow(), 200);
1183
+ this.persistTimer.unref?.();
1184
+ }
1185
+ persistNow() {
1186
+ if (!this.persistPath) return;
1187
+ if (this.persistTimer) {
1188
+ clearTimeout(this.persistTimer);
1189
+ this.persistTimer = null;
1190
+ }
1191
+ try {
1192
+ fs5.mkdirSync(path5.dirname(this.persistPath), { recursive: true });
1193
+ fs5.writeFileSync(
1194
+ this.persistPath,
1195
+ JSON.stringify({ version: 1, entries: [...this.map] }),
1196
+ { mode: 384 }
1197
+ );
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ };
1202
+
1203
+ // src/core/engine.ts
1204
+ import { randomUUID } from "crypto";
1205
+ import * as fs6 from "fs";
1206
+ import * as os5 from "os";
1207
+ import * as path6 from "path";
1208
+
1209
+ // src/core/sse.ts
1210
+ var SseSynthesizer = class {
1211
+ constructor(id, model) {
1212
+ this.id = id;
1213
+ this.model = model;
1214
+ }
1215
+ id;
1216
+ model;
1217
+ index = -1;
1218
+ open = null;
1219
+ startedBlocks = 0;
1220
+ start() {
1221
+ return [
1222
+ {
1223
+ event: "message_start",
1224
+ data: {
1225
+ type: "message_start",
1226
+ message: {
1227
+ id: this.id,
1228
+ type: "message",
1229
+ role: "assistant",
1230
+ model: this.model,
1231
+ content: [],
1232
+ stop_reason: null,
1233
+ stop_sequence: null,
1234
+ usage: { input_tokens: 0, output_tokens: 0 }
1235
+ }
1236
+ }
1237
+ }
1238
+ ];
1239
+ }
1240
+ thinking(text) {
1241
+ if (text.length === 0) return [];
1242
+ const out = this.ensure("thinking");
1243
+ out.push({
1244
+ event: "content_block_delta",
1245
+ data: { type: "content_block_delta", index: this.index, delta: { type: "thinking_delta", thinking: text } }
1246
+ });
1247
+ return out;
1248
+ }
1249
+ text(text) {
1250
+ if (text.length === 0) return [];
1251
+ const out = this.ensure("text");
1252
+ out.push({
1253
+ event: "content_block_delta",
1254
+ data: { type: "content_block_delta", index: this.index, delta: { type: "text_delta", text } }
1255
+ });
1256
+ return out;
1257
+ }
1258
+ finish(usage, stopReason = "end_turn") {
1259
+ const out = [];
1260
+ if (this.startedBlocks === 0) out.push(...this.ensure("text"));
1261
+ out.push(...this.closeOpen());
1262
+ out.push({
1263
+ event: "message_delta",
1264
+ data: {
1265
+ type: "message_delta",
1266
+ delta: { stop_reason: stopReason, stop_sequence: null },
1267
+ usage: { input_tokens: usage.input_tokens, output_tokens: usage.output_tokens }
1268
+ }
1269
+ });
1270
+ out.push({ event: "message_stop", data: { type: "message_stop" } });
1271
+ return out;
1272
+ }
1273
+ ensure(kind) {
1274
+ if (this.open === kind) return [];
1275
+ const out = this.closeOpen();
1276
+ this.index += 1;
1277
+ this.startedBlocks += 1;
1278
+ this.open = kind;
1279
+ out.push({
1280
+ event: "content_block_start",
1281
+ data: {
1282
+ type: "content_block_start",
1283
+ index: this.index,
1284
+ content_block: kind === "text" ? { type: "text", text: "" } : { type: "thinking", thinking: "" }
1285
+ }
1286
+ });
1287
+ return out;
1288
+ }
1289
+ closeOpen() {
1290
+ if (this.open === null) return [];
1291
+ this.open = null;
1292
+ return [{ event: "content_block_stop", data: { type: "content_block_stop", index: this.index } }];
1293
+ }
1294
+ };
1295
+
1296
+ // src/core/transcript.ts
1297
+ import { createHash } from "crypto";
1298
+ var IGNORABLE_PARAMS = [
1299
+ "max_tokens",
1300
+ "temperature",
1301
+ "top_p",
1302
+ "top_k",
1303
+ "stop_sequences",
1304
+ "metadata",
1305
+ "service_tier"
1306
+ ];
1307
+ function extractSystemText(system) {
1308
+ if (system == null) return void 0;
1309
+ if (typeof system === "string") return system.length > 0 ? system : void 0;
1310
+ if (!Array.isArray(system)) {
1311
+ throw new ApiError(400, "invalid_request_error", "`system` must be a string or an array of text blocks");
1312
+ }
1313
+ const parts = [];
1314
+ for (const block of system) {
1315
+ if (block?.type !== "text" || typeof block.text !== "string") {
1316
+ throw new ApiError(400, "invalid_request_error", "yagami only supports text blocks in `system`");
1317
+ }
1318
+ parts.push(block.text);
1319
+ }
1320
+ const joined = parts.join("\n\n");
1321
+ return joined.length > 0 ? joined : void 0;
1322
+ }
1323
+ var USER_MEDIA_TYPES = /* @__PURE__ */ new Set(["image", "document"]);
1324
+ function contentToParts(content, role) {
1325
+ if (typeof content === "string") return { text: content, media: [] };
1326
+ if (!Array.isArray(content)) {
1327
+ throw new ApiError(400, "invalid_request_error", `message content for role "${role}" must be a string or an array of blocks`);
1328
+ }
1329
+ const parts = [];
1330
+ const media = [];
1331
+ for (const block of content) {
1332
+ if (block?.type === "text" && typeof block["text"] === "string") {
1333
+ parts.push(block["text"]);
1334
+ } else if (role === "assistant" && (block?.type === "thinking" || block?.type === "redacted_thinking")) {
1335
+ continue;
1336
+ } else if (role === "user" && USER_MEDIA_TYPES.has(String(block?.type))) {
1337
+ if (block["source"] == null || typeof block["source"] !== "object") {
1338
+ throw new ApiError(
1339
+ 400,
1340
+ "invalid_request_error",
1341
+ `"${block.type}" blocks must carry a \`source\` object`
1342
+ );
1343
+ }
1344
+ media.push(block);
1345
+ } else {
1346
+ throw new ApiError(
1347
+ 400,
1348
+ "invalid_request_error",
1349
+ role === "user" ? `unsupported content block type "${String(block?.type)}" (user messages may contain text, image, and document blocks; tool_use/tool_result are not supported)` : `assistant messages may only contain "text" blocks (got "${String(block?.type)}")`
1350
+ );
1351
+ }
1352
+ }
1353
+ return { text: parts.join("\n"), media };
1354
+ }
1355
+ function normalizeRequest(req) {
1356
+ if (req == null || typeof req !== "object") {
1357
+ throw new ApiError(400, "invalid_request_error", "request body must be a JSON object");
1358
+ }
1359
+ if (req.tools != null || req.tool_choice != null) {
1360
+ throw new ApiError(
1361
+ 400,
1362
+ "invalid_request_error",
1363
+ "yagami does not support `tools`/`tool_choice`: the backing engine runs as a pure completions endpoint and never executes or emits tool calls."
1364
+ );
1365
+ }
1366
+ if (!Array.isArray(req.messages) || req.messages.length === 0) {
1367
+ throw new ApiError(400, "invalid_request_error", "`messages` must be a non-empty array");
1368
+ }
1369
+ let messages = req.messages.map((m, i) => {
1370
+ if (m?.role !== "user" && m?.role !== "assistant") {
1371
+ throw new ApiError(400, "invalid_request_error", `messages[${i}].role must be "user" or "assistant"`);
1372
+ }
1373
+ const { text, media } = contentToParts(m.content, m.role);
1374
+ return media.length > 0 ? { role: m.role, text, media } : { role: m.role, text };
1375
+ });
1376
+ let prefill;
1377
+ if (messages[messages.length - 1].role === "assistant") {
1378
+ prefill = messages[messages.length - 1].text;
1379
+ messages = messages.slice(0, -1);
1380
+ if (prefill.length === 0) {
1381
+ throw new ApiError(400, "invalid_request_error", "assistant prefill must contain non-empty text");
1382
+ }
1383
+ if (messages.length === 0 || messages[messages.length - 1].role !== "user") {
1384
+ throw new ApiError(
1385
+ 400,
1386
+ "invalid_request_error",
1387
+ "assistant prefill must directly follow a user message"
1388
+ );
1389
+ }
1390
+ }
1391
+ const last = messages[messages.length - 1];
1392
+ if (last.role !== "user") {
1393
+ throw new ApiError(400, "invalid_request_error", 'the final message must have role "user"');
1394
+ }
1395
+ const ignored = IGNORABLE_PARAMS.filter((p) => req[p] != null);
1396
+ return {
1397
+ system: extractSystemText(req.system),
1398
+ messages,
1399
+ lastUserText: last.text,
1400
+ ...prefill !== void 0 ? { prefill } : {},
1401
+ ignored: [...ignored]
1402
+ };
1403
+ }
1404
+ function prefillDirective(prefill) {
1405
+ return [
1406
+ "<assistant-prefill>",
1407
+ prefill,
1408
+ "</assistant-prefill>",
1409
+ "",
1410
+ "Your reply has already been started with the exact text inside <assistant-prefill>.",
1411
+ "Continue seamlessly from where it stops. Output ONLY the continuation \u2014 do not",
1412
+ "repeat any part of the prefill and do not acknowledge these instructions."
1413
+ ].join("\n");
1414
+ }
1415
+ var PrefillStripper = class {
1416
+ constructor(prefill) {
1417
+ this.prefill = prefill;
1418
+ }
1419
+ prefill;
1420
+ buffer = "";
1421
+ settled = false;
1422
+ /** True while text is being held back pending the repeat/no-repeat call. */
1423
+ get pending() {
1424
+ return !this.settled && this.buffer.length > 0;
1425
+ }
1426
+ /** Feed a chunk of reply text; returns the text safe to emit now. */
1427
+ push(chunk) {
1428
+ if (this.settled) return chunk;
1429
+ this.buffer += chunk;
1430
+ if (this.buffer.length <= this.prefill.length) {
1431
+ if (this.prefill.startsWith(this.buffer)) return "";
1432
+ this.settled = true;
1433
+ const out2 = this.buffer;
1434
+ this.buffer = "";
1435
+ return out2;
1436
+ }
1437
+ this.settled = true;
1438
+ const out = this.buffer.startsWith(this.prefill) ? this.buffer.slice(this.prefill.length) : this.buffer;
1439
+ this.buffer = "";
1440
+ return out;
1441
+ }
1442
+ /** Emit whatever is still held once the reply has ended. */
1443
+ flush() {
1444
+ if (this.settled) return "";
1445
+ this.settled = true;
1446
+ const out = this.buffer === this.prefill ? "" : this.buffer;
1447
+ this.buffer = "";
1448
+ return out;
1449
+ }
1450
+ };
1451
+ function prefixKey(system, messages, provider = "claude") {
1452
+ const payload = JSON.stringify([
1453
+ provider,
1454
+ system ?? "",
1455
+ messages.map(
1456
+ (m) => m.media && m.media.length > 0 ? [m.role, m.text, createHash("sha256").update(JSON.stringify(m.media)).digest("hex")] : [m.role, m.text]
1457
+ )
1458
+ ]);
1459
+ return createHash("sha256").update(payload).digest("hex");
1460
+ }
1461
+ function flattenConversation(messages) {
1462
+ const history = messages.slice(0, -1);
1463
+ const last = messages[messages.length - 1];
1464
+ const lines = history.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.text}`);
1465
+ return [
1466
+ "<conversation-history>",
1467
+ "This is the conversation so far between the user (User) and you (Assistant):",
1468
+ ...lines,
1469
+ "</conversation-history>",
1470
+ "",
1471
+ "Continue that conversation naturally. Respond to the user's latest message:",
1472
+ "",
1473
+ last.text
1474
+ ].join("\n");
1475
+ }
1476
+
1477
+ // src/core/engine.ts
1478
+ var EFFORT_LEVELS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
1479
+ var THINKING_TYPES = /* @__PURE__ */ new Set(["enabled", "disabled", "adaptive"]);
1480
+ var YagamiEngine = class {
1481
+ providers;
1482
+ unavailable;
1483
+ defaultProviderId;
1484
+ defaultModel;
1485
+ cache;
1486
+ modelsPromises = /* @__PURE__ */ new Map();
1487
+ constructor(options = {}) {
1488
+ const workDir = options.workDir ?? path6.join(os5.tmpdir(), "yagami-workspace");
1489
+ fs6.mkdirSync(workDir, { recursive: true });
1490
+ this.defaultModel = options.defaultModel;
1491
+ this.cache = options.sessionCache ?? new SessionCache();
1492
+ if (options.providers) {
1493
+ this.providers = new Map(options.providers.map((p) => [p.id, p]));
1494
+ this.unavailable = /* @__PURE__ */ new Map();
1495
+ } else {
1496
+ const config = { ...options.providerConfig };
1497
+ if (options.claudePath || options.claudeConfigDir) {
1498
+ config["claude"] = {
1499
+ ...config["claude"],
1500
+ ...options.claudePath ? { path: options.claudePath } : {},
1501
+ ...options.claudeConfigDir ? { configDir: options.claudeConfigDir } : {}
1502
+ };
1503
+ }
1504
+ const loaded = loadProviders(config, { workDir, ...options.appName ? { appName: options.appName } : {} });
1505
+ this.providers = loaded.providers;
1506
+ this.unavailable = loaded.unavailable;
1507
+ }
1508
+ const wanted = options.defaultProvider ?? (this.providers.has("claude") ? "claude" : [...this.providers.keys()][0]);
1509
+ if (!wanted || !this.providers.has(wanted)) {
1510
+ const reason = wanted ? this.unavailable.get(wanted) : "no supported coding-agent CLI was found on this machine";
1511
+ throw new ProviderNotInstalledError(wanted ?? "(none)", reason ?? "not installed");
1512
+ }
1513
+ this.defaultProviderId = wanted;
1514
+ }
1515
+ get defaultProvider() {
1516
+ return this.providers.get(this.defaultProviderId);
1517
+ }
1518
+ /** Executable of the default provider. */
1519
+ get executable() {
1520
+ return this.defaultProvider.executable;
1521
+ }
1522
+ /** @deprecated Use `executable`. */
1523
+ get claudePath() {
1524
+ return this.executable;
1525
+ }
1526
+ get providerIds() {
1527
+ return [...this.providers.keys()];
1528
+ }
1529
+ /** Route a request's model id to a provider and its native model. */
1530
+ resolve(model) {
1531
+ const ref = parseModelRef(model ?? this.defaultModel, this.providers.keys());
1532
+ if (ref.providerId && !this.providers.has(ref.providerId)) {
1533
+ throw new ApiError(503, "api_error", `provider "${ref.providerId}" is not available: ${this.unavailable.get(ref.providerId) ?? "not installed"}`);
1534
+ }
1535
+ const provider = this.providers.get(ref.providerId ?? this.defaultProviderId);
1536
+ return ref.model ? { provider, model: ref.model } : { provider };
1537
+ }
1538
+ /**
1539
+ * Models across every available provider. The default provider's ids are
1540
+ * listed bare as well as qualified; others only as `provider:model`.
1541
+ * Providers whose probe fails are skipped (their error is not cached).
1542
+ */
1543
+ async listModels() {
1544
+ const out = [];
1545
+ const entries = await Promise.all(
1546
+ [...this.providers.entries()].map(async ([id, provider]) => {
1547
+ try {
1548
+ return [id, await this.providerModels(id, provider)];
1549
+ } catch {
1550
+ return [id, []];
1551
+ }
1552
+ })
1553
+ );
1554
+ for (const [id, models] of entries) {
1555
+ for (const m of models) {
1556
+ if (id === this.defaultProviderId) out.push({ ...m, provider: id });
1557
+ out.push({ ...m, id: qualifiedModel(id, m.id), provider: id });
1558
+ }
1559
+ }
1560
+ return out;
1561
+ }
1562
+ providerModels(id, provider) {
1563
+ let promise = this.modelsPromises.get(id);
1564
+ if (!promise) {
1565
+ promise = provider.listModels().catch((err) => {
1566
+ this.modelsPromises.delete(id);
1567
+ throw err;
1568
+ });
1569
+ this.modelsPromises.set(id, promise);
1570
+ }
1571
+ return promise;
1572
+ }
1573
+ prepare(req, opts = {}) {
1574
+ const norm = normalizeRequest(req);
1575
+ const { provider, model } = this.resolve(req.model);
1576
+ const caps = provider.capabilities;
1577
+ const ignored = [...norm.ignored];
1578
+ if (req.thinking != null) {
1579
+ if (!THINKING_TYPES.has(String(req.thinking.type))) {
1580
+ throw new ApiError(400, "invalid_request_error", `invalid \`thinking.type\`: ${String(req.thinking.type)}`);
1581
+ }
1582
+ if (!caps.thinking) ignored.push("thinking");
1583
+ }
1584
+ if (req.effort != null) {
1585
+ if (typeof req.effort !== "string" || !EFFORT_LEVELS.has(req.effort)) {
1586
+ throw new ApiError(400, "invalid_request_error", `invalid \`effort\`: ${String(req.effort)}`);
1587
+ }
1588
+ if (!caps.effort) ignored.push("effort");
1589
+ }
1590
+ const last = norm.messages[norm.messages.length - 1];
1591
+ const lastMedia = last.media ?? [];
1592
+ if (lastMedia.some((b) => b.type === "image") && !caps.images) {
1593
+ throw new ApiError(400, "invalid_request_error", `provider "${provider.id}" does not accept image blocks`);
1594
+ }
1595
+ if (lastMedia.some((b) => b.type === "document") && !caps.documents) {
1596
+ throw new ApiError(400, "invalid_request_error", `provider "${provider.id}" does not accept document blocks`);
1597
+ }
1598
+ let promptText = norm.lastUserText;
1599
+ let resume;
1600
+ let resumeKey;
1601
+ if (norm.messages.length > 1) {
1602
+ const history = norm.messages.slice(0, -1);
1603
+ if (caps.resume && !opts.skipResume) {
1604
+ resumeKey = prefixKey(norm.system, history, provider.id);
1605
+ resume = this.cache.get(resumeKey);
1606
+ if (resume && !caps.fork) this.cache.delete(resumeKey);
1607
+ }
1608
+ if (!resume) {
1609
+ if (history.some((m) => m.media && m.media.length > 0)) {
1610
+ throw new ApiError(
1611
+ 400,
1612
+ "invalid_request_error",
1613
+ "conversation history contains image/document blocks and no cached session matches this prefix; yagami can only replay text history. Continue such conversations against the server that produced them."
1614
+ );
1615
+ }
1616
+ promptText = flattenConversation(norm.messages);
1617
+ }
1618
+ }
1619
+ if (norm.prefill) promptText = `${promptText}
1620
+
1621
+ ${prefillDirective(norm.prefill)}`;
1622
+ if (norm.system !== void 0 && !caps.systemPrompt) {
1623
+ promptText = `<system>
1624
+ ${norm.system}
1625
+ </system>
1626
+
1627
+ ${promptText}`;
1628
+ }
1629
+ const turn = {
1630
+ prompt: promptText,
1631
+ ...lastMedia.length > 0 ? { media: lastMedia } : {},
1632
+ ...norm.system !== void 0 && caps.systemPrompt ? { system: norm.system } : {},
1633
+ ...model ? { model } : {},
1634
+ ...resume ? { resume } : {},
1635
+ ...req.thinking != null && caps.thinking ? { thinking: req.thinking } : {},
1636
+ ...typeof req.effort === "string" && caps.effort ? { effort: req.effort } : {}
1637
+ };
1638
+ const requestedModel = model ? provider.id === this.defaultProviderId ? model : qualifiedModel(provider.id, model) : provider.id;
1639
+ return { provider, turn, norm, requestedModel, ignored, ...resume && resumeKey ? { resumeKey } : {} };
1640
+ }
1641
+ /**
1642
+ * A failed resumed attempt usually means the cached session no longer
1643
+ * exists. Drop the stale mapping and re-prepare from scratch — the
1644
+ * transcript-replay path. Undefined when falling back is impossible.
1645
+ */
1646
+ prepareResumeFallback(req, failed) {
1647
+ if (!failed.resumeKey) return void 0;
1648
+ this.cache.delete(failed.resumeKey);
1649
+ try {
1650
+ return this.prepare(req, { skipResume: true });
1651
+ } catch {
1652
+ return void 0;
1653
+ }
1654
+ }
1655
+ storeSession(prepared, continuation, sessionId) {
1656
+ const { norm, provider } = prepared;
1657
+ const fullText = (norm.prefill ?? "") + continuation;
1658
+ if (!sessionId || !fullText || !provider.capabilities.resume) return;
1659
+ const played = [...norm.messages, { role: "assistant", text: fullText }];
1660
+ this.cache.set(prefixKey(norm.system, played, provider.id), sessionId);
1661
+ }
1662
+ async complete(req) {
1663
+ const prepared = this.prepare(req);
1664
+ try {
1665
+ return await this.attemptComplete(prepared);
1666
+ } catch (err) {
1667
+ const fallback = this.prepareResumeFallback(req, prepared);
1668
+ if (!fallback) throw toApiError(err);
1669
+ try {
1670
+ return await this.attemptComplete(fallback);
1671
+ } catch (err2) {
1672
+ throw toApiError(err2);
1673
+ }
1674
+ }
1675
+ }
1676
+ async attemptComplete(prepared) {
1677
+ const { provider, turn, norm, requestedModel, ignored } = prepared;
1678
+ const stripper = norm.prefill ? new PrefillStripper(norm.prefill) : void 0;
1679
+ let sessionId;
1680
+ let text = "";
1681
+ let thinking = "";
1682
+ let done;
1683
+ for await (const ev of provider.run(turn)) {
1684
+ if (ev.type === "session") sessionId = ev.sessionId;
1685
+ else if (ev.type === "text") text += stripper ? stripper.push(ev.text) : ev.text;
1686
+ else if (ev.type === "thinking") thinking += ev.text;
1687
+ else done = ev;
1688
+ }
1689
+ if (stripper) text += stripper.flush();
1690
+ if (!done) throw new ProviderError(provider.id, "turn ended without a result");
1691
+ this.storeSession(prepared, text, sessionId);
1692
+ const content = [
1693
+ ...thinking ? [{ type: "thinking", thinking, signature: "" }] : [],
1694
+ { type: "text", text }
1695
+ ];
1696
+ const response = {
1697
+ id: `msg_${randomUUID().replace(/-/g, "")}`,
1698
+ type: "message",
1699
+ role: "assistant",
1700
+ model: done.model ?? requestedModel,
1701
+ content,
1702
+ stop_reason: done.stopReason ?? "end_turn",
1703
+ stop_sequence: null,
1704
+ usage: done.usage
1705
+ };
1706
+ return {
1707
+ response,
1708
+ ...done.costUsd !== void 0 ? { costUsd: done.costUsd } : {},
1709
+ ...sessionId ? { sessionId } : {},
1710
+ provider: provider.id,
1711
+ ignored
1712
+ };
1713
+ }
1714
+ /**
1715
+ * Validates synchronously (throws ApiError), then returns a lazy generator
1716
+ * of Anthropic-style SSE events.
1717
+ */
1718
+ stream(req, streamOptions = {}) {
1719
+ const prepared = this.prepare(req);
1720
+ return {
1721
+ ignored: prepared.ignored,
1722
+ provider: prepared.provider.id,
1723
+ events: this.runStream(req, prepared, streamOptions)
1724
+ };
1725
+ }
1726
+ async *runStream(req, prepared, streamOptions) {
1727
+ const { signal } = streamOptions;
1728
+ let emitted = false;
1729
+ try {
1730
+ for await (const ev of this.attemptStream(prepared, streamOptions)) {
1731
+ emitted = true;
1732
+ yield ev;
1733
+ }
1734
+ return;
1735
+ } catch (err) {
1736
+ if (signal?.aborted) return;
1737
+ const fallback = emitted ? void 0 : this.prepareResumeFallback(req, prepared);
1738
+ if (!fallback) {
1739
+ yield { event: "error", data: toApiError(err).toBody() };
1740
+ return;
1741
+ }
1742
+ try {
1743
+ yield* this.attemptStream(fallback, streamOptions);
1744
+ } catch (err2) {
1745
+ if (!signal?.aborted) yield { event: "error", data: toApiError(err2).toBody() };
1746
+ }
1747
+ }
1748
+ }
1749
+ async *attemptStream(prepared, streamOptions) {
1750
+ const { provider, turn, norm, requestedModel } = prepared;
1751
+ const { signal } = streamOptions;
1752
+ const stripper = norm.prefill ? new PrefillStripper(norm.prefill) : void 0;
1753
+ const sse = new SseSynthesizer(`msg_${randomUUID().replace(/-/g, "")}`, requestedModel);
1754
+ let sessionId;
1755
+ let text = "";
1756
+ let done;
1757
+ let started = false;
1758
+ const start = () => {
1759
+ if (started) return [];
1760
+ started = true;
1761
+ return sse.start();
1762
+ };
1763
+ for await (const ev of provider.run({ ...turn, ...signal ? { signal } : {} })) {
1764
+ if (ev.type === "session") {
1765
+ sessionId = ev.sessionId;
1766
+ } else if (ev.type === "text") {
1767
+ const out = stripper ? stripper.push(ev.text) : ev.text;
1768
+ text += out;
1769
+ yield* start();
1770
+ yield* sse.text(out);
1771
+ } else if (ev.type === "thinking") {
1772
+ yield* start();
1773
+ yield* sse.thinking(ev.text);
1774
+ } else {
1775
+ done = ev;
1776
+ }
1777
+ }
1778
+ if (signal?.aborted) return;
1779
+ if (!done) throw new ProviderError(provider.id, "turn ended without a result");
1780
+ yield* start();
1781
+ if (stripper) {
1782
+ const held = stripper.flush();
1783
+ text += held;
1784
+ yield* sse.text(held);
1785
+ }
1786
+ yield* sse.finish(done.usage, done.stopReason ?? "end_turn");
1787
+ this.storeSession(prepared, text, sessionId);
1788
+ streamOptions.onResult?.({
1789
+ ...done.costUsd !== void 0 ? { costUsd: done.costUsd } : {},
1790
+ ...sessionId ? { sessionId } : {}
1791
+ });
1792
+ }
1793
+ };
1794
+
1795
+ export {
1796
+ ApiError,
1797
+ YagamiError,
1798
+ ProviderNotInstalledError,
1799
+ AuthRequiredError,
1800
+ ProviderError,
1801
+ classifyProviderFailure,
1802
+ toApiError,
1803
+ parseModelRef,
1804
+ qualifiedModel,
1805
+ findExecutable,
1806
+ resolveExecutable,
1807
+ resolveClaudeExecutable,
1808
+ AsyncQueue,
1809
+ VERSION,
1810
+ AcpProvider,
1811
+ ClaudeProvider,
1812
+ CodexProvider,
1813
+ PROVIDER_PRESETS,
1814
+ presetFor,
1815
+ createProvider,
1816
+ loadProviders,
1817
+ detectProviders,
1818
+ SessionCache,
1819
+ YagamiEngine
1820
+ };
1821
+ //# sourceMappingURL=chunk-ASS6MJ7C.js.map