@oxbit/cli 0.11.0 → 0.11.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.
@@ -7,33 +7,6 @@ import { randomBytes, createHash, randomUUID } from "node:crypto";
7
7
  import { homedir } from "node:os";
8
8
  import { spawn } from "node:child_process";
9
9
  import { createServer } from "node:http";
10
- const count = (...values) => values.find((value2) => Number.isSafeInteger(value2) && value2 > 0);
11
- async function readText$1(models2, request, signal) {
12
- let text = "";
13
- for await (const chunk of models2.stream(request, signal)) text += chunk;
14
- return text;
15
- }
16
- async function listModels(models2, endpoint, signal) {
17
- const body = JSON.parse(await readText$1(models2, { ...endpoint, method: "GET", path: "/v1/models" }, signal));
18
- if (!Array.isArray(body?.data)) throw new Error("The model list has no data array");
19
- return body.data.filter((item) => typeof item?.id === "string" && item.id).slice(0, 1e3).map((item) => ({
20
- id: item.id,
21
- name: typeof item.display_name === "string" ? item.display_name : typeof item.name === "string" ? item.name : item.id,
22
- contextWindow: count(item.max_input_tokens, item.context_length),
23
- maxOutput: count(item.max_tokens, item.top_provider?.max_completion_tokens)
24
- }));
25
- }
26
- function modelError(error) {
27
- const { status: status2, body } = error ?? {};
28
- if (typeof status2 !== "number") return error instanceof Error ? error : new Error(String(error));
29
- let detail = typeof body === "string" ? body.trim().slice(0, 1e3) : "";
30
- try {
31
- const parsed = JSON.parse(detail);
32
- detail = parsed?.error?.message ?? parsed?.message ?? detail;
33
- } catch {
34
- }
35
- return new Error(`Model API error ${status2}${detail ? `: ${detail}` : ""}`);
36
- }
37
10
  async function* lines(chunks) {
38
11
  let buffer = "";
39
12
  for await (const chunk of chunks) {
@@ -64,10 +37,10 @@ async function* parseSSE(chunks) {
64
37
  }
65
38
  if (line.startsWith(":")) continue;
66
39
  const colon = line.indexOf(":");
67
- const field = colon < 0 ? line : line.slice(0, colon);
40
+ const field2 = colon < 0 ? line : line.slice(0, colon);
68
41
  const value2 = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
69
- if (field === "event") event = value2;
70
- else if (field === "data") data.push(value2);
42
+ if (field2 === "event") event = value2;
43
+ else if (field2 === "data") data.push(value2);
71
44
  }
72
45
  }
73
46
  async function* jsonEvents(chunks) {
@@ -76,6 +49,210 @@ async function* jsonEvents(chunks) {
76
49
  if (value2 && typeof value2 === "object") yield { ...event && !value2.type ? { type: event } : {}, ...value2 };
77
50
  }
78
51
  }
