@oxbit/cli 0.11.1 → 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.
@@ -37,10 +37,10 @@ async function* parseSSE(chunks) {
37
37
  }
38
38
  if (line.startsWith(":")) continue;
39
39
  const colon = line.indexOf(":");
40
- const field = colon < 0 ? line : line.slice(0, colon);
40
+ const field2 = colon < 0 ? line : line.slice(0, colon);
41
41
  const value2 = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
42
- if (field === "event") event = value2;
43
- else if (field === "data") data.push(value2);
42
+ if (field2 === "event") event = value2;
43
+ else if (field2 === "data") data.push(value2);
44
44
  }
45
45
  }
46
46
  async function* jsonEvents(chunks) {
@@ -115,16 +115,17 @@ const openaiResponses = {
115
115
  };
116
116
  const CHATGPT_CLIENT_VERSION = "0.162.1";
117
117
  const CHATGPT_MODELS_PATH = `/codex/models?client_version=${CHATGPT_CLIENT_VERSION}`;
118
- const WINDOW = { contextWindow: 272e3 };
118
+ const XHIGH = ["low", "medium", "high", "xhigh"];
119
+ const MAX = { contextWindow: 272e3, efforts: [...XHIGH, "max"] };
119
120
  const CHATGPT_MODELS = [
120
- { id: "gpt-6.1-sol", name: "GPT-6.1-Sol", ...WINDOW },
121
- { id: "gpt-6-astra", name: "GPT-6-Astra", ...WINDOW },
122
- { id: "gpt-6-sol", name: "GPT-6-Sol", ...WINDOW },
123
- { id: "gpt-6-luna", name: "GPT-6-Luna", ...WINDOW },
124
- { id: "gpt-5.6-sol", name: "GPT-5.6-Sol", ...WINDOW },
125
- { id: "gpt-5.6-terra", name: "GPT-5.6-Terra", ...WINDOW },
126
- { id: "gpt-5.6-luna", name: "GPT-5.6-Luna", ...WINDOW },
127
- { id: "gpt-5.5", name: "GPT-5.5", ...WINDOW }
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 }
128
129
  ];
129
130
  const chatgptResponses = {
130
131
  request: (input2) => ({
@@ -138,36 +139,108 @@ const chatgptResponses = {
138
139
  }),
139
140
  events: openaiResponses.events
140
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
+ }
141
210
  const count = (...values) => values.find((value2) => Number.isSafeInteger(value2) && value2 > 0);
142
211
  async function readText$1(models2, request, signal) {
143
212
  let text = "";
144
213
  for await (const chunk of models2.stream(request, signal)) text += chunk;
145
214
  return text;
146
215
  }
216
+ const withEfforts = (info, item) => {
217
+ const efforts = listedEfforts(item);
218
+ return efforts ? { ...info, efforts } : info;
219
+ };
147
220
  async function listModels(models2, endpoint, signal) {
148
221
  if (endpoint.api === "chatgpt") return listChatGPTModels(models2, signal);
149
222
  const body = JSON.parse(await readText$1(models2, { ...endpoint, method: "GET", path: "/v1/models" }, signal));
150
223
  if (!Array.isArray(body?.data)) throw new Error("The model list has no data array");
151
- return body.data.filter((item) => typeof item?.id === "string" && item.id).slice(0, 1e3).map((item) => ({
224
+ return body.data.filter((item) => typeof item?.id === "string" && item.id).slice(0, 1e3).map((item) => withEfforts({
152
225
  id: item.id,
153
226
  name: typeof item.display_name === "string" ? item.display_name : typeof item.name === "string" ? item.name : item.id,
154
227
  contextWindow: count(item.max_input_tokens, item.context_length),
155
228
  maxOutput: count(item.max_tokens, item.top_provider?.max_completion_tokens)
156
- }));
229
+ }, item));
157
230
  }
158
231
  async function listChatGPTModels(models2, signal) {
159
232
  try {
160
233
  const body = JSON.parse(await readText$1(models2, { api: "chatgpt", method: "GET", path: CHATGPT_MODELS_PATH }, signal));
161
- 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) => ({
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({
162
235
  id: item.slug,
163
236
  name: typeof item.display_name === "string" && item.display_name ? item.display_name : item.slug,
164
237
  contextWindow: count(item.context_window)
165
- }));
238
+ }, item));
166
239
  if (listed.length) return listed;
