castle-web-cli 0.4.81 → 0.4.83
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/dist/agent-prompts.d.ts +0 -3
- package/dist/agent-prompts.js +3 -8
- package/dist/agent.d.ts +1 -2
- package/dist/agent.js +327 -157
- package/dist/castle-host/host.js +28 -0
- package/dist/ide.js +150 -1
- package/dist/init.js +1 -1
- package/dist/native/loop.js +15 -29
- package/dist/native/openrouter.d.ts +5 -1
- package/dist/native/openrouter.js +20 -1
- package/dist/native/tools.d.ts +0 -1
- package/dist/native/tools.js +3 -79
- package/dist/native/types.d.ts +4 -1
- package/dist/native/types.js +3 -3
- package/dist/shell/assets/index-BMkQt27u.css +1 -0
- package/dist/shell/assets/index-C9Zhmien.js +142 -0
- package/dist/shell/index.html +2 -2
- package/dist/shell/operator.png +0 -0
- package/kits/basic-2d/CLAUDE.md +27 -22
- package/kits/basic-2d/behaviors/Collider.jsx +24 -30
- package/kits/basic-2d/behaviors/Layout.jsx +9 -6
- package/kits/basic-2d/behaviors/Sprite.jsx +137 -7
- package/kits/basic-2d/blueprints/cauldron.scene +3 -5
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +11 -11
- package/kits/basic-2d/editors/SceneEditor.jsx +212 -50
- package/kits/basic-2d/editors/SelectionOverlay.jsx +73 -54
- package/kits/basic-2d/editors/inspectorSheet.js +5 -1
- package/kits/basic-2d/engine/ScenePlayer.jsx +102 -7
- package/kits/basic-2d/engine/autoInspector.jsx +26 -7
- package/kits/basic-2d/engine/blueprint.js +109 -11
- package/kits/basic-2d/engine/collider.js +146 -0
- package/kits/basic-2d/engine/scene.js +53 -30
- package/kits/basic-2d/engine/spriteGeometry.js +32 -0
- package/kits/basic-2d/engine/ui.jsx +89 -30
- package/kits/basic-2d/engine/ui.module.css +157 -53
- package/kits/basic-2d/scenes/main.scene +3 -3
- package/package.json +2 -1
- package/dist/shell/assets/index-D3unT7do.js +0 -141
- package/dist/shell/assets/index-RZrw5gQ2.css +0 -1
- package/kits/basic-2d/pnpm-workspace.yaml +0 -3
package/dist/agent.js
CHANGED
|
@@ -30,16 +30,38 @@ export const AGENT_ATTACHMENT_PREFIX = "/__castle/agent/attachments/";
|
|
|
30
30
|
// Playtest frame PNGs (tasks/<id>/playtest/<file>.png), served for the
|
|
31
31
|
// finished-task card's thumbnails -- see makePlaytestFrameHandler.
|
|
32
32
|
export const AGENT_PLAYTEST_PREFIX = "/__castle/agent/playtest/";
|
|
33
|
+
// Same-origin proxy for OpenRouter model capabilities (avoids browser CORS
|
|
34
|
+
// against openrouter.ai). GET ?model=<slug> -> ModelCaps JSON. Powers the
|
|
35
|
+
// settings popover's dynamic reasoning-effort / provider-tier pickers.
|
|
36
|
+
export const AGENT_MODEL_CAPS_PREFIX = "/__castle/agent/model-caps";
|
|
33
37
|
const DEFAULT_SETTINGS = {
|
|
34
38
|
router: "claude",
|
|
35
39
|
tasks: "claude",
|
|
36
40
|
routerClaudeModel: "opus",
|
|
37
41
|
// Tasks default one tier down: task agents run long unattended builds, so
|
|
38
|
-
// sonnet's cost/speed wins by default; the
|
|
42
|
+
// sonnet's cost/speed wins by default; the operator stays on opus.
|
|
39
43
|
tasksClaudeModel: "sonnet",
|
|
40
44
|
// Free-form -- change to any OpenRouter slug.
|
|
41
45
|
routerOpenrouterModel: "openai/gpt-5.6-sol",
|
|
42
46
|
tasksOpenrouterModel: "openai/gpt-5.6-terra",
|
|
47
|
+
// Both roles think at "medium": the operator stays snappy (the user waits
|
|
48
|
+
// on every operator turn), and task agents' multi-turn tool loops don't pay
|
|
49
|
+
// reasoning tax on mechanical read/edit/run turns. Deep decomposition
|
|
50
|
+
// quality comes from the operator prompt, not a higher effort default.
|
|
51
|
+
routerReasoningEffort: "medium",
|
|
52
|
+
tasksReasoningEffort: "medium",
|
|
53
|
+
// Routing splits by what each role optimizes for: the interactive operator
|
|
54
|
+
// routes for speed (nitro = throughput-sorted endpoints), unattended task
|
|
55
|
+
// agents route for correctness (exacto = benchmark-accurate endpoints,
|
|
56
|
+
// which matters for tool-calling fidelity over long loops).
|
|
57
|
+
routerRouting: "nitro",
|
|
58
|
+
tasksRouting: "exacto",
|
|
59
|
+
// Operator pins OpenAI's priority (low-latency SLA) tier for the default
|
|
60
|
+
// sol model; harmless with other slugs since the pin falls back when the
|
|
61
|
+
// tag doesn't exist (allow_fallbacks). Tasks stay on auto: high-volume
|
|
62
|
+
// background turns should ride the cheapest available capacity.
|
|
63
|
+
routerProviderTier: "openai/priority",
|
|
64
|
+
tasksProviderTier: "",
|
|
43
65
|
};
|
|
44
66
|
function normalizeBackend(value) {
|
|
45
67
|
return value === "cursor" || value === "claude" || value === "smith"
|
|
@@ -73,6 +95,48 @@ function normalizeOpenrouterModel(value) {
|
|
|
73
95
|
const trimmed = value.trim();
|
|
74
96
|
return trimmed && trimmed.length <= OPENROUTER_MODEL_MAX_LEN ? trimmed : null;
|
|
75
97
|
}
|
|
98
|
+
// OpenRouter's full effort superset -- validated against the union rather than
|
|
99
|
+
// a per-model list because supported efforts are model-specific (the client
|
|
100
|
+
// fetches them from the model-caps endpoint to build the picker) and
|
|
101
|
+
// OpenRouter maps an unsupported level to the nearest one anyway.
|
|
102
|
+
const REASONING_EFFORTS = [
|
|
103
|
+
"none",
|
|
104
|
+
"minimal",
|
|
105
|
+
"low",
|
|
106
|
+
"medium",
|
|
107
|
+
"high",
|
|
108
|
+
"xhigh",
|
|
109
|
+
"max",
|
|
110
|
+
];
|
|
111
|
+
function normalizeReasoningEffort(value) {
|
|
112
|
+
return REASONING_EFFORTS.includes(value)
|
|
113
|
+
? value
|
|
114
|
+
: null;
|
|
115
|
+
}
|
|
116
|
+
const ROUTING_MODES = [
|
|
117
|
+
"balanced",
|
|
118
|
+
"nitro",
|
|
119
|
+
"exacto",
|
|
120
|
+
"floor",
|
|
121
|
+
];
|
|
122
|
+
function normalizeRoutingMode(value) {
|
|
123
|
+
return ROUTING_MODES.includes(value)
|
|
124
|
+
? value
|
|
125
|
+
: null;
|
|
126
|
+
}
|
|
127
|
+
// Provider tier is an OpenRouter endpoint `tag` ("openai/flex", "azure/eu",
|
|
128
|
+
// ...) which is model-specific, so validation is loose like the model slug.
|
|
129
|
+
// Unlike the slug, empty string is VALID and meaningful: "auto" (no pin), so
|
|
130
|
+
// this returns "" rather than null for the clear case -- callers must treat
|
|
131
|
+
// null (invalid) and "" (clear) differently.
|
|
132
|
+
function normalizeProviderTier(value) {
|
|
133
|
+
if (typeof value !== "string")
|
|
134
|
+
return null;
|
|
135
|
+
const trimmed = value.trim();
|
|
136
|
+
if (trimmed.length > OPENROUTER_MODEL_MAX_LEN)
|
|
137
|
+
return null;
|
|
138
|
+
return trimmed;
|
|
139
|
+
}
|
|
76
140
|
// OpenRouter's Anthropic-compatible endpoint (confirmed current, 2026: it
|
|
77
141
|
// accepts the standard Anthropic Messages API shape -- text/tool-use/
|
|
78
142
|
// extended-thinking -- for ANY OpenRouter model slug, not just Anthropic
|
|
@@ -116,6 +180,127 @@ const OPENROUTER_KEY_NAME = "OPENROUTER_API_KEY";
|
|
|
116
180
|
function openrouterApiKey() {
|
|
117
181
|
return castleKeys()[OPENROUTER_KEY_NAME] ?? process.env[OPENROUTER_KEY_NAME] ?? "";
|
|
118
182
|
}
|
|
183
|
+
const MODEL_CAPS_TTL_MS = 10 * 60_000;
|
|
184
|
+
const OPENROUTER_API_BASE = "https://openrouter.ai/api/v1";
|
|
185
|
+
const modelCapsCache = new Map();
|
|
186
|
+
let modelsListCache = null;
|
|
187
|
+
function asRecord(v) {
|
|
188
|
+
return v && typeof v === "object" ? v : null;
|
|
189
|
+
}
|
|
190
|
+
async function openrouterModelsById() {
|
|
191
|
+
if (modelsListCache && Date.now() - modelsListCache.at < MODEL_CAPS_TTL_MS) {
|
|
192
|
+
return modelsListCache.byId;
|
|
193
|
+
}
|
|
194
|
+
const res = await fetch(`${OPENROUTER_API_BASE}/models`);
|
|
195
|
+
if (!res.ok)
|
|
196
|
+
throw new Error(`models list HTTP ${res.status}`);
|
|
197
|
+
const json = asRecord(await res.json());
|
|
198
|
+
const data = json && Array.isArray(json.data) ? json.data : [];
|
|
199
|
+
const byId = new Map();
|
|
200
|
+
for (const entry of data) {
|
|
201
|
+
const rec = asRecord(entry);
|
|
202
|
+
const id = rec && typeof rec.id === "string" ? rec.id : null;
|
|
203
|
+
if (id)
|
|
204
|
+
byId.set(id, entry);
|
|
205
|
+
}
|
|
206
|
+
modelsListCache = { byId, at: Date.now() };
|
|
207
|
+
return byId;
|
|
208
|
+
}
|
|
209
|
+
async function openrouterProviderTiers(slug) {
|
|
210
|
+
const res = await fetch(`${OPENROUTER_API_BASE}/models/${slug}/endpoints`);
|
|
211
|
+
if (!res.ok)
|
|
212
|
+
return [];
|
|
213
|
+
const json = asRecord(await res.json());
|
|
214
|
+
const data = asRecord(json?.data);
|
|
215
|
+
const endpoints = data && Array.isArray(data.endpoints) ? data.endpoints : [];
|
|
216
|
+
const tags = [];
|
|
217
|
+
for (const ep of endpoints) {
|
|
218
|
+
const rec = asRecord(ep);
|
|
219
|
+
const tag = rec && typeof rec.tag === "string" ? rec.tag : null;
|
|
220
|
+
if (tag && !tags.includes(tag))
|
|
221
|
+
tags.push(tag);
|
|
222
|
+
}
|
|
223
|
+
return tags;
|
|
224
|
+
}
|
|
225
|
+
async function fetchModelCaps(slug) {
|
|
226
|
+
const cached = modelCapsCache.get(slug);
|
|
227
|
+
if (cached && Date.now() - cached.at < MODEL_CAPS_TTL_MS)
|
|
228
|
+
return cached.caps;
|
|
229
|
+
// Best-effort per source: a failure in either leaves that half empty rather
|
|
230
|
+
// than failing the whole lookup, so a bad slug still yields a usable (empty)
|
|
231
|
+
// caps object the client can render as "no dynamic options".
|
|
232
|
+
let reasoningEfforts = null;
|
|
233
|
+
let defaultEffort = null;
|
|
234
|
+
try {
|
|
235
|
+
const model = asRecord((await openrouterModelsById()).get(slug));
|
|
236
|
+
const reasoning = asRecord(model?.reasoning);
|
|
237
|
+
const efforts = reasoning?.supported_efforts;
|
|
238
|
+
const supportedParams = model?.supported_parameters;
|
|
239
|
+
const acceptsEffort = Array.isArray(supportedParams) &&
|
|
240
|
+
(supportedParams.includes("reasoning_effort") ||
|
|
241
|
+
supportedParams.includes("reasoning"));
|
|
242
|
+
if (acceptsEffort && Array.isArray(efforts) && efforts.length > 0) {
|
|
243
|
+
reasoningEfforts = efforts.filter((e) => typeof e === "string");
|
|
244
|
+
defaultEffort =
|
|
245
|
+
typeof reasoning?.default_effort === "string"
|
|
246
|
+
? reasoning.default_effort
|
|
247
|
+
: null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// leave reasoning fields null
|
|
252
|
+
}
|
|
253
|
+
let providerTiers = [];
|
|
254
|
+
try {
|
|
255
|
+
providerTiers = await openrouterProviderTiers(slug);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// leave providerTiers empty
|
|
259
|
+
}
|
|
260
|
+
const caps = {
|
|
261
|
+
model: slug,
|
|
262
|
+
reasoningEfforts,
|
|
263
|
+
defaultEffort,
|
|
264
|
+
providerTiers,
|
|
265
|
+
};
|
|
266
|
+
modelCapsCache.set(slug, { caps, at: Date.now() });
|
|
267
|
+
return caps;
|
|
268
|
+
}
|
|
269
|
+
// GET AGENT_MODEL_CAPS_PREFIX?model=<slug>. Returns 400 for a missing/oversized
|
|
270
|
+
// slug, 200 ModelCaps otherwise (empty caps on upstream failure -- see
|
|
271
|
+
// fetchModelCaps). reqPath is already query-stripped; parse req.url for it.
|
|
272
|
+
function handleModelCaps(req, res) {
|
|
273
|
+
const send = (status, body) => {
|
|
274
|
+
res.writeHead(status, {
|
|
275
|
+
"content-type": "application/json",
|
|
276
|
+
"cache-control": "no-store",
|
|
277
|
+
});
|
|
278
|
+
res.end(JSON.stringify(body));
|
|
279
|
+
return true;
|
|
280
|
+
};
|
|
281
|
+
let slug = "";
|
|
282
|
+
try {
|
|
283
|
+
slug = (new URL(req.url ?? "", "http://localhost").searchParams.get("model") ?? "").trim();
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
slug = "";
|
|
287
|
+
}
|
|
288
|
+
if (!slug || slug.length > OPENROUTER_MODEL_MAX_LEN) {
|
|
289
|
+
return send(400, { error: "missing or invalid model" });
|
|
290
|
+
}
|
|
291
|
+
// Strip any routing suffix the client may have on the displayed slug so the
|
|
292
|
+
// OpenRouter lookup hits the base model id.
|
|
293
|
+
const baseSlug = slug.replace(/:(nitro|exacto|floor)$/, "");
|
|
294
|
+
fetchModelCaps(baseSlug)
|
|
295
|
+
.then((caps) => send(200, caps))
|
|
296
|
+
.catch(() => send(200, {
|
|
297
|
+
model: baseSlug,
|
|
298
|
+
reasoningEfforts: null,
|
|
299
|
+
defaultEffort: null,
|
|
300
|
+
providerTiers: [],
|
|
301
|
+
}));
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
119
304
|
// Build the headless CLI invocation for a spawning backend/role (smith never
|
|
120
305
|
// comes through here -- it has no CLI process; see runAgentSmith). Cursor's
|
|
121
306
|
// router runs in read-only ask mode; claude runs permission-mode auto for
|
|
@@ -520,16 +705,31 @@ const BACKEND_KEY_ENV = {
|
|
|
520
705
|
claude: "ANTHROPIC_API_KEY",
|
|
521
706
|
cursor: "CURSOR_API_KEY",
|
|
522
707
|
};
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
708
|
+
// cursor-agent rewrites ~/.config/cursor/auth.json on every successful run,
|
|
709
|
+
// including Castle's own CURSOR_API_KEY runs -- so the file existing does NOT
|
|
710
|
+
// mean a user logged in. A real login (OAuth, or a tester's own key) overwrites
|
|
711
|
+
// it and drops Castle's apiKey; a file whose apiKey is still Castle's key is just
|
|
712
|
+
// our cache. Treating that cache as a login would suppress the injected key, and
|
|
713
|
+
// once its ~60-min token expires cursor-agent (no headless refresh) fails auth.
|
|
714
|
+
function cursorHasUserLogin(home) {
|
|
715
|
+
const authPath = path.join(home, ".config", "cursor", "auth.json");
|
|
716
|
+
try {
|
|
717
|
+
const auth = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
718
|
+
return auth.apiKey !== castleKeys().CURSOR_API_KEY;
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// True when the user has their OWN saved auth for this backend -- a login that we
|
|
725
|
+
// should defer to (and bill to them) instead of injecting Castle's key.
|
|
526
726
|
function backendHasSavedAuth(backend) {
|
|
527
727
|
const home = os.homedir();
|
|
528
728
|
if (backend === "claude") {
|
|
529
729
|
return fs.existsSync(path.join(home, ".claude", ".credentials.json"));
|
|
530
730
|
}
|
|
531
731
|
if (backend === "cursor") {
|
|
532
|
-
return
|
|
732
|
+
return cursorHasUserLogin(home);
|
|
533
733
|
}
|
|
534
734
|
return false;
|
|
535
735
|
}
|
|
@@ -787,137 +987,12 @@ function readQuickReference(deckDir) {
|
|
|
787
987
|
function readWelcomeMessage(deckDir) {
|
|
788
988
|
return readClaudeSection(deckDir, "Welcome message");
|
|
789
989
|
}
|
|
790
|
-
const TOUCHED_FILE_LIMIT = 10;
|
|
791
|
-
function collectStrings(value, out) {
|
|
792
|
-
if (typeof value === "string") {
|
|
793
|
-
out.push(value);
|
|
794
|
-
}
|
|
795
|
-
else if (Array.isArray(value)) {
|
|
796
|
-
for (const item of value)
|
|
797
|
-
collectStrings(item, out);
|
|
798
|
-
}
|
|
799
|
-
else if (value && typeof value === "object") {
|
|
800
|
-
for (const item of Object.values(value)) {
|
|
801
|
-
collectStrings(item, out);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
function toolWritesFiles(name) {
|
|
806
|
-
const kind = name.toLowerCase();
|
|
807
|
-
return ["edit", "write", "notebookedit", "multiedit", "delete"].some((p) => kind.startsWith(p));
|
|
808
|
-
}
|
|
809
|
-
function toolRunsShell(name) {
|
|
810
|
-
const kind = name.toLowerCase();
|
|
811
|
-
return (kind.startsWith("bash") ||
|
|
812
|
-
kind.startsWith("shell") ||
|
|
813
|
-
kind.includes("terminal"));
|
|
814
|
-
}
|
|
815
|
-
function drawingPathForDrawArg(raw) {
|
|
816
|
-
const name = raw.replace(/^['"]|['"]$/g, "").trim();
|
|
817
|
-
if (!name || name.startsWith("-") || name.includes("\n"))
|
|
818
|
-
return null;
|
|
819
|
-
if (name.startsWith("drawings/")) {
|
|
820
|
-
return name.endsWith(".pxart") ? name : `${name}.pxart`;
|
|
821
|
-
}
|
|
822
|
-
return `drawings/${name.endsWith(".pxart") ? name : `${name}.pxart`}`;
|
|
823
|
-
}
|
|
824
|
-
function shellTouchedCandidates(command) {
|
|
825
|
-
const out = [];
|
|
826
|
-
const redirectRe = /(?:^|[\s;|])(?:\d*)>>?\s*(?!&)(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))/g;
|
|
827
|
-
for (const match of command.matchAll(redirectRe)) {
|
|
828
|
-
const target = match[1] ?? match[2] ?? match[3];
|
|
829
|
-
if (target)
|
|
830
|
-
out.push(target);
|
|
831
|
-
}
|
|
832
|
-
const drawRe = /npm\s+run\s+draw\s+--\s+([^\s;&|]+)/g;
|
|
833
|
-
for (const match of command.matchAll(drawRe)) {
|
|
834
|
-
const drawing = drawingPathForDrawArg(match[1] ?? "");
|
|
835
|
-
if (drawing)
|
|
836
|
-
out.push(drawing);
|
|
837
|
-
}
|
|
838
|
-
return out;
|
|
839
|
-
}
|
|
840
|
-
// Guards every touched-file candidate (shell redirects AND tool file-path
|
|
841
|
-
// args) against junk that isn't plausibly a path. Added because
|
|
842
|
-
// shellTouchedCandidates' redirect regex treats any `>`-plus-token as a
|
|
843
|
-
// write target, so a command merely CONTAINING `>=` (e.g. a numeric
|
|
844
|
-
// comparison inside a quoted inline JS/awk script) false-matches as a
|
|
845
|
-
// redirect to "=" (or "=5" with no space around the `>=`) -- neither looks
|
|
846
|
-
// like a real file. A leading "-" is rejected too, mirroring
|
|
847
|
-
// drawingPathForDrawArg's flag guard above.
|
|
848
|
-
function looksLikeTouchedPath(raw) {
|
|
849
|
-
return /[a-zA-Z0-9]/.test(raw) && raw[0] !== "-" && raw[0] !== "=";
|
|
850
|
-
}
|
|
851
|
-
function normalizeTouchedPath(cwd, raw) {
|
|
852
|
-
if (!raw || raw.includes("\n") || !looksLikeTouchedPath(raw))
|
|
853
|
-
return null;
|
|
854
|
-
const abs = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
855
|
-
const rel = path.relative(cwd, abs);
|
|
856
|
-
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
857
|
-
return null;
|
|
858
|
-
const normalized = rel.split(path.sep).join("/");
|
|
859
|
-
if (normalized.startsWith(".castle/") || PROGRESS_FILE_RE.test(normalized)) {
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
return normalized;
|
|
863
|
-
}
|
|
864
|
-
function addTouchedFiles(files, cwd, toolName, input) {
|
|
865
|
-
const candidates = [];
|
|
866
|
-
if (toolWritesFiles(toolName)) {
|
|
867
|
-
collectStrings([
|
|
868
|
-
input.file_path,
|
|
869
|
-
input.path,
|
|
870
|
-
input.notebook_path,
|
|
871
|
-
input.old_path,
|
|
872
|
-
input.new_path,
|
|
873
|
-
], candidates);
|
|
874
|
-
}
|
|
875
|
-
else if (toolRunsShell(toolName) && typeof input.command === "string") {
|
|
876
|
-
candidates.push(...shellTouchedCandidates(input.command));
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
for (const candidate of candidates) {
|
|
882
|
-
const normalized = normalizeTouchedPath(cwd, candidate);
|
|
883
|
-
if (normalized)
|
|
884
|
-
files.add(normalized);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
function cursorToolNameAndInput(ev) {
|
|
888
|
-
const call = ev.tool_call;
|
|
889
|
-
const key = call ? Object.keys(call).find((k) => k.endsWith("ToolCall")) : undefined;
|
|
890
|
-
if (!call || !key)
|
|
891
|
-
return null;
|
|
892
|
-
const input = call[key];
|
|
893
|
-
const args = input && typeof input === "object"
|
|
894
|
-
? input.args
|
|
895
|
-
: undefined;
|
|
896
|
-
return {
|
|
897
|
-
name: key.slice(0, -"ToolCall".length),
|
|
898
|
-
input: args && typeof args === "object"
|
|
899
|
-
? args
|
|
900
|
-
: input && typeof input === "object"
|
|
901
|
-
? input
|
|
902
|
-
: {},
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
function touchedFileList(files) {
|
|
906
|
-
const sorted = [...files].sort();
|
|
907
|
-
if (sorted.length <= TOUCHED_FILE_LIMIT)
|
|
908
|
-
return sorted;
|
|
909
|
-
return [
|
|
910
|
-
...sorted.slice(0, TOUCHED_FILE_LIMIT),
|
|
911
|
-
`+${sorted.length - TOUCHED_FILE_LIMIT} more`,
|
|
912
|
-
];
|
|
913
|
-
}
|
|
914
990
|
function createAgentStreamState() {
|
|
915
991
|
return {
|
|
916
992
|
accumulated: "",
|
|
917
993
|
finalText: "",
|
|
918
994
|
resultIsError: false,
|
|
919
995
|
usage: undefined,
|
|
920
|
-
filesTouched: new Set(),
|
|
921
996
|
sawResult: false,
|
|
922
997
|
segmentText: "",
|
|
923
998
|
needsGap: false,
|
|
@@ -961,6 +1036,62 @@ function logAgentUsage(label, backend, usage) {
|
|
|
961
1036
|
const output = formatTokenCount(usage.output_tokens);
|
|
962
1037
|
console.error(`[agent usage] ${label} ${backend}: input=${input} cache_read=${read} cache_created=${created} output=${output}`);
|
|
963
1038
|
}
|
|
1039
|
+
// Per-deck machine-readable usage ledger, appended to <deckDir>/.castle/agent/,
|
|
1040
|
+
// harvested out-of-band (by the cloud launcher) for rough per-user token
|
|
1041
|
+
// metering. Distinct from logAgentUsage's stderr line, which is lossy
|
|
1042
|
+
// (rounds to "3.2k") and gets truncated when the serve restarts.
|
|
1043
|
+
const USAGE_LEDGER_FILE = "usage.jsonl";
|
|
1044
|
+
// The ledger is only useful where the cloud launcher harvests it, so the managed
|
|
1045
|
+
// sandbox environments set CASTLE_USAGE_LEDGER=1 (E2B via cloudSandbox.serveOnPort,
|
|
1046
|
+
// castle-sandboxes via its image). A local `castle-web serve` leaves it unset, so
|
|
1047
|
+
// dev deck dirs don't accumulate a ledger nothing reads.
|
|
1048
|
+
const USAGE_LEDGER_ENABLED = process.env.CASTLE_USAGE_LEDGER === "1";
|
|
1049
|
+
// Concrete model behind a finished run: smith and claude-via-OpenRouter both
|
|
1050
|
+
// bill the OpenRouter slug; plain claude bills its own slug; cursor has no
|
|
1051
|
+
// per-model split tracked here.
|
|
1052
|
+
function resolveRunModel(backend, claudeModel, openrouterModel) {
|
|
1053
|
+
if (backend === "smith")
|
|
1054
|
+
return openrouterModel;
|
|
1055
|
+
if (backend === "claude") {
|
|
1056
|
+
return claudeModel === "openrouter" ? openrouterModel : claudeModel;
|
|
1057
|
+
}
|
|
1058
|
+
return backend;
|
|
1059
|
+
}
|
|
1060
|
+
// One record per finished run: the human-readable stderr line PLUS a precise
|
|
1061
|
+
// append-only JSONL line in the deck's usage ledger. Precise counts (not the
|
|
1062
|
+
// stderr line's rounded values) and self-describing (id/role/backend/model) so
|
|
1063
|
+
// the harvester can attribute and de-dup. Best-effort: a metering write must
|
|
1064
|
+
// never fail an agent run.
|
|
1065
|
+
function reportRunUsage(agentDir, role, backend, claudeModel, openrouterModel, usage, taskId) {
|
|
1066
|
+
logAgentUsage(taskId ? `task ${taskId}` : "router", backend, usage);
|
|
1067
|
+
if (!USAGE_LEDGER_ENABLED)
|
|
1068
|
+
return;
|
|
1069
|
+
// cursor-agent's stream-json doesn't report token usage, so a cursor run has no
|
|
1070
|
+
// `usage`; still record it (zero counts, tokens_reported:false) for run-count
|
|
1071
|
+
// visibility. Other backends only write once they actually produced usage.
|
|
1072
|
+
if (!usage && backend !== "cursor")
|
|
1073
|
+
return;
|
|
1074
|
+
try {
|
|
1075
|
+
const line = JSON.stringify({
|
|
1076
|
+
at: nowIso(),
|
|
1077
|
+
id: nanoid(),
|
|
1078
|
+
role,
|
|
1079
|
+
backend,
|
|
1080
|
+
model: resolveRunModel(backend, claudeModel, openrouterModel),
|
|
1081
|
+
...(taskId ? { taskId } : {}),
|
|
1082
|
+
tokens_reported: usage !== undefined,
|
|
1083
|
+
input_tokens: usage?.input_tokens ?? 0,
|
|
1084
|
+
output_tokens: usage?.output_tokens ?? 0,
|
|
1085
|
+
cache_read_input_tokens: usage?.cache_read_input_tokens ?? 0,
|
|
1086
|
+
cache_creation_input_tokens: usage?.cache_creation_input_tokens ?? 0,
|
|
1087
|
+
});
|
|
1088
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
1089
|
+
fs.appendFileSync(path.join(agentDir, USAGE_LEDGER_FILE), line + "\n");
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
/* best-effort: a metering write must never fail an agent run */
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
964
1095
|
// Build the per-run stdout event handler over a shared mutable parser state.
|
|
965
1096
|
// Splitting the cursor + claude stream decoding out of runAgentCli keeps each
|
|
966
1097
|
// within the max-lines budget; behavior is identical (same delta/activity/
|
|
@@ -1043,7 +1174,6 @@ function makeAgentEventHandler(opts, state) {
|
|
|
1043
1174
|
catch {
|
|
1044
1175
|
/* input JSON arrived partial -- fall back to a generic label */
|
|
1045
1176
|
}
|
|
1046
|
-
addTouchedFiles(state.filesTouched, opts.cwd, pending.name, input);
|
|
1047
1177
|
const label = claudeToolFeedLabel(pending.name, input);
|
|
1048
1178
|
if (label)
|
|
1049
1179
|
opts.onActivity?.(label);
|
|
@@ -1078,9 +1208,6 @@ function makeAgentEventHandler(opts, state) {
|
|
|
1078
1208
|
else if (ev.type === "tool_call") {
|
|
1079
1209
|
state.segmentText = "";
|
|
1080
1210
|
state.needsGap = true;
|
|
1081
|
-
const tool = cursorToolNameAndInput(ev);
|
|
1082
|
-
if (tool)
|
|
1083
|
-
addTouchedFiles(state.filesTouched, opts.cwd, tool.name, tool.input);
|
|
1084
1211
|
if (ev.subtype === "started")
|
|
1085
1212
|
opts.onActivity?.(toolActivityLabel(ev));
|
|
1086
1213
|
}
|
|
@@ -1140,7 +1267,6 @@ function runAgentCli(opts) {
|
|
|
1140
1267
|
finalText: state.finalText || state.accumulated,
|
|
1141
1268
|
error: "agent run timed out",
|
|
1142
1269
|
usage: state.usage,
|
|
1143
|
-
filesTouched: touchedFileList(state.filesTouched),
|
|
1144
1270
|
});
|
|
1145
1271
|
}, opts.timeoutMs);
|
|
1146
1272
|
const handleEvent = makeAgentEventHandler(opts, state);
|
|
@@ -1178,7 +1304,6 @@ function runAgentCli(opts) {
|
|
|
1178
1304
|
ok,
|
|
1179
1305
|
finalText: state.finalText || state.accumulated,
|
|
1180
1306
|
usage: state.usage,
|
|
1181
|
-
filesTouched: touchedFileList(state.filesTouched),
|
|
1182
1307
|
crashed: !state.sawResult,
|
|
1183
1308
|
error: ok
|
|
1184
1309
|
? undefined
|
|
@@ -1189,9 +1314,9 @@ function runAgentCli(opts) {
|
|
|
1189
1314
|
}
|
|
1190
1315
|
// One smith (native castle agent) run, adapted to runAgentCli's contract so
|
|
1191
1316
|
// every caller downstream of runAgentTurn is backend-agnostic:
|
|
1192
|
-
// - NativeRunResult.text -> finalText; error/usage/
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1317
|
+
// - NativeRunResult.text -> finalText; error/usage/crashed pass through by
|
|
1318
|
+
// name. `ok` is derived as !error && !crashed -- there is no process exit
|
|
1319
|
+
// code; those two fields are the whole story.
|
|
1195
1320
|
// - Cancellation: one AbortController per run, registered in the same
|
|
1196
1321
|
// `children` set the CLI runs use, via a handle whose kill() aborts it
|
|
1197
1322
|
// (see AgentRunHandle). Interrupts (killRouterChildren), task halts
|
|
@@ -1225,6 +1350,10 @@ async function runAgentSmith(opts) {
|
|
|
1225
1350
|
role: opts.role,
|
|
1226
1351
|
model: opts.model,
|
|
1227
1352
|
apiKey: openrouterApiKey(),
|
|
1353
|
+
reasoningEffort: opts.openrouterTuning?.reasoningEffort,
|
|
1354
|
+
routing: opts.openrouterTuning?.routing,
|
|
1355
|
+
// "" (auto) becomes undefined so no provider.order is sent.
|
|
1356
|
+
providerTier: opts.openrouterTuning?.providerTier || undefined,
|
|
1228
1357
|
prompt: opts.prompt,
|
|
1229
1358
|
systemReminder: opts.systemReminder,
|
|
1230
1359
|
attachments: opts.attachments,
|
|
@@ -1242,7 +1371,6 @@ async function runAgentSmith(opts) {
|
|
|
1242
1371
|
finalText: result.text,
|
|
1243
1372
|
error: result.error,
|
|
1244
1373
|
usage: result.usage,
|
|
1245
|
-
filesTouched: result.filesTouched,
|
|
1246
1374
|
playtestFrames: result.playtestFrames,
|
|
1247
1375
|
crashed: result.crashed,
|
|
1248
1376
|
};
|
|
@@ -1276,6 +1404,7 @@ function runAgentTurn(opts) {
|
|
|
1276
1404
|
// appends it to its own system framing).
|
|
1277
1405
|
systemReminder: opts.role === "task" ? CLAUDE_TASK_SYSTEM_REMINDER : undefined,
|
|
1278
1406
|
attachments: opts.attachments,
|
|
1407
|
+
openrouterTuning: opts.openrouterTuning,
|
|
1279
1408
|
timeoutMs: opts.timeoutMs,
|
|
1280
1409
|
logPath: opts.logPath,
|
|
1281
1410
|
playtest: opts.playtest,
|
|
@@ -1407,10 +1536,6 @@ function depsSummaryFor(tasks, task) {
|
|
|
1407
1536
|
.filter((dep) => !!dep)
|
|
1408
1537
|
.map((dep) => {
|
|
1409
1538
|
const parts = [`- "${dep.title}" finished ${dep.status}`];
|
|
1410
|
-
if (dep.files && dep.files.length > 0)
|
|
1411
|
-
parts.push(` files it touched: ${dep.files.join(", ")}`);
|
|
1412
|
-
if (dep.suspectNoChanges)
|
|
1413
|
-
parts.push(" caution: it finished without touching any tracked files (bash side effects aren't tracked) -- verify its work actually landed before building on it");
|
|
1414
1539
|
// The agent's own closing prose is the real handoff -- names it created,
|
|
1415
1540
|
// what it wired, what it left undone. The notes file is player-facing
|
|
1416
1541
|
// and deliberately stripped of that detail.
|
|
@@ -1534,6 +1659,7 @@ async function runTaskAgentIn(ctx, task) {
|
|
|
1534
1659
|
prompt: taskPrompt,
|
|
1535
1660
|
claudeModel: ctx.claudeModel,
|
|
1536
1661
|
openrouterModel: ctx.openrouterModel,
|
|
1662
|
+
openrouterTuning: ctx.openrouterTuning,
|
|
1537
1663
|
cwd: ctx.deckDir,
|
|
1538
1664
|
timeoutMs: TASK_TIMEOUT_MS,
|
|
1539
1665
|
logPath: path.join(dir, "log.jsonl"),
|
|
@@ -1552,7 +1678,7 @@ async function runTaskAgentIn(ctx, task) {
|
|
|
1552
1678
|
ctx.onFeed(`[${activity}]`);
|
|
1553
1679
|
},
|
|
1554
1680
|
});
|
|
1555
|
-
|
|
1681
|
+
reportRunUsage(path.dirname(ctx.tasksDir), "task", ctx.backend, ctx.claudeModel, ctx.openrouterModel, result.usage, task.id);
|
|
1556
1682
|
if (ctx.stopRequested.has(task.id))
|
|
1557
1683
|
return result;
|
|
1558
1684
|
if (!result.crashed)
|
|
@@ -1587,6 +1713,7 @@ function startTask(ctx, task) {
|
|
|
1587
1713
|
backend: ctx.backend(),
|
|
1588
1714
|
claudeModel: ctx.claudeModel(),
|
|
1589
1715
|
openrouterModel: ctx.openrouterModel(),
|
|
1716
|
+
openrouterTuning: ctx.openrouterTuning(),
|
|
1590
1717
|
stopRequested: ctx.stopRequested,
|
|
1591
1718
|
quickReference: ctx.quickReference,
|
|
1592
1719
|
playtest: ctx.playtest,
|
|
@@ -1598,7 +1725,7 @@ function startTask(ctx, task) {
|
|
|
1598
1725
|
siblings: ctx
|
|
1599
1726
|
.sorted()
|
|
1600
1727
|
.filter((t) => t.id !== task.id && !(t.acknowledged && isTerminal(t.status)))
|
|
1601
|
-
.map((t) => ({ title: t.title, status: t.status
|
|
1728
|
+
.map((t) => ({ title: t.title, status: t.status })),
|
|
1602
1729
|
onFeed: (entry) => ctx.onFeed(task, entry),
|
|
1603
1730
|
onRetry: (attempt) => ctx.onRetry(task, attempt),
|
|
1604
1731
|
onSignal: (signal) => {
|
|
@@ -1638,12 +1765,7 @@ function startTask(ctx, task) {
|
|
|
1638
1765
|
task.acknowledged = true;
|
|
1639
1766
|
if (result.ok && !wasStopped)
|
|
1640
1767
|
task.progress = 100;
|
|
1641
|
-
task.files = result.filesTouched ?? [];
|
|
1642
1768
|
task.playtestFrames = result.playtestFrames ?? [];
|
|
1643
|
-
// Flag, don't fail: see the TaskRecord.suspectNoChanges comment.
|
|
1644
|
-
if (task.status === "done" && task.files.length === 0) {
|
|
1645
|
-
task.suspectNoChanges = true;
|
|
1646
|
-
}
|
|
1647
1769
|
task.finishedAt = nowIso();
|
|
1648
1770
|
task.resultSummary = wasStopped
|
|
1649
1771
|
? "stopped by the router"
|
|
@@ -1787,6 +1909,7 @@ function createTaskStore(opts) {
|
|
|
1787
1909
|
backend: opts.backend,
|
|
1788
1910
|
claudeModel: opts.claudeModel,
|
|
1789
1911
|
openrouterModel: opts.openrouterModel,
|
|
1912
|
+
openrouterTuning: opts.openrouterTuning,
|
|
1790
1913
|
playtest: opts.playtest,
|
|
1791
1914
|
restart: opts.restart,
|
|
1792
1915
|
onStarted: opts.onStarted,
|
|
@@ -1983,10 +2106,8 @@ function asPromptTask(task) {
|
|
|
1983
2106
|
status: task.rejected ? "rejected by user" : task.status,
|
|
1984
2107
|
progress: task.progress,
|
|
1985
2108
|
notes: task.notes,
|
|
1986
|
-
files: task.files,
|
|
1987
2109
|
error: task.status === "failed" ? firstErrorLine(task.resultSummary) : undefined,
|
|
1988
2110
|
blockedBy: task.status === "blocked" ? task.blockedBy : undefined,
|
|
1989
|
-
suspectNoChanges: task.suspectNoChanges,
|
|
1990
2111
|
};
|
|
1991
2112
|
}
|
|
1992
2113
|
function asClientTask(task) {
|
|
@@ -2004,7 +2125,6 @@ function asClientTask(task) {
|
|
|
2004
2125
|
phase: task.phase,
|
|
2005
2126
|
acknowledged: task.acknowledged,
|
|
2006
2127
|
rejected: task.rejected,
|
|
2007
|
-
suspectNoChanges: task.suspectNoChanges,
|
|
2008
2128
|
playtestFrames: (task.playtestFrames ?? []).map((rel) => `${AGENT_PLAYTEST_PREFIX}${task.id}/${path.basename(rel)}`),
|
|
2009
2129
|
};
|
|
2010
2130
|
}
|
|
@@ -2305,6 +2425,7 @@ function runRouterTurnIn(ctx, instruction, attachments = []) {
|
|
|
2305
2425
|
prompt,
|
|
2306
2426
|
claudeModel: ctx.claudeModel(),
|
|
2307
2427
|
openrouterModel: ctx.openrouterModel(),
|
|
2428
|
+
openrouterTuning: ctx.openrouterTuning(),
|
|
2308
2429
|
attachments,
|
|
2309
2430
|
cwd: ctx.deckDir,
|
|
2310
2431
|
timeoutMs: ROUTER_TIMEOUT_MS,
|
|
@@ -2337,7 +2458,7 @@ function runRouterTurnIn(ctx, instruction, attachments = []) {
|
|
|
2337
2458
|
},
|
|
2338
2459
|
})
|
|
2339
2460
|
.then((result) => {
|
|
2340
|
-
|
|
2461
|
+
reportRunUsage(ctx.agentDir, "router", backend, ctx.claudeModel(), ctx.openrouterModel(), result.usage);
|
|
2341
2462
|
// Signals the finally -> onSettled(retryable): the turn failed cleanly
|
|
2342
2463
|
// enough (transient, nothing salvaged) that the queue may re-run it.
|
|
2343
2464
|
let retryable = false;
|
|
@@ -2467,14 +2588,37 @@ function applyAgentSettings(incoming, ctx) {
|
|
|
2467
2588
|
const value = normalizeClaudeModel(incoming[key]);
|
|
2468
2589
|
if (value && value !== settings[key]) {
|
|
2469
2590
|
settings[key] = value;
|
|
2470
|
-
changes.push(`${key === "routerClaudeModel" ? "
|
|
2591
|
+
changes.push(`${key === "routerClaudeModel" ? "operator" : "tasks"} claude model -> ${value}`);
|
|
2471
2592
|
}
|
|
2472
2593
|
}
|
|
2473
2594
|
for (const key of ["routerOpenrouterModel", "tasksOpenrouterModel"]) {
|
|
2474
2595
|
const value = normalizeOpenrouterModel(incoming[key]);
|
|
2475
2596
|
if (value && value !== settings[key]) {
|
|
2476
2597
|
settings[key] = value;
|
|
2477
|
-
changes.push(`${key === "routerOpenrouterModel" ? "
|
|
2598
|
+
changes.push(`${key === "routerOpenrouterModel" ? "operator" : "tasks"} openrouter model -> ${value}`);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
for (const key of ["routerReasoningEffort", "tasksReasoningEffort"]) {
|
|
2602
|
+
const value = normalizeReasoningEffort(incoming[key]);
|
|
2603
|
+
if (value && value !== settings[key]) {
|
|
2604
|
+
settings[key] = value;
|
|
2605
|
+
changes.push(`${key === "routerReasoningEffort" ? "operator" : "tasks"} reasoning effort -> ${value}`);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
for (const key of ["routerRouting", "tasksRouting"]) {
|
|
2609
|
+
const value = normalizeRoutingMode(incoming[key]);
|
|
2610
|
+
if (value && value !== settings[key]) {
|
|
2611
|
+
settings[key] = value;
|
|
2612
|
+
changes.push(`${key === "routerRouting" ? "operator" : "tasks"} routing -> ${value}`);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
for (const key of ["routerProviderTier", "tasksProviderTier"]) {
|
|
2616
|
+
// "" is a valid value (auto), so check for null (invalid) explicitly
|
|
2617
|
+
// rather than truthiness -- otherwise the tier could never be cleared.
|
|
2618
|
+
const value = normalizeProviderTier(incoming[key]);
|
|
2619
|
+
if (value !== null && value !== settings[key]) {
|
|
2620
|
+
settings[key] = value;
|
|
2621
|
+
changes.push(`${key === "routerProviderTier" ? "operator" : "tasks"} provider tier -> ${value || "auto"}`);
|
|
2478
2622
|
}
|
|
2479
2623
|
}
|
|
2480
2624
|
if (changes.length === 0)
|
|
@@ -2639,6 +2783,11 @@ function startRouterTurn(ctx, instruction, attachments = []) {
|
|
|
2639
2783
|
backend: () => ctx.settings.router,
|
|
2640
2784
|
claudeModel: () => ctx.settings.routerClaudeModel,
|
|
2641
2785
|
openrouterModel: () => ctx.settings.routerOpenrouterModel,
|
|
2786
|
+
openrouterTuning: () => ({
|
|
2787
|
+
reasoningEffort: ctx.settings.routerReasoningEffort,
|
|
2788
|
+
routing: ctx.settings.routerRouting,
|
|
2789
|
+
providerTier: ctx.settings.routerProviderTier,
|
|
2790
|
+
}),
|
|
2642
2791
|
canAutoRetry: () => !ctx.state.autoRetryUsed && ctx.state.pendingSends.length === 0,
|
|
2643
2792
|
onSettled: (retryable) => onRouterQueueSettled(ctx, retryable),
|
|
2644
2793
|
}, instruction, attachments);
|
|
@@ -2928,6 +3077,20 @@ export function createAgentServer(opts) {
|
|
|
2928
3077
|
tasksOpenrouterModel: normalizeOpenrouterModel(storedSettings?.tasksOpenrouterModel) ??
|
|
2929
3078
|
legacyOpenrouterModel ??
|
|
2930
3079
|
DEFAULT_SETTINGS.tasksOpenrouterModel,
|
|
3080
|
+
routerReasoningEffort: normalizeReasoningEffort(storedSettings?.routerReasoningEffort) ??
|
|
3081
|
+
DEFAULT_SETTINGS.routerReasoningEffort,
|
|
3082
|
+
tasksReasoningEffort: normalizeReasoningEffort(storedSettings?.tasksReasoningEffort) ??
|
|
3083
|
+
DEFAULT_SETTINGS.tasksReasoningEffort,
|
|
3084
|
+
routerRouting: normalizeRoutingMode(storedSettings?.routerRouting) ??
|
|
3085
|
+
DEFAULT_SETTINGS.routerRouting,
|
|
3086
|
+
tasksRouting: normalizeRoutingMode(storedSettings?.tasksRouting) ??
|
|
3087
|
+
DEFAULT_SETTINGS.tasksRouting,
|
|
3088
|
+
// Provider tier: "" is valid (auto), so keep a normalized "" over the
|
|
3089
|
+
// default rather than treating it as absent.
|
|
3090
|
+
routerProviderTier: normalizeProviderTier(storedSettings?.routerProviderTier) ??
|
|
3091
|
+
DEFAULT_SETTINGS.routerProviderTier,
|
|
3092
|
+
tasksProviderTier: normalizeProviderTier(storedSettings?.tasksProviderTier) ??
|
|
3093
|
+
DEFAULT_SETTINGS.tasksProviderTier,
|
|
2931
3094
|
};
|
|
2932
3095
|
const applySettings = (incoming) => applyAgentSettings(incoming, { settings, settingsPath, broadcast });
|
|
2933
3096
|
const taskFeeds = createTaskFeeds(broadcast);
|
|
@@ -2941,6 +3104,11 @@ export function createAgentServer(opts) {
|
|
|
2941
3104
|
backend: () => settings.tasks,
|
|
2942
3105
|
openrouterModel: () => settings.tasksOpenrouterModel,
|
|
2943
3106
|
claudeModel: () => settings.tasksClaudeModel,
|
|
3107
|
+
openrouterTuning: () => ({
|
|
3108
|
+
reasoningEffort: settings.tasksReasoningEffort,
|
|
3109
|
+
routing: settings.tasksRouting,
|
|
3110
|
+
providerTier: settings.tasksProviderTier,
|
|
3111
|
+
}),
|
|
2944
3112
|
playtest,
|
|
2945
3113
|
restart: opts.restart,
|
|
2946
3114
|
// Task lifecycle stays on the board only -- log lines for it were spam.
|
|
@@ -3040,6 +3208,8 @@ export function createAgentServer(opts) {
|
|
|
3040
3208
|
const handleAttachment = makeAttachmentHandler(attachmentsDir);
|
|
3041
3209
|
const handlePlaytestFrame = makePlaytestFrameHandler(tasksDir);
|
|
3042
3210
|
function handleHttpRequest(req, res, reqPath) {
|
|
3211
|
+
if (reqPath === AGENT_MODEL_CAPS_PREFIX)
|
|
3212
|
+
return handleModelCaps(req, res);
|
|
3043
3213
|
return handleAttachment(req, res, reqPath) || handlePlaytestFrame(req, res, reqPath);
|
|
3044
3214
|
}
|
|
3045
3215
|
function shutdown() {
|