@sagentlab/navarch-runtime 0.1.86 → 0.1.88
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/README.md +18 -6
- package/dist/adapters/claude.cjs +14 -10
- package/dist/adapters/gemini.cjs +41 -2
- package/dist/exit-conditions.cjs +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -691,6 +691,14 @@ post-tool answer as the report, and reads aggregate `input_tokens` and
|
|
|
691
691
|
`output_tokens` from the terminal `result` event. Gemini does not report USD
|
|
692
692
|
cost, so an unknown value remains absent rather than becoming zero.
|
|
693
693
|
|
|
694
|
+
Gemini's native sandbox launcher converts piped stdin back into a `--prompt`
|
|
695
|
+
argument for its child. The adapter rejects prompts of 128 KiB or more when
|
|
696
|
+
that sandbox is enabled, with an explicit diagnostic; reducing context or
|
|
697
|
+
selecting another runtime preserves the worktree boundary. Navarch Docker
|
|
698
|
+
mode disables the nested Gemini sandbox by default and can still pipe larger
|
|
699
|
+
prompts. All Gemini runs reject prompts above 8 MiB because Gemini's stdin
|
|
700
|
+
reader silently truncates input beyond that limit. These checks use UTF-8 bytes.
|
|
701
|
+
|
|
694
702
|
The OpenCode adapter uses `opencode run --format json` with the prompt on
|
|
695
703
|
stdin, and normalizes `text` and `step_finish` JSONL events into
|
|
696
704
|
the shared report/token/cost boundary. It translates the lease MCP document
|
|
@@ -722,6 +730,9 @@ rejects any single argv or environment string over 128 KiB
|
|
|
722
730
|
(`MAX_ARG_STRLEN`), so an argv prompt made large review sessions fail with
|
|
723
731
|
`spawn E2BIG` before the agent started. Docker-mode sessions forward the same
|
|
724
732
|
stdin through `docker exec -i`.
|
|
733
|
+
Gemini's own sandbox relaunch is an exception to this transport guarantee,
|
|
734
|
+
as described above; moving the initial spawn to stdin does not remove its
|
|
735
|
+
internal argv limit.
|
|
725
736
|
|
|
726
737
|
## Architecture
|
|
727
738
|
|
|
@@ -740,7 +751,7 @@ cli.cts
|
|
|
740
751
|
into Docker when NAVARCH_SANDBOX_MODE=docker
|
|
741
752
|
4. selectAdapter(config.agentType) (adapters/index.cts) picks one AgentAdapter
|
|
742
753
|
(adapters/types.cts) by NAVARCH_AGENT, then .run(...):
|
|
743
|
-
- claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> < prompt`
|
|
754
|
+
- claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> --output-format stream-json --verbose < prompt`
|
|
744
755
|
- codexAdapter (adapters/codex.cts) — `codex exec - --json -c mcp_servers.*=... < prompt`
|
|
745
756
|
- geminiAdapter (adapters/gemini.cts) — `gemini --output-format stream-json < prompt`
|
|
746
757
|
- openCodeAdapter (adapters/opencode.cts) — `opencode run --format json < prompt`
|
|
@@ -860,16 +871,17 @@ Claude Code headless → PR opens on GitHub → complete lands with evidence;
|
|
|
860
871
|
secrets absent from disk after exit"):
|
|
861
872
|
|
|
862
873
|
- Actually running `claude -p` headless and confirming its real
|
|
863
|
-
|
|
864
|
-
`--output-format json` by default
|
|
865
|
-
|
|
874
|
+
result shape (WP-13). `adapters/claude.cts` appends
|
|
875
|
+
`--output-format stream-json --verbose` by default, so the uploaded
|
|
876
|
+
transcript records every message and tool call, and best-effort parses the
|
|
877
|
+
last `type: "result"` record on stdout as a JSON result object shaped like `{ type, subtype, is_error, result,
|
|
866
878
|
session_id, total_cost_usd, usage: { input_tokens, output_tokens,
|
|
867
879
|
cache_creation_input_tokens, cache_read_input_tokens } }` (see
|
|
868
880
|
`exit-conditions.cts#parseClaudeJsonResult` for the exact assumption and
|
|
869
881
|
fallback behavior). Unit tests (`tests/adapter.test.cts`,
|
|
870
882
|
`tests/exit-conditions.test.cts`) cover the parsing and arg-construction
|
|
871
|
-
logic against fixed JSON strings
|
|
872
|
-
|
|
883
|
+
logic against fixed JSON strings recorded from Claude Code 2.1; a future
|
|
884
|
+
CLI release could still change this shape.
|
|
873
885
|
If it doesn't, parsing degrades to `null` and `tokensIn`/`tokensOut`/
|
|
874
886
|
`costUsd` fall back to 0 (session.cts's existing default) — never a thrown
|
|
875
887
|
error — but cost/spend data silently goes missing until the shape is
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -13,7 +13,7 @@ function hasFlag(args, flag) {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Headless Claude Code adapter (project-plan.md §3.9 / implementation-plan.md
|
|
16
|
-
* WP-07): `claude -p --mcp-config platform-mcp.json --output-format json`
|
|
16
|
+
* WP-07): `claude -p --mcp-config platform-mcp.json --output-format stream-json --verbose`
|
|
17
17
|
* with the context bundle on stdin (`claude -p` reads its prompt from stdin
|
|
18
18
|
* when none is given; Linux rejects any single argv string over 128 KiB, so
|
|
19
19
|
* an argv prompt failed `spawn E2BIG` before Claude started). Returns raw stdout/stderr/exit signals (plus
|
|
@@ -25,11 +25,10 @@ function hasFlag(args, flag) {
|
|
|
25
25
|
* (adapters/types.cts) — see adapters/codex.cts for the Codex CLI sibling
|
|
26
26
|
* session.cts picks between via NAVARCH_AGENT (config.cts's `agentType`).
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* real shape differs, parseClaudeJsonResult() returns null and
|
|
28
|
+
* `--output-format stream-json` prints one JSON record per line and ends with
|
|
29
|
+
* the same result object (`type: "result"`, `usage`, `total_cost_usd`) that
|
|
30
|
+
* `--output-format json` prints alone; parseClaudeJsonResult() reads that
|
|
31
|
+
* last record. If the real shape differs, parseClaudeJsonResult() returns null and
|
|
33
32
|
* tokensIn/tokensOut/costUsd fall back to 0 (session.cts's existing
|
|
34
33
|
* default) rather than throwing -- see runtime/README.md.
|
|
35
34
|
*/
|
|
@@ -135,9 +134,14 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
135
134
|
// Only append the default when the caller hasn't already asked for a
|
|
136
135
|
// specific --output-format (extraArgs wins so an operator can opt back
|
|
137
136
|
// into plain-text output, e.g. while diagnosing a live-verification
|
|
138
|
-
// mismatch, without an adapter code change).
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
// mismatch, without an adapter code change). stream-json records every
|
|
138
|
+
// message and tool call, which the task transcript viewer renders; plain
|
|
139
|
+
// `json` printed only the final result. Claude Code requires --verbose for
|
|
140
|
+
// stream-json under --print, and its last record is the same result object.
|
|
141
|
+
if (!hasFlag(options.extraArgs, "--output-format")) {
|
|
142
|
+
args.push("--output-format", "stream-json");
|
|
143
|
+
if (!hasFlag(options.extraArgs, "--verbose"))
|
|
144
|
+
args.push("--verbose");
|
|
141
145
|
}
|
|
142
146
|
args.push(...options.extraArgs);
|
|
143
147
|
// Project/task policy is appended after machine-wide extra arguments so a
|
|
@@ -157,7 +161,7 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
157
161
|
const raw = runOptions.dockerExec ? await runViaDocker(runOptions, args) : await runOnHost(runOptions, args);
|
|
158
162
|
return attachUsage(raw);
|
|
159
163
|
}
|
|
160
|
-
/** Parses
|
|
164
|
+
/** Parses the final `claude -p --output-format stream-json` result record for usage and folds it onto the raw result (best-effort; leaves tokensIn/tokensOut/costUsd unset when parsing fails). */
|
|
161
165
|
function attachUsage(result) {
|
|
162
166
|
const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout);
|
|
163
167
|
if (!parsed)
|
package/dist/adapters/gemini.cjs
CHANGED
|
@@ -16,10 +16,20 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
|
16
16
|
* temporary system-settings file and restricts the run to those server names.
|
|
17
17
|
*
|
|
18
18
|
* The prompt arrives on stdin rather than `--prompt`: Gemini runs headlessly
|
|
19
|
-
* whenever stdin is not a TTY and uses the piped text as the prompt
|
|
20
|
-
*
|
|
19
|
+
* whenever stdin is not a TTY and uses the piped text as the prompt. Its
|
|
20
|
+
* native sandbox puts stdin back into argv, so that path still has Linux's
|
|
21
|
+
* 128 KiB per-string limit. Gemini also caps stdin at 8 MiB.
|
|
21
22
|
*/
|
|
22
23
|
async function runGeminiAdapter(options) {
|
|
24
|
+
if (options.signal?.aborted) {
|
|
25
|
+
return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
|
|
26
|
+
}
|
|
27
|
+
const promptBytes = Buffer.byteLength(options.prompt, "utf8");
|
|
28
|
+
// Gemini's readStdin() silently truncates at 8 MiB. Reject before any
|
|
29
|
+
// settings writes or process launch rather than execute a partial task.
|
|
30
|
+
if (promptBytes > 8 * 1024 * 1024) {
|
|
31
|
+
return inputFailure(`Gemini CLI stdin is limited to 8 MiB; the task prompt is ${promptBytes} bytes. Reduce the context bundle or select another runtime.`);
|
|
32
|
+
}
|
|
23
33
|
const args = [];
|
|
24
34
|
const env = { ...options.env };
|
|
25
35
|
if (!hasArg(options.extraArgs, "--output-format", "-o")) {
|
|
@@ -57,12 +67,41 @@ async function runGeminiAdapter(options) {
|
|
|
57
67
|
args.push(...options.extraArgs);
|
|
58
68
|
if (options.model)
|
|
59
69
|
args.push("--model", options.model);
|
|
70
|
+
// Gemini's sandbox launcher reads stdin and injects it back into --prompt
|
|
71
|
+
// for its child process (packages/cli/src/gemini.tsx injectStdinIntoArgs).
|
|
72
|
+
// Moving our first spawn's prompt to stdin cannot fix that second spawn.
|
|
73
|
+
// Check the effective flag, including operator overrides, without disabling
|
|
74
|
+
// the worktree guard. Even on macOS a Docker sandbox may launch on Linux.
|
|
75
|
+
if (geminiSandboxEnabled(args) && promptBytes >= 128 * 1024) {
|
|
76
|
+
return inputFailure(`Gemini CLI re-encodes stdin as --prompt when launching its native sandbox; the ${promptBytes}-byte task prompt exceeds Linux's argv limit. Reduce the context bundle or select another runtime that supports large prompts in its sandbox.`);
|
|
77
|
+
}
|
|
60
78
|
const runOptions = { ...options, env };
|
|
61
79
|
const raw = runOptions.dockerExec
|
|
62
80
|
? await runViaDocker(runOptions, args)
|
|
63
81
|
: await runOnHost(runOptions, args);
|
|
64
82
|
return attachGeminiOutput(raw);
|
|
65
83
|
}
|
|
84
|
+
function inputFailure(stderr) {
|
|
85
|
+
return { exitCode: 1, timedOut: false, killedByLeaseLoss: false, stdout: "", stderr };
|
|
86
|
+
}
|
|
87
|
+
function geminiSandboxEnabled(args) {
|
|
88
|
+
// There is always a generated --sandbox flag unless extraArgs supplies one.
|
|
89
|
+
// Like yargs, later explicit values override earlier ones.
|
|
90
|
+
let enabled = false;
|
|
91
|
+
for (let index = 0; index < args.length; index++) {
|
|
92
|
+
const arg = args[index];
|
|
93
|
+
if (arg === "--sandbox" || arg === "-s") {
|
|
94
|
+
enabled = args[index + 1] !== "false";
|
|
95
|
+
}
|
|
96
|
+
else if (arg.startsWith("--sandbox=") || arg.startsWith("-s=")) {
|
|
97
|
+
enabled = arg.slice(arg.indexOf("=") + 1) !== "false";
|
|
98
|
+
}
|
|
99
|
+
else if (arg === "--no-sandbox") {
|
|
100
|
+
enabled = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return enabled;
|
|
104
|
+
}
|
|
66
105
|
function hasArg(args, ...flags) {
|
|
67
106
|
return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
68
107
|
}
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -24,8 +24,9 @@ function agentAuthFailureReport(detail) {
|
|
|
24
24
|
"Log the coding agent in again on this machine (`claude /login` or `codex login`), then resume the agent.";
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Best-effort parse of `claude -p --output-format json`
|
|
28
|
-
*
|
|
27
|
+
* Best-effort parse of `claude -p --output-format json` (or `stream-json`,
|
|
28
|
+
* whose last result record has the same shape) stdout into the assumed
|
|
29
|
+
* result shape above. Tries the whole trimmed stdout first (the
|
|
29
30
|
* expected case); if that fails, tries each non-empty line that looks like a
|
|
30
31
|
* JSON object, in case a warning/log line preceded the result (last such
|
|
31
32
|
* line wins, since the final result is expected last). Returns null rather
|
|
@@ -39,6 +40,9 @@ function parseClaudeJsonResult(stdout) {
|
|
|
39
40
|
const isUsableResult = (value) => typeof value === "object" &&
|
|
40
41
|
value !== null &&
|
|
41
42
|
!Array.isArray(value) &&
|
|
43
|
+
// stream-json output interleaves typed message/tool records with the
|
|
44
|
+
// final `type: "result"` record; only that record carries the outcome.
|
|
45
|
+
(!("type" in value) || value.type === "result") &&
|
|
42
46
|
(!("result" in value) || typeof value.result === "string") &&
|
|
43
47
|
("result" in value || "usage" in value || "cost_usd" in value || "total_cost_usd" in value);
|
|
44
48
|
try {
|
package/package.json
CHANGED