mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat: add codex fast mode and harden subagent launch
This commit is contained in:
+146
-89
@@ -1,123 +1,180 @@
|
||||
import { stat } from "fs/promises";
|
||||
import { resolve } from "path";
|
||||
import { createAgentSessionServices, getAgentDir, type SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
createAgentSessionServices,
|
||||
getAgentDir,
|
||||
type SettingsManager,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
||||
import { loadModelsWithCache, withModelRuntimeError, type ModelsData } from "@/lib/models-cache";
|
||||
import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access";
|
||||
import {
|
||||
loadModelsWithCache,
|
||||
withModelRuntimeError,
|
||||
type ModelsData,
|
||||
} from "@/lib/models-cache";
|
||||
import {
|
||||
getAllowedFileRoots,
|
||||
isExistingFilePathAllowed,
|
||||
} from "@/lib/file-access";
|
||||
import { projectTrustReloadOptions } from "@/lib/project-trust";
|
||||
import { createAppSettingsManager, getAppResourceLoaderOptions } from "@/lib/app-runtime";
|
||||
import {
|
||||
createAppSettingsManager,
|
||||
getAppResourceLoaderOptions,
|
||||
} from "@/lib/app-runtime";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const modelNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
||||
const modelNameCollator = new Intl.Collator(undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
|
||||
function compareModelEntries(
|
||||
a: { id: string; name: string; provider: string },
|
||||
b: { id: string; name: string; provider: string }
|
||||
a: { id: string; name: string; provider: string },
|
||||
b: { id: string; name: string; provider: string },
|
||||
): number {
|
||||
return modelNameCollator.compare(a.name || a.id, b.name || b.id)
|
||||
|| modelNameCollator.compare(a.provider, b.provider)
|
||||
|| modelNameCollator.compare(a.id, b.id);
|
||||
return (
|
||||
modelNameCollator.compare(a.name || a.id, b.name || b.id) ||
|
||||
modelNameCollator.compare(a.provider, b.provider) ||
|
||||
modelNameCollator.compare(a.id, b.id)
|
||||
);
|
||||
}
|
||||
|
||||
const THINKING_SUFFIXES = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
||||
const THINKING_SUFFIXES = new Set([
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
]);
|
||||
|
||||
function stripThinkingSuffix(modelRef: string): string {
|
||||
const trimmed = modelRef.trim();
|
||||
const colonIndex = trimmed.lastIndexOf(":");
|
||||
if (colonIndex === -1) return trimmed;
|
||||
const suffix = trimmed.substring(colonIndex + 1);
|
||||
return THINKING_SUFFIXES.has(suffix) ? trimmed.substring(0, colonIndex) : trimmed;
|
||||
const trimmed = modelRef.trim();
|
||||
const colonIndex = trimmed.lastIndexOf(":");
|
||||
if (colonIndex === -1) return trimmed;
|
||||
const suffix = trimmed.substring(colonIndex + 1);
|
||||
return THINKING_SUFFIXES.has(suffix)
|
||||
? trimmed.substring(0, colonIndex)
|
||||
: trimmed;
|
||||
}
|
||||
|
||||
function filterByExactEnabledModels<T extends { id: string; provider: string }>(
|
||||
available: readonly T[],
|
||||
enabledModels: string[] | undefined,
|
||||
available: readonly T[],
|
||||
enabledModels: string[] | undefined,
|
||||
): readonly T[] {
|
||||
if (!enabledModels || enabledModels.length === 0) return available;
|
||||
if (!enabledModels || enabledModels.length === 0) return available;
|
||||
|
||||
const refs = new Set(enabledModels.map(stripThinkingSuffix).filter(Boolean));
|
||||
const visible = available.filter((m) => refs.has(`${m.provider}/${m.id}`) || refs.has(m.id));
|
||||
return visible.length > 0 ? visible : available;
|
||||
const refs = new Set(enabledModels.map(stripThinkingSuffix).filter(Boolean));
|
||||
const visible = available.filter(
|
||||
(m) => refs.has(`${m.provider}/${m.id}`) || refs.has(m.id),
|
||||
);
|
||||
return visible.length > 0 ? visible : available;
|
||||
}
|
||||
|
||||
async function loadModels(cwd: string): Promise<ModelsData> {
|
||||
const nameMap = new Map<string, string>();
|
||||
let modelList: { id: string; name: string; provider: string }[] = [];
|
||||
let defaultModel: { provider: string; modelId: string } | null = null;
|
||||
const thinkingLevels: Record<string, string[]> = {};
|
||||
const thinkingLevelMaps: Record<string, Record<string, string | null>> = {};
|
||||
const nameMap = new Map<string, string>();
|
||||
let modelList: { id: string; name: string; provider: string }[] = [];
|
||||
let defaultModel: { provider: string; modelId: string } | null = null;
|
||||
const thinkingLevels: Record<string, string[]> = {};
|
||||
const thinkingLevelMaps: Record<string, Record<string, string | null>> = {};
|
||||
|
||||
const agentDir = getAgentDir();
|
||||
// Gate untrusted project extensions: enumerating models still imports and
|
||||
// runs a repository's .pi/extensions factories, so honor project trust here
|
||||
// too (see lib/project-trust.ts, #236).
|
||||
const trustReloadOptions = projectTrustReloadOptions(cwd, agentDir);
|
||||
const services = await createAgentSessionServices({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager: createAppSettingsManager(cwd, agentDir),
|
||||
resourceLoaderOptions: getAppResourceLoaderOptions(),
|
||||
...(trustReloadOptions ? { resourceLoaderReloadOptions: trustReloadOptions } : {}),
|
||||
});
|
||||
const available = await services.modelRuntime.getAvailable();
|
||||
const modelError = services.modelRuntime.getError();
|
||||
const settings: SettingsManager = services.settingsManager;
|
||||
const enabledModels = settings.getEnabledModels();
|
||||
const visible = filterByExactEnabledModels(available, enabledModels);
|
||||
modelList = visible.map((m: { id: string; name: string; provider: string }) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
provider: m.provider,
|
||||
})).sort(compareModelEntries);
|
||||
for (const m of visible) {
|
||||
const key = `${m.provider}:${m.id}`;
|
||||
nameMap.set(key, m.name);
|
||||
thinkingLevels[key] = getSupportedThinkingLevels(m);
|
||||
if (m.thinkingLevelMap) thinkingLevelMaps[key] = m.thinkingLevelMap;
|
||||
}
|
||||
const agentDir = getAgentDir();
|
||||
// Gate untrusted project extensions: enumerating models still imports and
|
||||
// runs a repository's .pi/extensions factories, so honor project trust here
|
||||
// too (see lib/project-trust.ts, #236).
|
||||
const trustReloadOptions = projectTrustReloadOptions(cwd, agentDir);
|
||||
const services = await createAgentSessionServices({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager: createAppSettingsManager(cwd, agentDir),
|
||||
resourceLoaderOptions: getAppResourceLoaderOptions(),
|
||||
...(trustReloadOptions
|
||||
? { resourceLoaderReloadOptions: trustReloadOptions }
|
||||
: {}),
|
||||
});
|
||||
const available = await services.modelRuntime.getAvailable();
|
||||
const modelError = services.modelRuntime.getError();
|
||||
const settings: SettingsManager = services.settingsManager;
|
||||
const enabledModels = settings.getEnabledModels();
|
||||
const visible = filterByExactEnabledModels(available, enabledModels);
|
||||
modelList = visible
|
||||
.map(
|
||||
(m: {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
serviceTier?: string;
|
||||
}) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
provider: m.provider,
|
||||
serviceTier: m.serviceTier,
|
||||
}),
|
||||
)
|
||||
.sort(compareModelEntries);
|
||||
for (const m of visible) {
|
||||
const key = `${m.provider}:${m.id}`;
|
||||
nameMap.set(key, m.name);
|
||||
thinkingLevels[key] = getSupportedThinkingLevels(m);
|
||||
if (m.thinkingLevelMap) thinkingLevelMaps[key] = m.thinkingLevelMap;
|
||||
}
|
||||
|
||||
const provider = settings.getDefaultProvider();
|
||||
const modelId = settings.getDefaultModel();
|
||||
if (provider && modelId && visible.some((m) => m.provider === provider && m.id === modelId)) {
|
||||
defaultModel = { provider, modelId };
|
||||
}
|
||||
const provider = settings.getDefaultProvider();
|
||||
const modelId = settings.getDefaultModel();
|
||||
if (
|
||||
provider &&
|
||||
modelId &&
|
||||
visible.some((m) => m.provider === provider && m.id === modelId)
|
||||
) {
|
||||
defaultModel = { provider, modelId };
|
||||
}
|
||||
|
||||
return withModelRuntimeError(
|
||||
{ models: Object.fromEntries(nameMap), modelList, defaultModel, thinkingLevels, thinkingLevelMaps },
|
||||
modelError,
|
||||
);
|
||||
return withModelRuntimeError(
|
||||
{
|
||||
models: Object.fromEntries(nameMap),
|
||||
modelList,
|
||||
defaultModel,
|
||||
thinkingLevels,
|
||||
thinkingLevelMaps,
|
||||
},
|
||||
modelError,
|
||||
);
|
||||
}
|
||||
|
||||
const EMPTY_MODELS: ModelsData = {
|
||||
models: {},
|
||||
modelList: [],
|
||||
defaultModel: null,
|
||||
thinkingLevels: {},
|
||||
thinkingLevelMaps: {},
|
||||
models: {},
|
||||
modelList: [],
|
||||
defaultModel: null,
|
||||
thinkingLevels: {},
|
||||
thinkingLevelMaps: {},
|
||||
};
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const requestedCwd = new URL(req.url).searchParams.get("cwd") || process.cwd();
|
||||
const cwd = resolve(requestedCwd);
|
||||
const requestedCwd =
|
||||
new URL(req.url).searchParams.get("cwd") || process.cwd();
|
||||
const cwd = resolve(requestedCwd);
|
||||
|
||||
let cwdStat;
|
||||
try {
|
||||
cwdStat = await stat(cwd);
|
||||
} catch {
|
||||
return Response.json({ error: `Directory does not exist: ${cwd}` }, { status: 400 });
|
||||
}
|
||||
if (!cwdStat.isDirectory()) {
|
||||
return Response.json({ error: `Not a directory: ${cwd}` }, { status: 400 });
|
||||
}
|
||||
const allowedRoots = await getAllowedFileRoots();
|
||||
if (!isExistingFilePathAllowed(cwd, allowedRoots)) {
|
||||
return Response.json({ error: "Access denied" }, { status: 403 });
|
||||
}
|
||||
let cwdStat;
|
||||
try {
|
||||
cwdStat = await stat(cwd);
|
||||
} catch {
|
||||
return Response.json(
|
||||
{ error: `Directory does not exist: ${cwd}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (!cwdStat.isDirectory()) {
|
||||
return Response.json({ error: `Not a directory: ${cwd}` }, { status: 400 });
|
||||
}
|
||||
const allowedRoots = await getAllowedFileRoots();
|
||||
if (!isExistingFilePathAllowed(cwd, allowedRoots)) {
|
||||
return Response.json({ error: "Access denied" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
return Response.json(await loadModelsWithCache(cwd, () => loadModels(cwd)));
|
||||
} catch {
|
||||
return Response.json(EMPTY_MODELS);
|
||||
}
|
||||
try {
|
||||
return Response.json(await loadModelsWithCache(cwd, () => loadModels(cwd)));
|
||||
} catch {
|
||||
return Response.json(EMPTY_MODELS);
|
||||
}
|
||||
}
|
||||
|
||||
+3440
-2204
File diff suppressed because it is too large
Load Diff
+2158
-1539
File diff suppressed because it is too large
Load Diff
+70
-56
@@ -1,80 +1,94 @@
|
||||
export interface ModelsData {
|
||||
models: Record<string, string>;
|
||||
modelList: { id: string; name: string; provider: string }[];
|
||||
defaultModel: { provider: string; modelId: string } | null;
|
||||
thinkingLevels: Record<string, string[]>;
|
||||
thinkingLevelMaps: Record<string, Record<string, string | null>>;
|
||||
modelError?: string;
|
||||
models: Record<string, string>;
|
||||
modelList: {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
serviceTier?: string;
|
||||
}[];
|
||||
defaultModel: { provider: string; modelId: string } | null;
|
||||
thinkingLevels: Record<string, string[]>;
|
||||
thinkingLevelMaps: Record<string, Record<string, string | null>>;
|
||||
modelError?: string;
|
||||
}
|
||||
|
||||
interface ModelsCacheState {
|
||||
entries: Map<string, { data: ModelsData; expiresAt: number }>;
|
||||
inFlight: Map<string, Promise<ModelsData>>;
|
||||
generation: number;
|
||||
entries: Map<string, { data: ModelsData; expiresAt: number }>;
|
||||
inFlight: Map<string, Promise<ModelsData>>;
|
||||
generation: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
var __piModelsCacheState: ModelsCacheState | undefined;
|
||||
var __piModelsCacheState: ModelsCacheState | undefined;
|
||||
}
|
||||
|
||||
const MODELS_CACHE_TTL_MS = 60_000;
|
||||
const MAX_MODELS_CACHE_ENTRIES = 32;
|
||||
|
||||
function getModelsCacheState(): ModelsCacheState {
|
||||
if (!globalThis.__piModelsCacheState) {
|
||||
globalThis.__piModelsCacheState = {
|
||||
entries: new Map(),
|
||||
inFlight: new Map(),
|
||||
generation: 0,
|
||||
};
|
||||
}
|
||||
return globalThis.__piModelsCacheState;
|
||||
if (!globalThis.__piModelsCacheState) {
|
||||
globalThis.__piModelsCacheState = {
|
||||
entries: new Map(),
|
||||
inFlight: new Map(),
|
||||
generation: 0,
|
||||
};
|
||||
}
|
||||
return globalThis.__piModelsCacheState;
|
||||
}
|
||||
|
||||
export function invalidateModelsCache(): void {
|
||||
const state = getModelsCacheState();
|
||||
state.generation += 1;
|
||||
state.entries.clear();
|
||||
state.inFlight.clear();
|
||||
const state = getModelsCacheState();
|
||||
state.generation += 1;
|
||||
state.entries.clear();
|
||||
state.inFlight.clear();
|
||||
}
|
||||
|
||||
export function withModelRuntimeError(data: ModelsData, modelError: string | undefined): ModelsData {
|
||||
return modelError ? { ...data, modelError } : data;
|
||||
export function withModelRuntimeError(
|
||||
data: ModelsData,
|
||||
modelError: string | undefined,
|
||||
): ModelsData {
|
||||
return modelError ? { ...data, modelError } : data;
|
||||
}
|
||||
|
||||
export function loadModelsWithCache(cwd: string, loader: () => Promise<ModelsData>): Promise<ModelsData> {
|
||||
const state = getModelsCacheState();
|
||||
const cached = state.entries.get(cwd);
|
||||
if (cached) {
|
||||
if (cached.expiresAt > Date.now()) return Promise.resolve(cached.data);
|
||||
state.entries.delete(cwd);
|
||||
}
|
||||
export function loadModelsWithCache(
|
||||
cwd: string,
|
||||
loader: () => Promise<ModelsData>,
|
||||
): Promise<ModelsData> {
|
||||
const state = getModelsCacheState();
|
||||
const cached = state.entries.get(cwd);
|
||||
if (cached) {
|
||||
if (cached.expiresAt > Date.now()) return Promise.resolve(cached.data);
|
||||
state.entries.delete(cwd);
|
||||
}
|
||||
|
||||
const existingLoad = state.inFlight.get(cwd);
|
||||
if (existingLoad) return existingLoad;
|
||||
const existingLoad = state.inFlight.get(cwd);
|
||||
if (existingLoad) return existingLoad;
|
||||
|
||||
const generation = state.generation;
|
||||
const loadPromise: Promise<ModelsData> = Promise.resolve()
|
||||
.then(loader)
|
||||
.then((data) => {
|
||||
if (state.generation === generation && state.inFlight.get(cwd) === loadPromise) {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of state.entries) {
|
||||
if (entry.expiresAt <= now) state.entries.delete(key);
|
||||
}
|
||||
while (state.entries.size >= MAX_MODELS_CACHE_ENTRIES) {
|
||||
const oldestKey = state.entries.keys().next().value;
|
||||
if (oldestKey === undefined) break;
|
||||
state.entries.delete(oldestKey);
|
||||
}
|
||||
state.entries.set(cwd, { data, expiresAt: now + MODELS_CACHE_TTL_MS });
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.finally(() => {
|
||||
if (state.inFlight.get(cwd) === loadPromise) state.inFlight.delete(cwd);
|
||||
});
|
||||
const generation = state.generation;
|
||||
const loadPromise: Promise<ModelsData> = Promise.resolve()
|
||||
.then(loader)
|
||||
.then((data) => {
|
||||
if (
|
||||
state.generation === generation &&
|
||||
state.inFlight.get(cwd) === loadPromise
|
||||
) {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of state.entries) {
|
||||
if (entry.expiresAt <= now) state.entries.delete(key);
|
||||
}
|
||||
while (state.entries.size >= MAX_MODELS_CACHE_ENTRIES) {
|
||||
const oldestKey = state.entries.keys().next().value;
|
||||
if (oldestKey === undefined) break;
|
||||
state.entries.delete(oldestKey);
|
||||
}
|
||||
state.entries.set(cwd, { data, expiresAt: now + MODELS_CACHE_TTL_MS });
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.finally(() => {
|
||||
if (state.inFlight.get(cwd) === loadPromise) state.inFlight.delete(cwd);
|
||||
});
|
||||
|
||||
state.inFlight.set(cwd, loadPromise);
|
||||
return loadPromise;
|
||||
state.inFlight.set(cwd, loadPromise);
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user