// RFC-0006 — pre-spawn model guard and child-side model resolution (PIC-62) seam.
//
// Under RFC 0006 the whole callee runs in a spawned child `pi` process and the
// model crosses the boundary as a REFERENCE (`--provider
--model `),
// re-resolved child-side. PIC-62 pins the two model-resolution obligations that
// bracket the spawn:
//
// - PARENT-SIDE pre-spawn guard (`guardResolvedModel`): when the theta's
// resolved model is `undefined` — frontmatter `model:` absent and the
// inherited session model `undefined` — the runtime MUST NOT spawn the
// child; it fails the invocation with `theta/runtime/subagent-model-unresolved`
// and, to an `invoke` parent, `Err(InvokeInfraError { cause:
// "subagent_model_unresolved" })`. The check is ENTRY-POINT-AGNOSTIC: it
// runs identically at the slash-command, `tools:` `tool.execute`, and
// `invoke(...)` entry points (it consumes only the resolved model, so no
// entry-point discriminator enters the seam).
// - CHILD-SIDE resolution confirmation (`confirmChildModel`): the child
// re-resolves the marshalled reference against its OWN model registry (no
// RPC state surface) and, on mismatch, fails the invocation with
// `theta/runtime/subagent-model-preflight-mismatch` and `Err(InvokeInfraError
// { cause: "subagent_model_preflight_mismatch" })`, its message naming the
// expected vs. the child-resolved model. The failure reaches the parent
// through the return envelope ([PIC-59](./subagent.md#pic-59)), not over any
// RPC round-trip.
//
// WHY a dedicated seam succeeds the RFC-0005 PIC-40 pair (`subagent-isolation.ts`):
// PIC-40 pinned a child-side pre-flight over the RPC `get_state` /
// `get_available_models` state surface; RFC 0006's `-p` child has no RPC state
// surface, so re-resolution is local to the child and any mismatch is reported
// through the envelope. The pinned diagnostic codes and `InvokeInfraError`
// causes are preserved (stable diagnostics contract); the mechanism is re-coined.
//
// RED EXPECTATION (RFC-0006 not yet implemented): `guardResolvedModel` /
// `confirmChildModel` throw `not implemented: RFC 0006`, so each assertion reds
// on its primary behaviour; the paired implementation leaf greens them.
//
// Spec: pi-integration-contract/subagent.md (PIC-62 #subagent-pre-spawn-model-guard,
// #subagent-model-marshalling), diagnostics/code-registry-runtime.md
// (`theta/runtime/subagent-model-unresolved`,
// `theta/runtime/subagent-model-preflight-mismatch`),
// errors-and-results/queryerror-variants.md (the `invoke_infra` causes).
import type { Diagnostic } from "../diagnostics/diagnostic";
import type { InvokeInfraError } from "./query-error";
// ---------------------------------------------------------------------------
// Diagnostic codes (registry-pinned).
// ---------------------------------------------------------------------------
/**
* `theta/runtime/subagent-model-unresolved` — the pre-spawn model guard found
* the resolved `model` to be `undefined`, so the runtime refused to spawn the
* child (diagnostics/code-registry-runtime.md).
*/
export const SUBAGENT_MODEL_UNRESOLVED_CODE = "theta/runtime/subagent-model-unresolved";
/**
* `theta/runtime/subagent-model-preflight-mismatch` — the child re-resolved the
* marshalled `--provider`/`--model` reference to a different model than intended
* (diagnostics/code-registry-runtime.md).
*/
export const SUBAGENT_MODEL_PREFLIGHT_MISMATCH_CODE = "theta/runtime/subagent-model-preflight-mismatch";
/**
* The registry-pinned Message-column string for `theta/runtime/subagent-model-unresolved`
* (code-registry-runtime.md).
*/
export const SUBAGENT_MODEL_UNRESOLVED_MESSAGE =
"subagent invocation has no resolved model: frontmatter 'model:' is absent and the inherited session model is undefined";
/**
* The registry-pinned Message-column template for
* `theta/runtime/subagent-model-preflight-mismatch` (code-registry-runtime.md):
* `subagent model pre-flight mismatch: expected '', child resolved ''`.
* Names expected vs. the child-resolved model.
*/
export function renderModelPreflightMismatchMessage(expected: string, resolved: string): string {
return `subagent model pre-flight mismatch: expected '${expected}', child resolved '${resolved}'`;
}
// ---------------------------------------------------------------------------
// Parent-side pre-spawn guard.
// ---------------------------------------------------------------------------
/**
* The parent-side pre-spawn guard verdict: proceed to spawn, or refuse fail-closed
* with the `theta/runtime/subagent-model-unresolved` diagnostic and the
* `Err(InvokeInfraError { cause: "subagent_model_unresolved" })` an `invoke`
* parent observes.
*/
export type ModelGuardVerdict =
| { readonly ok: true }
| { readonly ok: false; readonly error: InvokeInfraError; readonly diagnostic: Diagnostic };
/**
* PIC-62 obligation 1 (parent-side). Decide whether the child spawn may proceed
* given the theta's resolved model. A resolved `undefined` (frontmatter `model:`
* absent and inherited `ctx.model` `undefined`) MUST NOT spawn the child; it
* fails the invocation with `theta/runtime/subagent-model-unresolved` and, to an
* `invoke` parent, `Err(InvokeInfraError { cause: "subagent_model_unresolved" })`.
* The seam consumes ONLY the resolved model, so it is entry-point-agnostic: it
* behaves identically at the slash-command, `tools:` `tool.execute`, and
* `invoke(...)` entry points.
*/
export function guardResolvedModel(model: string | undefined): ModelGuardVerdict {
// PIC-62: a resolved `undefined` model (frontmatter `model:` absent and the
// inherited session model `undefined`) MUST NOT spawn the child. The seam
// consumes only the resolved model, so it is entry-point-agnostic.
if (model === undefined) {
return {
ok: false,
error: {
kind: "invoke_infra",
message: SUBAGENT_MODEL_UNRESOLVED_MESSAGE,
callee_path: "",
cause: "subagent_model_unresolved",
},
diagnostic: {
severity: "error",
code: SUBAGENT_MODEL_UNRESOLVED_CODE,
message: SUBAGENT_MODEL_UNRESOLVED_MESSAGE,
},
};
}
return { ok: true };
}
// ---------------------------------------------------------------------------
// Child-side resolution confirmation.
// ---------------------------------------------------------------------------
/**
* The child-side confirmation verdict: proceed, or fail the invocation with the
* `theta/runtime/subagent-model-preflight-mismatch` diagnostic and the
* `Err(InvokeInfraError { cause: "subagent_model_preflight_mismatch" })` the
* envelope carries back to the parent.
*/
export type ChildModelVerdict =
| { readonly ok: true }
| { readonly ok: false; readonly error: InvokeInfraError; readonly diagnostic: Diagnostic };
/**
* PIC-62 obligation 2 (child-side). Confirm the marshalled `--provider`/`--model`
* reference re-resolved child-side to the intended model. On mismatch, fail the
* invocation with `theta/runtime/subagent-model-preflight-mismatch` and
* `Err(InvokeInfraError { cause: "subagent_model_preflight_mismatch" })`, the
* message naming `expected` vs. the child-`resolved` model. Reported to the
* parent through the return envelope, never over an RPC state surface.
*/
export function confirmChildModel(expected: string, resolved: string): ChildModelVerdict {
if (expected === resolved) {
return { ok: true };
}
// PIC-62: the marshalled reference re-resolved child-side to a different model —
// terminal for the invocation (the runtime does not retry). Reported to the
// parent through the return envelope, never over an RPC state surface.
const message = renderModelPreflightMismatchMessage(expected, resolved);
return {
ok: false,
error: {
kind: "invoke_infra",
message,
callee_path: "",
cause: "subagent_model_preflight_mismatch",
},
diagnostic: {
severity: "error",
code: SUBAGENT_MODEL_PREFLIGHT_MISMATCH_CODE,
message,
},
};
}