167
240
  } catch (error) {
168
241
  if (signal?.aborted) throw error;
169
242
  }
170
- return CHATGPT_MODELS.map((model) => ({ ...model }));
243
+ return CHATGPT_MODELS.map((model) => ({ ...model, ...model.efforts ? { efforts: [...model.efforts] } : {} }));
171
244
  }
172
245
  function modelError(error) {
173
246
  const { status: status2, body } = error ?? {};
@@ -445,6 +518,7 @@ async function runTool(call, context, tools = TOOLS, gate = {}) {
445
518
  const toolCallId = call.id;
446
519
  const update = (status2, output2) => context.update({ sessionUpdate: "tool_call_update", toolCallId, status: status2, rawOutput: output2.slice(0, 8e3) });
447
520
  const where = locations(context.root, args);
521
+ const meta = tool?.meta?.(args);
448
522
  context.update({
449
523
  sessionUpdate: "tool_call",
450
524
  toolCallId,
@@ -452,7 +526,8 @@ async function runTool(call, context, tools = TOOLS, gate = {}) {
452
526
  kind: tool?.kind ?? "other",
453
527
  status: "in_progress",
454
528
  rawInput: args,
455
- ...where ? { locations: where } : {}
529
+ ...where ? { locations: where } : {},
530
+ ...meta ? { _meta: meta } : {}
456
531
  });
457
532
  try {
458
533
  if (typeof parsed === "string") throw new Error(parsed);
@@ -522,13 +597,15 @@ function replay(turns, tools, update) {
522
597
  }
523
598
  const tool = tools.find((item) => item.spec.name === call.name);
524
599
  const args = rawInput && typeof rawInput === "object" ? rawInput : {};
600
+ const meta = tool?.meta?.(args);
525
601
  update({
526
602
  sessionUpdate: "tool_call",
527
603
  toolCallId: call.id,
528
604
  title: (tool?.title(args) ?? call.name).slice(0, 200),
529
605
  kind: tool?.kind ?? "other",
530
606
  status: "in_progress",
531
- rawInput
607
+ rawInput,
608
+ ...meta ? { _meta: meta } : {}
532
609
  });
533
610
  }
534
611
  } else
@@ -541,6 +618,56 @@ function replay(turns, tools, update) {
541
618
  });
542
619
  }
543
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
+ }
544
671
  async function* abortable(source, signal) {
545
672
  const iterator = source[Symbol.asyncIterator]();
546
673
  const aborted = new Promise((_, reject) => {
@@ -560,7 +687,29 @@ async function* abortable(source, signal) {
560
687
  });
561
688
  }
562
689
  }