52
+ function replayable(items) {
53
+ const kept = items.filter((item) => item.type !== "reasoning" || item.encrypted_content);
54
+ return kept.length === items.length ? items : kept.map((item) => item.type === "reasoning" ? item : Object.fromEntries(Object.entries(item).filter(([key]) => key !== "id")));
55
+ }
56
+ function responsesBody({ model, system, turns, tools, effort }, replay2) {
57
+ const input2 = [];
58
+ for (const turn of turns) {
59
+ if (turn.role === "user") input2.push({ type: "message", role: "user", content: [{ type: "input_text", text: turn.text }] });
60
+ else if (turn.role === "tool")
61
+ for (const result of turn.results) input2.push({ type: "function_call_output", call_id: result.id, output: result.output });
62
+ else if (turn.native && replay2.includes(turn.native.api)) input2.push(...replayable(turn.native.items));
63
+ else {
64
+ if (turn.text) input2.push({ type: "message", role: "assistant", content: turn.text });
65
+ for (const call of turn.calls) input2.push({ type: "function_call", call_id: call.id, name: call.name, arguments: call.arguments });
66
+ }
67
+ }
68
+ return {
69
+ model,
70
+ instructions: system,
71
+ input: input2,
72
+ stream: true,
73
+ store: false,
74
+ ...tools.length ? { tools: tools.map((tool) => ({ type: "function", ...tool, strict: false })) } : {},
75
+ ...effort ? { reasoning: { effort, summary: "auto" } } : {}
76
+ };
77
+ }
78
+ const openaiResponses = {
79
+ request: (input2) => ({ method: "POST", path: "/v1/responses", body: responsesBody(input2, ["openai"]) }),
80
+ async *events(chunks) {
81
+ const items = [];
82
+ for await (const event of jsonEvents(chunks)) {
83
+ switch (event.type) {
84
+ case "response.output_text.delta":
85
+ if (event.delta) yield { type: "text", text: event.delta };
86
+ break;
87
+ case "response.reasoning_summary_text.delta":
88
+ case "response.reasoning_text.delta":
89
+ if (event.delta) yield { type: "reasoning", text: event.delta };
90
+ break;
91
+ case "response.reasoning_summary_part.added":
92
+ if (event.summary_index > 0) yield { type: "reasoning", text: "\n\n" };
93
+ break;
94
+ case "response.output_item.done":
95
+ items[event.output_index ?? items.length] = event.item;
96
+ if (event.item?.type === "function_call")
97
+ yield { type: "tool_call", call: { id: event.item.call_id, name: event.item.name, arguments: event.item.arguments ?? "" } };
98
+ break;
99
+ case "response.completed":
100
+ case "response.incomplete": {
101
+ const { usage, incomplete_details: incomplete } = event.response ?? {};
102
+ if (usage) yield { type: "usage", input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 };
103
+ const native = items.filter(Boolean);
104
+ const reason = native.some((item) => item.type === "function_call") ? "tool_use" : event.type === "response.completed" ? "end_turn" : incomplete?.reason === "content_filter" ? "refusal" : "max_tokens";
105
+ yield { type: "stop", reason, native };
106
+ return;
107
+ }
108
+ case "response.failed":
109
+ throw new Error(event.response?.error?.message ?? "The model response failed");
110
+ case "error":
111
+ throw new Error(event.message ?? "The model stream failed");
112
+ }
113
+ }
114
+ }
115
+ };
116
+ const CHATGPT_CLIENT_VERSION = "0.162.1";
117
+ const CHATGPT_MODELS_PATH = `/codex/models?client_version=${CHATGPT_CLIENT_VERSION}`;
118
+ const XHIGH = ["low", "medium", "high", "xhigh"];
119
+ const MAX = { contextWindow: 272e3, efforts: [...XHIGH, "max"] };
120
+ const CHATGPT_MODELS = [
121
+ { id: "gpt-6.1-sol", name: "GPT-6.1-Sol", ...MAX },
122
+ { id: "gpt-6-astra", name: "GPT-6-Astra", ...MAX },
123
+ { id: "gpt-6-sol", name: "GPT-6-Sol", ...MAX },
124
+ { id: "gpt-6-luna", name: "GPT-6-Luna", ...MAX },
125
+ { id: "gpt-5.6-sol", name: "GPT-5.6-Sol", ...MAX },
126
+ { id: "gpt-5.6-terra", name: "GPT-5.6-Terra", ...MAX },
127
+ { id: "gpt-5.6-luna", name: "GPT-5.6-Luna", ...MAX },
128
+ { id: "gpt-5.5", name: "GPT-5.5", contextWindow: 272e3, efforts: XHIGH }
129
+ ];
130
+ const chatgptResponses = {
131
+ request: (input2) => ({
132
+ method: "POST",
133
+ path: "/codex/responses",
134
+ body: {
135
+ ...responsesBody({ ...input2, system: input2.system || "You are a helpful assistant." }, ["openai", "chatgpt"]),
136
+ include: ["reasoning.encrypted_content"],
137
+ tool_choice: "auto"
138
+ }
139
+ }),
140
+ events: openaiResponses.events
141
+ };
142
+ const ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
143
+ const LABELS = { none: "None", minimal: "Minimal", low: "Low", medium: "Medium", high: "High", xhigh: "Extra high", max: "Max" };
144
+ const UNKNOWN_EFFORTS = ["low", "medium", "high"];
145
+ const FIVE = ["low", "medium", "high", "xhigh", "max"];
146
+ const NONE_TO_MAX = ["none", ...FIVE];
147
+ const NONE_TO_XHIGH = ["none", "low", "medium", "high", "xhigh"];
148
+ const OPENAI = [
149
+ ["gpt-6-astra", FIVE],
150
+ ["gpt-6.1-sol", FIVE],
151
+ ["gpt-6-sol", NONE_TO_MAX],
152
+ ["gpt-6-luna", NONE_TO_MAX],
153
+ ["gpt-5.6", FIVE],
154
+ ["gpt-5.5", NONE_TO_XHIGH],
155
+ ["gpt-5.4", NONE_TO_XHIGH],
156
+ ["gpt-5.2", NONE_TO_XHIGH],
157
+ ["gpt-5.1", ["none", "low", "medium", "high"]],
158
+ ["gpt-5", ["minimal", "low", "medium", "high"]],
159
+ ["gpt-4", []],
160
+ ["gpt-4o", []],
161
+ ["gpt-4.1", []],
162
+ ["gpt-4.5", []],
163
+ ["gpt-3.5", []],
164
+ ["chatgpt-4o", []]
165
+ ];
166
+ const ANTHROPIC = [
167
+ ["claude-fable-5-1", FIVE],
168
+ ["claude-mythos-5-1", FIVE],
169
+ ["claude-fable-5", FIVE],
170
+ ["claude-mythos-5", FIVE],
171
+ ["claude-opus-5-5", FIVE],
172
+ ["claude-sonnet-5-5", FIVE],
173
+ ["claude-haiku-5-5", FIVE],
174
+ ["claude-opus-5", FIVE],
175
+ ["claude-sonnet-5", FIVE],
176
+ ["claude-opus-4-8", FIVE],
177
+ ["claude-opus-4-7", FIVE],
178
+ ["claude-mythos-preview", ["low", "medium", "high", "max"]],
179
+ ["claude-opus-4-6", ["low", "medium", "high", "max"]],
180
+ ["claude-sonnet-4-6", ["low", "medium", "high", "max"]],
181
+ ["claude-opus-4-5", ["low", "medium", "high"]]
182
+ ];
183
+ const CODEX_CLIENT = /* @__PURE__ */ new Set(["ultra", "persistent"]);
184
+ const family = (table, id2) => table.find(([prefix]) => id2 === prefix || id2.startsWith(`${prefix}-`))?.[1];
185
+ const rank = (level) => ORDER.indexOf(level) + 1 || ORDER.length + 1;
186
+ const sorted = (levels) => [...new Set(levels)].sort((a, b) => rank(a) - rank(b));
187
+ const effortLabel = (level) => LABELS[level] ?? level.charAt(0).toUpperCase() + level.slice(1);
188
+ function modelEfforts(api, model) {
189
+ if (model.efforts) return [...model.efforts];
190
+ const id2 = model.id.toLowerCase();
191
+ if (api === "anthropic") return [...family(ANTHROPIC, id2) ?? []];
192
+ if (api === "openai" || api === "chatgpt") return [...family(OPENAI, id2) ?? UNKNOWN_EFFORTS];
193
+ return [...UNKNOWN_EFFORTS];
194
+ }
195
+ function listedEfforts(item) {
196
+ const effort = item?.capabilities?.effort;
197
+ if (effort && typeof effort === "object")
198
+ return effort.supported === false ? [] : FIVE.filter((level) => effort[level]?.supported === true);
199
+ if (Array.isArray(item?.supported_reasoning_levels))
200
+ return sorted(item.supported_reasoning_levels.map((preset) => preset?.effort).filter((level) => typeof level === "string" && !!level && !CODEX_CLIENT.has(level)));
201
+ const reasoning = item?.reasoning;
202
+ if (reasoning && typeof reasoning === "object") {
203
+ const levels = Array.isArray(reasoning.supported_efforts) ? reasoning.supported_efforts.filter((level) => typeof level === "string" && !!level) : ORDER;
204
+ return sorted(reasoning.mandatory ? levels.filter((level) => level !== "none") : levels);
205
+ }
206
+ if (Array.isArray(item?.supported_parameters))
207
+ return item.supported_parameters.some((name) => name === "reasoning" || name === "reasoning_effort") ? void 0 : [];
208
+ return void 0;
209
+ }
210
+ const count = (...values) => values.find((value2) => Number.isSafeInteger(value2) && value2 > 0);
211
+ async function readText$1(models2, request, signal) {
212
+ let text = "";
213
+ for await (const chunk of models2.stream(request, signal)) text += chunk;
214
+ return text;
215
+ }
216
+ const withEfforts = (info, item) => {
217
+ const efforts = listedEfforts(item);
218
+ return efforts ? { ...info, efforts } : info;
219
+ };
220
+ async function listModels(models2, endpoint, signal) {
221
+ if (endpoint.api === "chatgpt") return listChatGPTModels(models2, signal);
222
+ const body = JSON.parse(await readText$1(models2, { ...endpoint, method: "GET", path: "/v1/models" }, signal));
223
+ if (!Array.isArray(body?.data)) throw new Error("The model list has no data array");
224
+ return body.data.filter((item) => typeof item?.id === "string" && item.id).slice(0, 1e3).map((item) => withEfforts({
225
+ id: item.id,
226
+ name: typeof item.display_name === "string" ? item.display_name : typeof item.name === "string" ? item.name : item.id,
227
+ contextWindow: count(item.max_input_tokens, item.context_length),
228
+ maxOutput: count(item.max_tokens, item.top_provider?.max_completion_tokens)
229
+ }, item));
230
+ }
231
+ async function listChatGPTModels(models2, signal) {
232
+ try {
233
+ const body = JSON.parse(await readText$1(models2, { api: "chatgpt", method: "GET", path: CHATGPT_MODELS_PATH }, signal));
234
+ const listed = (Array.isArray(body?.models) ? body.models : []).filter((item) => typeof item?.slug === "string" && item.slug && item.visibility === "list").sort((a, b) => (Number(a.priority) || 0) - (Number(b.priority) || 0)).map((item) => withEfforts({
235
+ id: item.slug,
236
+ name: typeof item.display_name === "string" && item.display_name ? item.display_name : item.slug,
237
+ contextWindow: count(item.context_window)
238
+ }, item));
239
+ if (listed.length) return listed;
240
+ } catch (error) {
241
+ if (signal?.aborted) throw error;
242
+ }
243
+ return CHATGPT_MODELS.map((model) => ({ ...model, ...model.efforts ? { efforts: [...model.efforts] } : {} }));
244
+ }
245
+ function modelError(error) {
246
+ const { status: status2, body } = error ?? {};
247
+ if (typeof status2 !== "number") return error instanceof Error ? error : new Error(String(error));
248
+ let detail = typeof body === "string" ? body.trim().slice(0, 1e3) : "";
249
+ try {
250
+ const parsed = JSON.parse(detail);
251
+ detail = parsed?.error?.message ?? parsed?.message ?? detail;
252
+ } catch {
253
+ }
254
+ return new Error(`Model API error ${status2}${detail ? `: ${detail}` : ""}`);
255
+ }
79
256
  const OUTPUT_LIMIT$1 = 1e5;
80
257
  const BASH_TIMEOUT = 12e4;
81
258
  const BASH_MAX_TIMEOUT = 6e5;
@@ -341,6 +518,7 @@ async function runTool(call, context, tools = TOOLS, gate = {}) {
341
518
  const toolCallId = call.id;
342
519
  const update = (status2, output2) => context.update({ sessionUpdate: "tool_call_update", toolCallId, status: status2, rawOutput: output2.slice(0, 8e3) });
343
520
  const where = locations(context.root, args);
521
+ const meta = tool?.meta?.(args);
344
522
  context.update({
345
523
  sessionUpdate: "tool_call",
346
524
  toolCallId,
@@ -348,7 +526,8 @@ async function runTool(call, context, tools = TOOLS, gate = {}) {
348
526
  kind: tool?.kind ?? "other",
349
527
  status: "in_progress",
350
528
  rawInput: args,
351
- ...where ? { locations: where } : {}
529
+ ...where ? { locations: where } : {},
530
+ ...meta ? { _meta: meta } : {}
352
531
  });
353
532
  try {
354
533
  if (typeof parsed === "string") throw new Error(parsed);
@@ -418,13 +597,15 @@ function replay(turns, tools, update) {
418
597
  }
419
598
  const tool = tools.find((item) => item.spec.name === call.name);
420
599
  const args = rawInput && typeof rawInput === "object" ? rawInput : {};
600
+ const meta = tool?.meta?.(args);
421
601
  update({
422
602
  sessionUpdate: "tool_call",
423
603
  toolCallId: call.id,
424
604
  title: (tool?.title(args) ?? call.name).slice(0, 200),
425
605
  kind: tool?.kind ?? "other",
426
606
  status: "in_progress",
427
- rawInput
607
+ rawInput,
608
+ ...meta ? { _meta: meta } : {}
428
609
  });
429
610
  }
430
611
  } else
@@ -437,6 +618,56 @@ function replay(turns, tools, update) {
437
618
  });
