Skip to content

feat(loops): inProcessSandboxClient — typed in-process box, no more SandboxInstance casts#379

Merged
drewstone merged 1 commit into
mainfrom
feat/in-process-sandbox-client
Jun 24, 2026
Merged

feat(loops): inProcessSandboxClient — typed in-process box, no more SandboxInstance casts#379
drewstone merged 1 commit into
mainfrom
feat/in-process-sandbox-client

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

What

Lifts the hand-rolled offline-box pattern into a reusable substrate primitive, inProcessSandboxClient, next to inlineSandboxClient. Examples no longer cast as unknown as SandboxInstance.

Why

Four example spots each built the SAME shape by hand — a SandboxClient whose create() returns a box implementing only the subset runLoop / openSandboxRun actually call ({id, streamPrompt} + optional fs.read/write / exec / delete) — and cast it via as unknown as SandboxInstance. The cast is forced because SandboxInstance is a declare class with private fields that no object literal can structurally satisfy. The pattern already earned a substrate home once (createInProcessUiAuditClient) and is the structural sibling of inlineSandboxClient (which adapts an Executor → box). This gives the offline-box pattern its own typed home so user code never casts.

The primitive

inProcessSandboxClient({ onPrompt, workdir?, id? }) returns a properly-typed SandboxClient. The single unavoidable cast (object literal → declare class) lives inside the primitive, documented and tested.

  • onPrompt(prompt, ctx) => SandboxEvent[] | AsyncIterable<SandboxEvent> — the per-turn behavior IS the box. ctx carries round (per-box turn counter), workdir, and signal.
  • workdir? — opt in to a real temp-dir-backed box: fs.read/fs.write + exec over it, delete() tears it down. Omit for a pure event-only box.
  • id?: string | (seq) => string — override the box id (placement demos read on a meaningful sandbox id).

Casts removed (4 spots → 0)

File Before After
examples/driver-loop/scripted-worker.ts { id, streamPrompt } as unknown as SandboxInstance one-line onPrompt callback
examples/researcher-loop/synthetic-researcher.ts { id, streamPrompt } as unknown as SandboxInstance one-line onPrompt callback
examples/fleet-delegation/fleet-delegation.ts (×2) { id } as unknown as SandboxInstance inProcessSandboxClient({ id, onPrompt: () => [] })

Each example keeps its scenario script + teaching intact (driver-loop's fold, researcher-loop's namespace-leak firewall, fleet-delegation's placement tagging — meaningful ids preserved via the id override). The coding-benchmark/offline-box.ts cast is out of scope. The one correct cast inside src/runtime/inline-sandbox-client.ts (and now in-process-sandbox-client.ts) stays.

Verification

  • pnpm run build / pnpm run typecheck (src + examples) / pnpm run lint — all green
  • pnpm test — 116 files, 1125 passed, 2 skipped, 0 failed (incl. a new 5-case test for the primitive: end-to-end runLoop drive, round counter, workdir fs/exec + delete cleanup, id override, async-iterable stream)
  • pnpm run docs:api regenerated the catalog for the new export; pnpm run docs:check green
  • driver-loop + researcher-loop + fleet-delegation run offline and fire unchanged
  • grep confirms 0 as unknown as SandboxInstance in the rewired example files

…andboxInstance casts

Lift the repeated offline-box pattern into a substrate primitive. Each example
spot built the SAME shape by hand — a SandboxClient whose create() returns a box
implementing only the subset runLoop/openSandboxRun call ({id, streamPrompt} +
optional fs/exec/delete) — and cast it via `as unknown as SandboxInstance`
because SandboxInstance is a declare class with private fields that no object
literal can structurally satisfy.

inProcessSandboxClient({ onPrompt, workdir?, id? }) returns a properly-typed
SandboxClient; the one unavoidable cast lives inside the primitive, documented
and tested. It is the structural sibling of inlineSandboxClient (Executor → box)
and createInProcessUiAuditClient — onPrompt(prompt, ctx) → events is the box.

Rewired the four cast sites — driver-loop, researcher-loop, and both
fleet-delegation spots — to the primitive; the casts collapse to a one-line
onPrompt callback. Each example keeps its scenario script and teaching (the
fold; the namespace-leak firewall; placement tagging via the id override).

@tangletools tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Auto-approved PR — 99057fea

Blanket team auto-approval is enabled for this reviewer service.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: blanket_auto_approve · 2026-06-24T18:09:25Z

@drewstone drewstone merged commit 404a66d into main Jun 24, 2026
1 check passed
drewstone added a commit that referenced this pull request Jun 24, 2026
…ransport seam (#382)

Publish the merged #379 (inProcessSandboxClient) and #380 (injectable
completion transport on the worker seam) substrate exports. Bump version
0.76.0 -> 0.77.0 and refresh the generated primitive-catalog version stamp.
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