563
- 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) {
564
713
  const calls = [];
565
714
  const turn = { role: "assistant", text: "", calls };
566
715
  const result = { turn };
@@ -568,12 +717,16 @@ async function streamStep({ adapter, models: models2, endpoint, input: input2, s
568
717
  const request = adapter.request(input2);
569
718
  for await (const event of adapter.events(abortable(models2.stream({ ...endpoint, ...request }, signal), signal))) {
570
719
  if (event.type === "text") {
720
+ progress.streamed = true;
571
721
  turn.text += event.text;
572
722
  update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: event.text } });
573
- } else if (event.type === "reasoning")
723
+ } else if (event.type === "reasoning") {
724
+ progress.streamed = true;
574
725
  update({ sessionUpdate: "agent_thought_chunk", content: { type: "text", text: event.text } });
575
- else if (event.type === "tool_call") calls.push(event.call);
576
- 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") {
577
730
  result.usage = { input: event.input, output: event.output };
578
731
  if (contextWindow) update({ sessionUpdate: "usage_update", used: event.input + event.output, size: contextWindow });
579
732
  } else if (event.type === "stop") {
@@ -583,7 +736,7 @@ async function streamStep({ adapter, models: models2, endpoint, input: input2, s
583
736
  }
584
737
  if (!result.stop && !signal.aborted) throw new Error("The model stream ended before the response finished");
585
738
  } catch (error) {
586
- if (!signal.aborted) throw modelError(error);
739
+ if (!signal.aborted) throw error;
587
740
  } finally {
588
741
  if (!result.stop) calls.length = 0;
589
742
  if (turn.text || calls.length) turns.push(turn);
@@ -971,6 +1124,173 @@ Additional focus: ${focus}` : "");
971
1124
  }
972
1125
  }
973
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
+ }
974
1294
  const ADAPTERS = {
975
1295
  openai: openaiResponses,
976
1296
  "openai-compatible": chatCompletions,
@@ -978,6 +1298,7 @@ const ADAPTERS = {
978
1298
  chatgpt: chatgptResponses
979
1299
  };
980
1300
  const MAX_STEPS = 64;
1301
+ const STOP_BLOCK_LIMIT = 10;
981
1302
  const CONTEXT_LIMIT$1 = 1e4;
982
1303
  const basePrompt = (root, reviewed, shell) => [
983
1304
  `You are Oxbit Agent, a coding agent inside the Oxbit editor. You work in the workspace "${root.split("/").pop()}" through tools.`,
@@ -1000,17 +1321,24 @@ class PromptTurn {
1000
1321
  /** Set by a hook that returns continue: false. */
1001
1322
  halt;
1002
1323
  stopHookActive = false;
1003
- snapshotted = false;
1324
+ stopBlocks = 0;
1325
+ snapshotting;
1004
1326
  async hook(event, input2) {
1005
1327
  const runner = this.host.extensions?.hooks;
1006
1328
  if (!runner) return {};
1007
- const outcome = await runner.run(event, {
1008
- session_id: this.session.id,
1009
- cwd: this.session.cwd,
1010
- hook_event_name: event,
1011
- permission_mode: CLAUDE_PERMISSION_MODES[modeOf(this.session.meta)],
1012
- ...input2
1013
- }, 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
+ }
1014
1342
  if (outcome.systemMessage) this.notice(outcome.systemMessage);
1015
1343
  if (outcome.continue === false) this.halt ??= outcome.stopReason || "A hook stopped the agent";
1016
1344
  return outcome;
@@ -1030,6 +1358,8 @@ ${text}
1030
1358
  async tools() {
1031
1359
  const provided = await Promise.all((this.host.extensions?.tools ?? []).map((provider2) => provider2.tools(this.session)));
1032
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()) }));
1033
1363
  for (const tool of provided.flat()) if (!tools.some((item) => item.spec.name === tool.spec.name)) tools.push(tool);
1034
1364
  return tools;
1035
1365
  }
@@ -1056,7 +1386,7 @@ ${text}
1056
1386
  let steps = 0;
1057
1387
  while (steps++ < MAX_STEPS) {
1058
1388
  const tools = await this.tools();
1059
- this.snapshotted = false;
1389
+ this.snapshotting = void 0;
1060
1390
  const result = await streamStep({
1061
1391
  adapter,
1062
1392
  models: models2,
@@ -1065,6 +1395,7 @@ ${text}
1065
1395
  update,
1066
1396
  turns: session.turns,
1067
1397
  contextWindow: info?.contextWindow,
1398
+ retry: this.host.retry,
1068
1399
  input: {
1069
1400
  model: session.model,
1070
1401
  system: prompt,
@@ -1086,11 +1417,7 @@ ${text}
1086
1417
  request: (method, params) => this.host.request(method, params),
1087
1418
  filesystem: session.cwd === normalize(this.host.root) ? this.host.filesystem : this.host.filesystemFor(session.cwd)
1088
1419
  };
1089
- const results = [];
1090
- for (const call of calls) {
1091
- const skipped = signal.aborted ? "Cancelled by the user" : this.halt;
1092
- results.push(skipped ? { id: call.id, name: call.name, output: skipped, error: true } : await runTool(call, context, tools, gate));
1093
- }
1420
+ const results = await this.runCalls(calls, context, tools, gate);
1094
1421
  if (results.length) session.turns.push({ role: "tool", results });
1095
1422
  if (signal.aborted) return { stopReason: "cancelled" };
1096
1423
  try {
@@ -1113,12 +1440,42 @@ ${text}
1113
1440
  }
1114
1441
  return { stopReason: "max_turn_requests" };
1115
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
+ }
1116
1469
  /** Stop hooks, then continuation providers. Returns the text of the next user message, if any. */
1117
1470
  async afterStop() {
1118
1471
  const last = this.session.turns.at(-1);
1119
1472
  const outcome = await this.hook("Stop", { stop_hook_active: this.stopHookActive, last_assistant_message: last?.role === "assistant" ? last.text : "" });
1120
1473
  if (this.halt) return void 0;
1121
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
+ }
1122
1479
  this.stopHookActive = true;
1123
1480
  return outcome.reason || "A stop hook asked you to continue.";
1124
1481
  }
@@ -1139,6 +1496,7 @@ ${text}
1139
1496
  update: () => {
1140
1497
  },
1141
1498
  turns: [],
1499
+ retry: this.host.retry,
1142
1500
  input: { model: this.session.model, system: prompt, turns, tools: [] }
1143
1501
  });
1144
1502
  if (!result.stop) throw new Error("Cancelled");
@@ -1157,10 +1515,15 @@ ${text}
1157
1515
  await this.hook("PostCompact", { trigger });
1158
1516
  return true;
1159
1517
  }
1160
- async snapshot(label) {
1518
+ /** The first mutating call of a step captures; concurrent calls wait for that capture. */
1519
+ snapshot(label) {
1161
1520
  const store = this.host.extensions?.snapshots;
1162
- if (!store || this.snapshotted) return;
1163
- 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) {
1164
1527
  const snapshot2 = await store.capture(label, this.session.cwd);
1165
1528
  const list2 = Array.isArray(this.session.meta.snapshots) ? this.session.meta.snapshots : [];
1166
1529
  if (snapshot2) this.session.meta.snapshots = [...list2, { id: snapshot2, label, turn: this.session.turns.length }];
@@ -1289,13 +1652,13 @@ function contextLegend(usage) {
1289
1652
  }
1290
1653
  function contextNotes(usage) {
1291
1654
  const model = usage.model || "no model";
1292
- 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.`;
1293
1656
  const footer = [
1294
1657
  ...usage.window ? [usage.threshold === void 0 ? "Auto compaction is off." : `Auto compaction runs at ${formatTokens(usage.threshold)} tokens (${formatPercent(usage.threshold, usage.window)}).`] : [],
1295
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).`] : [],
1296
1659
  "Category counts are estimates at 4 characters per token."
1297
1660
  ];
1298
- return { header, footer };
1661
+ return { header: header2, footer };
1299
1662
  }
1300
1663
  function renderContextUsage(usage) {
1301
1664
  const legend = contextLegend(usage);
@@ -1307,8 +1670,8 @@ function renderContextUsage(usage) {
1307
1670
  const counts = legend.map((row) => formatTokens(row.tokens));
1308
1671
  const digits = Math.max(...counts.map((count2) => count2.length));
1309
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)}` : ""));
1310
- const { header, footer } = contextNotes(usage);
1311
- 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");
1312
1675
  }
