Pin local hook signatures to pi-agent-core compile-time contracts (C5)
Adds src/main/worker/pi-hook-contract.ts, a compile-only contract that re-states every pi-agent-core hook callback shape (beforeToolCall / afterToolCall / transformContext) as a named alias and pins the local worker implementations against them via type-level ExpectHookType assertions. pi-agent-factory.ts imports the contract for side effects so tsc drags it into the build graph. A future pi upgrade that renames a field or changes an argument now fails typecheck here instead of at first destructive tool invocation.
This commit is contained in:
1 parent
f6a58f6a37
commit
5a41489bc4
2 files changed
+122
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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<AgentOptions[...]>` 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<AgentOptions["beforeToolCall"]>;
|
||||
export type PiAfterToolCall = NonNullable<AgentOptions["afterToolCall"]>;
|
||||
export type PiTransformContext = NonNullable<AgentOptions["transformContext"]>;
|
||||
|
||||
/**
|
||||
* 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<Slot, Fn extends Slot> = 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<BeforeToolCallResult | undefined>;
|
||||
|
||||
type LocalAfterToolCallShape = (
|
||||
ctx: AfterToolCallContext,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<AfterToolCallResult | undefined>;
|
||||
|
||||
type LocalTransformContextShape = (
|
||||
messages: AgentMessage[],
|
||||
signal?: AbortSignal,
|
||||
) => Promise<AgentMessage[]>;
|
||||
|
||||
/**
|
||||
* 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<PiBeforeToolCall, LocalBeforeToolCallShape>;
|
||||
export type AfterToolCallContract = ExpectHookType<PiAfterToolCall, LocalAfterToolCallShape>;
|
||||
export type TransformContextContract = ExpectHookType<PiTransformContext, LocalTransformContextShape>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in new issue
Block a user