From 83514f01c6280cd455ad5832539e74ada83ef188 Mon Sep 17 00:00:00 2001 From: omp-port Date: Tue, 20 Oct 2026 21:44:51 +0000 Subject: [PATCH] port: sync from Mike/pygienium@f01d4c22 --- AGENTS.md | 3 ++- package.json | 2 +- src/agent-runner.ts | 57 +++++++++++++++++++++++++++++++++++++++ src/commands.ts | 11 ++++++++ src/index.ts | 2 ++ src/modes/all.ts | 8 ++++++ src/modes/check-runner.ts | 10 ++++++- 7 files changed, 90 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 59c4dd4..aa7baac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ Pygienium is a `pi` extension: code-hygiene checks that scan a target with isola - Checks: `gate` (skip reason), `buildScanTask`, optional `buildFixTask`, optional `verify`. Scan tasks embed `!write`/`!echo` lines as deterministic fallback for the fake runner. - Sub-agent sessions must be pinned to the invoking session's model: thread `ctx.model` through `PygieniumCtx` → run options → `AgentTaskOptions.model` → `createAgentSession({ model })`. +- A pinned model needs its provider too: a provider an extension registered (`pi.registerProvider`) exists only in the invoking `ModelRegistry`, and the sub-session runs `noExtensions`, so thread `ctx.modelRegistry` (narrowed by `asProviderRegistry`) through the same chain to `AgentTaskOptions.modelRegistry` → `providerForwardingExtension`, which re-registers that one provider through an inline factory. Drop that link and every check dies with `No API key found for `. - Keep files modular; no compatibility shims. ## Commit process (IMPORTANT) @@ -29,7 +30,7 @@ Commits are gated by the pre-commit hook (enable: `git config core.hooksPath .gi - `src/index.ts` — removes `mode: ctx.mode` from the `pygieniumCtx` literal - `src/commands.ts` — removes `mode` from the `PygieniumCtx` Pick; swaps `ctx.mode` for `ctx.hasUI` in `print` -- `src/agent-runner.ts` — swaps the SDK import + loader for the omp SDK's `createAgentSession` options (`toolNames`, `systemPrompt`, `agentRegistry`, …) +- `src/agent-runner.ts` — swaps the SDK import + loader for the omp SDK's `createAgentSession` options (`toolNames`, `systemPrompt`, `agentRegistry`, …); reduces `ProviderRegistry` to an SDK-free shape and stubs `providerForwardingExtension` (the port disables extension discovery, so it has nothing to forward) - `src/agents.ts` — `find` → `glob` After touching any of those, run `bun port-to-omp.mjs --out ` and `(cd && bun run typecheck)` before committing; the hook does the same. diff --git a/package.json b/package.json index 5d4e650..de81b55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mikefreno/omp-pygienium", - "version": "0.1.2", + "version": "0.1.3", "description": "Code hygiene extension for omp (port of the pi extension) — isolated sub-agent checks that scan a target, apply fixes, and emit a findings+changes report.", "keywords": [ "omp", diff --git a/src/agent-runner.ts b/src/agent-runner.ts index af4773a..6faef57 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -20,10 +20,53 @@ import { dirname, isAbsolute, join } from "node:path"; import type { AgentSession, AgentSessionEvent, + ExtensionAPI, ExtensionCommandContext, } from "@oh-my-pi/pi-coding-agent"; import { loadAgents, extensionRoot, type AgentDef } from "./agents.js"; +/** + * Provider registrations as the omp port passes them around. The port does not + * forward any: omp sessions disable extension discovery, and its ModelRegistry + * keeps no extension provider registrations to forward. + */ +export interface ProviderRegistry { + getRegisteredNativeProvider(providerId: string): object | undefined; + getRegisteredProviderConfig(providerId: string): object | undefined; +} + +/** + * The inline extension that re-registers a provider in a sub-session. Spelled out + * structurally — an `InlineExtension` in pi — so the generated omp port, which + * names the type differently, still compiles. + */ +export interface InlineProviderExtension { + name: string; + hidden: boolean; + factory: (pi: ExtensionAPI) => void; +} + +/** + * Narrow a host context's `modelRegistry` to a {@link ProviderRegistry}, + * returning `undefined` when it cannot answer the two questions this module + * asks. This is the decode boundary: everything downstream works with the + * narrow interface, and a host whose registry does not know extension + * registrations simply gets no provider forwarding. + */ +export function asProviderRegistry( + candidate: unknown, +): ProviderRegistry | undefined { + if (!candidate || typeof candidate !== "object") return undefined; + const registry = candidate as Partial; + if ( + typeof registry.getRegisteredNativeProvider !== "function" || + typeof registry.getRegisteredProviderConfig !== "function" + ) { + return undefined; + } + return registry as ProviderRegistry; +} + /** * Env var overriding the per-agent settle timeout (ms). Guards against a * sub-agent session that never settles (stalled provider stream, hung retry / @@ -58,6 +101,12 @@ export interface AgentTaskOptions { * `createAgentSession` falls back to the settings default model. */ model?: ExtensionCommandContext["model"]; + /** + * The invoking session's provider registry, used to make the pinned + * `model`'s provider resolvable inside the sub-session (see + * {@link providerForwardingExtension}). + */ + modelRegistry?: ProviderRegistry; /** Optional explicit agent definition (skips `loadAgents`). */ agent?: AgentDef; /** @@ -166,6 +215,14 @@ export function resetAgentRunner(): void { currentRunner = defaultAgentRunner; } +/** + * The omp port forwards no provider registrations (see `ProviderRegistry`), so + * sub-agent sessions get no inline extension. + */ +export function providerForwardingExtension(): undefined { + return undefined; +} + /** * Real sub-agent runner: spins up an in-memory `AgentSession` scoped to `cwd`, * overrides the system prompt with the agent definition's body, restricts tools diff --git a/src/commands.ts b/src/commands.ts index 1430767..4a8806f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -44,12 +44,20 @@ import { type ExportFilters, } from "./export.js"; import type { SendChatMessage } from "./phases.js"; +import type { ProviderRegistry } from "./agent-runner.js"; /** Narrow context slice handlers need (a subset of `ExtensionCommandContext`). */ export type PygieniumCtx = Pick< ExtensionCommandContext, "cwd" | "hasUI" | "ui" | "model" > & { + /** + * The invoking session's extension provider registrations, already narrowed + * at the extension entry point (see `asProviderRegistry`). Carried into every + * sub-agent so a pinned model whose provider only an extension registered + * stays resolvable there. + */ + modelRegistry?: ProviderRegistry; /** Optional callback to post messages to the chat window. */ sendChatMessage?: SendChatMessage; /** Optional callback forwarding raw sub-agent events to the chat stream. */ @@ -177,6 +185,7 @@ export async function handleCheckCommand( ui: ctx.ui, hasUI: ctx.hasUI, model: ctx.model, + modelRegistry: ctx.modelRegistry, sendChatMessage: ctx.sendChatMessage, onAgentEvent: ctx.onAgentEvent, sendPhaseLine: ctx.sendPhaseLine, @@ -217,6 +226,7 @@ export async function handleAllCommand( ui: ctx.ui, hasUI: ctx.hasUI, model: ctx.model, + modelRegistry: ctx.modelRegistry, sendChatMessage: ctx.sendChatMessage, onAgentEvent: ctx.onAgentEvent, sendPhaseLine: ctx.sendPhaseLine, @@ -306,6 +316,7 @@ export async function handleResumeCommand( ui: ctx.ui, hasUI: ctx.hasUI, model: ctx.model, + modelRegistry: ctx.modelRegistry, existingState: state, sendChatMessage: ctx.sendChatMessage, onAgentEvent: ctx.onAgentEvent, diff --git a/src/index.ts b/src/index.ts index 6977bf0..25eb127 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ import type { import { Box, Text } from "@oh-my-pi/pi-tui"; import "./checks/all.js"; import { registerPygieniumCommands, type PygieniumCtx } from "./commands.js"; +import { asProviderRegistry } from "./agent-runner.js"; import { type SendChatMessage, type CheckCompletionDetails, @@ -404,6 +405,7 @@ export default async function pygieniumExtension( hasUI: ctx.hasUI, ui: ctx.ui, model: ctx.model, + modelRegistry: asProviderRegistry(ctx.modelRegistry), sendChatMessage, onAgentEvent, sendPhaseLine, diff --git a/src/modes/all.ts b/src/modes/all.ts index 9941bb6..ce065d3 100644 --- a/src/modes/all.ts +++ b/src/modes/all.ts @@ -29,6 +29,7 @@ import type { } from "@oh-my-pi/pi-coding-agent"; import { getAllChecks, type CheckDefinition } from "../checks/registry.js"; import { runCheck } from "./check-runner.js"; +import type { ProviderRegistry } from "../agent-runner.js"; import { createPhaseStrip, type SendChatMessage } from "../phases.js"; import { createPipelineFooter, type ItemStatus } from "../footer.js"; import { runRecon } from "../recon.js"; @@ -83,6 +84,12 @@ export interface AllRunOptions { * the settings default. */ model?: ExtensionCommandContext["model"]; + /** + * The invoking session's provider registry; carried into every check's + * sub-agents so a provider only an extension registered stays resolvable + * (see `providerForwardingExtension` in `agent-runner.ts`). + */ + modelRegistry?: ProviderRegistry; /** Optional callback to post completion messages into the chat. */ sendChatMessage?: SendChatMessage; /** Optional callback forwarding raw sub-agent events to the chat stream. */ @@ -379,6 +386,7 @@ export async function runAllChecks( ui: opts.ui, hasUI, model: opts.model, + modelRegistry: opts.modelRegistry, existingState: state, sendChatMessage: opts.sendChatMessage, onAgentEvent: opts.onAgentEvent, diff --git a/src/modes/check-runner.ts b/src/modes/check-runner.ts index c562eb7..495f104 100644 --- a/src/modes/check-runner.ts +++ b/src/modes/check-runner.ts @@ -22,7 +22,7 @@ import type { ExtensionUIContext, } from "@oh-my-pi/pi-coding-agent"; import type { CheckDefinition, CheckScope } from "../checks/registry.js"; -import { runAgentTask } from "../agent-runner.js"; +import { runAgentTask, type ProviderRegistry } from "../agent-runner.js"; import { runRecon } from "../recon.js"; import { createPhaseStrip, @@ -93,6 +93,12 @@ export interface RunCheckOptions { * settings default. */ model?: ExtensionCommandContext["model"]; + /** + * The invoking session's provider registry; carried into every sub-agent so a + * provider only an extension registered stays resolvable (see + * `providerForwardingExtension` in `agent-runner.ts`). + */ + modelRegistry?: ProviderRegistry; /** Pre-existing run state to update (for `/pygienium-all` and resume). */ existingState?: RunState; /** Optional callback to post completion messages into the chat. */ @@ -288,6 +294,7 @@ async function runCheckImplInner( agentName: check.agentName, task: scanTask, model: opts.model, + modelRegistry: opts.modelRegistry, onEvent: forward(PHASE_ANALYSIS), }); findings = scanResult.text; @@ -321,6 +328,7 @@ async function runCheckImplInner( agentName: check.fixAgentName ?? "fixer", task: fixTask, model: opts.model, + modelRegistry: opts.modelRegistry, onEvent: forward(PHASE_FIX), }); changes = fixResult.text;