1313
1676
  function contextCommand(host, session) {
1314
1677
  return {
@@ -1321,7 +1684,6 @@ function contextCommand(host, session) {
1321
1684
  }
1322
1685
  };
1323
1686
  }
1324
- const EFFORTS = ["default", "low", "medium", "high"];
1325
1687
  const COMMAND = /^\/([\w:.-]+)(?:\s+([\s\S]*))?$/;
1326
1688
  const id = (params, key) => {
1327
1689
  if (typeof params[key] !== "string" || !params[key]) throw new Error(`Invalid ${key}`);
@@ -1341,7 +1703,7 @@ class Agent {
1341
1703
  protocolVersion: 1,
1342
1704
  agentCapabilities: {
1343
1705
  loadSession: saved,
1344
- sessionCapabilities: { fork: {}, ...saved ? { list: {} } : {} },
1706
+ sessionCapabilities: { fork: {}, ...saved ? { list: {} } : {}, ...this.host.subagents ? { subagents: {} } : {} },
1345
1707
  promptCapabilities: { embeddedContext: true },
1346
1708
  ...this.host.capabilities.mcp ? { mcpCapabilities: this.host.capabilities.mcp } : {}
1347
1709
  },
@@ -1385,6 +1747,12 @@ class Agent {
1385
1747
  if (!session) throw new Error("Unknown ACP session");
1386
1748
  return session;
1387
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. */
1388
1756
  options(session) {
1389
1757
  const models2 = {
1390
1758
  id: "model",
@@ -1394,13 +1762,14 @@ class Agent {
1394
1762
  currentValue: session.model,
1395
1763
  options: session.catalog.map((model) => ({ value: model.id, name: model.name }))
1396
1764
  };
1765
+ const efforts = this.efforts(session);
1397
1766
  const reasoning = {
1398
1767
  id: "reasoning",
1399
1768
  name: "Reasoning",
1400
1769
  category: "thought_level",
1401
1770
  type: "select",
1402
1771
  currentValue: session.effort,
1403
- 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) }))]
1404
1773
  };
