mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix: deliver prompt via stdin for non-Claude agents to avoid spawn ENAMETOOLONG on Windows (#15)
The composed prompt (system instructions + skill body + cwd hint + file listing + user message) can easily exceed Windows' CreateProcess limit of ~32 KB when passed as a CLI argument via -p <string>. This causes spawn ENAMETOOLONG whenever Gemini CLI (or Codex, OpenCode, Cursor Agent, Qwen) is selected on Windows — even for short user messages, because the skill / design-system system prompt is folded in. Fix: add promptViaStdin: true to every plain-text agent definition. The daemon's /api/chat handler checks this flag, opens stdin as a pipe, writes the composed prompt to it and closes the stream. Claude Code is unaffected — it still uses the -p argv path and a separate stream-json parser. docs/agent-adapters.md: update §5.5 Gemini CLI to document the stdin delivery strategy, and update the Windows open-question note to reflect the fix. Co-authored-by: KNIGHTABDO <abdessamad.aabida-etu@etu.univh2c.ma> Co-authored-by: pftom <1043269994@qq.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: lefarcen <20859779+lefarcen@users.noreply.github.com>
This commit is contained in:
parent
6625674925
commit
623444fe48
3 changed files with 66 additions and 19 deletions
|
|
@ -154,9 +154,12 @@ export const AGENT_DEFS = [
|
|||
{ id: 'medium', label: 'Medium' },
|
||||
{ id: 'high', label: 'High' },
|
||||
],
|
||||
buildArgs: (prompt, _imagePaths, _extra, options = {}) => {
|
||||
// Keep Codex in workspace-write sandbox while avoiding interactive
|
||||
// permission prompts in terminal-less web UI.
|
||||
// Prompt delivered via stdin (`codex exec -`) to avoid Windows
|
||||
// `spawn ENAMETOOLONG` — CreateProcess caps argv at ~32 KB and the
|
||||
// composed prompt easily exceeds that. `--full-auto` keeps Codex in
|
||||
// its workspace-write sandbox while skipping interactive permission
|
||||
// prompts in the no-TTY web UI.
|
||||
buildArgs: (_prompt, _imagePaths, _extra, options = {}) => {
|
||||
const args = ['exec', '--full-auto'];
|
||||
if (options.model && options.model !== 'default') {
|
||||
args.push('--model', options.model);
|
||||
|
|
@ -166,9 +169,10 @@ export const AGENT_DEFS = [
|
|||
// is exposed as `model_reasoning_effort`.
|
||||
args.push('-c', `model_reasoning_effort="${options.reasoning}"`);
|
||||
}
|
||||
args.push(prompt);
|
||||
args.push('-');
|
||||
return args;
|
||||
},
|
||||
promptViaStdin: true,
|
||||
streamFormat: 'plain',
|
||||
},
|
||||
{
|
||||
|
|
@ -181,14 +185,18 @@ export const AGENT_DEFS = [
|
|||
{ id: 'gemini-2.5-pro', label: 'gemini-2.5-pro' },
|
||||
{ id: 'gemini-2.5-flash', label: 'gemini-2.5-flash' },
|
||||
],
|
||||
buildArgs: (prompt, _imagePaths, _extra, options = {}) => {
|
||||
// Gemini reads from stdin when `-p` is omitted and stdin is a pipe.
|
||||
// Passing the full composed prompt as a CLI arg causes ENAMETOOLONG on
|
||||
// Windows (CreateProcess limit ~32 KB) for any non-trivial prompt.
|
||||
// `--yolo` skips interactive approval prompts in the no-TTY web UI.
|
||||
buildArgs: (_prompt, _imagePaths, _extra, options = {}) => {
|
||||
const args = ['--yolo'];
|
||||
if (options.model && options.model !== 'default') {
|
||||
args.push('--model', options.model);
|
||||
}
|
||||
args.push('-p', prompt);
|
||||
return args;
|
||||
},
|
||||
promptViaStdin: true,
|
||||
streamFormat: 'plain',
|
||||
},
|
||||
{
|
||||
|
|
@ -208,14 +216,17 @@ export const AGENT_DEFS = [
|
|||
{ id: 'openai/gpt-5', label: 'openai/gpt-5' },
|
||||
{ id: 'google/gemini-2.5-pro', label: 'google/gemini-2.5-pro' },
|
||||
],
|
||||
buildArgs: (prompt, _imagePaths, _extra, options = {}) => {
|
||||
// Prompt delivered via stdin (`opencode run -`) to avoid Windows
|
||||
// `spawn ENAMETOOLONG` for large composed prompts.
|
||||
buildArgs: (_prompt, _imagePaths, _extra, options = {}) => {
|
||||
const args = ['run'];
|
||||
if (options.model && options.model !== 'default') {
|
||||
args.push('--model', options.model);
|
||||
}
|
||||
args.push(prompt);
|
||||
args.push('-');
|
||||
return args;
|
||||
},
|
||||
promptViaStdin: true,
|
||||
streamFormat: 'plain',
|
||||
},
|
||||
{
|
||||
|
|
@ -242,14 +253,18 @@ export const AGENT_DEFS = [
|
|||
{ id: 'sonnet-4-thinking', label: 'sonnet-4-thinking' },
|
||||
{ id: 'gpt-5', label: 'gpt-5' },
|
||||
],
|
||||
buildArgs: (prompt, _imagePaths, _extra, options = {}) => {
|
||||
// Prompt delivered via stdin (`cursor-agent -`) to avoid Windows
|
||||
// `spawn ENAMETOOLONG` for large composed prompts. `--force` skips
|
||||
// interactive approval prompts in the no-TTY web UI.
|
||||
buildArgs: (_prompt, _imagePaths, _extra, options = {}) => {
|
||||
const args = ['--force'];
|
||||
if (options.model && options.model !== 'default') {
|
||||
args.push('--model', options.model);
|
||||
}
|
||||
args.push('-p', prompt);
|
||||
args.push('-');
|
||||
return args;
|
||||
},
|
||||
promptViaStdin: true,
|
||||
streamFormat: 'plain',
|
||||
},
|
||||
{
|
||||
|
|
@ -262,15 +277,18 @@ export const AGENT_DEFS = [
|
|||
{ id: 'qwen3-coder-plus', label: 'qwen3-coder-plus' },
|
||||
{ id: 'qwen3-coder-flash', label: 'qwen3-coder-flash' },
|
||||
],
|
||||
buildArgs: (prompt, _imagePaths, _extra, options = {}) => {
|
||||
// Qwen Code is a Gemini-CLI fork and supports the same `--yolo` mode.
|
||||
// Prompt delivered via stdin (`qwen -`) to avoid Windows
|
||||
// `spawn ENAMETOOLONG` for large composed prompts. Qwen Code is a
|
||||
// Gemini-CLI fork and supports the same `--yolo` non-interactive mode.
|
||||
buildArgs: (_prompt, _imagePaths, _extra, options = {}) => {
|
||||
const args = ['--yolo'];
|
||||
if (options.model && options.model !== 'default') {
|
||||
args.push('--model', options.model);
|
||||
}
|
||||
args.push('-p', prompt);
|
||||
args.push('-');
|
||||
return args;
|
||||
},
|
||||
promptViaStdin: true,
|
||||
streamFormat: 'plain',
|
||||
},
|
||||
{
|
||||
|
|
@ -322,7 +340,7 @@ export const AGENT_DEFS = [
|
|||
},
|
||||
];
|
||||
|
||||
function resolveOnPath(bin) {
|
||||
export function resolveOnPath(bin) {
|
||||
const exts =
|
||||
process.platform === 'win32'
|
||||
? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';')
|
||||
|
|
@ -412,6 +430,7 @@ function stripFns(def) {
|
|||
return rest;
|
||||
}
|
||||
|
||||
|
||||
export async function detectAgents() {
|
||||
const results = await Promise.all(AGENT_DEFS.map(probe));
|
||||
// Refresh the validation cache from whatever we just surfaced to the UI
|
||||
|
|
|
|||
|
|
@ -1010,12 +1010,29 @@ export async function startServer({ port = 7456 } = {}) {
|
|||
|
||||
let child;
|
||||
try {
|
||||
// When the agent definition sets `promptViaStdin`, pipe the composed
|
||||
// prompt through stdin instead of embedding it in argv. Bypasses the
|
||||
// OS command-line length limit (Windows CreateProcess caps at ~32 KB)
|
||||
// which causes `spawn ENAMETOOLONG` for any non-trivial prompt.
|
||||
const stdinMode = def.promptViaStdin ? 'pipe' : 'ignore';
|
||||
child = spawn(resolvedBin, args, {
|
||||
env: { ...process.env },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
stdio: [stdinMode, 'pipe', 'pipe'],
|
||||
cwd: cwd || undefined,
|
||||
shell: useShell,
|
||||
});
|
||||
if (def.promptViaStdin && child.stdin) {
|
||||
// EPIPE from a fast-exiting CLI (bad auth, missing model, exit on
|
||||
// launch) would otherwise surface as an unhandled stream error and
|
||||
// crash the daemon. Swallow it — the regular exit/close handlers
|
||||
// below already route the underlying failure to SSE via stderr.
|
||||
child.stdin.on('error', (err) => {
|
||||
if (err.code !== 'EPIPE') {
|
||||
send('error', { message: `stdin: ${err.message}` });
|
||||
}
|
||||
});
|
||||
child.stdin.end(composed, 'utf8');
|
||||
}
|
||||
} catch (err) {
|
||||
send('error', { message: `spawn failed: ${err.message}` });
|
||||
return res.end();
|
||||
|
|
|
|||
|
|
@ -155,11 +155,19 @@ The adapter declares which strategy to use via `capabilities().nativeSkillLoadin
|
|||
|
||||
### 5.5 Gemini CLI
|
||||
|
||||
- Invocation: `gemini --prompt "<prompt>" --cwd <dir>`.
|
||||
- Streaming: yes, but less structured tool events. Expect fewer surgical-edit capabilities.
|
||||
- Invocation: `gemini` with the composed prompt delivered via **stdin** (no `-p` flag).
|
||||
Gemini CLI enters headless mode automatically when stdin is a pipe and no `-p` flag is
|
||||
supplied — verified with `gemini@0.1.x`.
|
||||
- Streaming: yes, plain text to stdout.
|
||||
- Skill loading: prompt injection only.
|
||||
- Surgical edits: regenerate whole file.
|
||||
- **Gotcha:** Gemini's tool-use format is distinct; we translate our file-write tool to its `file_tool` equivalent.
|
||||
- **Gotcha — `spawn ENAMETOOLONG` on Windows:** Passing the full composed prompt as a
|
||||
`-p <string>` CLI argument hits Windows' `CreateProcess` hard limit of ~32 KB for the
|
||||
entire command line. The fix is to set `promptViaStdin: true` in the agent definition
|
||||
and write the prompt to `child.stdin` after spawning. The daemon's `/api/chat` handler
|
||||
checks this flag and opens stdin as a pipe instead of `'ignore'`.
|
||||
- **Gotcha:** Gemini's tool-use format is distinct; we translate our file-write tool to its
|
||||
`file_tool` equivalent when that feature is implemented.
|
||||
|
||||
### 5.6 OpenCode / OpenClaw
|
||||
|
||||
|
|
@ -263,5 +271,8 @@ Each adapter is a separate module so community contributions can add new ones wi
|
|||
|
||||
- **Nested agents.** What if Claude Code's agent itself spawns a subagent? We receive events from the outer process only. v1 policy: surface only top-level events; summarize subagent activity as "sub-task" placeholder.
|
||||
- **Billing awareness.** Some agents bill per message, some per token. OD doesn't track cost in MVP; v1 adds an optional "usage" event from adapters that expose it.
|
||||
- **Windows support.** PATH scanning and `spawn` semantics differ on Windows. v1 targets macOS and Linux; Windows is best-effort.
|
||||
- **Windows support.** PATH scanning and `spawn` semantics differ on Windows. v1 targets
|
||||
macOS and Linux; Windows is best-effort. Known issue fixed: `spawn ENAMETOOLONG` when
|
||||
running Gemini CLI (and other plain-text agents) on Windows — resolved by routing the
|
||||
composed prompt through stdin instead of as a CLI argument (see §5.5).
|
||||
- **Docker-contained agents.** Some users run Claude Code in a container. Adapter needs a "remote" mode — probably same interface but talks over SSH. Phase 2+.
|
||||
|
|
|
|||
Loading…
Reference in a new issue