Add in-process executor to SDK via `executor:` field
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 2
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Background
The SDK currently exposes only one execution strategy: spawn(profile.command, profile.args, ...) (see sdk/node/src/runner.js around the run() function). The profile schema only allows command: + args:, which forces every host to fork a bridge subprocess (Python or Node) that itself forks the target CLI. This is a 2-fork overhead baked into the protocol's only execution path.
The hub/<cli>/bridge.{py,js} files are essentially reusable: each one defines buildArgs(message, sessionId, env) + parseEvent(event), with readTurn() from stdin and emit*() to stdout. That same buildArgs + parseEvent pair can be invoked in-process — the only thing that changes is the I/O boundary.
Proposal
Add an executor: field to the profile's agentproc: block. When present and recognised by the SDK, the runner skips spawn and invokes an in-process executor registered in the SDK.
agentproc:
executor: claude-code # NEW: SDK-known kind, looked up in an executors table
command: python3 # EXISTING: fallback, used when executor is absent or unrecognised
args: ["{{PROFILE_DIR}}/bridge.py"]
cwd: ...
env: ...
timeout_secs: 600
streaming: true
permission: false
Resolution rules (proposed):
executor: |
command: |
Behavior |
|---|---|---|
| absent | present | spawn (current behaviour, unchanged) |
| present + SDK recognises it | present or absent | call executors[executor] in-process; ignore command/args |
| present + SDK does NOT recognise it | present | fall back to spawn command (best-effort compat) |
| present + SDK does NOT recognise it | absent | hard fail with a clear error listing known executors |
SDK changes
sdk/node/src/:
- New file
executors.js— built-in executor registry keyed by name (e.g.claude-code,codex,recursive,agy,echo-agent, …). Each entry is{ buildArgs, parseEvent }, code copied verbatim from the correspondinghub/<cli>/bridge.js(the I/O parts —readTurnandprocess.stdout.write— are stripped; everything else stays). runner.js— add a branch inrun():
Theif (profile.executor && executors[profile.executor]) { return await runViaExecutor(profile, options, executors[profile.executor]) } // ... existing spawn path, unchangedrunViaExecutoradapter turns the in-process executor's emit-callback into the sameRunResultshape (reply,sessionId,error,exitCode,timedOut) and pipesusagethrough (see below).- New export
executorNamesso hosts can introspect which executors the SDK ships with.
Protocol spec changes
spec/protocol.md:
- New field
executor:under the profileagentproc:block. Document the four resolution rules above. - Add a paragraph clarifying that when
executor:is used, the protocol contract (NDJSON events on the runner↔SDK boundary) is unchanged — the SDK internally synthesises the same event stream an out-of-process bridge would have produced.
Why executor: and not kind:
I considered kind: first, but it overlaps semantically with command: — both fields look like "an identifier for this agent" from a user's perspective, which makes the schema confusing. executor: names the SDK concept (an executor registered in the SDK) and contrasts cleanly with command: (an OS-level binary). The two coexist with a clear priority order.
I also considered dropping parser: as a separate field. Decision: don't expose it. Each executor's parseEvent is part of its implementation; users don't pick parsers at runtime.
Why this matters
- Hosts that already spawn the CLI (e.g. flowx / flowcast) can consume AgentProc profiles without paying for an extra fork. This unlocks direct integration of the 16 hub profiles with zero per-call overhead.
- Reuses the bridge implementations. The hub bridges aren't deprecated — IM bridges still use them. We just gain a second consumer of the same
buildArgs + parseEventpair. - Clean SDK abstraction. The runner becomes "protocol contract"; the executor table becomes "execution strategy". Today those concerns are entangled in
spawn().
Compatibility
- All 16 existing hub profiles keep working unchanged. Adding
executor: <name>to any of them just upgrades them to the in-process path on hosts whose SDK recognises the name. - The
command:/args:fallback keeps legacy bridges functional forever.
Related
Once this lands, a follow-up issue should ask the runner to pass usage through from the result event into RunResult — the runner currently drops it (see runner.js line 469-477, RunResult shape has no usage field; stream_utils.js's emitResult also doesn't accept it). Protocol already permits it (spec/protocol.md lines 262-264); it's a pure implementation fix.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with sdk/node/src/runner.js around run(), then compare the buildArgs and parseEvent implementations in the hub bridge files. Add the executor registry and in-process adapter, preserve the documented spawn fallback and resolution rules, export executorNames, and update spec/protocol.md to describe the unchanged NDJSON contract.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, python
- Domain
- devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100