1405
1774
  const mode = {
1406
1775
  id: "mode",
@@ -1410,7 +1779,7 @@ class Agent {
1410
1779
  currentValue: modeOf(session.meta),
1411
1780
  options: MODES.map(({ id: id2, name }) => ({ value: id2, name }))
1412
1781
  };
1413
- 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) ?? []];
1414
1783
  }
1415
1784
  state(session) {
1416
1785
  return {
@@ -1521,10 +1890,12 @@ class Agent {
1521
1890
  void this.end(session, "clear");
1522
1891
  }
1523
1892
  }
1893
+ /** A reasoning level the new model lacks falls back to the model default. */
1524
1894
  setModel(sessionId, modelId) {
1525
1895
  const session = this.session(sessionId);
1526
1896
  if (!session.catalog.some((model) => model.id === modelId)) throw new Error("This model is not in the model list");
1527
1897
  session.model = modelId;
1898
+ if (!this.efforts(session).includes(session.effort)) session.effort = "default";
1528
1899
  this.host.saveModel?.(modelId);
1529
1900
  return { configOptions: this.options(session) };
1530
1901
  }
@@ -1544,7 +1915,8 @@ class Agent {
1544
1915
  return { configOptions: this.options(session) };
1545
1916
  }
1546
1917
  if (configId !== "reasoning" && await this.host.setConfigOption?.(session, configId, value2)) return { configOptions: this.options(session) };
1547
- 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");
1548
1920
  session.effort = value2;
1549
1921
  return { configOptions: this.options(session) };
1550
1922
  }
@@ -1597,8 +1969,11 @@ class Agent {
1597
1969
  const endpoint = this.host.endpoint();
1598
1970
  let text = promptText(blocks);
1599
1971
  const submit = await turn.hook("UserPromptSubmit", { prompt: text });
1600
- if (submit.block) throw new Error(submit.reason || "A hook blocked this prompt");
1601
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
+ }
1602
1977
  if (submit.additionalContext) text += `
1603
1978
 
1604
1979
  ${submit.additionalContext.slice(0, CONTEXT_LIMIT$1)}`;
