@justin06lee/yagami 0.6.1 → 0.8.2

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.
@@ -183,10 +183,10 @@ function resolveClaudeExecutable(explicit) {
183
183
  }
184
184
 
185
185
  // src/version.ts
186
- var VERSION = "0.5.0";
186
+ var VERSION = "0.8.2";
187
187
 
188
188
  // src/core/providers/acp.ts
189
- import { spawn, spawnSync } from "child_process";
189
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
190
190
  import * as fs2 from "fs";
191
191
  import * as os2 from "os";
192
192
  import * as path2 from "path";
@@ -198,12 +198,165 @@ import {
198
198
  PROTOCOL_VERSION
199
199
  } from "@agentclientprotocol/sdk";
200
200
 
201
+ // src/core/interaction.ts
202
+ function record(value) {
203
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
204
+ }
205
+ function optionsOf(schema) {
206
+ const oneOf = Array.isArray(schema["oneOf"]) ? schema["oneOf"] : void 0;
207
+ if (oneOf) {
208
+ const options = oneOf.flatMap((item) => {
209
+ const value = record(item);
210
+ if (!value || typeof value["const"] !== "string") return [];
211
+ return [{
212
+ value: value["const"],
213
+ label: typeof value["title"] === "string" ? value["title"] : value["const"],
214
+ ...typeof value["description"] === "string" ? { description: value["description"] } : {}
215
+ }];
216
+ });
217
+ if (options.length > 0) return options;
218
+ }
219
+ const values = Array.isArray(schema["enum"]) ? schema["enum"].filter((value) => typeof value === "string") : [];
220
+ return values.length > 0 ? values.map((value) => ({ value, label: value })) : void 0;
221
+ }
222
+ function inputFields(schema) {
223
+ const root = record(schema);
224
+ const properties = record(root?.["properties"]);
225
+ if (!properties) return [];
226
+ const required = new Set(
227
+ Array.isArray(root?.["required"]) ? root["required"].filter((value) => typeof value === "string") : []
228
+ );
229
+ return Object.entries(properties).flatMap(([id, value]) => {
230
+ const property = record(value);
231
+ if (!property || typeof property["type"] !== "string") return [];
232
+ let type;
233
+ let options = optionsOf(property);
234
+ if (property["type"] === "array") {
235
+ type = "multiselect";
236
+ options = optionsOf(record(property["items"]) ?? {});
237
+ } else if (property["type"] === "string" && options) {
238
+ type = "select";
239
+ } else if (["string", "number", "integer", "boolean"].includes(property["type"])) {
240
+ type = property["type"];
241
+ } else {
242
+ return [];
243
+ }
244
+ const defaultValue = property["default"];
245
+ return [{
246
+ id,
247
+ label: typeof property["title"] === "string" ? property["title"] : id,
248
+ type,
249
+ required: required.has(id),
250
+ ...typeof property["description"] === "string" ? { description: property["description"] } : {},
251
+ ...options ? { options } : {},
252
+ ...typeof property["format"] === "string" ? { format: property["format"] } : {},
253
+ ...typeof property["minimum"] === "number" ? { minimum: property["minimum"] } : {},
254
+ ...typeof property["maximum"] === "number" ? { maximum: property["maximum"] } : {},
255
+ ...typeof property["minLength"] === "number" ? { minLength: property["minLength"] } : {},
256
+ ...typeof property["maxLength"] === "number" ? { maxLength: property["maxLength"] } : {},
257
+ ...typeof defaultValue === "string" || typeof defaultValue === "number" || typeof defaultValue === "boolean" || Array.isArray(defaultValue) && defaultValue.every((item) => typeof item === "string") ? { default: defaultValue } : {}
258
+ }];
259
+ });
260
+ }
261
+ function elicitationRequest(provider, sessionId, raw) {
262
+ const mode = raw["mode"];
263
+ if (mode === "url") {
264
+ return {
265
+ provider,
266
+ ...sessionId ? { sessionId } : {},
267
+ kind: "url",
268
+ message: typeof raw["message"] === "string" ? raw["message"] : "Open the requested URL",
269
+ ...typeof raw["serverName"] === "string" ? { source: raw["serverName"] } : {},
270
+ ...typeof raw["url"] === "string" ? { url: raw["url"] } : {},
271
+ raw
272
+ };
273
+ }
274
+ return {
275
+ provider,
276
+ ...sessionId ? { sessionId } : {},
277
+ kind: "form",
278
+ message: typeof raw["message"] === "string" ? raw["message"] : "Input requested",
279
+ ...typeof raw["serverName"] === "string" ? { source: raw["serverName"] } : {},
280
+ fields: inputFields(raw["requestedSchema"]),
281
+ raw
282
+ };
283
+ }
284
+ function declineInput() {
285
+ return { action: "decline" };
286
+ }
287
+ function elicitationResponse(response) {
288
+ return response.action === "accept" ? { action: "accept", content: response.values ?? null } : { action: response.action };
289
+ }
290
+
291
+ // src/core/providers/process.ts
292
+ import { execFileSync, spawnSync } from "child_process";
293
+ var GRACE_MS = 3e3;
294
+ var ending = /* @__PURE__ */ new WeakSet();
295
+ function descendants(root) {
296
+ let listing;
297
+ try {
298
+ listing = execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8", timeout: 2e3 });
299
+ } catch {
300
+ return [];
301
+ }
302
+ const children = /* @__PURE__ */ new Map();
303
+ for (const line of listing.split("\n")) {
304
+ const [pid, ppid] = line.trim().split(/\s+/).map(Number);
305
+ if (!pid || ppid === void 0 || Number.isNaN(ppid)) continue;
306
+ const list = children.get(ppid);
307
+ if (list) list.push(pid);
308
+ else children.set(ppid, [pid]);
309
+ }
310
+ const found = [];
311
+ const stack = [root];
312
+ while (stack.length > 0) {
313
+ for (const child of children.get(stack.pop()) ?? []) {
314
+ found.push(child);
315
+ stack.push(child);
316
+ }
317
+ }
318
+ return found;
319
+ }
320
+ function isAlive(pid) {
321
+ try {
322
+ process.kill(pid, 0);
323
+ return true;
324
+ } catch (err) {
325
+ return err.code === "EPERM";
326
+ }
327
+ }
328
+ function signal(pid, sig) {
329
+ try {
330
+ process.kill(pid, sig);
331
+ } catch {
332
+ }
333
+ }
334
+ function killTree(child, graceMs = GRACE_MS) {
335
+ if (!child || child.pid === void 0 || ending.has(child)) return;
336
+ if (child.exitCode !== null || child.signalCode !== null) return;
337
+ ending.add(child);
338
+ const pid = child.pid;
339
+ if (process.platform === "win32") {
340
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
341
+ return;
342
+ }
343
+ const tree = [pid, ...descendants(pid)];
344
+ for (const member of tree) signal(member, "SIGTERM");
345
+ const timer = setTimeout(() => {
346
+ for (const member of tree) if (isAlive(member)) signal(member, "SIGKILL");
347
+ }, graceMs);
348
+ timer.unref?.();
349
+ }
350
+
201
351
  // src/core/providers/queue.ts
202
352
  var AsyncQueue = class {
203
353
  buffer = [];
204
354
  waiting = null;
205
355
  ended = false;
206
356
  error = void 0;
357
+ /** Called once if the consumer stops iterating before the queue ends —
358
+ * the producer's cue to stop whatever is feeding it. */
359
+ onReturn;
207
360
  push(value) {
208
361
  if (this.ended) return;
209
362
  if (this.waiting) {
@@ -251,7 +404,10 @@ var AsyncQueue = class {
251
404
  });
252
405
  },
253
406
  return: () => {
407
+ const stop = this.ended ? void 0 : this.onReturn;
408
+ this.onReturn = void 0;
254
409
  this.ended = true;
410
+ stop?.();
255
411
  return Promise.resolve({ value: void 0, done: true });
256
412
  }
257
413
  };
