diff --git a/src/main/worker/pi-agent-factory.ts b/src/main/worker/pi-agent-factory.ts index a2627060..17978cc3 100644 --- a/src/main/worker/pi-agent-factory.ts +++ b/src/main/worker/pi-agent-factory.ts @@ -30,6 +30,9 @@ import type { TransformContextRpcResult, } from "@main/agent-runtime/hook-rpc"; import type { WorkerRpcClient } from "./worker-rpc"; +// Side-effect import: drags the hook signature contract into the typecheck +// graph so any drift in pi-agent-core hook shapes breaks the build here. +import "./pi-hook-contract"; export interface PiAgentFactoryInput { runId: string; diff --git a/src/main/worker/pi-hook-contract.ts b/src/main/worker/pi-hook-contract.ts new file mode 100644 index 00000000..3cbfbbd6 --- /dev/null +++ b/src/main/worker/pi-hook-contract.ts @@ -0,0 +1,119 @@ +/** + * Compile-time contract between local hook implementations and + * `pi-agent-core` callback signatures. + * + * B1 pinned `@mariozechner/pi-agent-core` to 0.67.68 (in-tree reference) + * while our worker wires local hooks — `beforeToolCall`, `afterToolCall`, + * `transformContext` — into the pi Agent constructor. Those hooks carry + * deep structural coupling: pi decides the context shape passed in and + * the result shape we return. A silent signature drift on upgrade would + * slip past runtime tests until a real tool call fires in production. + * + * This module re-states every hook callback type as a named alias sourced + * from pi, then pins the concrete inline callbacks from `pi-agent-factory` + * to those aliases via the `ExpectHookType` helper. If pi ever renames a + * type, drops a field, or changes an argument, `tsc` fails here — not at + * the first destructive-tool invocation on a user machine. + * + * The contract is compile-only: no runtime validator, no fallback path. + * Importing this file for side effects (see `pi-agent-factory`) is enough + * to drag it into the typecheck graph. + */ + +import type { + AfterToolCallContext, + AfterToolCallResult, + AgentMessage, + AgentOptions, + BeforeToolCallContext, + BeforeToolCallResult, +} from "@mariozechner/pi-agent-core"; + +/** + * Canonical hook callback types sourced directly from `AgentOptions`. + * + * Using `NonNullable` keeps a single upstream source + * of truth — if pi changes the option signature the alias fails, which + * cascades into every `satisfies` check below. + */ +export type PiBeforeToolCall = NonNullable; +export type PiAfterToolCall = NonNullable; +export type PiTransformContext = NonNullable; + +/** + * Context/result types carried across the hook boundary. Pinned by name + * here so `hook-rpc.ts` payloads stay anchored to the pi-provided shapes + * even when the local RPC wrapper strips or re-packs fields. + */ +export type PiBeforeToolCallContext = BeforeToolCallContext; +export type PiBeforeToolCallResult = BeforeToolCallResult; +export type PiAfterToolCallContext = AfterToolCallContext; +export type PiAfterToolCallResult = AfterToolCallResult; +export type PiAgentMessage = AgentMessage; + +/** + * Type-level assertion: `Fn` must be assignable to the pi hook slot `Slot`. + * + * The helper produces `never` when `Fn` drifts, so any `satisfies` use + * resolves to `never` and fails to satisfy the target. That failure is + * the compile-time breakage we want — a single red squiggle pointing at + * the offending hook. + */ +type ExpectHookType = Fn; + +/** + * Callable shapes actually installed by `pi-agent-factory.createPiAgent`. + * + * These mirror the inline hook functions passed into `new Agent({...})`. + * Keeping them as standalone named types means a future refactor that + * pulls them out of the factory closure into named exports only has to + * swap the `typeof` lookup — the contract check below continues to hold. + */ +type LocalBeforeToolCallShape = ( + ctx: BeforeToolCallContext, + signal?: AbortSignal, +) => Promise; + +type LocalAfterToolCallShape = ( + ctx: AfterToolCallContext, + signal?: AbortSignal, +) => Promise; + +type LocalTransformContextShape = ( + messages: AgentMessage[], + signal?: AbortSignal, +) => Promise; + +/** + * Contract assertions — these are pure type positions; no runtime code. + * + * Each alias forces `tsc` to verify the local shape is still a valid + * substitute for the corresponding pi slot. Removing or renaming a + * field on either side breaks the build. + */ +export type BeforeToolCallContract = ExpectHookType; +export type AfterToolCallContract = ExpectHookType; +export type TransformContextContract = ExpectHookType; + +/** + * Hook handler return envelopes used by `agent-runtime/hook-handlers.ts`. + * + * The handlers run inside main and ship results back to the worker over + * the RPC channel, so they do not implement the raw pi signatures. + * Pinning the mapped shapes here guarantees that any change to the + * upstream result type surfaces in the RPC payload layer too. + */ +export type BeforeToolCallHandlerResult = { + readonly block: boolean; + readonly reason: string | null; +}; + +export type AfterToolCallHandlerResult = { + readonly contentOverride: AfterToolCallResult["content"] | null; + readonly isErrorOverride: AfterToolCallResult["isError"] | null; +}; + +export type TransformContextHandlerResult = { + readonly changed: boolean; + readonly messages: AgentMessage[] | null; +};