@sagentlab/navarch-runtime 0.1.122 → 0.1.123
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-model.cjs +80 -0
- package/dist/exit-conditions.cjs +67 -3
- package/dist/session.cjs +9 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectAgentModelUnavailable = detectAgentModelUnavailable;
|
|
4
|
+
/**
|
|
5
|
+
* OpenCode's words for "no configured provider serves this model": its JSON
|
|
6
|
+
* error event (`{"type":"error","error":{"type":"provider.no-route",
|
|
7
|
+
* "message":"Model unavailable: opencode-go/deepseek-v4.1-flash"}}`) and the
|
|
8
|
+
* matching ERROR log line (`"Failed to drain Session" cause
|
|
9
|
+
* "SessionRunnerModel.ModelUnavailableError: Model unavailable: ..."`).
|
|
10
|
+
* Verified against OpenCode 2.0.29 on flotilla-agent-1435 (2026-10-19).
|
|
11
|
+
*/
|
|
12
|
+
const NO_ROUTE_PATTERN = /\bprovider\.no-route\b|\bModelUnavailableError\b|\bModel unavailable:/i;
|
|
13
|
+
const MODEL_NAME_PATTERN = /\bModel unavailable:\s*([^\s"'\\,;)]+)/i;
|
|
14
|
+
function clip(text) {
|
|
15
|
+
const trimmed = text.trim().replace(/\s+/g, " ");
|
|
16
|
+
return trimmed.length > 300 ? `${trimmed.slice(0, 299)}…` : trimmed;
|
|
17
|
+
}
|
|
18
|
+
function failure(detail) {
|
|
19
|
+
const model = detail.match(MODEL_NAME_PATTERN)?.[1]?.replace(/[.:]+$/, "") ?? null;
|
|
20
|
+
return { model, detail: clip(detail) };
|
|
21
|
+
}
|
|
22
|
+
/** Messages of the CLI's own error events on stdout; never assistant text events. */
|
|
23
|
+
function errorEventMessages(stdout) {
|
|
24
|
+
const messages = [];
|
|
25
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
26
|
+
if (!line.trim().startsWith("{"))
|
|
27
|
+
continue;
|
|
28
|
+
let event;
|
|
29
|
+
try {
|
|
30
|
+
event = JSON.parse(line);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (event.type !== "error" && event.type !== "session.error")
|
|
36
|
+
continue;
|
|
37
|
+
const properties = event.properties;
|
|
38
|
+
const error = (event.error ?? properties?.error);
|
|
39
|
+
if (typeof error === "string") {
|
|
40
|
+
messages.push(error);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const message = error?.data?.message ?? error?.message;
|
|
44
|
+
const kind = [error?.type, error?.name].filter((value) => typeof value === "string").join(" ");
|
|
45
|
+
const text = [kind, typeof message === "string" ? message : ""].filter(Boolean).join(": ");
|
|
46
|
+
if (text)
|
|
47
|
+
messages.push(text);
|
|
48
|
+
}
|
|
49
|
+
return messages;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Detects a turn the coding agent CLI could not start because the model it
|
|
53
|
+
* was told to use has no route on this machine (OpenCode `provider.no-route`).
|
|
54
|
+
* Like a dead login (agent-auth.cts), every task the machine claims then fails
|
|
55
|
+
* the same way within seconds, so the control plane requeues the task without
|
|
56
|
+
* spending a retry and pauses the machine until the model is fixed.
|
|
57
|
+
*
|
|
58
|
+
* Matched only against CLI-authored channels — OpenCode's error events and
|
|
59
|
+
* ERROR log lines — and only for a turn that failed or produced no report, so
|
|
60
|
+
* an agent writing about this very error cannot pause its own machine.
|
|
61
|
+
*/
|
|
62
|
+
function detectAgentModelUnavailable(result) {
|
|
63
|
+
if (result.killedByLeaseLoss || result.timedOut || result.capacityError || result.authError)
|
|
64
|
+
return null;
|
|
65
|
+
if (result.exitCode === 0 && result.reportText)
|
|
66
|
+
return null;
|
|
67
|
+
for (const message of errorEventMessages(result.stdout)) {
|
|
68
|
+
if (NO_ROUTE_PATTERN.test(message))
|
|
69
|
+
return failure(message);
|
|
70
|
+
}
|
|
71
|
+
if (result.exitCode === 0)
|
|
72
|
+
return null;
|
|
73
|
+
for (const line of result.stderr.split(/\r?\n/)) {
|
|
74
|
+
if (/ERROR/.test(line) && NO_ROUTE_PATTERN.test(line)) {
|
|
75
|
+
const cause = line.match(/Model unavailable:[^"\n]*/i)?.[0];
|
|
76
|
+
return failure(cause ?? line);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AGENT_AUTH_FAILURE_PREFIX = void 0;
|
|
3
|
+
exports.AGENT_MODEL_UNAVAILABLE_PREFIX = exports.AGENT_AUTH_FAILURE_PREFIX = void 0;
|
|
4
4
|
exports.agentAuthFailureReport = agentAuthFailureReport;
|
|
5
|
+
exports.agentModelUnavailableReport = agentModelUnavailableReport;
|
|
5
6
|
exports.parseClaudeJsonResult = parseClaudeJsonResult;
|
|
6
7
|
exports.extractUsageFromClaudeJson = extractUsageFromClaudeJson;
|
|
7
8
|
exports.parseCodexJsonEvents = parseCodexJsonEvents;
|
|
@@ -10,6 +11,7 @@ exports.extractFinalMessageFromCodexEvents = extractFinalMessageFromCodexEvents;
|
|
|
10
11
|
exports.leadingVerificationVerdict = leadingVerificationVerdict;
|
|
11
12
|
exports.leadWithReviewFixMarker = leadWithReviewFixMarker;
|
|
12
13
|
exports.leadWithReviewAttestation = leadWithReviewAttestation;
|
|
14
|
+
exports.failureOutputText = failureOutputText;
|
|
13
15
|
exports.mapExitCondition = mapExitCondition;
|
|
14
16
|
/**
|
|
15
17
|
* Leading text of every report for a turn the coding agent CLI refused because
|
|
@@ -24,6 +26,19 @@ function agentAuthFailureReport(detail) {
|
|
|
24
26
|
return `${exports.AGENT_AUTH_FAILURE_PREFIX}: ${detail} ` +
|
|
25
27
|
"Log the coding agent in again on this machine (`claude /login`, `codex login`, an interactive `agy` session, or open `muse` and run `/login`), then resume the agent.";
|
|
26
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Leading text of every report for a turn the coding agent CLI could not start
|
|
31
|
+
* because the model it was given has no route on this machine (agent-model.cts,
|
|
32
|
+
* OpenCode `provider.no-route`). The control plane recognizes it and, as for a
|
|
33
|
+
* dead login, requeues the task without spending a retry while pausing this
|
|
34
|
+
* machine's claims (lib/navarch/failure-attribution.ts
|
|
35
|
+
* `provider.model_unavailable`).
|
|
36
|
+
*/
|
|
37
|
+
exports.AGENT_MODEL_UNAVAILABLE_PREFIX = "Coding agent model unavailable";
|
|
38
|
+
function agentModelUnavailableReport(detail) {
|
|
39
|
+
return `${exports.AGENT_MODEL_UNAVAILABLE_PREFIX}: ${detail} ` +
|
|
40
|
+
"Make that model reachable from the coding agent on this machine (log in to its provider, e.g. `opencode auth login`, or update the CLI), or connect the agent with a model it can reach, then resume the agent.";
|
|
41
|
+
}
|
|
27
42
|
/**
|
|
28
43
|
* Best-effort parse of `claude -p --output-format json` (or `stream-json`,
|
|
29
44
|
* whose last result record has the same shape) stdout into the assumed
|
|
@@ -298,6 +313,47 @@ function declaresBuildFailure(report) {
|
|
|
298
313
|
const firstLine = report.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
299
314
|
return /^(?:Build|Task) outcome:[*_`~]*\s*[*_`~]*(?:FAILED|BLOCKED)(?![a-z0-9_])/i.test(firstLine.replace(/^(?:#{1,6}\s+|>\s*|[*_`~]+\s*)+/, ""));
|
|
300
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* A CLI-authored error line: a JSON error event (OpenCode/Codex `error`,
|
|
318
|
+
* `session.error`, `turn.failed`), an ERROR/FATAL log line, or a leading
|
|
319
|
+
* `error:`/`fatal:`. Case-sensitive on the log levels so prose saying "error"
|
|
320
|
+
* stays where it was.
|
|
321
|
+
*/
|
|
322
|
+
function isFailureLine(line) {
|
|
323
|
+
return /(?:^|[^A-Za-z])(?:ERROR|FATAL)(?:[^A-Za-z]|$)/.test(line) ||
|
|
324
|
+
/"type"\s*:\s*"(?:error|session\.error|turn\.failed)"|^\s*(?:error|fatal)\b\s*[:\[]/i.test(line);
|
|
325
|
+
}
|
|
326
|
+
/** A JSON error event on stdout; text and tool events never qualify, whatever they say. */
|
|
327
|
+
function isErrorEventLine(line) {
|
|
328
|
+
if (!line.startsWith("{"))
|
|
329
|
+
return false;
|
|
330
|
+
try {
|
|
331
|
+
const type = JSON.parse(line)?.type;
|
|
332
|
+
return type === "error" || type === "session.error" || type === "turn.failed";
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The failure output of a CLI that exited badly, error lines first. OpenCode
|
|
340
|
+
* prints its INFO startup log ("cli starting ... serve --stdio") ahead of the
|
|
341
|
+
* real error and puts its JSON error event on stdout, so summarizing stderr
|
|
342
|
+
* from the top showed only the INFO lines on the board (2026-10-19). The JSON
|
|
343
|
+
* error events on stdout are CLI-authored, unlike its text events, so they are
|
|
344
|
+
* safe to lift into the summary.
|
|
345
|
+
*/
|
|
346
|
+
function failureOutputText(stdout, stderr) {
|
|
347
|
+
const stdoutErrors = stdout
|
|
348
|
+
.split(/\r?\n/)
|
|
349
|
+
.map((line) => line.trim())
|
|
350
|
+
.filter(isErrorEventLine);
|
|
351
|
+
const lines = [...stdoutErrors, ...stderr.split(/\r?\n/)].map((line) => line.trim()).filter(Boolean);
|
|
352
|
+
const errors = lines.filter(isFailureLine);
|
|
353
|
+
if (errors.length === 0)
|
|
354
|
+
return stderr;
|
|
355
|
+
return [...errors, ...lines.filter((line) => !isFailureLine(line))].join("\n");
|
|
356
|
+
}
|
|
301
357
|
function summarize(text, maxLen = 500) {
|
|
302
358
|
const trimmed = text.trim();
|
|
303
359
|
if (!trimmed)
|
|
@@ -344,6 +400,14 @@ function mapExitCondition(result) {
|
|
|
344
400
|
evidenceUrls,
|
|
345
401
|
};
|
|
346
402
|
}
|
|
403
|
+
if (result.modelUnavailableError) {
|
|
404
|
+
return {
|
|
405
|
+
leaseOutcome: "failed",
|
|
406
|
+
exitStatus: "failed",
|
|
407
|
+
reportSummary: summarize(agentModelUnavailableReport(result.modelUnavailableError)),
|
|
408
|
+
evidenceUrls,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
347
411
|
if (result.timedOut) {
|
|
348
412
|
return {
|
|
349
413
|
leaseOutcome: "failed",
|
|
@@ -362,7 +426,7 @@ function mapExitCondition(result) {
|
|
|
362
426
|
return {
|
|
363
427
|
leaseOutcome: "failed",
|
|
364
428
|
exitStatus: "crashed",
|
|
365
|
-
reportSummary: summarize(result.stderr) || "Adapter process crashed before exiting.",
|
|
429
|
+
reportSummary: summarize(failureOutputText(result.stdout, result.stderr)) || "Adapter process crashed before exiting.",
|
|
366
430
|
evidenceUrls,
|
|
367
431
|
};
|
|
368
432
|
}
|
|
@@ -373,7 +437,7 @@ function mapExitCondition(result) {
|
|
|
373
437
|
return {
|
|
374
438
|
leaseOutcome: "failed",
|
|
375
439
|
exitStatus: "failed",
|
|
376
|
-
reportSummary: summarize(parsedJson?.result || result.stderr || result.stdout) ||
|
|
440
|
+
reportSummary: summarize(parsedJson?.result || failureOutputText(result.stdout, result.stderr) || result.stdout) ||
|
|
377
441
|
`Adapter exited with code ${result.exitCode}.`,
|
|
378
442
|
evidenceUrls,
|
|
379
443
|
};
|
package/dist/session.cjs
CHANGED
|
@@ -31,6 +31,7 @@ const lease_heartbeat_cjs_1 = require("./lease-heartbeat.cjs");
|
|
|
31
31
|
const completion_retry_cjs_1 = require("./completion-retry.cjs");
|
|
32
32
|
const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
|
|
33
33
|
const agent_auth_cjs_1 = require("./agent-auth.cjs");
|
|
34
|
+
const agent_model_cjs_1 = require("./agent-model.cjs");
|
|
34
35
|
const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
|
|
35
36
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
36
37
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
@@ -691,6 +692,13 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
691
692
|
turnResult.authError = authFailure.detail;
|
|
692
693
|
log.warn(`session ${leaseId}: coding agent authentication failed: ${authFailure.detail}`);
|
|
693
694
|
}
|
|
695
|
+
// A model the CLI has no route to (OpenCode `provider.no-route`) fails
|
|
696
|
+
// every task the same way, so it gets the same treatment as a dead login.
|
|
697
|
+
const modelUnavailable = capacityLimit || authFailure ? null : (0, agent_model_cjs_1.detectAgentModelUnavailable)(turnResult);
|
|
698
|
+
if (modelUnavailable) {
|
|
699
|
+
turnResult.modelUnavailableError = modelUnavailable.detail;
|
|
700
|
+
log.warn(`session ${leaseId}: coding agent model unavailable: ${modelUnavailable.detail}`);
|
|
701
|
+
}
|
|
694
702
|
// Close the small race between a naturally completed turn and the next
|
|
695
703
|
// scheduled heartbeat. If guidance landed, run another turn before the
|
|
696
704
|
// lease can be completed. A terminal heartbeat response means the
|
|
@@ -703,7 +711,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
703
711
|
log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
|
|
704
712
|
return sessionOutcome;
|
|
705
713
|
}
|
|
706
|
-
if (!capacityLimit && !authFailure && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0) {
|
|
714
|
+
if (!capacityLimit && !authFailure && !modelUnavailable && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0) {
|
|
707
715
|
// Muse cancellation kills the whole task-owned container. Guidance
|
|
708
716
|
// still holds the lease, so restart that SAME container and restore
|
|
709
717
|
// its tmpfs environment before the next turn; never launch into a
|
package/package.json
CHANGED