438
619
  }
439
620
  }
621
+ const sleep$1 = (ms, signal) => new Promise((resolve2) => {
622
+ if (signal.aborted) return resolve2(false);
623
+ const abort = () => {
624
+ clearTimeout(timer);
625
+ resolve2(false);
626
+ };
627
+ const timer = setTimeout(() => {
628
+ signal.removeEventListener("abort", abort);
629
+ resolve2(true);
630
+ }, ms);
631
+ signal.addEventListener("abort", abort, { once: true });
632
+ });
633
+ const DEFAULT_RETRY = { retries: 4, baseMs: 1e3, maxMs: 3e4, sleep: sleep$1 };
634
+ const NETWORK = /network|connection|connect to|timed out|timeout|load failed|failed to fetch|fetch failed|offline|socket|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|EAI_AGAIN|stream ended before the response finished/i;
635
+ const BUSY = /overloaded|rate.?limit/i;
636
+ const QUOTA = /insufficient_quota|exceeded your current quota|billing/i;
637
+ const header = (headers, name) => {
638
+ if (!headers || typeof headers !== "object") return void 0;
639
+ if (typeof headers.get === "function") return headers.get(name) ?? void 0;
640
+ const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name);
641
+ return typeof entry?.[1] === "string" ? entry[1] : void 0;
642
+ };
643
+ function retryAfterMs(error) {
644
+ const value2 = error ?? {};
645
+ if (typeof value2.retryAfterMs === "number" && value2.retryAfterMs >= 0) return value2.retryAfterMs;
646
+ const ms = header(value2.headers, "retry-after-ms");
647
+ if (ms?.trim() && Number(ms) >= 0) return Number(ms);
648
+ const raw = value2.retryAfter ?? header(value2.headers, "retry-after");
649
+ if (typeof raw === "number") return raw >= 0 ? raw * 1e3 : void 0;
650
+ if (typeof raw !== "string" || !raw.trim()) return void 0;
651
+ const seconds = Number(raw);
652
+ if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : void 0;
653
+ const date = Date.parse(raw);
654
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
655
+ }
656
+ function isTransient(error) {
657
+ const { status: status2, body } = error ?? {};
658
+ const message2 = error instanceof Error ? error.message : String(error);
659
+ const text = `${typeof body === "string" ? body : ""} ${message2}`;
660
+ if (QUOTA.test(text)) return false;
661
+ if (typeof status2 === "number")
662
+ return status2 === 408 || status2 === 409 || status2 === 429 || status2 >= 500 && status2 !== 501 && status2 !== 505 || BUSY.test(text);
663
+ return BUSY.test(message2) || NETWORK.test(message2) || error instanceof TypeError && /fetch|load/i.test(message2);
664
+ }
665
+ function retryDelay(error, attempt, policy) {
666
+ if (attempt >= policy.retries || !isTransient(error)) return void 0;
667
+ const after = retryAfterMs(error);
668
+ if (after !== void 0) return after <= policy.maxMs ? after : void 0;
669
+ return Math.min(policy.maxMs, policy.baseMs * 2 ** attempt);
670
+ }
440
671
  async function* abortable(source, signal) {
441
672
  const iterator = source[Symbol.asyncIterator]();
442
673
  const aborted = new Promise((_, reject) => {
@@ -456,7 +687,29 @@ async function* abortable(source, signal) {
456
687
  });
457
688
  }
458
689
  }