@@ -259,6 +415,8 @@ var AsyncQueue = class {
259
415
  };
260
416
 
261
417
  // src/core/providers/acp.ts
418
+ var HANDSHAKE_TIMEOUT_MS = 3e4;
419
+ var PROBE_TIMEOUT_MS = 2e4;
262
420
  function rejectOption(p) {
263
421
  const pick = p.options.find((o) => o.kind === "reject_once") ?? p.options.find((o) => o.kind === "reject_always") ?? p.options[0];
264
422
  if (!pick) return { outcome: { outcome: "cancelled" } };
@@ -277,14 +435,18 @@ var AcpProvider = class {
277
435
  systemPrompt: false,
278
436
  thinking: false,
279
437
  effort: false,
280
- streaming: "tokens"
438
+ streaming: "tokens",
439
+ serverTools: false
281
440
  };
441
+ sessionCapabilities = { fork: false };
282
442
  args;
283
443
  env;
284
444
  workDir;
285
445
  appName;
286
446
  modelConfigId;
287
447
  connectImpl;
448
+ handshakeTimeoutMs;
449
+ probeTimeoutMs;
288
450
  constructor(options) {
289
451
  this.id = options.id;
290
452
  this.label = options.label;
@@ -298,6 +460,8 @@ var AcpProvider = class {
298
460
  this.appName = options.appName ?? "yagami";
299
461
  this.modelConfigId = options.modelConfigId ?? "model";
300
462
  this.connectImpl = options.connect ?? ((cwd) => this.spawnConnection(cwd));
463
+ this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS;
464
+ this.probeTimeoutMs = options.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
301
465
  fs2.mkdirSync(this.workDir, { recursive: true });
302
466
  }
303
467
  spawnConnection(cwd) {
@@ -323,6 +487,9 @@ var AcpProvider = class {
323
487
  const agent = new ClientSideConnection(
324
488
  () => ({
325
489
  requestPermission: (p) => handlers.onPermission ? handlers.onPermission(p) : rejectOption(p),
490
+ unstable_createElicitation: (p) => handlers.onInput ? handlers.onInput(p) : Promise.resolve({ action: "decline" }),
491
+ unstable_completeElicitation: () => {
492
+ },
326
493
  sessionUpdate: (n) => {
327
494
  handlers.onUpdate?.(n);
328
495
  }
@@ -330,37 +497,52 @@ var AcpProvider = class {
330
497
  stream
331
498
  );
332
499
  let settled = false;
500
+ const handshake = setTimeout(() => {
501
+ if (settled) return;
502
+ settled = true;
503
+ killTree(child);
504
+ reject(new ProviderError(this.id, `${this.label} did not finish the ACP handshake within ${Math.round(this.handshakeTimeoutMs / 1e3)}s`));
505
+ }, this.handshakeTimeoutMs);
506
+ handshake.unref?.();
333
507
  child.on("error", (err) => {
334
508
  if (settled) return;
335
509
  settled = true;
510
+ clearTimeout(handshake);
336
511
  reject(classifyProviderFailure(this.id, this.loginCommand, err));
337
512
  });
338
513
  child.on("exit", (code) => {
339
514
  if (settled) return;
340
515
  settled = true;
516
+ clearTimeout(handshake);
341
517
  reject(classifyProviderFailure(this.id, this.loginCommand, new Error(`${this.executable} exited with code ${code}${stderr ? `: ${stderr.trim().slice(-400)}` : ""}`)));
342
518
  });
343
519
  agent.initialize({
344
520
  protocolVersion: PROTOCOL_VERSION,
345
521
  clientInfo: { name: this.appName, version: VERSION },
346
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
522
+ clientCapabilities: {
523
+ fs: { readTextFile: false, writeTextFile: false },
524
+ terminal: false,
525
+ session: { configOptions: { boolean: {} } },
526
+ plan: {},
527
+ elicitation: { form: {}, url: {} }
528
+ }
347
529
  }).then((init) => {
348
530
  if (settled) return;
349
531
  settled = true;
532
+ clearTimeout(handshake);
350
533
  resolve({
351
534
  agent,
352
535
  init,
353
536
  setHandlers: (h) => {
354
537
  handlers = h;
355
538
  },
356
- close: () => {
357
- child.kill("SIGTERM");
358
- }
539
+ close: () => killTree(child)
359
540
  });
360
541
  }).catch((err) => {
361
542
  if (settled) return;
362
543
  settled = true;
363
- child.kill("SIGTERM");
544
+ clearTimeout(handshake);
545
+ killTree(child);
364
546
  reject(this.classify(err, stderr));
365
547
  });
366
548
  });
@@ -381,7 +563,13 @@ var AcpProvider = class {
381
563
  const onAbort = () => {
382
564
  if (sessionId) void conn.agent.cancel({ sessionId }).catch(() => {
383
565
  });
566
+ else conn.close();
384
567
  };
568
+ if (req.signal?.aborted) {
569
+ conn.close();
570
+ return;
571
+ }
572
+ req.signal?.addEventListener("abort", onAbort, { once: true });
385
573
  try {
386
574
  let configOptions;
387
575
  let modes;
@@ -410,6 +598,7 @@ var AcpProvider = class {
410
598
  });
411
599
  }
412
600
  if (req.model) await this.selectModel(conn, sessionId, configOptions, req.model);
601
+ if (req.effort) await this.selectEffort(conn, sessionId, configOptions, req.effort);
413
602
  const sid = sessionId;
414
603
  conn.setHandlers({
415
604
  onPermission: async (p) => rejectOption(p),
@@ -426,7 +615,6 @@ var AcpProvider = class {
426
615
  }
427
616
  }
428
617
  });
429
- req.signal?.addEventListener("abort", onAbort, { once: true });
430
618
  conn.agent.prompt({ sessionId, prompt: toAcpBlocks(req.prompt, req.media ?? [], this.id) }).then((res) => {
431
619
  queue.push({
432
620
  type: "done",
@@ -455,6 +643,7 @@ var AcpProvider = class {
455
643
  connect: (cwd) => this.connectImpl(cwd),
456
644
  classify: (err, ctx) => this.classify(err, ctx),
457
645
  selectModel: (conn, sessionId, configOptions, model) => this.selectModel(conn, sessionId, configOptions, model),
646
+ selectEffort: (conn, sessionId, configOptions, effort) => this.selectEffort(conn, sessionId, configOptions, effort),
458
647
  options
459
648
  });
460
649
  }
@@ -468,22 +657,64 @@ var AcpProvider = class {
468
657
  throw this.classify(err);
469
658
  });
470
659
  }
471
- async listModels() {
660
+ async selectEffort(conn, sessionId, configOptions, effort) {
661
+ const option = configOptions?.find(
662
+ (candidate) => candidate.category === "thought_level" || /^(?:thought[_-]?level|reasoning[_-]?effort|effort)$/i.test(candidate.id)
663
+ );
664
+ if (!option || option.type !== "select" || option.currentValue === effort) return;
665
+ if (!flattenSelectOptions(option).some((candidate) => candidate.value === effort)) return;
666
+ await conn.agent.setSessionConfigOption({ sessionId, configId: option.id, value: effort }).catch((err) => {
667
+ throw this.classify(err);
668
+ });
669
+ }
670
+ /**
671
+ * Run a short question against a fresh agent and close it, whatever
672
+ * happens. The deadline covers the whole exchange: an agent that answers
673
+ * the handshake and then sits on newSession forever (Gemini, signed out
674
+ * or mid-update) used to hold the probe open — and its process alive —
675
+ * for as long as the host ran.
676
+ */
677
+ async probe(ask) {
472
678
  const conn = await this.connectImpl(this.workDir);
679
+ let timer;
473
680
  try {
681
+ return await Promise.race([
682
+ ask(conn),
683
+ new Promise((_, reject) => {
684
+ timer = setTimeout(
685
+ () => reject(new ProviderError(this.id, `${this.label} did not answer within ${Math.round(this.probeTimeoutMs / 1e3)}s`)),
686
+ this.probeTimeoutMs
687
+ );
688
+ timer.unref?.();
689
+ })
690
+ ]);
691
+ } finally {
692
+ if (timer) clearTimeout(timer);
693
+ conn.close();
694
+ }
695
+ }
696
+ async listModels() {
697
+ return this.probe(async (conn) => {
474
698
  const created = await conn.agent.newSession({ cwd: this.workDir, mcpServers: [] }).catch((err) => {
475
699
  throw this.classify(err);
476
700
  });
477
701
  const option = created.configOptions?.find((o) => o.id === this.modelConfigId) ?? created.configOptions?.find((o) => o.category === "model");
478
702
  if (!option || option.type !== "select") return [];
703
+ const effortOption = created.configOptions?.find(
704
+ (candidate) => candidate.type === "select" && (candidate.category === "thought_level" || /^(?:thought[_-]?level|reasoning[_-]?effort|effort)$/i.test(candidate.id))
705
+ );
706
+ const efforts = effortOption?.type === "select" ? flattenSelectOptions(effortOption).map((entry) => ({
707
+ id: entry.value,
708
+ ...entry.description ? { description: entry.description } : {}
709
+ })) : [];
479
710
  return flattenSelectOptions(option).map((o) => ({
480
711
  id: o.value,
481
712
  display_name: o.name,
482
- ...o.description ? { description: o.description } : {}
713
+ ...o.description ? { description: o.description } : {},
714
+ ...efforts.length > 0 ? { reasoning_efforts: efforts } : {},
715
+ ...effortOption?.type === "select" ? { default_reasoning_effort: effortOption.currentValue } : {}
483
716
  }));
484
- } finally {
485
- conn.close();
486
- }
717
+ });
487
718
  }
488
719
  /**
489
720
  * The agent's self-reported name/version from the ACP handshake. When the
@@ -506,7 +737,7 @@ var AcpProvider = class {
506
737
  }
507
738
  let plain;
508
739
  try {
509
- const out = spawnSync(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
740
+ const out = spawnSync2(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
510
741
  plain = out.stdout?.trim().split("\n")[0] || void 0;
511
742
  } catch {
512
743
  plain = void 0;
@@ -559,8 +790,10 @@ var AcpAgentSession = class {
559
790
  if (mode) await conn.agent.setSessionMode({ sessionId, modeId: mode }).catch(() => {
560
791
  });
561
792
  if (options.model) await this.cfg.selectModel(conn, sessionId, configOptions, options.model);
793
+ if (options.effort) await this.cfg.selectEffort(conn, sessionId, configOptions, options.effort);
562
794
  conn.setHandlers({
563
795
  onPermission: (p) => this.onPermission(p),
796
+ onInput: (p) => this.onInput(p),
564
797
  onUpdate: (n) => this.onUpdate(n)
565
798
  });
566
799
  }
@@ -593,6 +826,23 @@ var AcpAgentSession = class {
593
826
  }
594
827
  return rejectOption(p);
595
828
  }
829
+ async onInput(p) {
830
+ const request = elicitationRequest(
831
+ this.provider,
832
+ this.sessionId,
833
+ p
834
+ );
835
+ const handler = this.cfg.options.input;
836
+ let response = declineInput();
837
+ if (handler) {
838
+ try {
839
+ response = await handler.respond(request);
840
+ } catch {
841
+ response = { action: "cancel" };
842
+ }
843
+ }
844
+ return elicitationResponse(response);
845
+ }
596
846
  onUpdate(n) {
597
847
  if (n.sessionId !== this.sessionId) return;
598
848
  const u = n.update;
@@ -603,6 +853,21 @@ var AcpAgentSession = class {
603
853
  case "agent_thought_chunk":
604
854
  if (u.content.type === "text") this.queue?.push({ type: "thinking", text: u.content.text });
605
855
  break;
856
+ case "plan":
857
+ this.queue?.push({ type: "plan", plan: acpPlan(u) });
858
+ break;
859
+ case "plan_update":
860
+ this.queue?.push({ type: "plan", plan: acpPlan(u.plan ?? {}) });
861
+ break;
862
+ case "plan_removed":
863
+ this.queue?.push({
864
+ type: "plan",
865
+ plan: {
866
+ ...typeof u.planId === "string" ? { id: u.planId } : {},
867
+ removed: true
868
+ }
869
+ });
870
+ break;
606
871
  case "tool_call": {
607
872
  const t = u;
608
873
  const meta = { name: t.kind ?? "tool", ...t.title ? { title: t.title } : {}, ...t.kind ? { kind: t.kind } : {} };
@@ -683,6 +948,24 @@ var AcpAgentSession = class {
683
948
  this.conn = void 0;
684
949
  }
685
950
  };
951
+ function acpPlan(raw) {
952
+ const entries = Array.isArray(raw["entries"]) ? raw["entries"].flatMap((value) => {
953
+ const entry = value;
954
+ if (typeof entry["content"] !== "string") return [];
955
+ const item = {
956
+ content: entry["content"],
957
+ status: entry["status"] === "completed" ? "completed" : entry["status"] === "in_progress" ? "in_progress" : "pending",
958
+ ...["high", "medium", "low"].includes(String(entry["priority"])) ? { priority: entry["priority"] } : {}
959
+ };
960
+ return [item];
961
+ }) : void 0;
962
+ return {
963
+ ...typeof raw["planId"] === "string" ? { id: raw["planId"] } : {},
964
+ ...entries ? { entries } : {},
965
+ ...typeof raw["content"] === "string" ? { markdown: raw["content"] } : {},
966
+ ...typeof raw["uri"] === "string" ? { uri: raw["uri"] } : {}
967
+ };
968
+ }
686
969
  function supportsResume(init) {
687
970
  const caps = init.agentCapabilities;
688
971
  return caps?.sessionCapabilities?.resume !== void 0;
@@ -751,7 +1034,7 @@ function jsonLinesOnly(input, onNoise) {
751
1034
 
752
1035
  // src/core/providers/claude.ts
753
1036
  import { createRequire } from "module";
754
- import { spawnSync as spawnSync2 } from "child_process";
1037
+ import { spawnSync as spawnSync3 } from "child_process";
755
1038
  import * as fs3 from "fs";
756
1039
  import * as os3 from "os";
757
1040
  import * as path3 from "path";
@@ -763,6 +1046,15 @@ var DENY_ALL_TOOLS = async (toolName) => ({
763
1046
  message: `yagami is a completions-only endpoint; tool "${toolName}" is disabled.`,
764
1047
  interrupt: true
765
1048
  });
1049
+ var allowOnly = (enabled) => {
1050
+ const allowed = new Set(enabled);
1051
+ return async (toolName, input) => allowed.has(toolName) ? { behavior: "allow", updatedInput: input } : {
1052
+ behavior: "deny",
1053
+ message: `yagami is a completions-only endpoint; tool "${toolName}" is disabled.`,
1054
+ interrupt: true
1055
+ };
1056
+ };
1057
+ var SERVER_TOOL_MAX_TURNS = 24;
766
1058
  var ClaudeProvider = class {
767
1059
  id = "claude";
768
1060
  label = "Claude Code";
@@ -776,7 +1068,8 @@ var ClaudeProvider = class {
776
1068
  systemPrompt: true,
777
1069
  thinking: true,
778
1070
  effort: true,
779
- streaming: "tokens"
1071
+ streaming: "tokens",
1072
+ serverTools: true
780
1073
  };
781
1074
  configDir;
782
1075
  workDir;
@@ -811,6 +1104,11 @@ var ClaudeProvider = class {
811
1104
  const onAbort = () => abortController.abort();
812
1105
  req.signal?.addEventListener("abort", onAbort, { once: true });
813
1106
  const options = { ...this.baseOptions(), abortController, includePartialMessages: true };
1107
+ if (req.serverTools && req.serverTools.length > 0) {
1108
+ options.tools = [...req.serverTools];
1109
+ options.canUseTool = allowOnly(req.serverTools);
1110
+ options.maxTurns = SERVER_TOOL_MAX_TURNS;
1111
+ }
814
1112
  if (req.model) options.model = req.model;
815
1113
  if (req.system !== void 0) options.systemPrompt = req.system;
816
1114
  if (req.resume) {
@@ -889,7 +1187,11 @@ var ClaudeProvider = class {
889
1187
  id: m.value,
890
1188
  display_name: m.displayName,
891
1189
  ...m.description ? { description: m.description } : {},
892
- ...m.resolvedModel ? { resolved_model: m.resolvedModel } : {}
1190
+ ...m.resolvedModel ? { resolved_model: m.resolvedModel } : {},
1191
+ ...m.supportedEffortLevels?.length ? { reasoning_efforts: m.supportedEffortLevels.map((id) => ({ id })) } : {},
1192
+ ...m.supportsAdaptiveThinking ? { supports_adaptive_thinking: true } : {},
1193
+ ...m.supportsFastMode ? { supports_fast_mode: true } : {},
1194
+ ...m.supportsAutoMode ? { supports_auto_mode: true } : {}
893
1195
  }));
894
1196
  } catch (err) {
895
1197
  throw classifyProviderFailure(this.id, this.loginCommand, err);
@@ -901,7 +1203,7 @@ var ClaudeProvider = class {
901
1203
  }
902
1204
  async version() {
903
1205
  try {
904
- const out = spawnSync2(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
1206
+ const out = spawnSync3(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
905
1207
  return out.stdout?.trim().split("\n")[0] || void 0;
906
1208
  } catch {
907
1209
  return void 0;
@@ -975,7 +1277,7 @@ function mediaPrompt(text, media) {
975
1277
  }
976
1278
 
977
1279
  // src/core/providers/codex.ts
978
- import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
1280
+ import { spawn as spawn4, spawnSync as spawnSync4 } from "child_process";
979
1281
  import * as fs4 from "fs";
980
1282
  import * as os4 from "os";
981
1283
  import * as path4 from "path";
@@ -999,12 +1301,17 @@ var CodexAgentSession = class {
999
1301
  lastUsage;
1000
1302
  opening;
1001
1303
  closed = false;
1304
+ sending = false;
1305
+ turnAbort;
1306
+ reasoningEmitted = /* @__PURE__ */ new Map();
1307
+ incoming = /* @__PURE__ */ new Map();
1002
1308
  /** Item text already emitted as deltas, so item/completed only fills gaps. */
1003
1309
  emitted = /* @__PURE__ */ new Map();
1004
1310
  get id() {
1005
1311
  return this.threadId;
1006
1312
  }
1007
1313
  fail(err) {
1314
+ this.turnAbort?.abort();
1008
1315
  for (const [, p] of this.pending) p.reject(err);
1009
1316
  this.pending.clear();
1010
1317
  this.queue?.fail(err);
@@ -1029,6 +1336,7 @@ var CodexAgentSession = class {
1029
1336
  return promise;
1030
1337
  }
1031
1338
  respond(id, result) {
1339
+ if (this.incoming.get(id)?.signal.aborted) return;
1032
1340
  this.child?.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}
1033
1341
  `);
1034
1342
  }
@@ -1082,13 +1390,20 @@ var CodexAgentSession = class {
1082
1390
  ...native.config ? { config: native.config } : {},
1083
1391
  ...options.systemPrompt ? { developerInstructions: options.systemPrompt } : {}
1084
1392
  };
1393
+ if (options.resume && (options.fork || options.forkAt)) {
1394
+ const forked = await this.request("thread/fork", {
1395
+ threadId: options.resume,
1396
+ ...options.forkAt ? { lastTurnId: options.forkAt } : {},
1397
+ ...overrides
1398
+ });
1399
+ this.threadId = forked["thread"]?.id;
1400
+ if (!this.threadId) throw new ProviderError("codex", "thread/fork returned no thread id");
1401
+ return;
1402
+ }
1085
1403
  if (options.resume) {
1086
- try {
1087
- const resumed = await this.request("thread/resume", { threadId: options.resume, ...overrides });
1088
- this.threadId = resumed["thread"]?.id ?? options.resume;
1089
- return;
1090
- } catch {
1091
- }
1404
+ const resumed = await this.request("thread/resume", { threadId: options.resume, ...overrides });
1405
+ this.threadId = resumed["thread"]?.id ?? options.resume;
1406
+ return;
1092
1407
  }
1093
1408
  const started = await this.request("thread/start", overrides);
1094
1409
  this.threadId = started["thread"]?.id;
@@ -1106,7 +1421,17 @@ var CodexAgentSession = class {
1106
1421
  }
1107
1422
  if (!msg.method) return;
1108
1423
  if (msg.id !== void 0) {
1109
- void this.handleServerRequest(msg.method, msg.id, msg.params ?? {});
1424
+ const id = msg.id;
1425
+ const controller = new AbortController();
1426
+ const parent = this.turnAbort?.signal;
1427
+ const cancel = () => controller.abort();
1428
+ if (parent?.aborted) controller.abort();
1429
+ else parent?.addEventListener("abort", cancel, { once: true });
1430
+ this.incoming.set(id, controller);
1431
+ void this.handleServerRequest(msg.method, id, msg.params ?? {}, controller.signal).finally(() => {
1432
+ parent?.removeEventListener("abort", cancel);
1433
+ this.incoming.delete(id);
1434
+ });
1110
1435
  return;
1111
1436
  }
1112
1437
  this.handleNotification(msg.method, msg.params ?? {});
@@ -1116,7 +1441,13 @@ var CodexAgentSession = class {
1116
1441
  }
1117
1442
  handleNotification(method, params) {
1118
1443
  if (params["threadId"] !== void 0 && params["threadId"] !== this.threadId) return;
1444
+ if (this.currentTurnId && typeof params["turnId"] === "string" && params["turnId"] !== this.currentTurnId) return;
1119
1445
  switch (method) {
1446
+ case "serverRequest/resolved": {
1447
+ const requestId = params["requestId"];
1448
+ if (typeof requestId === "string" || typeof requestId === "number") this.incoming.get(requestId)?.abort();
1449
+ break;
1450
+ }
1120
1451
  case "item/agentMessage/delta": {
1121
1452
  const itemId = params["itemId"];
1122
1453
  const delta = params["delta"];
@@ -1124,6 +1455,14 @@ var CodexAgentSession = class {
1124
1455
  this.push({ type: "text", text: delta });
1125
1456
  break;
1126
1457
  }
1458
+ case "item/reasoning/summaryTextDelta": {
1459
+ const delta = params["delta"];
1460
+ if (typeof delta !== "string") break;
1461
+ const key = `${String(params["itemId"])}:${String(params["summaryIndex"] ?? 0)}`;
1462
+ this.reasoningEmitted.set(key, (this.reasoningEmitted.get(key) ?? 0) + delta.length);
1463
+ this.push({ type: "thinking", text: delta });
1464
+ break;
1465
+ }
1127
1466
  case "item/started":
1128
1467
  case "item/completed": {
1129
1468
  this.handleItem(params["item"], method === "item/completed");
@@ -1142,11 +1481,32 @@ var CodexAgentSession = class {
1142
1481
  }
1143
1482
  break;
1144
1483
  }
1484
+ case "turn/plan/updated": {
1485
+ const steps = Array.isArray(params["plan"]) ? params["plan"] : [];
1486
+ this.push({
1487
+ type: "plan",
1488
+ plan: {
1489
+ ...typeof params["explanation"] === "string" ? { explanation: params["explanation"] } : {},
1490
+ entries: steps.flatMap((value) => {
1491
+ const step = value;
1492
+ if (typeof step["step"] !== "string") return [];
1493
+ return [{
1494
+ content: step["step"],
1495
+ status: codexPlanStatus(step["status"])
1496
+ }];
1497
+ })
1498
+ }
1499
+ });
1500
+ break;
1501
+ }
1145
1502
  case "turn/completed": {
1146
1503
  const turn = params["turn"];
1504
+ const turnId = params["turn"]?.id;
1505
+ if (this.currentTurnId && turnId && turnId !== this.currentTurnId) break;
1147
1506
  const queue = this.queue;
1148
1507
  this.queue = null;
1149
1508
  this.currentTurnId = void 0;
1509
+ this.turnAbort?.abort();
1150
1510
  if (!queue) break;
1151
1511
  if (turn.status === "failed") {
1152
1512
  queue.fail(this.classify(new Error(turn.error?.message ?? "turn failed")));
@@ -1183,8 +1543,13 @@ var CodexAgentSession = class {
1183
1543
  }
1184
1544
  case "reasoning": {
1185
1545
  if (!completed) break;
1186
- const summary = item["summary"]?.join("\n") ?? "";
1187
- if (summary) this.push({ type: "thinking", text: summary });
1546
+ const summaries = item["summary"] ?? [];
1547
+ summaries.forEach((summary, index) => {
1548
+ const key = `${id}:${index}`;
1549
+ const seen = this.reasoningEmitted.get(key) ?? 0;
1550
+ if (summary.length > seen) this.push({ type: "thinking", text: summary.slice(seen) });
1551
+ this.reasoningEmitted.delete(key);
1552
+ });
1188
1553
  break;
1189
1554
  }
1190
1555
  case "commandExecution": {
@@ -1237,8 +1602,59 @@ var CodexAgentSession = class {
1237
1602
  });
1238
1603
  break;
1239
1604
  }
1240
- case "userMessage":
1605
+ case "dynamicToolCall": {
1606
+ const failed = item["status"] === "failed" || item["success"] === false;
1607
+ const namespace = typeof item["namespace"] === "string" ? `${item["namespace"]}.` : "";
1608
+ this.push({
1609
+ type: "tool_call",
1610
+ id,
1611
+ name: `${namespace}${String(item["tool"] ?? "tool")}`,
1612
+ status: completed ? failed ? "failed" : "completed" : "started",
1613
+ kind: "other",
1614
+ input: item["arguments"],
1615
+ ...completed ? { output: item["contentItems"] } : {}
1616
+ });
1617
+ break;
1618
+ }
1619
+ case "collabAgentToolCall": {
1620
+ const status = item["status"];
1621
+ const failed = status === "failed" || status === "interrupted";
1622
+ const tool = collabToolName(item["tool"]);
1623
+ this.push({
1624
+ type: "tool_call",
1625
+ id,
1626
+ name: tool,
1627
+ status: completed ? failed ? "failed" : "completed" : "started",
1628
+ title: typeof item["prompt"] === "string" && item["prompt"] ? item["prompt"] : tool,
1629
+ kind: "other",
1630
+ input: {
1631
+ prompt: item["prompt"],
1632
+ model: item["model"],
1633
+ effort: item["reasoningEffort"],
1634
+ receiverThreadIds: item["receiverThreadIds"]
1635
+ },
1636
+ ...completed ? { output: item["agentsStates"] } : {}
1637
+ });
1638
+ break;
1639
+ }
1640
+ case "imageView": {
1641
+ this.push({
1642
+ type: "tool_call",
1643
+ id,
1644
+ name: "read_file",
1645
+ status: completed ? "completed" : "started",
1646
+ title: String(item["path"] ?? "image"),
1647
+ kind: "read",
1648
+ input: { path: item["path"] }
1649
+ });
1650
+ break;
1651
+ }
1241
1652
  case "plan":
1653
+ if (completed && typeof item["text"] === "string") {
1654
+ this.push({ type: "plan", plan: { id, markdown: item["text"] } });
1655
+ }
1656
+ break;
1657
+ case "userMessage":
1242
1658
  break;
1243
1659
  default:
1244
1660
  this.push({ type: "raw", provider: "codex", payload: item });
@@ -1246,16 +1662,17 @@ var CodexAgentSession = class {
1246
1662
  }
1247
1663
  }
1248
1664
  // ── approvals: forwarded to the host, answered like the TUI would ──
1249
- async decide(request) {
1665
+ async decide(request, signal2) {
1666
+ if (signal2?.aborted) return "deny";
1250
1667
  try {
1251
- const decision = await this.config.options.permissions.decide(request);
1668
+ const decision = await this.config.options.permissions.decide(request, signal2);
1252
1669
  this.push({ type: "permission", request, decision });
1253
1670
  return decision;
1254
1671
  } catch {
1255
1672
  return "deny";
1256
1673
  }
1257
1674
  }
1258
- async handleServerRequest(method, id, params) {
1675
+ async handleServerRequest(method, id, params, signal2) {
1259
1676
  switch (method) {
1260
1677
  case "item/commandExecution/requestApproval": {
1261
1678
  const decision = await this.decide({
@@ -1266,7 +1683,7 @@ var CodexAgentSession = class {
1266
1683
  title: String(params["command"] ?? "command"),
1267
1684
  input: { command: params["command"], cwd: params["cwd"], reason: params["reason"] },
1268
1685
  raw: params
1269
- });
1686
+ }, signal2);
1270
1687
  this.respond(id, {
1271
1688
  decision: decision === "allow" ? "accept" : decision === "allow_always" ? "acceptForSession" : "decline"
1272
1689
  });
@@ -1281,7 +1698,7 @@ var CodexAgentSession = class {
1281
1698
  title: String(params["reason"] ?? "apply file changes"),
1282
1699
  input: { reason: params["reason"], grantRoot: params["grantRoot"] },
1283
1700
  raw: params
1284
- });
1701
+ }, signal2);
1285
1702
  this.respond(id, {
1286
1703
  decision: decision === "allow" ? "accept" : decision === "allow_always" ? "acceptForSession" : "decline"
1287
1704
  });
@@ -1297,7 +1714,7 @@ var CodexAgentSession = class {
1297
1714
  title: String(params["reason"] ?? "extra permissions"),
1298
1715
  input: requested,
1299
1716
  raw: params
1300
- });
1717
+ }, signal2);
1301
1718
  const granted = decision === "allow" || decision === "allow_always";
1302
1719
  this.respond(id, {
1303
1720
  permissions: granted ? { network: requested?.["network"] ?? void 0, fileSystem: requested?.["fileSystem"] ?? void 0 } : {},
@@ -1316,18 +1733,32 @@ var CodexAgentSession = class {
1316
1733
  title: String(params["command"] ?? params["reason"] ?? "approval"),
1317
1734
  input: params,
1318
1735
  raw: params
1319
- });
1736
+ }, signal2);
1320
1737
  this.respond(id, {
1321
1738
  decision: decision === "allow" ? "approved" : decision === "allow_always" ? "approved_for_session" : { denied: { rejection: "denied by the user" } }
1322
1739
  });
1323
1740
  break;
1324
1741
  }
1325
1742
  case "item/tool/requestUserInput": {
1326
- this.respond(id, { answers: {} });
1743
+ const response = await this.input(codexQuestionRequest(this.threadId, params), signal2);
1744
+ const values = response.action === "accept" ? response.values ?? {} : {};
1745
+ this.respond(id, {
1746
+ answers: Object.fromEntries(
1747
+ Object.entries(values).map(([key, value]) => [
1748
+ key,
1749
+ { answers: (Array.isArray(value) ? value : [value]).map(String) }
1750
+ ])
1751
+ )
1752
+ });
1327
1753
  break;
1328
1754
  }
1329
1755
  case "mcpServer/elicitation/request": {
1330
- this.respond(id, { action: "decline", content: null, _meta: null });
1756
+ const response = await this.input(elicitationRequest("codex", this.threadId, params), signal2);
1757
+ this.respond(id, { ...elicitationResponse(response), _meta: null });
1758
+ break;
1759
+ }
1760
+ case "currentTime/read": {
1761
+ this.respond(id, { currentTimeAt: Math.floor(Date.now() / 1e3) });
1331
1762
  break;
1332
1763
  }
1333
1764
  default: {
@@ -1339,51 +1770,133 @@ var CodexAgentSession = class {
1339
1770
  }
1340
1771
  }
1341
1772
  }
1773
+ async input(request, signal2) {
1774
+ const handler = this.config.options.input;
1775
+ if (!handler) return declineInput();
1776
+ try {
1777
+ if (signal2?.aborted) return { action: "cancel" };
1778
+ return await handler.respond(request, signal2);
1779
+ } catch {
1780
+ return { action: "cancel" };
1781
+ }
1782
+ }
1342
1783
  // ── the ProviderSession surface ────────────────────────────────────
1343
1784
  async *send(input) {
1344
1785
  if (this.closed) throw new ProviderError("codex", "session is closed");
1345
- if (this.queue) throw new ProviderError("codex", "a turn is already running");
1346
- await this.ensureOpen();
1347
- const blocks = typeof input === "string" ? [{ type: "text", text: input }] : input;
1348
- const { paths, cleanup } = writeTempImages(blocks);
1349
- const items = [];
1350
- for (const block of blocks) {
1351
- if (block.type === "text" && typeof block.text === "string") {
1352
- items.push({ type: "text", text: block.text, text_elements: [] });
1353
- }
1354
- }
1355
- for (const p of paths) items.push({ type: "localImage", path: p });
1356
- if (items.length === 0) items.push({ type: "text", text: "", text_elements: [] });
1786
+ if (this.sending) throw new ProviderError("codex", "a turn is already running");
1787
+ this.sending = true;
1788
+ const controller = new AbortController();
1789
+ this.turnAbort = controller;
1790
+ let cleanup = () => {
1791
+ };
1357
1792
  const queue = new AsyncQueue();
1358
- this.queue = queue;
1359
- this.lastUsage = void 0;
1360
1793
  try {
1794
+ await this.ensureOpen();
1795
+ if (controller.signal.aborted) {
1796
+ yield { type: "done", stopReason: "interrupted" };
1797
+ return;
1798
+ }
1799
+ const blocks = typeof input === "string" ? [{ type: "text", text: input }] : input;
1800
+ const images = writeTempImages(blocks);
1801
+ cleanup = images.cleanup;
1802
+ const items = [];
1803
+ for (const block of blocks) {
1804
+ if (block.type === "text" && typeof block.text === "string") {
1805
+ items.push({ type: "text", text: block.text, text_elements: [] });
1806
+ }
1807
+ }
1808
+ for (const p of images.paths) items.push({ type: "localImage", path: p });
1809
+ if (items.length === 0) items.push({ type: "text", text: "", text_elements: [] });
1810
+ this.queue = queue;
1811
+ this.lastUsage = void 0;
1812
+ this.emitted.clear();
1813
+ this.reasoningEmitted.clear();
1361
1814
  const result = await this.request("turn/start", {
1362
1815
  threadId: this.threadId,
1363
1816
  input: items,
1364
1817
  ...this.config.options.effort ? { effort: this.config.options.effort } : {}
1365
1818
  });
1366
- this.currentTurnId = result["turn"]?.id;
1819
+ const turnId = result["turn"]?.id;
1820
+ if (this.queue === queue) this.currentTurnId = turnId;
1821
+ if (controller.signal.aborted && this.currentTurnId) await this.interrupt();
1367
1822
  yield { type: "session", sessionId: this.threadId };
1823
+ if (turnId) yield { type: "turn", id: turnId };
1368
1824
  for await (const event of queue) yield event;
1369
1825
  } finally {
1370
1826
  cleanup();
1827
+ controller.abort();
1828
+ if (this.queue === queue) await this.interrupt();
1371
1829
  if (this.queue === queue) this.queue = null;
1830
+ this.currentTurnId = void 0;
1831
+ this.turnAbort = void 0;
1832
+ this.sending = false;
1372
1833
  }
1373
1834
  }
1374
1835
  async interrupt() {
1375
- if (!this.threadId || !this.currentTurnId) return;
1376
- await this.request("turn/interrupt", { threadId: this.threadId, turnId: this.currentTurnId }).catch(() => {
1377
- });
1836
+ const request = this.threadId && this.currentTurnId ? this.request("turn/interrupt", { threadId: this.threadId, turnId: this.currentTurnId }).catch(() => {
1837
+ }) : Promise.resolve();
1838
+ this.turnAbort?.abort();
1839
+ await request;
1378
1840
  }
1379
1841
  async close() {
1380
1842
  if (this.closed) return;
1381
1843
  this.closed = true;
1382
1844
  this.fail(new ProviderError("codex", "session closed"));
1383
- this.child?.kill("SIGTERM");
1845
+ killTree(this.child);
1384
1846
  this.child = void 0;
1385
1847
  }
1386
1848
  };
1849
+ function collabToolName(value) {
1850
+ const names = {
1851
+ spawnAgent: "spawn_agent",
1852
+ sendInput: "send_input",
1853
+ resumeAgent: "resume_agent",
1854
+ closeAgent: "close_agent",
1855
+ sendMessage: "send_message",
1856
+ followupTask: "followup_task",
1857
+ interruptAgent: "interrupt_agent",
1858
+ listAgents: "list_agents"
1859
+ };
1860
+ return names[String(value)] ?? String(value ?? "agent");
1861
+ }
1862
+ function codexPlanStatus(value) {
1863
+ return value === "completed" ? "completed" : value === "inProgress" || value === "in_progress" ? "in_progress" : "pending";
1864
+ }
1865
+ function codexQuestionRequest(sessionId, raw) {
1866
+ const questions = Array.isArray(raw["questions"]) ? raw["questions"] : [];
1867
+ const fields = questions.flatMap((value) => {
1868
+ const question = value;
1869
+ if (typeof question["id"] !== "string" || typeof question["question"] !== "string") return [];
1870
+ const choices = Array.isArray(question["options"]) ? question["options"].flatMap((option) => {
1871
+ const item = option;
1872
+ if (typeof item["label"] !== "string") return [];
1873
+ return [{
1874
+ value: item["label"],
1875
+ label: item["label"],
1876
+ ...typeof item["description"] === "string" ? { description: item["description"] } : {}
1877
+ }];
1878
+ }) : [];
1879
+ return [{
1880
+ id: question["id"],
1881
+ label: question["question"],
1882
+ ...typeof question["header"] === "string" ? { description: question["header"] } : {},
1883
+ type: choices.length > 0 ? "select" : "string",
1884
+ required: true,
1885
+ secret: question["isSecret"] === true,
1886
+ allowOther: question["isOther"] === true,
1887
+ ...choices.length > 0 ? { options: choices } : {}
1888
+ }];
1889
+ });
1890
+ return {
1891
+ provider: "codex",
1892
+ ...sessionId ? { sessionId } : {},
1893
+ kind: "questions",
1894
+ message: fields.length === 1 ? fields[0].label : "Input requested",
1895
+ fields,
1896
+ blocking: raw["isBlocking"] !== false,
1897
+ raw
1898
+ };
1899
+ }
1387
1900
 
1388
1901
  // src/core/providers/jsonl.ts
1389
1902
  import { spawn as spawn3 } from "child_process";
@@ -1420,12 +1933,17 @@ function spawnJsonl(options) {
1420
1933
  }
1421
1934
  });
1422
1935
  const onAbort = () => {
1423
- child.kill("SIGTERM");
1936
+ killTree(child);
1424
1937
  queue.end();
1425
1938
  };
1426
1939
  options.signal?.addEventListener("abort", onAbort, { once: true });
1940
+ queue.onReturn = () => {
1941
+ options.signal?.removeEventListener("abort", onAbort);
1942
+ killTree(child);
1943
+ };
1427
1944
  child.on("error", (err) => queue.fail(err));
1428
1945
  child.on("close", (code) => {
1946
+ queue.onReturn = void 0;
1429
1947
  options.signal?.removeEventListener("abort", onAbort);
1430
1948
  if (options.signal?.aborted) return queue.end();
1431
1949
  if (code !== 0) queue.fail(new ProcessExitError(code, stderr));
@@ -1452,8 +1970,10 @@ var CodexProvider = class {
1452
1970
  systemPrompt: false,
1453
1971
  thinking: false,
1454
1972
  effort: true,
1455
- streaming: "chunks"
1973
+ streaming: "chunks",
1974
+ serverTools: false
1456
1975
  };
1976
+ sessionCapabilities = { fork: true };
1457
1977
  workDir;
1458
1978
  sandbox;
1459
1979
  env;
@@ -1549,7 +2069,7 @@ var CodexProvider = class {
1549
2069
  if (settled) return;
1550
2070
  settled = true;
1551
2071
  clearTimeout(timer);
1552
- child.kill("SIGTERM");
2072
+ killTree(child);
1553
2073
  fn();
1554
2074
  };
1555
2075
  const timer = setTimeout(() => finish(() => reject(new ProviderError(this.id, "timed out listing models via app-server"))), 15e3);
@@ -1578,11 +2098,34 @@ var CodexProvider = class {
1578
2098
  finish(() => reject(classifyProviderFailure(this.id, this.loginCommand, new Error(msg.error?.message ?? "model/list failed"))));
1579
2099
  return;
1580
2100
  }
1581
- const models = (msg.result?.data ?? []).filter((m) => m["hidden"] !== true).map((m) => ({
1582
- id: String(m["id"] ?? m["model"]),
1583
- display_name: String(m["displayName"] ?? m["id"] ?? m["model"]),
1584
- ...typeof m["description"] === "string" ? { description: m["description"] } : {}
1585
- }));
2101
+ const models = (msg.result?.data ?? []).filter((m) => m["hidden"] !== true).map((m) => {
2102
+ const efforts = Array.isArray(m["supportedReasoningEfforts"]) ? m["supportedReasoningEfforts"].flatMap(
2103
+ (entry) => typeof entry["reasoningEffort"] === "string" ? [{
2104
+ id: entry["reasoningEffort"],
2105
+ ...typeof entry["description"] === "string" ? { description: entry["description"] } : {}
2106
+ }] : []
2107
+ ) : [];
2108
+ const tiers = Array.isArray(m["serviceTiers"]) ? m["serviceTiers"].flatMap(
2109
+ (entry) => typeof entry["id"] === "string" ? [{
2110
+ id: entry["id"],
2111
+ display_name: typeof entry["name"] === "string" ? entry["name"] : entry["id"],
2112
+ ...typeof entry["description"] === "string" ? { description: entry["description"] } : {}
2113
+ }] : []
2114
+ ) : [];
2115
+ return {
2116
+ id: String(m["id"] ?? m["model"]),
2117
+ display_name: String(m["displayName"] ?? m["id"] ?? m["model"]),
2118
+ ...typeof m["description"] === "string" ? { description: m["description"] } : {},
2119
+ ...efforts.length > 0 ? { reasoning_efforts: efforts } : {},
2120
+ ...typeof m["defaultReasoningEffort"] === "string" ? { default_reasoning_effort: m["defaultReasoningEffort"] } : {},
2121
+ ...Array.isArray(m["inputModalities"]) ? { input_modalities: m["inputModalities"].filter((value) => typeof value === "string") } : {},
2122
+ ...m["supportsPersonality"] === true ? { supports_personality: true } : {},
2123
+ ...typeof m["multiAgentVersion"] === "string" ? { multi_agent: m["multiAgentVersion"] } : {},
2124
+ ...tiers.length > 0 ? { service_tiers: tiers } : {},
2125
+ ...typeof m["defaultServiceTier"] === "string" ? { default_service_tier: m["defaultServiceTier"] } : {},
2126
+ ...m["isDefault"] === true ? { is_default: true } : {}
2127
+ };
2128
+ });
1586
2129
  finish(() => resolve(models));
1587
2130
  }
1588
2131
  });
@@ -1599,7 +2142,7 @@ var CodexProvider = class {
1599
2142
  }
1600
2143
  async version() {
1601
2144
  try {
1602
- const out = spawnSync3(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
2145
+ const out = spawnSync4(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
1603
2146
  return out.stdout?.trim().split("\n")[0] || void 0;
1604
2147
  } catch {
1605
2148
  return void 0;
@@ -1901,6 +2444,61 @@ var SseSynthesizer = class {
1901
2444
 
1902
2445
  // src/core/transcript.ts
1903
2446
  import { createHash } from "crypto";
2447
+
2448
+ // src/core/serverTools.ts
2449
+ var SERVER_TOOLS = {
2450
+ web_search: "WebSearch",
2451
+ web_fetch: "WebFetch"
2452
+ };
2453
+ function serverToolFor(type) {
2454
+ if (typeof type !== "string") return void 0;
2455
+ for (const [prefix, tool] of Object.entries(SERVER_TOOLS)) {
2456
+ if (type === prefix || type.startsWith(`${prefix}_`)) return tool;
2457
+ }
2458
+ return void 0;
2459
+ }
2460
+ function resolveServerTools(tools, toolChoice) {
2461
+ if (tools == null) {
2462
+ if (toolChoice != null) {
2463
+ throw new ApiError(
2464
+ 400,
2465
+ "invalid_request_error",
2466
+ "`tool_choice` was given without `tools`."
2467
+ );
2468
+ }
2469
+ return void 0;
2470
+ }
2471
+ if (!Array.isArray(tools)) {
2472
+ throw new ApiError(400, "invalid_request_error", "`tools` must be an array");
2473
+ }
2474
+ if (tools.length === 0) return void 0;
2475
+ const resolved = /* @__PURE__ */ new Set();
2476
+ for (const tool of tools) {
2477
+ const entry = tool;
2478
+ const mapped = serverToolFor(entry?.type);
2479
+ if (!mapped) {
2480
+ const label = typeof entry?.name === "string" ? `"${entry.name}"` : `type "${String(entry?.type)}"`;
2481
+ throw new ApiError(
2482
+ 400,
2483
+ "invalid_request_error",
2484
+ `yagami supports Anthropic server tools (${Object.keys(SERVER_TOOLS).join(", ")}) but not custom tools: ${label} would have to be executed by you, and this endpoint never emits tool_use blocks. Drop it, or ask for the result as text/JSON instead.`
2485
+ );
2486
+ }
2487
+ resolved.add(mapped);
2488
+ }
2489
+ const choice = toolChoice?.type;
2490
+ if (choice != null && choice !== "auto" && choice !== "none") {
2491
+ throw new ApiError(
2492
+ 400,
2493
+ "invalid_request_error",
2494
+ `\`tool_choice.type\` must be "auto" or "none" with server tools (got "${String(choice)}"): server tools run on the engine's side and cannot be forced.`
2495
+ );
2496
+ }
2497
+ if (choice === "none") return void 0;
2498
+ return [...resolved];
2499
+ }
2500
+
2501
+ // src/core/transcript.ts
1904
2502
  var IGNORABLE_PARAMS = [
1905
2503
  "max_tokens",
1906
2504
  "temperature",
@@ -1962,13 +2560,7 @@ function normalizeRequest(req) {
1962
2560
  if (req == null || typeof req !== "object") {
1963
2561
  throw new ApiError(400, "invalid_request_error", "request body must be a JSON object");
1964
2562
  }
1965
- if (req.tools != null || req.tool_choice != null) {
1966
- throw new ApiError(
1967
- 400,
1968
- "invalid_request_error",
1969
- "yagami does not support `tools`/`tool_choice`: the backing engine runs as a pure completions endpoint and never executes or emits tool calls."
1970
- );
1971
- }
2563
+ const serverTools = resolveServerTools(req.tools, req.tool_choice);
1972
2564
  if (!Array.isArray(req.messages) || req.messages.length === 0) {
1973
2565
  throw new ApiError(400, "invalid_request_error", "`messages` must be a non-empty array");
1974
2566
  }
@@ -2004,7 +2596,8 @@ function normalizeRequest(req) {
2004
2596
  messages,
2005
2597
  lastUserText: last.text,
2006
2598
  ...prefill !== void 0 ? { prefill } : {},
2007
- ignored: [...ignored]
2599
+ ignored: [...ignored],
2600
+ ...serverTools ? { serverTools } : {}
2008
2601
  };
2009
2602
  }
2010
2603
  function prefillDirective(prefill) {
@@ -2232,6 +2825,13 @@ ${norm.system}
2232
2825
 
2233
2826
  ${promptText}`;
2234
2827
  }
2828
+ if (norm.serverTools && !caps.serverTools) {
2829
+ throw new ApiError(
2830
+ 400,
2831
+ "invalid_request_error",
2832
+ `provider "${provider.id}" cannot run server tools (${norm.serverTools.join(", ")}). Answering without them would silently drop the lookup you asked for, so the request is refused instead. Use the claude provider, or drop \`tools\`.`
2833
+ );
2834
+ }
2235
2835
  const turn = {
2236
2836
  prompt: promptText,
2237
2837
  ...lastMedia.length > 0 ? { media: lastMedia } : {},
@@ -2239,7 +2839,8 @@ ${promptText}`;
2239
2839
  ...model ? { model } : {},
2240
2840
  ...resume ? { resume } : {},
2241
2841
  ...req.thinking != null && caps.thinking ? { thinking: req.thinking } : {},
2242
- ...typeof req.effort === "string" && caps.effort ? { effort: req.effort } : {}
2842
+ ...typeof req.effort === "string" && caps.effort ? { effort: req.effort } : {},
2843
+ ...norm.serverTools ? { serverTools: norm.serverTools } : {}
2243
2844
  };
2244
2845
  const requestedModel = model ? provider.id === this.defaultProviderId ? model : qualifiedModel(provider.id, model) : provider.id;
2245
2846
  return { provider, turn, norm, requestedModel, ignored, ...resume && resumeKey ? { resumeKey } : {} };
@@ -2330,7 +2931,7 @@ ${promptText}`;
2330
2931
  };
2331
2932
  }
2332
2933
  async *runStream(req, prepared, streamOptions) {
2333
- const { signal } = streamOptions;
2934
+ const { signal: signal2 } = streamOptions;
2334
2935
  let emitted = false;
2335
2936
  try {
2336
2937
  for await (const ev of this.attemptStream(prepared, streamOptions)) {
@@ -2339,7 +2940,7 @@ ${promptText}`;
2339
2940
  }
2340
2941
  return;
2341
2942
  } catch (err) {
2342
- if (signal?.aborted) return;
2943
+ if (signal2?.aborted) return;
2343
2944
  const fallback = emitted ? void 0 : this.prepareResumeFallback(req, prepared);
2344
2945
  if (!fallback) {
2345
2946
  yield { event: "error", data: toApiError(err).toBody() };
@@ -2348,13 +2949,13 @@ ${promptText}`;
2348
2949
  try {
2349
2950
  yield* this.attemptStream(fallback, streamOptions);
2350
2951
  } catch (err2) {
2351
- if (!signal?.aborted) yield { event: "error", data: toApiError(err2).toBody() };
2952
+ if (!signal2?.aborted) yield { event: "error", data: toApiError(err2).toBody() };
2352
2953
  }
2353
2954
  }
2354
2955
  }
2355
2956
  async *attemptStream(prepared, streamOptions) {
2356
2957
  const { provider, turn, norm, requestedModel } = prepared;
2357
- const { signal } = streamOptions;
2958
+ const { signal: signal2 } = streamOptions;
2358
2959
  const stripper = norm.prefill ? new PrefillStripper(norm.prefill) : void 0;
2359
2960
  const sse = new SseSynthesizer(`msg_${randomUUID().replace(/-/g, "")}`, requestedModel);
2360
2961
  let sessionId;
@@ -2366,7 +2967,7 @@ ${promptText}`;
2366
2967
  started = true;
2367
2968
  return sse.start();
2368
2969
  };
2369
- for await (const ev of provider.run({ ...turn, ...signal ? { signal } : {} })) {
2970
+ for await (const ev of provider.run({ ...turn, ...signal2 ? { signal: signal2 } : {} })) {
2370
2971
  if (ev.type === "session") {
2371
2972
  sessionId = ev.sessionId;
2372
2973
  } else if (ev.type === "text") {
@@ -2381,7 +2982,7 @@ ${promptText}`;
2381
2982
  done = ev;
2382
2983
  }
2383
2984
  }
2384
- if (signal?.aborted) return;
2985
+ if (signal2?.aborted) return;
2385
2986
  if (!done) throw new ProviderError(provider.id, "turn ended without a result");
2386
2987
  yield* start();
2387
2988
  if (stripper) {
@@ -2499,7 +3100,7 @@ function chatToMessagesRequest(body) {
2499
3100
  throw new ApiError(
2500
3101
  400,
2501
3102
  "invalid_request_error",
2502
- "yagami does not support `tools`/function calling: the backing engine runs as a pure completions endpoint and never executes or emits tool calls."
3103
+ "yagami does not support OpenAI function calling: the backing engine never emits tool calls for you to execute. Anthropic **server** tools (web_search, web_fetch) do run \u2014 request them through the Anthropic dialect at /v1/messages."
2503
3104
  );
2504
3105
  }
2505
3106
  if (body.n != null && body.n !== 1) {
@@ -2692,4 +3293,4 @@ export {
2692
3293
  ChatChunkTranslator,
2693
3294
  modelListBody
2694
3295
  };
2695
- //# sourceMappingURL=chunk-ZYHC7PXX.js.map
3296
+ //# sourceMappingURL=chunk-EKN223KD.js.map