* feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3] Phase 3 slice 3 (AC3). Two model-layer adapters selected by the negotiated modelMode axis (host-integration.cts): - passive: formalizes today's tier routing from src/model-resolver.cts — resolveModel delegates straight to resolveModelForTier (byte-for-behavior). This is the CLI runtimes (claude/gemini/codex/opencode/cursor/...): GSD injects prompts / a per-agent model field. - active: a host-supplied sendRequest seam (VS Code vscode.lm / pi providers) — GSD calls the model through the host. Ships as a fail-closed seam (throws until a provider is bound); Phase 5 wires a concrete provider. createModelAdapter({modelMode}, {sendRequest?}) — factory gating throws on invalid mode. Proactive CI gates applied: ADR-457 eslint ignores entry + INVENTORY-MANIFEST cli_modules entry + comment-wording audited for the injection-scan substring trap. * chore(changeset): add Changed fragment for model adapter seam (#1680)
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
/**
|
|
* Model adapter seam (ADR-1239 Phase C-1, AC3 / #1680).
|
|
*
|
|
* Two model-layer adapters selected by the negotiated `modelMode` axis
|
|
* (host-integration.cts):
|
|
*
|
|
* - `passive` — GSD can only inject prompts / a per-agent `model` field (the
|
|
* CLI runtimes: claude/gemini/codex/opencode/cursor/…). Formalizes today's
|
|
* tier routing from src/model-resolver.cts: `resolveModel` delegates
|
|
* straight to `resolveModelForTier`, so passive reproduces current behavior
|
|
* byte-for-behavior.
|
|
* - `active` — the host exposes a provider `sendRequest` (VS Code `vscode.lm`,
|
|
* pi providers). GSD calls the model through the host. Ships here as a SEAM:
|
|
* a host-supplied `sendRequest` slot, fail-closed until a real consumer
|
|
* binds it (Phase 5 / #1682).
|
|
*
|
|
* Minimal (per ADR-1239 open wire-shape question): one factory, two shapes
|
|
* discriminated by `mode`. Concrete provider protocol (request/response shape)
|
|
* is fixed when a real active host lands in Phase 5.
|
|
*/
|
|
'use strict';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
import modelResolver = require('./model-resolver.cjs');
|
|
|
|
export type ModelMode = 'passive' | 'active';
|
|
|
|
export interface ModelAdapter {
|
|
readonly mode: ModelMode;
|
|
}
|
|
|
|
export interface PassiveModelAdapter extends ModelAdapter {
|
|
readonly mode: 'passive';
|
|
/** Resolve a model id for a tier. Delegates to model-resolver's tier routing. */
|
|
resolveModel(args: { cwd: string; agentType: string; attempt?: number }): string;
|
|
}
|
|
|
|
export interface ActiveModelAdapter extends ModelAdapter {
|
|
readonly mode: 'active';
|
|
/** Host-supplied model-call primitive. Throws (fail-closed) if not bound. */
|
|
sendRequest(req: unknown): unknown;
|
|
}
|
|
|
|
export interface CreateModelAdapterOptions {
|
|
/** Required for `active`: the host's model-call primitive. Ignored for `passive`. */
|
|
sendRequest?: (req: unknown) => unknown;
|
|
}
|
|
|
|
export function createModelAdapter(
|
|
{ modelMode }: { modelMode: ModelMode },
|
|
options: CreateModelAdapterOptions = {},
|
|
): ModelAdapter {
|
|
if (modelMode !== 'passive' && modelMode !== 'active') {
|
|
throw new TypeError(`createModelAdapter: modelMode must be 'passive' | 'active' (got ${JSON.stringify(modelMode)})`);
|
|
}
|
|
if (modelMode === 'passive') {
|
|
return Object.freeze({
|
|
mode: 'passive' as const,
|
|
resolveModel({ cwd, agentType, attempt }: { cwd: string; agentType: string; attempt?: number }): string {
|
|
return modelResolver.resolveModelForTier(cwd, agentType, attempt);
|
|
},
|
|
});
|
|
}
|
|
// active: bind the host's sendRequest, fail-closed if absent.
|
|
const sendRequest = options.sendRequest;
|
|
return Object.freeze({
|
|
mode: 'active' as const,
|
|
sendRequest(req: unknown): unknown {
|
|
if (typeof sendRequest !== 'function') {
|
|
throw new Error(
|
|
'ActiveModelAdapter.sendRequest: no host provider bound — the active model seam ' +
|
|
'requires a sendRequest primitive from the host (Phase 5 wires a concrete provider).',
|
|
);
|
|
}
|
|
return sendRequest(req);
|
|
},
|
|
});
|
|
}
|