459
- async function streamStep({ adapter, models: models2, endpoint, input: input2, signal, turns, update, contextWindow }) {
690
+ async function streamStep(options) {
691
+ const policy = { ...DEFAULT_RETRY, ...options.retry };
692
+ for (let attempt = 0; ; attempt++) {
693
+ const progress = { streamed: false };
694
+ try {
695
+ return await attemptStep(options, progress);
696
+ } catch (error) {
697
+ const delay = progress.streamed ? void 0 : retryDelay(error, attempt, policy);
698
+ if (delay === void 0) throw modelError(error);
699
+ const seconds = Math.round(delay / 100) / 10;
700
+ options.update({ sessionUpdate: "agent_message_chunk", content: {
701
+ type: "text",
702
+ text: `
703
+
704
+ ${modelError(error).message}. Retrying in ${seconds} s (${attempt + 1} of ${policy.retries}).
705
+
706
+ `
707
+ } });
708
+ if (!await policy.sleep(delay, options.signal)) return { turn: { role: "assistant", text: "", calls: [] } };
709
+ }
710
+ }
711
+ }
712
+ async function attemptStep({ adapter, models: models2, endpoint, input: input2, signal, turns, update, contextWindow }, progress) {
460
713
  const calls = [];
461
714
  const turn = { role: "assistant", text: "", calls };
462
715
  const result = { turn };
@@ -464,12 +717,16 @@ async function streamStep({ adapter, models: models2, endpoint, input: input2, s
464
717
  const request = adapter.request(input2);
465
718
  for await (const event of adapter.events(abortable(models2.stream({ ...endpoint, ...request }, signal), signal))) {
466
719
  if (event.type === "text") {
720
+ progress.streamed = true;
467
721
  turn.text += event.text;
468
722
  update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: event.text } });
469
- } else if (event.type === "reasoning")
723
+ } else if (event.type === "reasoning") {
724
+ progress.streamed = true;
470
725
  update({ sessionUpdate: "agent_thought_chunk", content: { type: "text", text: event.text } });
471
- else if (event.type === "tool_call") calls.push(event.call);
472
- else if (event.type === "usage") {
726
+ } else if (event.type === "tool_call") {
727
+ progress.streamed = true;
728
+ calls.push(event.call);
729
+ } else if (event.type === "usage") {
473
730
  result.usage = { input: event.input, output: event.output };
474
731
  if (contextWindow) update({ sessionUpdate: "usage_update", used: event.input + event.output, size: contextWindow });
475
732
  } else if (event.type === "stop") {
@@ -479,80 +736,13 @@ async function streamStep({ adapter, models: models2, endpoint, input: input2, s
479
736
  }
480
737
  if (!result.stop && !signal.aborted) throw new Error("The model stream ended before the response finished");
481
738
  } catch (error) {
482
- if (!signal.aborted) throw modelError(error);
739
+ if (!signal.aborted) throw error;
483
740
  } finally {
484
741
  if (!result.stop) calls.length = 0;
485
742
  if (turn.text || calls.length) turns.push(turn);
486
743
  }
487
744
  return result;
488
745
  }
489
- function replayable(items) {
490
- const kept = items.filter((item) => item.type !== "reasoning" || item.encrypted_content);
491
- return kept.length === items.length ? items : kept.map((item) => item.type === "reasoning" ? item : Object.fromEntries(Object.entries(item).filter(([key]) => key !== "id")));
492
- }
493
- const openaiResponses = {
494
- request({ model, system, turns, tools, effort }) {
495
- const input2 = [];
496
- for (const turn of turns) {
497
- if (turn.role === "user") input2.push({ type: "message", role: "user", content: [{ type: "input_text", text: turn.text }] });
498
- else if (turn.role === "tool")
499
- for (const result of turn.results) input2.push({ type: "function_call_output", call_id: result.id, output: result.output });
500
- else if (turn.native?.api === "openai") input2.push(...replayable(turn.native.items));
501
- else {
502
- if (turn.text) input2.push({ type: "message", role: "assistant", content: turn.text });
503
- for (const call of turn.calls) input2.push({ type: "function_call", call_id: call.id, name: call.name, arguments: call.arguments });
504
- }
505
- }
506
- return {
507
- method: "POST",
508
- path: "/v1/responses",
509
- body: {
510
- model,
511
- instructions: system,
512
- input: input2,
513
- stream: true,
514
- store: false,
515
- ...tools.length ? { tools: tools.map((tool) => ({ type: "function", ...tool, strict: false })) } : {},
516
- ...effort ? { reasoning: { effort, summary: "auto" } } : {}
517
- }
518
- };
519
- },
520
- async *events(chunks) {
521
- const items = [];
522
- for await (const event of jsonEvents(chunks)) {
523
- switch (event.type) {
524
- case "response.output_text.delta":
525
- if (event.delta) yield { type: "text", text: event.delta };
526
- break;
527
- case "response.reasoning_summary_text.delta":
528
- case "response.reasoning_text.delta":
529
- if (event.delta) yield { type: "reasoning", text: event.delta };
530
- break;
531
- case "response.reasoning_summary_part.added":
532
- if (event.summary_index > 0) yield { type: "reasoning", text: "\n\n" };
533
- break;
534
- case "response.output_item.done":
535
- items[event.output_index ?? items.length] = event.item;
536
- if (event.item?.type === "function_call")
537
- yield { type: "tool_call", call: { id: event.item.call_id, name: event.item.name, arguments: event.item.arguments ?? "" } };
538
- break;
539
- case "response.completed":
540
- case "response.incomplete": {
541
- const { usage, incomplete_details: incomplete } = event.response ?? {};
542
- if (usage) yield { type: "usage", input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 };
543
- const native = items.filter(Boolean);
544
- const reason = native.some((item) => item.type === "function_call") ? "tool_use" : event.type === "response.completed" ? "end_turn" : incomplete?.reason === "content_filter" ? "refusal" : "max_tokens";
545
- yield { type: "stop", reason, native };
546
- return;
547
- }
548
- case "response.failed":
549
- throw new Error(event.response?.error?.message ?? "The model response failed");
550
- case "error":
551
- throw new Error(event.message ?? "The model stream failed");
552
- }
553
- }
554
- }
555
- };
556
746
  const STOP$1 = { stop: "end_turn", tool_calls: "tool_use", function_call: "tool_use", length: "max_tokens", content_filter: "refusal" };
557
747
  function mergeDetails(details, fragments) {
558
748
  if (!Array.isArray(fragments)) return;
@@ -934,12 +1124,181 @@ Additional focus: ${focus}` : "");
934
1124
  }
935
1125
  }
936
1126
  }
1127
+ const OXBIT_SUBAGENT_TOOL = "task";
1128
+ const FORWARDED_HOOKS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure", "PreCompact", "PostCompact"]);
1129
+ const childCallId = (spawnId, id2) => `${spawnId}/${id2}`;
1130
+ function childExtensions(host, { parent, childId }) {
1131
+ const source = host.extensions;
1132
+ if (!source) return {};
1133
+ const view = () => ({ id: parent.id, cwd: parent.cwd, model: parent.model, turns: [], meta: { mcpServers: parent.meta.mcpServers } });
1134
+ const runner = source.hooks;
1135
+ const hooks = runner && {
1136
+ run: async (event, input2, signal) => FORWARDED_HOOKS.has(event) ? runner.run(event, { ...input2, session_id: parent.id, agent_id: childId }, signal) : {}
1137
+ };
1138
+ return {
1139
+ tools: source.tools?.map((provider2) => ({ tools: () => provider2.tools(view()) })),
1140
+ context: source.context?.map((provider2) => ({ label: provider2.label, system: () => provider2.system(view()) })),
1141
+ ...hooks ? { hooks } : {},
1142
+ ...source.compactor ? { compactor: source.compactor } : {}
1143
+ };
1144
+ }
1145
+ function childUpdate(host, { parent, spawnId, native }) {
1146
+ if (native) return (sessionId, update) => host.update(sessionId, update);
1147
+ return (_sessionId, update) => {
1148
+ if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") return;
1149
+ host.update(parent.id, {
1150
+ ...update,
1151
+ toolCallId: childCallId(spawnId, String(update.toolCallId)),
1152
+ _meta: { ...update._meta, oxbit: { ...update._meta?.oxbit, subagentOf: spawnId } }
1153
+ });
1154
+ };
1155
+ }
1156
+ const forwarded = (params, { parent, spawnId }) => ({
1157
+ ...params,
1158
+ ..."sessionId" in params ? { sessionId: parent.id } : {},
1159
+ ...typeof params.toolCallId === "string" ? { toolCallId: childCallId(spawnId, params.toolCallId) } : {},
1160
+ ...typeof params.toolCall?.toolCallId === "string" ? { toolCall: { ...params.toolCall, toolCallId: childCallId(spawnId, params.toolCall.toolCallId) } } : {}
1161
+ });
1162
+ function childRequest(host, link) {
1163
+ const aborted = new Promise((_, reject) => {
1164
+ const fail = () => reject(new Error("Cancelled by the user"));
1165
+ if (link.signal.aborted) fail();
1166
+ else link.signal.addEventListener("abort", fail, { once: true });
1167
+ });
1168
+ aborted.catch(() => {
1169
+ });
1170
+ return (method, params) => {
1171
+ const raw = host.request(method, link.native ? params : forwarded(params, link));
1172
+ raw.catch(() => {
1173
+ });
1174
+ return Promise.race([raw, aborted]);
1175
+ };
1176
+ }
1177
+ const childHost = (host, link) => ({
1178
+ ...host,
1179
+ subagents: void 0,
1180
+ generateTitles: false,
1181
+ update: childUpdate(host, link),
1182
+ request: childRequest(host, link),
1183
+ extensions: childExtensions(host, link)
1184
+ });
1185
+ const RESULT_LIMIT$1 = 4e4;
1186
+ const PROMPT_LIMIT = 1e5;
1187
+ const TASK_META_LIMIT = 4096;
1188
+ const CANCELLED = "Cancelled by the user";
1189
+ const SUBAGENT_PROMPT = [
1190
+ "You are a subagent. Another agent delegated the task in the user message to you.",
1191
+ "You cannot see that agent's conversation and cannot ask the user questions. Work until the task is done or you are blocked.",
1192
+ "End with a self-contained report: what you found or changed, with workspace-relative paths, and what is left open. The other agent sees only that report."
1193
+ ].join("\n");
1194
+ const DESCRIPTION = `Delegate a self-contained task to a subagent. It starts with a fresh context and the same tools except ${OXBIT_SUBAGENT_TOOL}, works until done, and returns its final message as this tool's result. The call blocks until the subagent finishes. The user does not see that message, so summarize what matters. Use it for broad searches, multi-file investigations or independent changes, to keep this conversation's context small. Several ${OXBIT_SUBAGENT_TOOL} calls in one response run in parallel. Its file edits and commands go through the same user review as yours.`;
1195
+ const field = (args, key) => typeof args[key] === "string" ? args[key] : "";
1196
+ function report(child, stopReason) {
1197
+ const last = [...child.turns].reverse().find((turn) => turn.role === "assistant");
1198
+ let output = (last?.role === "assistant" ? last.text.trim() : "") || "The subagent finished without a final message.";
1199
+ if (stopReason !== "end_turn") output += `
1200
+
1201
+ [The subagent stopped: ${stopReason}]`;
1202
+ return output.length > RESULT_LIMIT$1 ? `${output.slice(0, RESULT_LIMIT$1)}
1203
+ [Truncated at ${RESULT_LIMIT$1} characters.]` : output;
1204
+ }
1205
+ function taskTool({ host, parent, runChild }) {
1206
+ const meta = (args, state, sessionId) => {
1207
+ const subagent = {
1208
+ name: field(args, "description").trim().slice(0, 200),
1209
+ task: field(args, "prompt").slice(0, TASK_META_LIMIT),
1210
+ model: parent.model,
1211
+ ...sessionId ? { sessionId } : {},
1212
+ state
1213
+ };
1214
+ return { oxbit: { subagent } };
1215
+ };
1216
+ return {
1217
+ spec: { name: OXBIT_SUBAGENT_TOOL, description: DESCRIPTION, parameters: {
1218
+ type: "object",
1219
+ additionalProperties: false,
1220
+ required: ["description", "prompt"],
1221
+ properties: {
1222
+ description: { type: "string", maxLength: 200, description: "The task in 3 to 8 words" },
1223
+ prompt: {
1224
+ type: "string",
1225
+ minLength: 1,
1226
+ maxLength: PROMPT_LIMIT,
1227
+ description: "The full task: the goal, relevant paths and facts from this conversation, constraints, and what to report back"
1228
+ }
1229
+ }
1230
+ } },
1231
+ kind: "other",
1232
+ mutates: true,
1233
+ parallel: true,
1234
+ title: (args) => `Task: ${field(args, "description").trim() || "subagent"}`,
1235
+ meta: (args) => meta(args, "pending"),
1236
+ async run(args, context) {
1237
+ const description = field(args, "description").trim();
1238
+ const prompt = field(args, "prompt");
1239
+ if (!description || description.length > 200) throw new Error("description must be a non-empty string of at most 200 characters");
1240
+ if (!prompt.trim() || prompt.length > PROMPT_LIMIT) throw new Error(`prompt must be 1 to ${PROMPT_LIMIT} characters`);
1241
+ if (context.signal.aborted) throw new Error(CANCELLED);
1242
+ const childId = crypto.randomUUID();
1243
+ const native = !!host.capabilities.childSessions;
1244
+ const sessionId = native ? childId : void 0;
1245
+ const abort = new AbortController();
1246
+ const child = {
1247
+ id: childId,
1248
+ cwd: parent.cwd,
1249
+ model: parent.model,
1250
+ effort: parent.effort,
1251
+ catalog: parent.catalog,
1252
+ turns: [{ role: "user", text: prompt }],
1253
+ context: [SUBAGENT_PROMPT],
1254
+ meta: { subagent: { parentSessionId: parent.id, toolCallId: context.toolCallId }, get mode() {
1255
+ return parent.meta.mode;
1256
+ } }
1257
+ };
1258
+ let state;
1259
+ const finish = (next) => {
1260
+ if (state) return;
1261
+ state = next;
1262
+ if (native) host.update(parent.id, { sessionUpdate: "subagent_state_update", subagentSessionId: childId, state: next });
1263
+ context.update({ sessionUpdate: "tool_call_update", toolCallId: context.toolCallId, _meta: meta(args, next, sessionId) });
1264
+ };
1265
+ const cancel = () => {
1266
+ abort.abort();
1267
+ finish("cancelled");
1268
+ };
1269
+ context.signal.addEventListener("abort", cancel, { once: true });
1270
+ try {
1271
+ if (native) {
1272
+ host.update(parent.id, { sessionUpdate: "subagent_spawned", subagentSessionId: childId, name: description, task: prompt });
1273
+ host.update(childId, { sessionUpdate: "user_message_chunk", content: { type: "text", text: prompt } });
1274
+ }
1275
+ context.update({ sessionUpdate: "tool_call_update", toolCallId: context.toolCallId, _meta: meta(args, "running", sessionId) });
1276
+ const link = { parent, childId, spawnId: context.toolCallId, native, signal: abort.signal };
1277
+ const { stopReason } = await runChild(childHost(host, link), child, abort.signal);
1278
+ if (abort.signal.aborted) throw new Error(CANCELLED);
1279
+ finish("completed");
1280
+ return report(child, stopReason);
1281
+ } catch (error) {
1282
+ if (abort.signal.aborted) {
1283
+ finish("cancelled");
1284
+ throw new Error(CANCELLED);
1285
+ }
1286
+ finish("failed");
1287
+ throw error;
1288
+ } finally {
1289
+ context.signal.removeEventListener("abort", cancel);
1290
+ }
1291
+ }
1292
+ };
1293
+ }
937
1294
  const ADAPTERS = {
938
1295
  openai: openaiResponses,
939
1296
  "openai-compatible": chatCompletions,
940
- anthropic: anthropicMessages
1297
+ anthropic: anthropicMessages,
1298
+ chatgpt: chatgptResponses
941
1299
  };
942
1300
  const MAX_STEPS = 64;
1301
+ const STOP_BLOCK_LIMIT = 10;
943
1302
  const CONTEXT_LIMIT$1 = 1e4;
944
1303
  const basePrompt = (root, reviewed, shell) => [
945
1304
  `You are Oxbit Agent, a coding agent inside the Oxbit editor. You work in the workspace "${root.split("/").pop()}" through tools.`,
@@ -962,17 +1321,24 @@ class PromptTurn {
962
1321
  /** Set by a hook that returns continue: false. */
963
1322
  halt;
964
1323
  stopHookActive = false;
965
- snapshotted = false;
1324
+ stopBlocks = 0;
1325
+ snapshotting;
966
1326
  async hook(event, input2) {
967
1327
  const runner = this.host.extensions?.hooks;
968
1328
  if (!runner) return {};
969
- const outcome = await runner.run(event, {
970
- session_id: this.session.id,
971
- cwd: this.session.cwd,
972
- hook_event_name: event,
973
- permission_mode: CLAUDE_PERMISSION_MODES[modeOf(this.session.meta)],
974
- ...input2
975
- }, this.signal);
1329
+ let outcome;
1330
+ try {
1331
+ outcome = await runner.run(event, {
1332
+ session_id: this.session.id,
1333
+ cwd: this.session.cwd,
1334
+ hook_event_name: event,
1335
+ permission_mode: CLAUDE_PERMISSION_MODES[modeOf(this.session.meta)],
1336
+ ...input2
1337
+ }, this.signal);
1338
+ } catch (error) {
1339
+ if (!this.signal.aborted) this.notice(`The ${event} hooks failed: ${error instanceof Error ? error.message : String(error)}`);
1340
+ return {};
1341
+ }
976
1342
  if (outcome.systemMessage) this.notice(outcome.systemMessage);
977
1343
  if (outcome.continue === false) this.halt ??= outcome.stopReason || "A hook stopped the agent";
978
1344
  return outcome;
@@ -992,6 +1358,8 @@ ${text}
992
1358
  async tools() {
993
1359
  const provided = await Promise.all((this.host.extensions?.tools ?? []).map((provider2) => provider2.tools(this.session)));
994
1360
  const tools = [...builtinTools(this.host.capabilities)];
1361
+ if (this.host.subagents && !this.session.meta.subagent)
1362
+ tools.push(taskTool({ host: this.host, parent: this.session, runChild: (host, child, signal) => new PromptTurn(host, child, signal).run(host.models(), host.endpoint()) }));
995
1363
  for (const tool of provided.flat()) if (!tools.some((item) => item.spec.name === tool.spec.name)) tools.push(tool);
996
1364
  return tools;
997
1365
  }
@@ -1018,7 +1386,7 @@ ${text}
1018
1386
  let steps = 0;
1019
1387
  while (steps++ < MAX_STEPS) {
1020
1388
  const tools = await this.tools();
1021
- this.snapshotted = false;
1389
+ this.snapshotting = void 0;
1022
1390
  const result = await streamStep({
1023
1391
  adapter,
1024
1392
  models: models2,
@@ -1027,6 +1395,7 @@ ${text}
1027
1395
  update,
1028
1396
  turns: session.turns,
1029
1397
  contextWindow: info?.contextWindow,
1398
+ retry: this.host.retry,
1030
1399
  input: {
1031
1400
  model: session.model,
1032
1401
  system: prompt,
@@ -1048,11 +1417,7 @@ ${text}
1048
1417
  request: (method, params) => this.host.request(method, params),
1049
1418
  filesystem: session.cwd === normalize(this.host.root) ? this.host.filesystem : this.host.filesystemFor(session.cwd)
1050
1419
  };
1051
- const results = [];
1052
- for (const call of calls) {
1053
- const skipped = signal.aborted ? "Cancelled by the user" : this.halt;
1054
- results.push(skipped ? { id: call.id, name: call.name, output: skipped, error: true } : await runTool(call, context, tools, gate));
1055
- }
1420
+ const results = await this.runCalls(calls, context, tools, gate);
1056
1421
  if (results.length) session.turns.push({ role: "tool", results });
1057
1422
  if (signal.aborted) return { stopReason: "cancelled" };
1058
1423
  try {
@@ -1075,12 +1440,42 @@ ${text}
1075
1440
  }
1076
1441
  return { stopReason: "max_turn_requests" };
1077
1442
  }
1443
+ /** Runs calls in order; consecutive calls to parallel tools run together, up to the host's subagent concurrency. */
1444
+ async runCalls(calls, context, tools, gate) {
1445
+ const results = [];
1446
+ const one = async (index) => {
1447
+ const call = calls[index];
1448
+ const skipped = this.signal.aborted ? "Cancelled by the user" : this.halt;
1449
+ results[index] = skipped ? { id: call.id, name: call.name, output: skipped, error: true } : await runTool(call, context, tools, gate);
1450
+ };
1451
+ const parallel = (call) => !!tools.find((tool) => tool.spec.name === call.name)?.parallel;
1452
+ const limit = Math.max(1, this.host.subagents?.concurrency ?? 4);
1453
+ for (let index = 0; index < calls.length; ) {
1454
+ let end = index;
1455
+ while (end < calls.length && parallel(calls[end])) end++;
1456
+ if (end === index) {
1457
+ await one(index++);
1458
+ continue;
1459
+ }
1460
+ let next = index;
1461
+ const worker = async () => {
1462
+ while (next < end) await one(next++);
1463
+ };
1464
+ await Promise.all(Array.from({ length: Math.min(limit, end - index) }, worker));
1465
+ index = end;
1466
+ }
1467
+ return results;
1468
+ }
1078
1469
  /** Stop hooks, then continuation providers. Returns the text of the next user message, if any. */
1079
1470
  async afterStop() {
1080
1471
  const last = this.session.turns.at(-1);
1081
1472
  const outcome = await this.hook("Stop", { stop_hook_active: this.stopHookActive, last_assistant_message: last?.role === "assistant" ? last.text : "" });
1082
1473
  if (this.halt) return void 0;
1083
1474
  if (outcome.block) {
1475
+ if (++this.stopBlocks > STOP_BLOCK_LIMIT) {
1476
+ this.notice(`Stop hooks blocked the end of this turn ${STOP_BLOCK_LIMIT} times, so the turn ends here.`);
1477
+ return void 0;
1478
+ }
1084
1479
  this.stopHookActive = true;
1085
1480
  return outcome.reason || "A stop hook asked you to continue.";
1086
1481
  }
@@ -1101,6 +1496,7 @@ ${text}
1101
1496
  update: () => {
1102
1497
  },
1103
1498
  turns: [],
1499
+ retry: this.host.retry,
1104
1500
  input: { model: this.session.model, system: prompt, turns, tools: [] }
1105
1501
  });
1106
1502
  if (!result.stop) throw new Error("Cancelled");
@@ -1119,10 +1515,15 @@ ${text}
1119
1515
  await this.hook("PostCompact", { trigger });
1120
1516
  return true;
1121
1517
  }
1122
- async snapshot(label) {
1518
+ /** The first mutating call of a step captures; concurrent calls wait for that capture. */
1519
+ snapshot(label) {
1123
1520
  const store = this.host.extensions?.snapshots;
1124
- if (!store || this.snapshotted) return;
1125
- this.snapshotted = true;
1521
+ if (!store) return Promise.resolve();
1522
+ if (this.snapshotting) return this.snapshotting.catch(() => {
1523
+ });
1524
+ return this.snapshotting = this.capture(store, label);
1525
+ }
1526
+ async capture(store, label) {
1126
1527
  const snapshot2 = await store.capture(label, this.session.cwd);
1127
1528
  const list2 = Array.isArray(this.session.meta.snapshots) ? this.session.meta.snapshots : [];
1128
1529
  if (snapshot2) this.session.meta.snapshots = [...list2, { id: snapshot2, label, turn: this.session.turns.length }];
@@ -1251,13 +1652,13 @@ function contextLegend(usage) {
1251
1652
  }
1252
1653
  function contextNotes(usage) {
1253
1654
  const model = usage.model || "no model";
1254
- const header = usage.window ? `Context usage for ${model}: ${formatTokens(usage.used)} of ${formatTokens(usage.window)} tokens (${formatPercent(usage.used, usage.window)}).` : `Context usage for ${model}: about ${formatTokens(usage.used)} tokens. The context window of this model is unknown.`;
1655
+ const header2 = usage.window ? `Context usage for ${model}: ${formatTokens(usage.used)} of ${formatTokens(usage.window)} tokens (${formatPercent(usage.used, usage.window)}).` : `Context usage for ${model}: about ${formatTokens(usage.used)} tokens. The context window of this model is unknown.`;
1255
1656
  const footer = [
1256
1657
  ...usage.window ? [usage.threshold === void 0 ? "Auto compaction is off." : `Auto compaction runs at ${formatTokens(usage.threshold)} tokens (${formatPercent(usage.threshold, usage.window)}).`] : [],
1257
1658
  ...usage.reported ? [`Provider count for the last request: ${formatTokens(usage.reported.input + usage.reported.output)} tokens (${formatTokens(usage.reported.input)} input, ${formatTokens(usage.reported.output)} output).`] : [],
1258
1659
  "Category counts are estimates at 4 characters per token."
1259
1660
  ];
1260
- return { header, footer };
1661
+ return { header: header2, footer };
1261
1662
  }
1262
1663
  function renderContextUsage(usage) {
1263
1664
  const legend = contextLegend(usage);
@@ -1269,8 +1670,8 @@ function renderContextUsage(usage) {
1269
1670
  const counts = legend.map((row) => formatTokens(row.tokens));
1270
1671
  const digits = Math.max(...counts.map((count2) => count2.length));
1271
1672
  const lines2 = legend.map((row, index) => `${row.glyph} ${row.label.padEnd(width)} ${counts[index].padStart(digits)}` + (usage.window ? ` ${formatPercent(row.tokens, usage.window).padStart(6)}` : ""));
1272
- const { header, footer } = contextNotes(usage);
1273
- return [header, "", "```text", ...rows.length ? [...rows, ""] : [], ...lines2, "```", "", footer.join(" ")].join("\n");
1673
+ const { header: header2, footer } = contextNotes(usage);
1674
+ return [header2, "", "```text", ...rows.length ? [...rows, ""] : [], ...lines2, "```", "", footer.join(" ")].join("\n");
1274
1675
  }
1275
1676
  function contextCommand(host, session) {
1276
1677
  return {
@@ -1283,7 +1684,6 @@ function contextCommand(host, session) {
1283
1684
  }
1284
1685
  };
1285
1686
  }
1286
- const EFFORTS = ["default", "low", "medium", "high"];
1287
1687
  const COMMAND = /^\/([\w:.-]+)(?:\s+([\s\S]*))?$/;
1288
1688
  const id = (params, key) => {
1289
1689
  if (typeof params[key] !== "string" || !params[key]) throw new Error(`Invalid ${key}`);
@@ -1303,7 +1703,7 @@ class Agent {
1303
1703
  protocolVersion: 1,
1304
1704
  agentCapabilities: {
1305
1705
  loadSession: saved,
1306
- sessionCapabilities: { fork: {}, ...saved ? { list: {} } : {} },
1706
+ sessionCapabilities: { fork: {}, ...saved ? { list: {} } : {}, ...this.host.subagents ? { subagents: {} } : {} },
1307
1707
  promptCapabilities: { embeddedContext: true },
1308
1708
  ...this.host.capabilities.mcp ? { mcpCapabilities: this.host.capabilities.mcp } : {}
1309
1709
  },
@@ -1347,6 +1747,12 @@ class Agent {
1347
1747
  if (!session) throw new Error("Unknown ACP session");
1348
1748
  return session;
1349
1749
  }
1750
+ /** The session model's reasoning levels; empty when the model has no reasoning control. */
1751
+ efforts(session) {
1752
+ const model = session.catalog.find((item) => item.id === session.model) ?? { id: session.model, name: session.model };
1753
+ return modelEfforts(this.host.endpoint().api, model);
1754
+ }
1755
+ /** A model without reasoning levels gets no Reasoning option. */
1350
1756
  options(session) {
1351
1757
  const models2 = {
1352
1758
  id: "model",
@@ -1356,13 +1762,14 @@ class Agent {
1356
1762
  currentValue: session.model,
1357
1763
  options: session.catalog.map((model) => ({ value: model.id, name: model.name }))
1358
1764
  };
1765
+ const efforts = this.efforts(session);
1359
1766
  const reasoning = {
1360
1767
  id: "reasoning",
1361
1768
  name: "Reasoning",
1362
1769
  category: "thought_level",
1363
1770
  type: "select",
1364
1771
  currentValue: session.effort,
1365
- options: EFFORTS.map((value2) => ({ value: value2, name: value2 === "default" ? "Model default" : value2[0].toUpperCase() + value2.slice(1) }))
1772
+ options: [{ value: "default", name: "Model default" }, ...efforts.map((value2) => ({ value: value2, name: effortLabel(value2) }))]
1366
1773
  };
1367
1774
  const mode = {
1368
1775
  id: "mode",
@@ -1372,7 +1779,7 @@ class Agent {
1372
1779
  currentValue: modeOf(session.meta),
1373
1780
  options: MODES.map(({ id: id2, name }) => ({ value: id2, name }))
1374
1781
  };
1375
- return [...session.catalog.length ? [models2] : [], reasoning, mode, ...this.host.configOptions?.(session) ?? []];
1782
+ return [...session.catalog.length ? [models2] : [], ...efforts.length ? [reasoning] : [], mode, ...this.host.configOptions?.(session) ?? []];
1376
1783
  }
1377
1784
  state(session) {
1378
1785
  return {
@@ -1392,7 +1799,7 @@ class Agent {
1392
1799
  if (cwd !== normalize(this.host.root) && !this.host.filesystemFor) throw new Error(`This agent serves ${this.host.root} only`);
1393
1800
  return cwd;
1394
1801
  }
1395
- /** The host catalog or `GET /v1/models`, with `model` first when the list lacks it. */
1802
+ /** The host catalog or `listModels`, with `model` first when the list lacks it. */
1396
1803
  async models(model) {
1397
1804
  const listed = this.host.catalog ? Promise.resolve().then(() => this.host.catalog()) : this.catalog ??= listModels(this.service(), this.host.endpoint());
1398
1805
  const catalog = await listed.catch(() => {
@@ -1483,10 +1890,12 @@ class Agent {
1483
1890
  void this.end(session, "clear");
1484
1891
  }
1485
1892
  }
1893
+ /** A reasoning level the new model lacks falls back to the model default. */
1486
1894
  setModel(sessionId, modelId) {
1487
1895
  const session = this.session(sessionId);
1488
1896
  if (!session.catalog.some((model) => model.id === modelId)) throw new Error("This model is not in the model list");
1489
1897
  session.model = modelId;
1898
+ if (!this.efforts(session).includes(session.effort)) session.effort = "default";
1490
1899
  this.host.saveModel?.(modelId);
1491
1900
  return { configOptions: this.options(session) };
1492
1901
  }
@@ -1506,7 +1915,8 @@ class Agent {
1506
1915
  return { configOptions: this.options(session) };
1507
1916
  }
1508
1917
  if (configId !== "reasoning" && await this.host.setConfigOption?.(session, configId, value2)) return { configOptions: this.options(session) };
1509
- if (configId !== "reasoning" || !EFFORTS.includes(value2)) throw new Error("Unknown agent setting");
1918
+ if (configId !== "reasoning") throw new Error("Unknown agent setting");
1919
+ if (value2 !== "default" && !this.efforts(session).includes(value2)) throw new Error("This model does not support that reasoning level");
1510
1920
  session.effort = value2;
1511
1921
  return { configOptions: this.options(session) };
1512
1922
  }
@@ -1559,8 +1969,11 @@ class Agent {
1559
1969
  const endpoint = this.host.endpoint();
1560
1970
  let text = promptText(blocks);
1561
1971
  const submit = await turn.hook("UserPromptSubmit", { prompt: text });
1562
- if (submit.block) throw new Error(submit.reason || "A hook blocked this prompt");
1563
1972
  if (turn.halt) return turn.halted();
1973
+ if (submit.block) {
1974
+ turn.notice(submit.reason || "A hook blocked this prompt");
1975
+ return { stopReason: "end_turn" };
1976
+ }
1564
1977
  if (submit.additionalContext) text += `
1565
1978
 
1566
1979
  ${submit.additionalContext.slice(0, CONTEXT_LIMIT$1)}`;
@@ -1615,7 +2028,11 @@ ${submit.additionalContext.slice(0, CONTEXT_LIMIT$1)}`;
1615
2028
  const key = JSON.stringify(available);
1616
2029
  if (key === (session.commands ?? "[]")) return;
1617
2030
  session.commands = key;
1618
- this.host.update(session.id, { sessionUpdate: "available_commands_update", availableCommands: available });
2031
+ try {
2032
+ this.host.update(session.id, { sessionUpdate: "available_commands_update", availableCommands: available });
2033
+ } catch {
2034
+ session.commands = void 0;
2035
+ }
1619
2036
  }
1620
2037
  }
1621
2038
  const CLAUDE_TOOL_NAMES = {
@@ -1852,8 +2269,8 @@ function createHookRunner(options) {
1852
2269
  if (sources.some((source) => isObject$1(source.config) && source.config.disableAllHooks === true)) return {};
1853
2270
  const tool = TOOL_EVENTS.has(event) && typeof input2.tool_name === "string" ? input2.tool_name : void 0;
1854
2271
  const names = tool ? [tool, ...CLAUDE_TOOL_NAMES[tool] ?? []] : void 0;
1855
- const field = MATCH_FIELD[event];
1856
- const values = names ?? (field && typeof input2[field] === "string" ? [input2[field]] : void 0);
2272
+ const field2 = MATCH_FIELD[event];
2273
+ const values = names ?? (field2 && typeof input2[field2] === "string" ? [input2[field2]] : void 0);
1857
2274
  const selected = /* @__PURE__ */ new Map();
1858
2275
  for (const handler2 of sources.flatMap(handlersOf)) {
1859
2276
  if (handler2.event !== event || values && !matchesHook(handler2.matcher, values) || options.skip?.(handler2)) continue;
@@ -3204,11 +3621,11 @@ function headerParams(schema) {
3204
3621
  const walk = (node, path2) => {
3205
3622
  for (const [key, property] of Object.entries(node?.properties ?? {})) {
3206
3623
  if (!property || typeof property !== "object") continue;
3207
- const header = property["x-mcp-header"];
3208
- if (header !== void 0) {
3209
- if (typeof header !== "string" || !HEADER_TOKEN.test(header) || !PRIMITIVES.has(property.type) || seen.has(header.toLowerCase())) return false;
3210
- seen.add(header.toLowerCase());
3211
- found.push({ path: [...path2, key], header });
3624
+ const header2 = property["x-mcp-header"];
3625
+ if (header2 !== void 0) {
3626
+ if (typeof header2 !== "string" || !HEADER_TOKEN.test(header2) || !PRIMITIVES.has(property.type) || seen.has(header2.toLowerCase())) return false;
3627
+ seen.add(header2.toLowerCase());
3628
+ found.push({ path: [...path2, key], header: header2 });
3212
3629
  }
3213
3630
  if (property.type === "object" && !walk(property, [...path2, key])) return false;
3214
3631
  }
@@ -3218,11 +3635,11 @@ function headerParams(schema) {
3218
3635
  }
3219
3636
  function paramHeaders(params, args) {
3220
3637
  const headers = {};
3221
- for (const { path: path2, header } of params) {
3638
+ for (const { path: path2, header: header2 } of params) {
3222
3639
  let value2 = args;
3223
3640
  for (const key of path2) value2 = value2 && typeof value2 === "object" ? value2[key] : void 0;
3224
- if (typeof value2 === "string") headers[`Mcp-Param-${header}`] = headerValue(value2);
3225
- else if (typeof value2 === "number" || typeof value2 === "boolean") headers[`Mcp-Param-${header}`] = String(value2);
3641
+ if (typeof value2 === "string") headers[`Mcp-Param-${header2}`] = headerValue(value2);
3642
+ else if (typeof value2 === "number" || typeof value2 === "boolean") headers[`Mcp-Param-${header2}`] = String(value2);
3226
3643
  }
3227
3644
  return headers;
3228
3645
  }
@@ -3556,7 +3973,9 @@ function httpModels(options) {
3556
3973
  }
3557
3974
  if (!response.ok) {
3558
3975
  const body = await bodyText(response);
3559
- throw httpError(response.status, options.describe ? options.describe(response.status, body) : body);
3976
+ const error = httpError(response.status, options.describe ? options.describe(response.status, body) : body);
3977
+ const after = retryAfterMs({ headers: response.headers });
3978
+ throw after === void 0 ? error : Object.assign(error, { retryAfterMs: after });
3560
3979
  }
3561
3980
  yield* textChunks(response.body);
3562
3981
  },
@@ -3667,12 +4086,12 @@ const json = (part) => {
3667
4086
  async function verifyIdToken(token, check) {
3668
4087
  const parts = token.split(".");
3669
4088
  if (parts.length !== 3) throw new Error("The ID token is malformed");
3670
- const header = json(parts[0]);
4089
+ const header2 = json(parts[0]);
3671
4090
  const claims = json(parts[1]);
3672
- if (header?.alg !== "RS256") throw new Error(`The ID token uses an unsupported algorithm: ${header?.alg}`);
4091
+ if (header2?.alg !== "RS256") throw new Error(`The ID token uses an unsupported algorithm: ${header2?.alg}`);
3673
4092
  const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
3674
4093
  const signature = fromBase64url(parts[2]);
3675
- const matches = (keys) => keys.filter((key) => key.kty === "RSA" && (!key.use || key.use === "sig") && (!key.alg || key.alg === "RS256") && (header.kid === void 0 || key.kid === header.kid));
4094
+ const matches = (keys) => keys.filter((key) => key.kty === "RSA" && (!key.use || key.use === "sig") && (!key.alg || key.alg === "RS256") && (header2.kid === void 0 || key.kid === header2.kid));
3676
4095
  let candidates = matches(await check.jwks.keys(false));
3677
4096
  if (!candidates.length) candidates = matches(await check.jwks.keys(true));
3678
4097
  if (!candidates.length) throw new Error("No signing key matches the ID token");
@@ -4011,11 +4430,15 @@ const providerSpec = (id2) => PROVIDERS.find((spec) => spec.id === id2);
4011
4430
  const apiKeySlot = (provider2, baseUrl) => provider2 === "openai-compatible" ? `apikey:openai-compatible ${new URL(baseUrl ?? "").origin}` : `apikey:${provider2}`;
4012
4431
  function planModels(body) {
4013
4432
  if (!Array.isArray(body?.models)) throw new Error("The ChatGPT model list has no models array");
4014
- return body.models.filter((model) => typeof model?.slug === "string" && model.slug && model.visibility === "list" && model.supported_in_api !== false).map((model) => ({
4015
- id: model.slug,
4016
- name: typeof model.display_name === "string" ? model.display_name : model.slug,
4017
- ...Number.isSafeInteger(model.context_window) && model.context_window > 0 ? { contextWindow: model.context_window } : {}
4018
- }));
4433
+ return body.models.filter((model) => typeof model?.slug === "string" && model.slug && model.visibility === "list" && model.supported_in_api !== false).map((model) => {
4434
+ const efforts = listedEfforts(model);
4435
+ return {
4436
+ id: model.slug,
4437
+ name: typeof model.display_name === "string" ? model.display_name : model.slug,
4438
+ ...Number.isSafeInteger(model.context_window) && model.context_window > 0 ? { contextWindow: model.context_window } : {},
4439
+ ...efforts ? { efforts } : {}
4440
+ };
4441
+ });
4019
4442
  }
4020
4443
  function provider(selection, host) {
4021
4444
  const spec = providerSpec(selection.provider);
@@ -4373,7 +4796,7 @@ class FileCatalogCache {
4373
4796
  }
4374
4797
  }
4375
4798
  function agentVersion() {
4376
- return "0.11.0";
4799
+ return "0.11.2";
4377
4800
  }
4378
4801
  const clone = (message2) => JSON.parse(JSON.stringify(message2));
4379
4802
  class MemoryConnection {
@@ -4868,13 +5291,13 @@ async function firstLine$2(file2) {
4868
5291
  await handle.close();
4869
5292
  }
4870
5293
  }
4871
- const summary = (header) => header?.type === "session" && typeof header.id === "string" && typeof header.cwd === "string" ? {
4872
- id: header.id,
4873
- cwd: header.cwd,
4874
- title: String(header.title ?? ""),
4875
- model: String(header.model ?? ""),
4876
- meta: header.meta && typeof header.meta === "object" ? header.meta : {},
4877
- updatedAt: String(header.updatedAt ?? "")
5294
+ const summary = (header2) => header2?.type === "session" && typeof header2.id === "string" && typeof header2.cwd === "string" ? {
5295
+ id: header2.id,
5296
+ cwd: header2.cwd,
5297
+ title: String(header2.title ?? ""),
5298
+ model: String(header2.model ?? ""),
5299
+ meta: header2.meta && typeof header2.meta === "object" ? header2.meta : {},
5300
+ updatedAt: String(header2.updatedAt ?? "")
4878
5301
  } : void 0;
4879
5302
  class JsonlSessionStore {
4880
5303
  constructor(dir) {
@@ -4888,8 +5311,8 @@ class JsonlSessionStore {
4888
5311
  async save(record) {
4889
5312
  if (!VALID_ID.test(record.id)) throw new Error("Invalid session ID");
4890
5313
  const file2 = this.fileFor(record);
4891
- const { turns, ...header } = record;
4892
- const lines2 = [JSON.stringify({ type: "session", version: 1, ...header }), ...turns.map((turn) => JSON.stringify({ type: "turn", turn }))];
5314
+ const { turns, ...header2 } = record;
5315
+ const lines2 = [JSON.stringify({ type: "session", version: 1, ...header2 }), ...turns.map((turn) => JSON.stringify({ type: "turn", turn }))];
4893
5316
  await writeFileAtomic(file2, `${lines2.join("\n")}
4894
5317
  `, 384);
4895
5318
  const previous = this.files.get(record.id);
@@ -6292,7 +6715,8 @@ function nodeHost(setup, link) {
6292
6715
  config.saveSettings({ providers: { ...providers, [provider2.id]: { ...providers[provider2.id], model } } }).catch((error) => log?.(`Could not save the model: ${error.message}`));
6293
6716
  },
6294
6717
  get capabilities() {
6295
- return { fs: true, terminal: true, diagnostics: !!link.capabilities()._meta?.oxbit?.diagnostics, mcp: { http: true, sse: true } };
6718
+ const client = link.capabilities();
6719
+ return { fs: true, terminal: true, diagnostics: !!client._meta?.oxbit?.diagnostics, mcp: { http: true, sse: true }, childSessions: !!client.subagents };
6296
6720
  },
6297
6721
  filesystem: filesystemFor(root),
6298
6722
  filesystemFor,
@@ -6302,7 +6726,8 @@ function nodeHost(setup, link) {
6302
6726
  generateTitles: setup.generateTitles,
6303
6727
  configOptions: compactOptions2.configOptions,
6304
6728
  setConfigOption: (_session, configId, value2) => compactOptions2.setConfigOption(configId, value2),
6305
- extensions
6729
+ extensions,
6730
+ subagents: {}
6306
6731
  };
6307
6732
  const dispose = async () => {
6308
6733
  local.dispose();
@@ -6447,7 +6872,13 @@ ${USAGE}`);
6447
6872
  const settings = createSettingsManager(deps);
6448
6873
  const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2, settings });
6449
6874
  if (command === "acp") {
6450
- await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory(setup(true)), log });
6875
+ const rejected = (error) => log(`Unhandled rejection: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
6876
+ process.on("unhandledRejection", rejected);
6877
+ try {
6878
+ await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory(setup(true)), log });
6879
+ } finally {
6880
+ process.off("unhandledRejection", rejected);
6881
+ }
6451
6882
  return 0;
6452
6883
  }
6453
6884
  const sessionId = await resumeId(config, values, io.cwd);
@@ -6468,7 +6899,7 @@ ${USAGE}`);
6468
6899
  }
6469
6900
  }
6470
6901
  if (!io.stdout.isTTY) throw new UsageError("stdout is not a terminal. Use -p <prompt> for one reply or `oxbit-agent acp` for ACP");
6471
- const [tui, memory] = await Promise.all([import("./assets/index-T_6HaJx-.js"), import("./assets/memory-Bdy8hvTl.js")]);
6902
+ const [tui, memory] = await Promise.all([import("./assets/index-hc_7c2Gx.js"), import("./assets/memory-Bdy8hvTl.js")]);
6472
6903
  const label = () => ({ provider: provider2.runtime.label, usingChatGPTPlan: provider2.runtime.method === "chatgpt" });
6473
6904
  const panels = createSettingsManager({ ...deps, rebuild: async (id2) => {
6474
6905
  await config.reload();