Skip to content

feat(ipc): runner-aware tools — protocol + transport (partial)#346

Draft
branchseer wants to merge 13 commits intomainfrom
runner-aware-tools
Draft

feat(ipc): runner-aware tools — protocol + transport (partial)#346
branchseer wants to merge 13 commits intomainfrom
runner-aware-tools

Conversation

@branchseer
Copy link
Copy Markdown
Member

Heads up: experimental, partial work

This is a large, exploratory undertaking to make tools runner-aware — i.e. let tasks communicate with the runner over IPC so they can report ignored inputs/outputs, requested env vars, or opt out of caching. Rather than land it as one giant PR, this opens the work early with only the foundational pieces, so design choices can be reviewed incrementally.

Tracking plan: docs/runner-task-ipc/plan.md.

Goal

Give task runners a bidirectional IPC channel with the processes they spawn, so:

  • tools can declare at runtime what inputs they actually read / outputs they actually produced
  • tools can request additional env vars be tracked in the cache key
  • tools can tell the runner "don't cache me this time"
  • the runner can feed that data back into its caching decisions

What's in this PR (done)

  • Step 1 — Protocol (vite_task_ipc_shared): message types + serialization shared by both ends.
  • Step 2 — Transport (vite_task_server + vite_task_client): async server/client, tested Rust-to-Rust. Includes a Recorder handler that will be the runtime Handler used by vite_task.
  • Docs: design notes and implementation plan under docs/runner-task-ipc/.

What's coming in future PRs

  • Step 3 — Extract artifact crate out of fspy (prerequisite for dylib embedding). Note: already landed on main via refactor: extract materialized_artifact crate out of fspy #344 as materialized_artifact — plan.md will be updated.
  • Step 4 — JS bridge: real vite_task_client_napi impl + @voidzero-dev/vite-task-client JS wrapper (with fetchEnvs logic).
  • Step 5 — Runner integration: start a server per task execution, embed/extract the client dylib, inject IPC envs via serve()'s returned iterator.
  • Step 6 — Cache integration: runner consumes reported data (ignored inputs/outputs, requested envs, disable-cache) and adjusts caching behavior.

Test plan

  • cargo test passes for new crates
  • Rust-to-Rust client/server integration covered
  • E2E tests land alongside Step 5 (runner integration)

https://claude.ai/code/session_01LKpCFB3asEF9LXjR4KttKU

branchseer and others added 13 commits April 18, 2026 10:27
Pure Rust vite_task_client library with napi-rs wrapper in
vite_task_client_napi, exposing a demo `add` function as a
Node.js native module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

# Conflicts:
#	Cargo.toml

# Conflicts:
#	Cargo.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defines the shared IPC message types between the task runner
and tools (Request, Response) using wincode's zero-copy schemas.

Also documents the README.md convention for new crates and
packages in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vite_task_server exposes a `Handler` trait and a `serve(&handler, shutdown)`
free function. The returned `!Send` future accepts clients until `shutdown`
resolves, then stops accepting and drains in-flight per-client futures to
completion. Uses `FuturesUnordered` (not `spawn_local`) so the handler can
be borrowed and hold `!Send` state (`Rc`, `RefCell`).

vite_task_client is a sync, blocking client with `&mut self` methods.
`from_env()` reads `VP_RUN_IPC`; `from_name(&OsStr)` is the explicit form
used by tests.

Also: change `ResponseBody::Env(&NativeStr)` to `Env(Option<&NativeStr>)`
so the server can signal env-not-found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The driver now takes ownership of the handler (via internal `Rc<H>`),
and returns it by move when drain completes. Callers no longer need
to keep a separate reference or unwrap an `Rc` themselves.

The external `shutdown` parameter is replaced by a `StopAccepting`
handle exposed in `ServerHandle` — the internal `CancellationToken`
is hidden from the public API.

Also adds a design doc (docs/runner-task-ipc/server-design.md)
covering the API, driver lifecycle, and the planned integration
into `execute_spawn`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The driver future owns the handler via `Rc<H>` and only needs `H` to
outlive the future. Parameterize `ServerHandle` and `serve` over a
lifetime so callers with non-`'static` handlers are supported.

