@oxbit/cli 0.11.1 → 0.11.3
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.
- package/agent/dist/assets/{index-T_6HaJx-.js → index-DU-a4mnM.js} +216 -28
- package/agent/dist/index.js +482 -86
- package/package.json +1 -1
- package/runtime/dist/index.js +80 -18
- package/runtime/package.json +1 -1
- package/web/dist/assets/{index-X52PEMpo.js → index-C8DIIrnZ.js} +262 -163
- package/web/dist/assets/{index-CHlNlHrU.css → index-CMDbkm04.css} +1 -1
- package/web/dist/assets/{typescript-DLb7ioGk.js → typescript-Z_qrZEol.js} +2 -2
- package/web/dist/index.html +2 -2
- package/web/dist/schemas/settings.v1.schema.json +41 -0
- package/web/dist/sdk/index.js +43 -0
package/agent/dist/index.js
CHANGED
|
@@ -37,10 +37,10 @@ async function* parseSSE(chunks) {
|
|
|
37
37
|
}
|
|
38
38
|
if (line.startsWith(":")) continue;
|
|
39
39
|
const colon = line.indexOf(":");
|
|
40
|
-
const
|
|
40
|
+
const field2 = colon < 0 ? line : line.slice(0, colon);
|
|
41
41
|
const value2 = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
42
|
-
if (
|
|
43
|
-
else if (
|
|
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
|
|
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", ...
|
|
121
|
-
{ id: "gpt-6-astra", name: "GPT-6-Astra", ...
|
|
122
|
-
{ id: "gpt-6-sol", name: "GPT-6-Sol", ...
|
|
123
|
-
{ id: "gpt-6-luna", name: "GPT-6-Luna", ...
|
|
124
|
-
{ id: "gpt-5.6-sol", name: "GPT-5.6-Sol", ...
|
|
125
|
-
{ id: "gpt-5.6-terra", name: "GPT-5.6-Terra", ...
|
|
126
|
-
{ id: "gpt-5.6-luna", name: "GPT-5.6-Luna", ...
|
|
127
|
-
{ id: "gpt-5.5", name: "GPT-5.5",
|
|
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(
|
|
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")
|
|
576
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
1163
|
-
this.
|
|
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
|
|
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 [
|
|
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:
|
|
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"
|
|
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
|
-
|
|
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
|
|
1894
|
-
const values = names ?? (
|
|
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
|
|
3246
|
-
if (
|
|
3247
|
-
if (typeof
|
|
3248
|
-
seen.add(
|
|
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-${
|
|
3263
|
-
else if (typeof value2 === "number" || typeof value2 === "boolean") headers[`Mcp-Param-${
|
|
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
|
-
|
|
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
|
|
4089
|
+
const header2 = json(parts[0]);
|
|
3709
4090
|
const claims = json(parts[1]);
|
|
3710
|
-
if (
|
|
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") && (
|
|
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
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
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.
|
|
4799
|
+
return "0.11.3";
|
|
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 = (
|
|
4910
|
-
id:
|
|
4911
|
-
cwd:
|
|
4912
|
-
title: String(
|
|
4913
|
-
model: String(
|
|
4914
|
-
meta:
|
|
4915
|
-
updatedAt: String(
|
|
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, ...
|
|
4930
|
-
const lines2 = [JSON.stringify({ type: "session", version: 1, ...
|
|
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
|
-
|
|
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();
|
|
@@ -6425,6 +6812,7 @@ Options:
|
|
|
6425
6812
|
--mode <mode> ${MODES.map((mode) => mode.id).join(", ")}
|
|
6426
6813
|
-c, --continue Continue the latest session in this directory
|
|
6427
6814
|
-r, --resume <id> Resume a saved session
|
|
6815
|
+
--inline Render the terminal UI inline instead of fullscreen
|
|
6428
6816
|
-v, --version Print the version
|
|
6429
6817
|
-h, --help Print this help
|
|
6430
6818
|
|
|
@@ -6437,6 +6825,7 @@ const OPTIONS = {
|
|
|
6437
6825
|
mode: { type: "string" },
|
|
6438
6826
|
continue: { type: "boolean", short: "c" },
|
|
6439
6827
|
resume: { type: "string", short: "r" },
|
|
6828
|
+
inline: { type: "boolean" },
|
|
6440
6829
|
version: { type: "boolean", short: "v" },
|
|
6441
6830
|
help: { type: "boolean", short: "h" }
|
|
6442
6831
|
};
|
|
@@ -6485,7 +6874,13 @@ ${USAGE}`);
|
|
|
6485
6874
|
const settings = createSettingsManager(deps);
|
|
6486
6875
|
const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2, settings });
|
|
6487
6876
|
if (command === "acp") {
|
|
6488
|
-
|
|
6877
|
+
const rejected = (error) => log(`Unhandled rejection: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
6878
|
+
process.on("unhandledRejection", rejected);
|
|
6879
|
+
try {
|
|
6880
|
+
await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory(setup(true)), log });
|
|
6881
|
+
} finally {
|
|
6882
|
+
process.off("unhandledRejection", rejected);
|
|
6883
|
+
}
|
|
6489
6884
|
return 0;
|
|
6490
6885
|
}
|
|
6491
6886
|
const sessionId = await resumeId(config, values, io.cwd);
|
|
@@ -6506,7 +6901,7 @@ ${USAGE}`);
|
|
|
6506
6901
|
}
|
|
6507
6902
|
}
|
|
6508
6903
|
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-
|
|
6904
|
+
const [tui, memory] = await Promise.all([import("./assets/index-DU-a4mnM.js"), import("./assets/memory-Bdy8hvTl.js")]);
|
|
6510
6905
|
const label = () => ({ provider: provider2.runtime.label, usingChatGPTPlan: provider2.runtime.method === "chatgpt" });
|
|
6511
6906
|
const panels = createSettingsManager({ ...deps, rebuild: async (id2) => {
|
|
6512
6907
|
await config.reload();
|
|
@@ -6522,7 +6917,8 @@ ${USAGE}`);
|
|
|
6522
6917
|
sessionId,
|
|
6523
6918
|
prompt: positionals.join(" ") || void 0,
|
|
6524
6919
|
version: agentVersion(),
|
|
6525
|
-
history: memory.fileHistory(config.paths.history)
|
|
6920
|
+
history: memory.fileHistory(config.paths.history),
|
|
6921
|
+
fullscreen: !values.inline
|
|
6526
6922
|
});
|
|
6527
6923
|
if (last) io.stderr.write(`Resume this session with: oxbit-agent -r ${last}
|
|
6528
6924
|
`);
|