@@ -1653,7 +2028,11 @@ ${submit.additionalContext.slice(0, CONTEXT_LIMIT$1)}`;
1653
2028
  const key = JSON.stringify(available);
1654
2029
  if (key === (session.commands ?? "[]")) return;
1655
2030
  session.commands = key;
1656
- 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
+ }
1657
2036
  }
1658
2037
  }
1659
2038
  const CLAUDE_TOOL_NAMES = {
@@ -1890,8 +2269,8 @@ function createHookRunner(options) {
1890
2269
  if (sources.some((source) => isObject$1(source.config) && source.config.disableAllHooks === true)) return {};
1891
2270
  const tool = TOOL_EVENTS.has(event) && typeof input2.tool_name === "string" ? input2.tool_name : void 0;
1892
2271
  const names = tool ? [tool, ...CLAUDE_TOOL_NAMES[tool] ?? []] : void 0;
1893
- const field = MATCH_FIELD[event];
1894
- 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);
1895
2274
  const selected = /* @__PURE__ */ new Map();
1896
2275
  for (const handler2 of sources.flatMap(handlersOf)) {
1897
2276
  if (handler2.event !== event || values && !matchesHook(handler2.matcher, values) || options.skip?.(handler2)) continue;
@@ -3242,11 +3621,11 @@ function headerParams(schema) {
3242
3621
  const walk = (node, path2) => {
3243
3622
  for (const [key, property] of Object.entries(node?.properties ?? {})) {
3244
3623
  if (!property || typeof property !== "object") continue;
3245
- const header = property["x-mcp-header"];
3246
- if (header !== void 0) {
3247
- if (typeof header !== "string" || !HEADER_TOKEN.test(header) || !PRIMITIVES.has(property.type) || seen.has(header.toLowerCase())) return false;
3248
- seen.add(header.toLowerCase());
3249
- 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 });
3250
3629
  }
3251
3630
  if (property.type === "object" && !walk(property, [...path2, key])) return false;
3252
3631
  }
@@ -3256,11 +3635,11 @@ function headerParams(schema) {
3256
3635
  }
3257
3636
  function paramHeaders(params, args) {
3258
3637
  const headers = {};
3259
- for (const { path: path2, header } of params) {
3638
+ for (const { path: path2, header: header2 } of params) {
3260
3639
  let value2 = args;
3261
3640
  for (const key of path2) value2 = value2 && typeof value2 === "object" ? value2[key] : void 0;
3262
- if (typeof value2 === "string") headers[`Mcp-Param-${header}`] = headerValue(value2);
3263
- 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);
3264
3643
  }
3265
3644
  return headers;
3266
3645
  }
@@ -3594,7 +3973,9 @@ function httpModels(options) {
3594
3973
  }
3595
3974
  if (!response.ok) {
3596
3975
  const body = await bodyText(response);
3597
- 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 });
3598
3979
  }
3599
3980
  yield* textChunks(response.body);
3600
3981
  },
@@ -3705,12 +4086,12 @@ const json = (part) => {
3705
4086
  async function verifyIdToken(token, check) {
3706
4087
  const parts = token.split(".");
3707
4088
  if (parts.length !== 3) throw new Error("The ID token is malformed");
3708
- const header = json(parts[0]);
4089
+ const header2 = json(parts[0]);
3709
4090
  const claims = json(parts[1]);
3710
- 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}`);
3711
4092
  const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
3712
4093
  const signature = fromBase64url(parts[2]);
3713
- 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));
3714
4095
  let candidates = matches(await check.jwks.keys(false));
3715
4096
  if (!candidates.length) candidates = matches(await check.jwks.keys(true));
3716
4097
  if (!candidates.length) throw new Error("No signing key matches the ID token");
@@ -4049,11 +4430,15 @@ const providerSpec = (id2) => PROVIDERS.find((spec) => spec.id === id2);
4049
4430
  const apiKeySlot = (provider2, baseUrl) => provider2 === "openai-compatible" ? `apikey:openai-compatible ${new URL(baseUrl ?? "").origin}` : `apikey:${provider2}`;
