fix(coding-agent): scope SDK extension packages
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
* @see ./omp-plugins.ts
|
||||
* @see ./builtin.ts `loadExtensionModules`
|
||||
*/
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { getAgentDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils";
|
||||
@@ -41,8 +42,18 @@ interface InjectedRoot {
|
||||
level: "user" | "project";
|
||||
}
|
||||
|
||||
export type OmpExtensionRootMode = "merge" | "explicit-only";
|
||||
|
||||
interface InvocationRootScope {
|
||||
/** Raw SDK spellings, resolved against the LoadContext that performs discovery. */
|
||||
paths: readonly string[];
|
||||
mode: OmpExtensionRootMode;
|
||||
}
|
||||
|
||||
const invocationRootScope = new AsyncLocalStorage<InvocationRootScope>();
|
||||
|
||||
let injectedCliRoots: InjectedRoot[] = [];
|
||||
let injectedCliRootMode: "merge" | "explicit-only" = "merge";
|
||||
let injectedCliRootMode: OmpExtensionRootMode = "merge";
|
||||
|
||||
export interface InjectOmpExtensionCliRootOptions {
|
||||
/**
|
||||
@@ -50,11 +61,25 @@ export interface InjectOmpExtensionCliRootOptions {
|
||||
* with `--no-extensions` so configured and installed packages cannot
|
||||
* contribute sibling capabilities through the `omp-plugins` provider.
|
||||
*/
|
||||
mode?: "merge" | "explicit-only";
|
||||
mode?: OmpExtensionRootMode;
|
||||
/** Replace roots from an earlier invocation instead of extending them. */
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one SDK invocation with its own extension-package roots. Async resources
|
||||
* started inside `callback` retain this scope, including discovery deliberately
|
||||
* deferred until the end of session startup. Raw relative paths are resolved by
|
||||
* {@link listOmpExtensionRoots} against that invocation's active cwd.
|
||||
*/
|
||||
export function withOmpExtensionRootScope<T>(
|
||||
paths: readonly string[],
|
||||
mode: OmpExtensionRootMode,
|
||||
callback: () => T,
|
||||
): T {
|
||||
return invocationRootScope.run({ paths: [...paths], mode }, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register CLI-provided extension package paths (e.g. from `--extension`/`-e`)
|
||||
* so the sub-discovery providers can find their sibling `skills/`, `hooks/`,
|
||||
@@ -146,7 +171,8 @@ async function isDirectory(p: string): Promise<boolean> {
|
||||
* Sources, in order of precedence (later entries with the same absolute path
|
||||
* are dropped):
|
||||
*
|
||||
* 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
|
||||
* 1. Invocation-scoped SDK roots, when present; otherwise CLI roots injected
|
||||
* via {@link injectOmpExtensionCliRoots}
|
||||
* 2. Project `<cwd>/.omp/settings.json#extensions`
|
||||
* 3. User `~/.omp/agent/settings.json#extensions`
|
||||
* 4. Enabled npm/link plugins installed under `<plugins>/node_modules/` (for
|
||||
@@ -159,10 +185,14 @@ async function isDirectory(p: string): Promise<boolean> {
|
||||
* other sources still surface.
|
||||
*/
|
||||
export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtensionRoot[]> {
|
||||
let candidates: InjectedRoot[] = injectedCliRoots.map(root =>
|
||||
root.relativePath ? { ...root, path: path.resolve(ctx.cwd, root.relativePath) } : root,
|
||||
);
|
||||
if (injectedCliRootMode === "merge") {
|
||||
const scopedRoots = invocationRootScope.getStore();
|
||||
const rootMode = scopedRoots?.mode ?? injectedCliRootMode;
|
||||
let candidates: InjectedRoot[] = scopedRoots
|
||||
? scopedRoots.paths.map(raw => ({ path: resolveAgainst(raw, ctx), level: "user" }))
|
||||
: injectedCliRoots.map(root =>
|
||||
root.relativePath ? { ...root, path: path.resolve(ctx.cwd, root.relativePath) } : root,
|
||||
);
|
||||
if (rootMode === "merge") {
|
||||
const { project, user } = scopeDirs(ctx);
|
||||
const [projectExtensions, userExtensions, installedPlugins] = await Promise.all([
|
||||
readSettingsExtensions(path.join(project, "settings.json")),
|
||||
@@ -177,7 +207,7 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
|
||||
];
|
||||
}
|
||||
|
||||
// First-seen-wins dedup preserves CLI > project-settings > user-settings > installed precedence.
|
||||
// First-seen-wins dedup preserves invocation/CLI > project-settings > user-settings > installed precedence.
|
||||
const seen = new Set<string>();
|
||||
const unique: InjectedRoot[] = [];
|
||||
for (const candidate of candidates) {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { CursorExecHandlers, type CursorMcpResourceAdapter } from "./cursor";
|
||||
import { createBridgeEditTool, createBridgeGrepFactory } from "./cursor-bridge-tools";
|
||||
import "./discovery";
|
||||
import { initializeWithSettings } from "./discovery";
|
||||
import { withOmpExtensionRootScope } from "./discovery/omp-extension-roots";
|
||||
import { disposeAllJuliaKernelSessions, disposeJuliaKernelSessionsByOwner } from "./eval/jl/executor";
|
||||
import { disposeVmContextsByOwner } from "./eval/js/context-manager";
|
||||
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
|
||||
@@ -1217,6 +1218,13 @@ export function createAutoLearnCaptureRunner(
|
||||
* ```
|
||||
*/
|
||||
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
|
||||
const rootMode = options.disableExtensionDiscovery ? "explicit-only" : "merge";
|
||||
return await withOmpExtensionRootScope(options.additionalExtensionPaths ?? [], rootMode, () =>
|
||||
createAgentSessionScoped(options),
|
||||
);
|
||||
}
|
||||
|
||||
async function createAgentSessionScoped(options: CreateAgentSessionOptions): Promise<CreateAgentSessionResult> {
|
||||
const cwd = options.cwd ?? getProjectDir();
|
||||
const agentDir = options.agentDir ?? getAgentDir();
|
||||
const eventBus = options.eventBus ?? new EventBus();
|
||||
|
||||
Reference in New Issue
Block a user