import { oc } from "@orpc/contract"; import { streamOf } from "../protocol/routes.js"; import { z } from "zod"; import { AgentEventSchema } from "../events/agent-events.js"; import { RunnerCommandFrameSchema, RunnerCommandSchema, RunnerDefinitionAppliedSchema, RunnerFactsSchema, RunnerSyncLineSchema, RunnerSyncSchema, RunnerTurnSchema, } from "../protocol/doors/runner-protocol.js"; import { EditorContextSchema } from "../schemas/agent.js"; import { AgentReplySchema } from "../schemas/providers/plan-limits.js"; import { OkSchema } from "../schemas/shared.js"; // What a runner can be asked, over the socket it opened; same inversion as deviceContract, the runner is the oRPC server, // the parent the client. No `.route()`: the procedure path is the address. Typed throughout, unlike deviceContract's // `mcp` hole, since a runner is the same daemon image as the parent, released together. export const runnerContract = { // Hardware facts, refreshed on demand; parity facts (image, overlay hash) ride the hello, not this call. describe: oc.output(RunnerFactsSchema), // `pull` updates the checkout, `push` returns a turn's result; streamed since a first sync clones repos. syncWorkspace: oc.input(RunnerSyncSchema).output(streamOf(RunnerSyncLineSchema)), // One turn in, the frames a local turn would produce out; the parent republishes them into the local pipeline. runTurn: oc.input(RunnerTurnSchema).output(streamOf(AgentEventSchema)), // Returns `applied` rather than throwing: a missing id is an ordinary race, same as local NOT_FOUND. reply: oc.input(AgentReplySchema).output(z.object({ applied: z.boolean() })), // Attachments/editor context travel uncomposed: the note must build against the runner's own workspace root. steer: oc .input( z.object({ conversationId: z.string().min(1), text: z.string(), attachments: z.array(z.string()).optional(), mentions: z.array(z.string()).optional(), editorContext: EditorContextSchema.optional(), }), ) .output(z.object({ applied: z.boolean(), invalid: z.string().optional() })), // REPLACE semantics: a key the definition omits returns to its default, unlike the owner apply's merge-beside. applyDefinition: oc.input(z.object({ toml: z.string() })).output(RunnerDefinitionAppliedSchema), // Stop the running turn; the parent's stop button reaching through. interrupt: oc.input(RunnerTurnSchema.pick({ conversationId: true })).output(OkSchema), // One shell line the parent offloaded (settings `offload`), run against a snapshot of the parent's tree; its output // streams back as it comes, then its exit with what it changed. An older runner lacks this procedure, which the // parent reads as "can't take it" and runs the line itself. runCommand: oc.input(RunnerCommandSchema).output(streamOf(RunnerCommandFrameSchema)), // Stops an offloaded line and everything it started; the parent's stop reaching through. cancelCommand: oc.input(RunnerCommandSchema.pick({ runId: true })).output(OkSchema), // Liveness driven by the parent: keepalive and gone-detection in one. ping: oc.output(OkSchema), };