4050
4431
  function planModels(body) {
4051
4432
  if (!Array.isArray(body?.models)) throw new Error("The ChatGPT model list has no models array");
4052
- return body.models.filter((model) => typeof model?.slug === "string" && model.slug && model.visibility === "list" && model.supported_in_api !== false).map((model) => ({
4053
- id: model.slug,
4054
- name: typeof model.display_name === "string" ? model.display_name : model.slug,
4055
- ...Number.isSafeInteger(model.context_window) && model.context_window > 0 ? { contextWindow: model.context_window } : {}
4056
- }));
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
+ });
4057
4442
  }
4058
4443
  function provider(selection, host) {
4059
4444
  const spec = providerSpec(selection.provider);
@@ -4411,7 +4796,7 @@ class FileCatalogCache {
4411
4796
  }
4412
4797
  }
4413
4798
  function agentVersion() {
4414
- return "0.11.1";
4799
+ return "0.11.2";
4415
4800
  }
4416
4801
  const clone = (message2) => JSON.parse(JSON.stringify(message2));
4417
4802
  class MemoryConnection {
@@ -4906,13 +5291,13 @@ async function firstLine$2(file2) {
4906
5291
  await handle.close();
4907
5292
  }
4908
5293
  }
4909
- const summary = (header) => header?.type === "session" && typeof header.id === "string" && typeof header.cwd === "string" ? {
4910
- id: header.id,
4911
- cwd: header.cwd,
4912
- title: String(header.title ?? ""),
4913
- model: String(header.model ?? ""),
4914
- meta: header.meta && typeof header.meta === "object" ? header.meta : {},
4915
- 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 ?? "")
4916
5301
  } : void 0;
4917
5302
  class JsonlSessionStore {
4918
5303
  constructor(dir) {
@@ -4926,8 +5311,8 @@ class JsonlSessionStore {
4926
5311
  async save(record) {
4927
5312
  if (!VALID_ID.test(record.id)) throw new Error("Invalid session ID");
4928
5313
  const file2 = this.fileFor(record);
4929
- const { turns, ...header } = record;
4930
- 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 }))];
4931
5316
  await writeFileAtomic(file2, `${lines2.join("\n")}
4932
5317
  `, 384);
4933
5318
  const previous = this.files.get(record.id);
@@ -6330,7 +6715,8 @@ function nodeHost(setup, link) {
6330
6715
  config.saveSettings({ providers: { ...providers, [provider2.id]: { ...providers[provider2.id], model } } }).catch((error) => log?.(`Could not save the model: ${error.message}`));
6331
6716
  },
6332
6717
  get capabilities() {
6333
- 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 };
6334
6720
  },
6335
6721
  filesystem: filesystemFor(root),
6336
6722
  filesystemFor,
@@ -6340,7 +6726,8 @@ function nodeHost(setup, link) {
6340
6726
  generateTitles: setup.generateTitles,
6341
6727
  configOptions: compactOptions2.configOptions,
6342
6728
  setConfigOption: (_session, configId, value2) => compactOptions2.setConfigOption(configId, value2),
6343
- extensions
6729
+ extensions,
6730
+ subagents: {}
6344
6731
  };
6345
6732
  const dispose = async () => {
6346
6733
  local.dispose();
@@ -6485,7 +6872,13 @@ ${USAGE}`);
6485
6872
  const settings = createSettingsManager(deps);
6486
6873
  const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2, settings });
6487
6874
  if (command === "acp") {
6488
- 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
+ }
6489
6882
  return 0;
6490
6883
  }
6491
6884
  const sessionId = await resumeId(config, values, io.cwd);
@@ -6506,7 +6899,7 @@ ${USAGE}`);
6506
6899
  }
6507
6900
  }
6508
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");
6509
- 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")]);
6510
6903
  const label = () => ({ provider: provider2.runtime.label, usingChatGPTPlan: provider2.runtime.method === "chatgpt" });
6511
6904
  const panels = createSettingsManager({ ...deps, rebuild: async (id2) => {
6512
6905
  await config.reload();