jeffkit / jeffkit/agentproc

Add in-process executor to SDK via `executor:` field

Open
#1 0 comments 0 reactions 0 assignees View on GitHub

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 corresponding hub/<cli>/bridge.js (the I/O parts — readTurn and process.stdout.write — are stripped; everything else stays).
  • runner.js — add a branch in run():
    if (profile.executor && executors[profile.executor]) {
      return await runViaExecutor(profile, options, executors[profile.executor])
    }
    // ... existing spawn path, unchanged
    
    The runViaExecutor adapter turns the in-process executor's emit-callback into the same RunResult shape (reply, sessionId, error, exitCode, timedOut) and pipes usage through (see below).
  • New export executorNames so hosts can introspect which executors the SDK ships with.

Protocol spec changes

spec/protocol.md:

  • New field executor: under the profile agentproc: 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

  1. 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.
  2. 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 + parseEvent pair.
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.