For `'static` handlers (the expected common case), nothing changes
at the call site — inference picks `'static`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The driver's async fn owns `handler: H` as a local. Per-client
futures borrow `&handler` from the same async state — Rust's
async-fn state machine makes the self-borrow sound (state is
pinned, never moves). When drain completes, the outer async
returns `handler` by move.

Removes the internal `Rc<H>` + `Rc::try_unwrap` path (and the
related panic doc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add `IPC_ENV_NAME` constant in `vite_task_ipc_shared` as the single
  source of truth for the IPC env var name (shared between server and
  client, not exposed to callers).
- `serve()` now returns `impl Iterator<Item = (&'static OsStr, OsString)>`
  instead of a bare `OsString` name. Callers chain the iterator directly
  into spawn's envs without knowing the env var name.
- `Client::from_env()` → `from_envs(envs)`: takes an env-pair iterator,
  scans for the IPC env. Symmetric with server output.
- Unify temp socket prefix to `vite_task_ipc_` on both platforms.
- Drop lingering `VP_RUN_IPC` / `VP_RUN_CLIENT_NAPI` references from docs;
  the env var names are now internal to server/client crates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recorder is a generic `Handler` impl parameterized by an env-lookup
closure. It records:
- ignored inputs / outputs (FxHashSet<Arc<AbsolutePath>>)
- cache_disabled flag
- env_records: name -> (tracked, resolved value), with `tracked`
  monotonically OR-ed across repeated get_env calls (once true, stays true)

The runtime caller (vite_task in step 5) constructs `Recorder::new(|name| ...)`
with a lookup closure over its own env source; after the driver resolves,
`recorder.into_reports()` yields the `Reports` for the cache-update phase.

Tests swap the ad-hoc `RecordingHandler` for `Recorder` — exercising
the same code path runtime code will use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Handler trait methods now take `&mut self`. The server wraps `H` in
`RefCell` internally so the N per-client futures that coexist in
`FuturesUnordered` can each call &mut-self methods; on the single-threaded
runtime the borrow check can't fail because handler methods are sync
(no borrow spans an await).

Recorder drops the generic env-lookup closure in favor of a concrete
`FxHashMap<Str, Arc<OsStr>>` taken at construction. Handler impl uses
plain owned fields (no interior mutability inside the handler itself).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the `id: u32` field from `Request::GetEnv` and the `Response`
  wrapper. Each IPC connection is request-response sequential with a
  single in-flight reply, so correlation adds no value. `Response` +
  `ResponseBody` collapse into a single `GetEnvResponse { env_value }`.
- Switch env-var names in requests from `&str` to `&NativeStr`, and
  `Handler::get_env` / `Client::get_env` to `&OsStr`. Handles non-UTF-8
  env names correctly on both platforms.
- `Client::get_env` returns `Arc<OsStr>` (was `OsString`); `Client::recv`
  is now generic over `T: SchemaRead` so future response types plug in
  without touching the framing layer.
- `Recorder` keys `env_map` / `env_records` by `Arc<OsStr>` (was `Str`).
- Windows socket name: build via `format!` directly (name always exceeds
  `Str` inline capacity, so the extra machinery provided no benefit).
- Drop `vite_str` dep from `vite_task_server`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits Step 4 of the runner-aware-tools plan: replaces the napi crate stub
with real module-level functions, adds the `@voidzero-dev/vite-task-client`
JS package that lazily loads the addon and falls back to no-ops outside a
`vp run` task, and renames the runner env vars to the `VP_RUN_` prefix
(`VITE_TASK_IPC_NAME` -> `VP_RUN_IPC_NAME`, new `VP_RUN_NODE_CLIENT_PATH`).

`vite_task_client::Client` now uses `&self` with `RefCell`s so the napi
layer can store it as per-env instance data without a Mutex. Adds an
end-to-end `#[ignore]` test driving the addon from Node against a live
`vite_task_server`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants