castle-web-cli 0.4.82 → 0.4.84
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-failures.d.ts +17 -0
- package/dist/agent-failures.js +151 -0
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +614 -57
- package/dist/ide.js +150 -1
- package/dist/native/loop.js +40 -1
- package/dist/native/openrouter.d.ts +12 -1
- package/dist/native/openrouter.js +45 -2
- package/dist/native/types.d.ts +6 -0
- package/dist/native/types.js +0 -38
- package/dist/openrouter-catalog.d.ts +28 -0
- package/dist/openrouter-catalog.js +299 -0
- package/dist/shell/assets/index-BOgm5T3W.js +144 -0
- package/dist/shell/assets/index-DonnH--m.css +1 -0
- package/dist/shell/index.html +2 -2
- package/dist/shell/operator.png +0 -0
- package/kits/basic-2d/CLAUDE.md +27 -23
- 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 +98 -7
- package/kits/basic-2d/engine/autoInspector.jsx +26 -7
- package/kits/basic-2d/engine/blueprint.js +35 -8
- 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-ByhgiJoP.js +0 -141
- package/dist/shell/assets/index-D6hM_VlW.css +0 -1
package/dist/ide.js
CHANGED
|
@@ -268,6 +268,136 @@ function handleFilesWrite(deckDir, req, res) {
|
|
|
268
268
|
}
|
|
269
269
|
})();
|
|
270
270
|
}
|
|
271
|
+
// Add `<rel>/**` to the deck's editor.visiblePaths so files created in a
|
|
272
|
+
// newly-made folder show in the curated Files tree. Only touches a deck that is
|
|
273
|
+
// ALREADY curated (non-empty visiblePaths) -- when visiblePaths is empty
|
|
274
|
+
// everything is visible, and adding a glob would wrongly start hiding things.
|
|
275
|
+
// No-op if an existing glob already covers the folder. Returns whether it wrote.
|
|
276
|
+
function ensureVisiblePath(deckDir, rel) {
|
|
277
|
+
const file = path.join(deckDir, "castle.json");
|
|
278
|
+
let data;
|
|
279
|
+
try {
|
|
280
|
+
data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return false; // no castle.json yet (deck never saved) -> treat as not curated
|
|
284
|
+
}
|
|
285
|
+
const visible = data.editor && Array.isArray(data.editor.visiblePaths)
|
|
286
|
+
? data.editor.visiblePaths.filter((v) => typeof v === "string")
|
|
287
|
+
: null;
|
|
288
|
+
if (!visible || visible.length === 0)
|
|
289
|
+
return false; // not curated -> all visible
|
|
290
|
+
const glob = `${rel}/**`;
|
|
291
|
+
if (visible.includes(glob))
|
|
292
|
+
return false;
|
|
293
|
+
if (picomatch(visible)(`${rel}/__probe__`))
|
|
294
|
+
return false; // already covered
|
|
295
|
+
visible.push(glob);
|
|
296
|
+
data.editor.visiblePaths = visible;
|
|
297
|
+
try {
|
|
298
|
+
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function handleFilesMkdir(deckDir, req, res) {
|
|
306
|
+
void (async () => {
|
|
307
|
+
let body;
|
|
308
|
+
try {
|
|
309
|
+
body = JSON.parse(await readRequestBody(req));
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
313
|
+
}
|
|
314
|
+
const resolved = resolveDeckPath(deckDir, body.path);
|
|
315
|
+
if (!resolved.ok)
|
|
316
|
+
return sendJson(res, 400, { error: resolved.error });
|
|
317
|
+
try {
|
|
318
|
+
fs.mkdirSync(resolved.abs, { recursive: true });
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
322
|
+
return sendJson(res, 500, { error: `Could not create folder ${resolved.rel}: ${message}` });
|
|
323
|
+
}
|
|
324
|
+
const visiblePathAdded = ensureVisiblePath(deckDir, resolved.rel);
|
|
325
|
+
sendJson(res, 200, { ok: true, path: resolved.rel, visiblePathAdded });
|
|
326
|
+
})();
|
|
327
|
+
}
|
|
328
|
+
// True when two paths resolve to the same underlying file (same inode+device) --
|
|
329
|
+
// e.g. the source and target of a case-only rename on a case-insensitive FS.
|
|
330
|
+
function isSameFile(a, b) {
|
|
331
|
+
try {
|
|
332
|
+
const sa = fs.statSync(a);
|
|
333
|
+
const sb = fs.statSync(b);
|
|
334
|
+
return sa.ino === sb.ino && sa.dev === sb.dev;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function handleFilesRename(deckDir, req, res) {
|
|
341
|
+
void (async () => {
|
|
342
|
+
let body;
|
|
343
|
+
try {
|
|
344
|
+
body = JSON.parse(await readRequestBody(req));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
348
|
+
}
|
|
349
|
+
const from = resolveDeckPath(deckDir, body.from);
|
|
350
|
+
if (!from.ok)
|
|
351
|
+
return sendJson(res, 400, { error: from.error });
|
|
352
|
+
const to = resolveDeckPath(deckDir, body.to);
|
|
353
|
+
if (!to.ok)
|
|
354
|
+
return sendJson(res, 400, { error: to.error });
|
|
355
|
+
if (!fs.existsSync(from.abs)) {
|
|
356
|
+
return sendJson(res, 404, { error: `Not found: ${from.rel}` });
|
|
357
|
+
}
|
|
358
|
+
// Block a collision with a DIFFERENT existing file. On a case-insensitive
|
|
359
|
+
// filesystem (default on macOS/Windows) `to` can "exist" only because it is
|
|
360
|
+
// `from` under a different case -- a case-only rename like bounce.jsx ->
|
|
361
|
+
// Bounce.jsx. Allow that by treating same-inode as not-a-collision.
|
|
362
|
+
if (from.abs !== to.abs && fs.existsSync(to.abs) && !isSameFile(from.abs, to.abs)) {
|
|
363
|
+
return sendJson(res, 409, { error: `Already exists: ${to.rel}` });
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
fs.mkdirSync(path.dirname(to.abs), { recursive: true });
|
|
367
|
+
fs.renameSync(from.abs, to.abs);
|
|
368
|
+
sendJson(res, 200, { ok: true, path: to.rel });
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
372
|
+
sendJson(res, 500, { error: `Could not rename ${from.rel}: ${message}` });
|
|
373
|
+
}
|
|
374
|
+
})();
|
|
375
|
+
}
|
|
376
|
+
function handleFilesDelete(deckDir, req, res) {
|
|
377
|
+
void (async () => {
|
|
378
|
+
let body;
|
|
379
|
+
try {
|
|
380
|
+
body = JSON.parse(await readRequestBody(req));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
384
|
+
}
|
|
385
|
+
const resolved = resolveDeckPath(deckDir, body.path);
|
|
386
|
+
if (!resolved.ok)
|
|
387
|
+
return sendJson(res, 400, { error: resolved.error });
|
|
388
|
+
if (!fs.existsSync(resolved.abs)) {
|
|
389
|
+
return sendJson(res, 404, { error: `Not found: ${resolved.rel}` });
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
fs.rmSync(resolved.abs, { recursive: true, force: true });
|
|
393
|
+
sendJson(res, 200, { ok: true, path: resolved.rel });
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
397
|
+
sendJson(res, 500, { error: `Could not delete ${resolved.rel}: ${message}` });
|
|
398
|
+
}
|
|
399
|
+
})();
|
|
400
|
+
}
|
|
271
401
|
// The builtin Files + code-editor backend: list / read / write deck files and
|
|
272
402
|
// report kit-owned editor extensions. Paths are deck-relative; resolveDeckPath
|
|
273
403
|
// rejects traversal and protected dirs.
|
|
@@ -286,7 +416,14 @@ function handleFilesApi(deckDir, req, res, reqPath) {
|
|
|
286
416
|
return true;
|
|
287
417
|
}
|
|
288
418
|
if (action === "list") {
|
|
289
|
-
|
|
419
|
+
// `?all=1` returns the unfiltered listing (the "show hidden files & folders"
|
|
420
|
+
// toggle) -- still minus the always-ignored dirs (node_modules/.castle/...),
|
|
421
|
+
// just without the deck's visible/hidden path curation.
|
|
422
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
423
|
+
const listed = listDeckFiles(deckDir);
|
|
424
|
+
const files = url.searchParams.get("all") === "1"
|
|
425
|
+
? listed
|
|
426
|
+
: filterDeckFiles(listed, readEditorConfig(deckDir));
|
|
290
427
|
sendJson(res, 200, { files });
|
|
291
428
|
return true;
|
|
292
429
|
}
|
|
@@ -308,6 +445,18 @@ function handleFilesApi(deckDir, req, res, reqPath) {
|
|
|
308
445
|
handleFilesWrite(deckDir, req, res);
|
|
309
446
|
return true;
|
|
310
447
|
}
|
|
448
|
+
if (action === "rename") {
|
|
449
|
+
handleFilesRename(deckDir, req, res);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
if (action === "delete") {
|
|
453
|
+
handleFilesDelete(deckDir, req, res);
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
if (action === "mkdir") {
|
|
457
|
+
handleFilesMkdir(deckDir, req, res);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
311
460
|
return sendJson(res, 404, { error: `Unknown files action: ${action}` }), true;
|
|
312
461
|
}
|
|
313
462
|
function defaultShell() {
|
package/dist/native/loop.js
CHANGED
|
@@ -565,6 +565,10 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
565
565
|
const imageLabels = new Map();
|
|
566
566
|
let finalText = "";
|
|
567
567
|
let totalUsage;
|
|
568
|
+
// Whether this run has called ANY tool, across every iteration -- not just
|
|
569
|
+
// the current one. Scoped to the run because the question is "did this task
|
|
570
|
+
// do anything at all", which one iteration can't answer.
|
|
571
|
+
let usedAnyTool = false;
|
|
568
572
|
// Once our own timeout/external-abort fires, whatever streamChatCompletion
|
|
569
573
|
// reports is moot -- the abort IS the reason for stopping, so we always
|
|
570
574
|
// attribute the final error to it (rather than a network error that might
|
|
@@ -598,7 +602,11 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
598
602
|
model: opts.model,
|
|
599
603
|
messages,
|
|
600
604
|
tools: toolSchemas,
|
|
601
|
-
|
|
605
|
+
// Settings-driven per-role effort; falls back to the built-in table
|
|
606
|
+
// when a caller doesn't supply one (e.g. the QA harness).
|
|
607
|
+
reasoningEffort: opts.reasoningEffort ?? REASONING_EFFORT[opts.role],
|
|
608
|
+
routing: opts.routing,
|
|
609
|
+
providerTier: opts.providerTier,
|
|
602
610
|
maxTokens: MAX_COMPLETION_TOKENS,
|
|
603
611
|
signal: controller.signal,
|
|
604
612
|
onDelta: opts.onDelta,
|
|
@@ -631,6 +639,9 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
631
639
|
return {
|
|
632
640
|
text: finalText,
|
|
633
641
|
error: streamResult.error ?? "openrouter stream ended without a final response",
|
|
642
|
+
// Present only for an HTTP status streamChat could label; absent for
|
|
643
|
+
// a dropped connection, which stays unclassified on purpose.
|
|
644
|
+
failure: streamResult.failure,
|
|
634
645
|
usage: totalUsage,
|
|
635
646
|
playtestFrames: playtestFrameList(playtestFrames),
|
|
636
647
|
crashed: streamResult.crashed,
|
|
@@ -653,12 +664,40 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
653
664
|
: {}),
|
|
654
665
|
});
|
|
655
666
|
if (toolCalls.length === 0) {
|
|
667
|
+
// No tool calls is the normal way a run ends -- the model is done and
|
|
668
|
+
// signing off. But a TASK that reaches this on iteration 1, having
|
|
669
|
+
// never called a single tool, did no work: it chatted. That used to
|
|
670
|
+
// return a bare success, so the card finalized "done" at 100% with
|
|
671
|
+
// nothing changed -- the loudest silent failure this backend had.
|
|
672
|
+
//
|
|
673
|
+
// Only tasks: the router legitimately answers in plain text, and a
|
|
674
|
+
// task that used tools and THEN signs off with prose is fine.
|
|
675
|
+
//
|
|
676
|
+
// This narrows the failure class rather than closing it: a task whose
|
|
677
|
+
// only tool calls were reads still counts as "used a tool" here and
|
|
678
|
+
// can still finish having changed nothing. Detecting that needs a
|
|
679
|
+
// notion of which tools mutate, which is a bigger change than this.
|
|
680
|
+
if (opts.role === "task" && !usedAnyTool) {
|
|
681
|
+
return {
|
|
682
|
+
text: finalText,
|
|
683
|
+
error: "the model replied without calling any tools, so nothing was done",
|
|
684
|
+
failure: {
|
|
685
|
+
kind: "no-work",
|
|
686
|
+
detail: "model returned prose with no tool calls",
|
|
687
|
+
model: opts.model,
|
|
688
|
+
verbose: finalText,
|
|
689
|
+
},
|
|
690
|
+
usage: totalUsage,
|
|
691
|
+
playtestFrames: playtestFrameList(playtestFrames),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
656
694
|
return {
|
|
657
695
|
text: finalText,
|
|
658
696
|
usage: totalUsage,
|
|
659
697
|
playtestFrames: playtestFrameList(playtestFrames),
|
|
660
698
|
};
|
|
661
699
|
}
|
|
700
|
+
usedAnyTool = true;
|
|
662
701
|
messages.push({
|
|
663
702
|
role: "assistant",
|
|
664
703
|
content: streamResult.message.content || null,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AgentFailure } from "../agent-failures.js";
|
|
1
2
|
import type { NativeUsage } from "./types.js";
|
|
2
3
|
export interface ORToolCall {
|
|
3
4
|
id: string;
|
|
@@ -27,13 +28,17 @@ export interface ORAssistantMessage {
|
|
|
27
28
|
content: string;
|
|
28
29
|
tool_calls?: ORToolCall[];
|
|
29
30
|
}
|
|
30
|
-
export type ORReasoningEffort = "low" | "medium";
|
|
31
|
+
export type ORReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
32
|
+
export type ORRoutingMode = "balanced" | "nitro" | "exacto" | "floor";
|
|
33
|
+
export declare function applyRoutingMode(model: string, routing?: ORRoutingMode): string;
|
|
31
34
|
export interface StreamChatOpts {
|
|
32
35
|
apiKey: string;
|
|
33
36
|
model: string;
|
|
34
37
|
messages: ORMessage[];
|
|
35
38
|
tools?: unknown[];
|
|
36
39
|
reasoningEffort?: ORReasoningEffort;
|
|
40
|
+
routing?: ORRoutingMode;
|
|
41
|
+
providerTier?: string;
|
|
37
42
|
maxTokens?: number;
|
|
38
43
|
signal?: AbortSignal;
|
|
39
44
|
onDelta?: (delta: string) => void;
|
|
@@ -51,5 +56,11 @@ export interface StreamChatResult {
|
|
|
51
56
|
reasoningTokens?: number;
|
|
52
57
|
crashed: boolean;
|
|
53
58
|
error?: string;
|
|
59
|
+
failure?: AgentFailure;
|
|
60
|
+
}
|
|
61
|
+
export declare class OpenrouterHttpError extends Error {
|
|
62
|
+
readonly status: number;
|
|
63
|
+
readonly body: string;
|
|
64
|
+
constructor(status: number, body: string);
|
|
54
65
|
}
|
|
55
66
|
export declare function streamChatCompletion(opts: StreamChatOpts): Promise<StreamChatResult>;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// the model already said, which the caller (native/loop.ts) is better placed
|
|
14
14
|
// to decide (or simply not do, matching today's crash-then-relaunch policy at
|
|
15
15
|
// the task-attempt level).
|
|
16
|
+
import { failureForStatus } from "../agent-failures.js";
|
|
16
17
|
// Overridable via CASTLE_OPENROUTER_URL so the QA harness (see
|
|
17
18
|
// scripts/tests/agent-qa/native/fake-openrouter.mjs) can point this client at
|
|
18
19
|
// a local fake server instead of the real API -- read per-call (not hoisted
|
|
@@ -24,6 +25,20 @@ function openrouterUrl() {
|
|
|
24
25
|
const DEFAULT_MAX_RETRIES = 2; // -> 3 total connect attempts
|
|
25
26
|
const RETRY_BASE_MS = 500;
|
|
26
27
|
const RETRY_MAX_MS = 4_000;
|
|
28
|
+
const ROUTING_SUFFIX = {
|
|
29
|
+
nitro: ":nitro",
|
|
30
|
+
exacto: ":exacto",
|
|
31
|
+
floor: ":floor",
|
|
32
|
+
};
|
|
33
|
+
// Append the routing-mode suffix to a model slug, first stripping any suffix
|
|
34
|
+
// we might have added on a previous turn (or that the user typed into the
|
|
35
|
+
// free-form slug field) so switching modes doesn't stack `:nitro:floor`.
|
|
36
|
+
export function applyRoutingMode(model, routing) {
|
|
37
|
+
const base = model.replace(/:(nitro|exacto|floor)$/, "");
|
|
38
|
+
if (!routing || routing === "balanced")
|
|
39
|
+
return base;
|
|
40
|
+
return base + ROUTING_SUFFIX[routing];
|
|
41
|
+
}
|
|
27
42
|
function sleep(ms) {
|
|
28
43
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
44
|
}
|
|
@@ -34,6 +49,22 @@ function backoffMs(attempt) {
|
|
|
34
49
|
function isRetryableStatus(status) {
|
|
35
50
|
return status === 429 || status >= 500;
|
|
36
51
|
}
|
|
52
|
+
// A non-retryable HTTP error from OpenRouter, carrying the status as a NUMBER
|
|
53
|
+
// rather than only baked into a message. This path knows the status
|
|
54
|
+
// first-hand, so classification upstream is a lookup instead of a regex over
|
|
55
|
+
// our own error prose (the claude-CLI path has no such luxury -- see
|
|
56
|
+
// classifyProviderError). `message` keeps the old "HTTP <status>: <body>"
|
|
57
|
+
// wording so logs and any string-matching callers read the same as before.
|
|
58
|
+
export class OpenrouterHttpError extends Error {
|
|
59
|
+
status;
|
|
60
|
+
body;
|
|
61
|
+
constructor(status, body) {
|
|
62
|
+
super(`HTTP ${status}${body ? `: ${body.slice(0, 300)}` : ""}`);
|
|
63
|
+
this.status = status;
|
|
64
|
+
this.body = body;
|
|
65
|
+
this.name = "OpenrouterHttpError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
37
68
|
async function safeReadText(res) {
|
|
38
69
|
try {
|
|
39
70
|
return (await res.text()).trim();
|
|
@@ -79,7 +110,7 @@ async function connectWithRetry(init, opts) {
|
|
|
79
110
|
continue;
|
|
80
111
|
}
|
|
81
112
|
const bodyText = await safeReadText(res);
|
|
82
|
-
throw new
|
|
113
|
+
throw new OpenrouterHttpError(res.status, bodyText);
|
|
83
114
|
}
|
|
84
115
|
// Unreachable (the loop above always returns or throws), but keeps the
|
|
85
116
|
// return type honest for TS.
|
|
@@ -188,7 +219,8 @@ function finalizeToolCalls(pending) {
|
|
|
188
219
|
}
|
|
189
220
|
export async function streamChatCompletion(opts) {
|
|
190
221
|
const body = {
|
|
191
|
-
|
|
222
|
+
// Routing mode rides the slug as a suffix (see applyRoutingMode).
|
|
223
|
+
model: applyRoutingMode(opts.model, opts.routing),
|
|
192
224
|
messages: opts.messages,
|
|
193
225
|
stream: true,
|
|
194
226
|
// Deprecated on OpenRouter's side (usage is always included now) but
|
|
@@ -199,6 +231,10 @@ export async function streamChatCompletion(opts) {
|
|
|
199
231
|
// See ORReasoningEffort / StreamChatOpts.reasoningEffort above for the
|
|
200
232
|
// doc reference and the graceful-degradation guarantee this relies on.
|
|
201
233
|
...(opts.reasoningEffort ? { reasoning: { effort: opts.reasoningEffort } } : {}),
|
|
234
|
+
// Pin a provider tier when requested (see StreamChatOpts.providerTier).
|
|
235
|
+
...(opts.providerTier
|
|
236
|
+
? { provider: { order: [opts.providerTier], allow_fallbacks: true } }
|
|
237
|
+
: {}),
|
|
202
238
|
// See StreamChatOpts.maxTokens above -- bounds one completion call's
|
|
203
239
|
// total output (content + tool-call arguments + reasoning).
|
|
204
240
|
...(opts.maxTokens !== undefined ? { max_tokens: opts.maxTokens } : {}),
|
|
@@ -223,6 +259,13 @@ export async function streamChatCompletion(opts) {
|
|
|
223
259
|
message: null,
|
|
224
260
|
crashed: false,
|
|
225
261
|
error: `could not run openrouter: ${err instanceof Error ? err.message : String(err)}`,
|
|
262
|
+
// Only an HTTP error carries a status worth classifying. A network-level
|
|
263
|
+
// failure (DNS, refused, reset) stays unlabelled so it keeps landing in
|
|
264
|
+
// the pre-existing "spawn" bucket via the "could not run" prefix --
|
|
265
|
+
// that's already the right read: the request never got going.
|
|
266
|
+
failure: err instanceof OpenrouterHttpError
|
|
267
|
+
? failureForStatus(err.status, err.body)
|
|
268
|
+
: undefined,
|
|
226
269
|
};
|
|
227
270
|
}
|
|
228
271
|
if (!res.body) {
|
package/dist/native/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { AgentFailure } from "../agent-failures.js";
|
|
1
2
|
import type { PlaytestExecutor } from "./playtest.js";
|
|
3
|
+
import type { ORReasoningEffort, ORRoutingMode } from "./openrouter.js";
|
|
2
4
|
export type NativeRole = "router" | "task";
|
|
3
5
|
export interface NativePlaytestOpts {
|
|
4
6
|
executor: PlaytestExecutor;
|
|
@@ -16,6 +18,9 @@ export interface NativeRunOpts {
|
|
|
16
18
|
role: NativeRole;
|
|
17
19
|
model: string;
|
|
18
20
|
apiKey: string;
|
|
21
|
+
reasoningEffort?: ORReasoningEffort;
|
|
22
|
+
routing?: ORRoutingMode;
|
|
23
|
+
providerTier?: string;
|
|
19
24
|
prompt: string;
|
|
20
25
|
systemReminder?: string;
|
|
21
26
|
attachments?: string[];
|
|
@@ -33,6 +38,7 @@ export interface NativeRunOpts {
|
|
|
33
38
|
export interface NativeRunResult {
|
|
34
39
|
text: string;
|
|
35
40
|
error?: string;
|
|
41
|
+
failure?: AgentFailure;
|
|
36
42
|
usage?: NativeUsage;
|
|
37
43
|
playtestFrames?: string[];
|
|
38
44
|
crashed?: boolean;
|
package/dist/native/types.js
CHANGED
|
@@ -1,41 +1,3 @@
|
|
|
1
1
|
// Shapes for the native (in-process, OpenRouter-backed) agent backend --
|
|
2
2
|
// see the plan at native_openrouter_agent_loop_fd7d9b3e.plan.md, "Path B".
|
|
3
|
-
//
|
|
4
|
-
// These MIRROR the non-exported CliRunOpts / CliRunResult / CliUsage
|
|
5
|
-
// interfaces in cli/src/agent.ts (see runAgentCli there), which normalize
|
|
6
|
-
// the headless `claude` / `cursor-agent` CLI backends into onDelta/onActivity/
|
|
7
|
-
// onThinking hooks plus a { finalText, error, usage, crashed } result. This
|
|
8
|
-
// file is a SEPARATE set of types for now (new-files-only
|
|
9
|
-
// constraint -- agent.ts is being edited concurrently by a sibling change),
|
|
10
|
-
// not a re-export, so field names differ in a few places. When the native
|
|
11
|
-
// backend is wired into agent.ts, expect one of these two outcomes:
|
|
12
|
-
//
|
|
13
|
-
// 1. CliRunOpts/CliRunResult are widened to a shape both backends share
|
|
14
|
-
// (command/args/parser and children become backend-specific extras), or
|
|
15
|
-
// 2. the call site maps between the two shapes directly:
|
|
16
|
-
// - NativeRunResult.text -> CliRunResult.finalText
|
|
17
|
-
// - NativeRunResult.error/usage/crashed -> same names
|
|
18
|
-
// - CliRunResult.ok <- derived as
|
|
19
|
-
// `!result.error && !result.crashed` (no native equivalent of
|
|
20
|
-
// a process exit code -- "ok" always follows from the other
|
|
21
|
-
// two fields)
|
|
22
|
-
// - CliRunOpts.children (Set<ChildProcess>) has no native
|
|
23
|
-
// equivalent -- runAgentNative takes `signal` (AbortSignal)
|
|
24
|
-
// instead, so a caller cancels a run the same way it would abort
|
|
25
|
-
// any other fetch-based operation. The wiring step would create one
|
|
26
|
-
// AbortController per run and adapt it to whatever cancellation
|
|
27
|
-
// registry agent.ts uses for CLI children.
|
|
28
|
-
// - CliRunOpts.command/args/parser have no native equivalent --
|
|
29
|
-
// replaced by `model` (the free-form OpenRouter model id) and
|
|
30
|
-
// `apiKey`.
|
|
31
|
-
// - CliRunOpts.logPath maps to NativeRunOpts.logPath: same file
|
|
32
|
-
// locations (tasks/<id>/log.jsonl, .castle/agent/router-log.jsonl),
|
|
33
|
-
// same append-JSONL discipline, but the native lines are STRUCTURED
|
|
34
|
-
// run events (init/assistant/tool_result/eviction/retry/result --
|
|
35
|
-
// see createRunLogger in loop.ts) rather than raw CLI stream-json.
|
|
36
|
-
//
|
|
37
|
-
// Turns stay stateless exactly as they are today: the caller rebuilds one big
|
|
38
|
-
// prompt string per turn (buildRouterPrompt/buildTaskPrompt) and passes it as
|
|
39
|
-
// `prompt`; runAgentNative holds the resulting message array only for the
|
|
40
|
-
// lifetime of that one call.
|
|
41
3
|
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface CatalogEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
supportedParameters: string[];
|
|
4
|
+
reasoning: unknown;
|
|
5
|
+
}
|
|
6
|
+
export type ModelCheck = {
|
|
7
|
+
status: "ok";
|
|
8
|
+
} | {
|
|
9
|
+
status: "unknown-model";
|
|
10
|
+
suggestions: string[];
|
|
11
|
+
} | {
|
|
12
|
+
status: "no-tools";
|
|
13
|
+
} | {
|
|
14
|
+
status: "unavailable";
|
|
15
|
+
};
|
|
16
|
+
export declare function primeOpenrouterCatalog(): void;
|
|
17
|
+
export type KeyCheck = {
|
|
18
|
+
status: "ok";
|
|
19
|
+
} | {
|
|
20
|
+
status: "bad-key";
|
|
21
|
+
} | {
|
|
22
|
+
status: "no-credits";
|
|
23
|
+
} | {
|
|
24
|
+
status: "unavailable";
|
|
25
|
+
};
|
|
26
|
+
export declare function checkOpenrouterKey(apiKey: string): Promise<KeyCheck>;
|
|
27
|
+
export declare function checkOpenrouterModel(slug: string): Promise<ModelCheck>;
|
|
28
|
+
export declare function openrouterCatalogEntry(slug: string): Promise<CatalogEntry | null>;
|