fix: route all AI lanes through runtime resolution so CLI-runtime models work everywhere
createFnAgent now delegates to createResolvedAgentSession — deriving the CLI runtime hint (cursor/claude/grok/omp/hermes), mock/test-mode forcing, and session:runtime-resolved visibility — using a host-registered default PluginRunner keyed by project root (published by InProcessRuntime at plugin init). DefaultPiRuntime re-enters via a __rawPiSession marker so the seam's own pi bridge cannot recurse, and the raw constructor survives as createPiAgentSessionRaw for that bridge and pi-internal tests. Mission and milestone/slice interviews additionally pass their request-scoped pluginRunner through the seam and prompt via the engine promptWithFallback dispatcher (plugin CLI runtime sessions have no session.prompt()). This fixes "Configured model cursor-cli/auto ... was not found in the pi model registry" in mission planning while chat on the same model worked, and closes the same gap for every remaining bare createFnAgent lane. Also updates the pi skill-filtering test that still asserted pre-FN-9114 allow-list narrowing; requested skill names are ensure-present since FN-9114. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/mission-interview-cli-runtime-routing.md
Normal file
7
.changeset/mission-interview-cli-runtime-routing.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Route every AI lane through runtime resolution so CLI-runtime models (e.g. Cursor CLI) work everywhere chat does.
|
||||||
|
category: fix
|
||||||
|
dev: `createFnAgent` now delegates to `createResolvedAgentSession` (CLI runtime hint derivation, mock forcing, runtime-resolved visibility) with a host-registered default PluginRunner per project root; `DefaultPiRuntime` re-enters via a `__rawPiSession` marker into `createPiAgentSessionRaw`. Mission and milestone/slice interviews also pass their request-scoped pluginRunner and prompt via the engine `promptWithFallback` dispatcher, fixing "cursor-cli/auto ... not found in the pi model registry" in mission planning.
|
||||||
@@ -10,13 +10,19 @@ fresh question instead of throwing "No active question in session". A completed
|
|||||||
|
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37:
|
||||||
|
Mission and milestone/slice interviews now build sessions through the shared
|
||||||
|
`createResolvedAgentSession` seam (so CLI-runtime model selections route to their
|
||||||
|
runtime plugins); mock that seam with the same scripted session factory.
|
||||||
|
*/
|
||||||
vi.mock("@fusion/engine", async (importOriginal) => {
|
vi.mock("@fusion/engine", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("@fusion/engine")>();
|
const actual = await importOriginal<typeof import("@fusion/engine")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
buildSessionSkillContextSync: vi.fn(() => ({ skillSelectionContext: undefined })),
|
buildSessionSkillContextSync: vi.fn(() => ({ skillSelectionContext: undefined })),
|
||||||
resolveMcpServersForStore: vi.fn(async () => ({ servers: [] })),
|
resolveMcpServersForStore: vi.fn(async () => ({ servers: [] })),
|
||||||
createFnAgent: vi.fn(async () => ({
|
createResolvedAgentSession: vi.fn(async () => ({
|
||||||
session: {
|
session: {
|
||||||
state: { messages: [] as Array<{ role: string; content: string }> },
|
state: { messages: [] as Array<{ role: string; content: string }> },
|
||||||
prompt: vi.fn(async function (this: { state: { messages: Array<{ role: string; content: string }> } }) {
|
prompt: vi.fn(async function (this: { state: { messages: Array<{ role: string; content: string }> } }) {
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ const { createFnAgentMock, createResolvedAgentSessionMock, resolveMcpServersForS
|
|||||||
FNXC:PlanningRuntimeResolution 2026-07-24-16:20:
|
FNXC:PlanningRuntimeResolution 2026-07-24-16:20:
|
||||||
Planning now builds its session through the shared runtime-resolving seam
|
Planning now builds its session through the shared runtime-resolving seam
|
||||||
(`createResolvedAgentSession`), not a bare `createFnAgent` call, so the MCP-forwarding
|
(`createResolvedAgentSession`), not a bare `createFnAgent` call, so the MCP-forwarding
|
||||||
contract for the planning lanes is asserted on that seam. The mission/target interview
|
contract for the planning lanes is asserted on that seam.
|
||||||
lanes still construct through `createFnAgent` and keep their own mock.
|
FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37: the mission and milestone/slice
|
||||||
|
interview lanes now build through the same seam so CLI-runtime model selections
|
||||||
|
(cursor-cli etc.) route to their runtime plugins; their assertions target it too.
|
||||||
*/
|
*/
|
||||||
createResolvedAgentSessionMock: vi.fn(makeScriptedAgent),
|
createResolvedAgentSessionMock: vi.fn(makeScriptedAgent),
|
||||||
resolveMcpServersForStoreMock: vi.fn(async () => ({
|
resolveMcpServersForStoreMock: vi.fn(async () => ({
|
||||||
@@ -184,7 +186,7 @@ describe("dashboard MCP lane forwarding", () => {
|
|||||||
await createMissionInterviewAgent(session, "/tmp/fusion-dashboard-test", store);
|
await createMissionInterviewAgent(session, "/tmp/fusion-dashboard-test", store);
|
||||||
|
|
||||||
expect(resolveMcpServersForStoreMock).toHaveBeenCalledWith(store);
|
expect(resolveMcpServersForStoreMock).toHaveBeenCalledWith(store);
|
||||||
expect(createFnAgentMock).toHaveBeenCalledWith(expect.objectContaining({
|
expect(createResolvedAgentSessionMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
cwd: "/tmp/fusion-dashboard-test",
|
cwd: "/tmp/fusion-dashboard-test",
|
||||||
tools: "readonly",
|
tools: "readonly",
|
||||||
allowMcpToolsInReadonly: true,
|
allowMcpToolsInReadonly: true,
|
||||||
@@ -197,7 +199,7 @@ describe("dashboard MCP lane forwarding", () => {
|
|||||||
|
|
||||||
await createMissionInterviewAgent({ id: "mission-empty", thinkingOutput: "" } as never, "/tmp/fusion-dashboard-test", {} as never);
|
await createMissionInterviewAgent({ id: "mission-empty", thinkingOutput: "" } as never, "/tmp/fusion-dashboard-test", {} as never);
|
||||||
|
|
||||||
expect(createFnAgentMock).toHaveBeenCalledWith(expect.objectContaining({ mcpServers: [] }));
|
expect(createResolvedAgentSessionMock).toHaveBeenCalledWith(expect.objectContaining({ mcpServers: [] }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forwards materialized MCP servers to milestone and slice interview agents", async () => {
|
it("forwards materialized MCP servers to milestone and slice interview agents", async () => {
|
||||||
@@ -207,7 +209,7 @@ describe("dashboard MCP lane forwarding", () => {
|
|||||||
await createTargetInterviewAgent(session, "/tmp/fusion-dashboard-test", store);
|
await createTargetInterviewAgent(session, "/tmp/fusion-dashboard-test", store);
|
||||||
|
|
||||||
expect(resolveMcpServersForStoreMock).toHaveBeenCalledWith(store);
|
expect(resolveMcpServersForStoreMock).toHaveBeenCalledWith(store);
|
||||||
expect(createFnAgentMock).toHaveBeenCalledWith(expect.objectContaining({
|
expect(createResolvedAgentSessionMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
cwd: "/tmp/fusion-dashboard-test",
|
cwd: "/tmp/fusion-dashboard-test",
|
||||||
tools: "readonly",
|
tools: "readonly",
|
||||||
allowMcpToolsInReadonly: true,
|
allowMcpToolsInReadonly: true,
|
||||||
@@ -220,6 +222,6 @@ describe("dashboard MCP lane forwarding", () => {
|
|||||||
|
|
||||||
await createTargetInterviewAgent({ id: "target-empty", targetType: "slice", thinkingOutput: "" } as never, "/tmp/fusion-dashboard-test", {} as never);
|
await createTargetInterviewAgent({ id: "target-empty", targetType: "slice", thinkingOutput: "" } as never, "/tmp/fusion-dashboard-test", {} as never);
|
||||||
|
|
||||||
expect(createFnAgentMock).toHaveBeenCalledWith(expect.objectContaining({ mcpServers: [] }));
|
expect(createResolvedAgentSessionMock).toHaveBeenCalledWith(expect.objectContaining({ mcpServers: [] }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,15 +104,28 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
|
|||||||
// Export the parse function for tests
|
// Export the parse function for tests
|
||||||
export { parseTargetInterviewResponseImpl as parseTargetInterviewResponse };
|
export { parseTargetInterviewResponseImpl as parseTargetInterviewResponse };
|
||||||
|
|
||||||
import { buildSessionSkillContextSync, createFnAgent as engineCreateFnAgent, resolveMcpServersForStore } from "@fusion/engine";
|
import { buildSessionSkillContextSync, createResolvedAgentSession, promptWithFallback as enginePromptWithFallback, resolveMcpServersForStore } from "@fusion/engine";
|
||||||
import { createPlanningBoardTools } from "./planning-board-tools.js";
|
import { createPlanningBoardTools } from "./planning-board-tools.js";
|
||||||
import { laneModelOptions, resolveLaneSessionModel } from "./lane-session-model.js";
|
import { laneModelOptions, resolveLaneSessionModel } from "./lane-session-model.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AgentResult = any;
|
type AgentResult = any;
|
||||||
type SkillSelectionPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
type SkillSelectionPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
||||||
|
/*
|
||||||
|
FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37:
|
||||||
|
Milestone/slice interviews must use the shared `createResolvedAgentSession` seam (like chat, Planning Mode, and the mission interview) instead of bare `createFnAgent`, which pins the session to the default pi runtime. Without the seam, a CLI-runtime model selection (cursor-cli, claude-cli, hermes, omp-cli, no-key grok-cli) failed with "not found in the pi model registry" while chat on the same model worked.
|
||||||
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const createFnAgent: any = engineCreateFnAgent;
|
const createFnAgent: any = async (options: any): Promise<AgentResult> => {
|
||||||
|
const { pluginRunner, runtimeHint, settings, ...runtimeOptions } = options ?? {};
|
||||||
|
return createResolvedAgentSession({
|
||||||
|
sessionPurpose: "executor",
|
||||||
|
...(runtimeHint ? { runtimeHint } : {}),
|
||||||
|
...(pluginRunner ? { pluginRunner } : {}),
|
||||||
|
...(settings ? { settings } : {}),
|
||||||
|
...runtimeOptions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function ensureEngineReady(): Promise<void> {
|
function ensureEngineReady(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -848,6 +861,8 @@ export async function createTargetInterviewAgent(
|
|||||||
mcpServers,
|
mcpServers,
|
||||||
...laneModelOptions(model),
|
...laneModelOptions(model),
|
||||||
allowMcpToolsInReadonly: true,
|
allowMcpToolsInReadonly: true,
|
||||||
|
// FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37: forward the plugin runner so the shared seam can route CLI-runtime model selections to their runtime plugins.
|
||||||
|
...(pluginRunner ? { pluginRunner } : {}),
|
||||||
customTools: [...createPlanningBoardTools(store)],
|
customTools: [...createPlanningBoardTools(store)],
|
||||||
/*
|
/*
|
||||||
FNXC:InterviewSkills 2026-06-17-21:42:
|
FNXC:InterviewSkills 2026-06-17-21:42:
|
||||||
@@ -990,7 +1005,9 @@ async function ensureInterviewAgent(
|
|||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
await session.agent!.session.prompt(
|
// FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37: prompt through the engine dispatcher — plugin CLI runtime sessions (cursor/grok/droid) have no session.prompt(); the shared seam bound the runtime's promptWithFallback onto the session and this delegates to it.
|
||||||
|
await enginePromptWithFallback(
|
||||||
|
session.agent!.session,
|
||||||
[
|
[
|
||||||
"Previous conversation summary:",
|
"Previous conversation summary:",
|
||||||
historySummary,
|
historySummary,
|
||||||
@@ -1074,7 +1091,7 @@ async function continueAgentConversation(session: TargetInterviewSession, messag
|
|||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
await agent.session.prompt(message, { signal: abortSignal });
|
await enginePromptWithFallback(agent.session, message, { signal: abortSignal });
|
||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
@@ -1121,7 +1138,8 @@ async function continueAgentConversation(session: TargetInterviewSession, messag
|
|||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
await agent.session.prompt(
|
await enginePromptWithFallback(
|
||||||
|
agent.session,
|
||||||
"Your previous response could not be parsed as JSON. " +
|
"Your previous response could not be parsed as JSON. " +
|
||||||
'Please respond with ONLY a valid JSON object: either {"type":"question","data":{...}} ' +
|
'Please respond with ONLY a valid JSON object: either {"type":"question","data":{...}} ' +
|
||||||
'or {"type":"complete","data":{"title":"...","description":"...","planningNotes":"...","verification":"..."}}' +
|
'or {"type":"complete","data":{"title":"...","description":"...","planningNotes":"...","verification":"..."}}' +
|
||||||
|
|||||||
@@ -29,15 +29,28 @@ import {
|
|||||||
} from "./ai-session-diagnostics.js";
|
} from "./ai-session-diagnostics.js";
|
||||||
import { createAbortError, GenerationGuard, isAbortError } from "./ai-session-timeout.js";
|
import { createAbortError, GenerationGuard, isAbortError } from "./ai-session-timeout.js";
|
||||||
|
|
||||||
import { buildSessionSkillContextSync, createFnAgent as engineCreateFnAgent, resolveMcpServersForStore } from "@fusion/engine";
|
import { buildSessionSkillContextSync, createResolvedAgentSession, promptWithFallback as enginePromptWithFallback, resolveMcpServersForStore } from "@fusion/engine";
|
||||||
import { createPlanningBoardTools } from "./planning-board-tools.js";
|
import { createPlanningBoardTools } from "./planning-board-tools.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AgentResult = any;
|
type AgentResult = any;
|
||||||
type SkillPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
type SkillPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
||||||
const MISSION_INTERVIEW_BUILTIN_WEB_TOOLS = ["WebSearch", "WebFetch"] as const;
|
const MISSION_INTERVIEW_BUILTIN_WEB_TOOLS = ["WebSearch", "WebFetch"] as const;
|
||||||
|
/*
|
||||||
|
FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37:
|
||||||
|
Mission interview sessions must go through the shared `createResolvedAgentSession` seam — the same one chat, Planning Mode (createPlanningRuntimeSession), executor, and reviewer use — NOT a bare `createFnAgent` call. Bare `createFnAgent` pins the session to the default pi runtime, so a CLI-runtime model selection (cursor-cli, claude-cli, hermes, omp-cli, no-key grok-cli) could never route to its runtime plugin and died with "Configured model cursor-cli/auto ... was not found in the pi model registry" while chat on the same model worked. The seam derives the CLI runtime hint from the selected provider and emits `session:runtime-resolved` run-audit visibility.
|
||||||
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const createFnAgent: any = engineCreateFnAgent;
|
const createFnAgent: any = async (options: any): Promise<AgentResult> => {
|
||||||
|
const { pluginRunner, runtimeHint, settings, ...runtimeOptions } = options ?? {};
|
||||||
|
return createResolvedAgentSession({
|
||||||
|
sessionPurpose: "executor",
|
||||||
|
...(runtimeHint ? { runtimeHint } : {}),
|
||||||
|
...(pluginRunner ? { pluginRunner } : {}),
|
||||||
|
...(settings ? { settings } : {}),
|
||||||
|
...runtimeOptions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared diagnostics helper for the mission-interview module.
|
* Shared diagnostics helper for the mission-interview module.
|
||||||
@@ -942,6 +955,8 @@ export async function createMissionInterviewAgent(
|
|||||||
tools: "readonly",
|
tools: "readonly",
|
||||||
mcpServers,
|
mcpServers,
|
||||||
allowMcpToolsInReadonly: true,
|
allowMcpToolsInReadonly: true,
|
||||||
|
// FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37: forward the plugin runner so the shared seam can route CLI-runtime model selections (cursor/claude/grok/omp/hermes) to their runtime plugins.
|
||||||
|
...(pluginRunner ? { pluginRunner } : {}),
|
||||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
builtinToolsAllowlist: [...MISSION_INTERVIEW_BUILTIN_WEB_TOOLS],
|
builtinToolsAllowlist: [...MISSION_INTERVIEW_BUILTIN_WEB_TOOLS],
|
||||||
customTools: [...createPlanningBoardTools(store)],
|
customTools: [...createPlanningBoardTools(store)],
|
||||||
@@ -1094,7 +1109,9 @@ async function ensureMissionInterviewAgent(
|
|||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
await session.agent!.session.prompt(
|
// FNXC:MissionInterviewRuntimeResolution 2026-08-16-14:37: prompt through the engine dispatcher — plugin CLI runtime sessions (cursor/grok/droid) have no session.prompt(); the shared seam bound the runtime's promptWithFallback onto the session and this delegates to it.
|
||||||
|
await enginePromptWithFallback(
|
||||||
|
session.agent!.session,
|
||||||
[
|
[
|
||||||
"Previous conversation summary:",
|
"Previous conversation summary:",
|
||||||
historySummary,
|
historySummary,
|
||||||
@@ -1167,7 +1184,7 @@ async function continueAgentConversation(session: MissionInterviewSession, messa
|
|||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
await agent.session.prompt(message, { signal: abortSignal });
|
await enginePromptWithFallback(agent.session, message, { signal: abortSignal });
|
||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
@@ -1214,7 +1231,8 @@ async function continueAgentConversation(session: MissionInterviewSession, messa
|
|||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw createAbortError();
|
throw createAbortError();
|
||||||
}
|
}
|
||||||
await agent.session.prompt(
|
await enginePromptWithFallback(
|
||||||
|
agent.session,
|
||||||
"Your previous response could not be parsed as JSON. " +
|
"Your previous response could not be parsed as JSON. " +
|
||||||
'Please respond with ONLY a valid JSON object: either {"type":"question","data":{...}} ' +
|
'Please respond with ONLY a valid JSON object: either {"type":"question","data":{...}} ' +
|
||||||
'or {"type":"complete","data":{"missionTitle":"...","missionDescription":"...","milestones":[...]}}. ' +
|
'or {"type":"complete","data":{"missionTitle":"...","missionDescription":"...","milestones":[...]}}. ' +
|
||||||
|
|||||||
@@ -1333,6 +1333,15 @@ describe("wrapToolsWithActionGate", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:CliRuntimeRouting 2026-08-16-14:37:
|
||||||
|
This file pins the RAW pi session constructor's behavior (auth, principals,
|
||||||
|
skills, registry resolution, tool wrapping). The public `createFnAgent` now
|
||||||
|
routes through createResolvedAgentSession, so every direct call here aliases
|
||||||
|
`createPiAgentSessionRaw` — the function DefaultPiRuntime bridges into — to
|
||||||
|
keep asserting pi internals rather than the routing seam (covered by
|
||||||
|
agent-session-helpers/runtime-resolution tests).
|
||||||
|
*/
|
||||||
describe("createFnAgent", () => {
|
describe("createFnAgent", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -1370,7 +1379,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("binds a durable chat principal to the host-tool prompt invocation when pi omits agentId", async () => {
|
it("binds a durable chat principal to the host-tool prompt invocation when pi omits agentId", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const {
|
const {
|
||||||
__clearFusionSessionIdentityRegistryForTests,
|
__clearFusionSessionIdentityRegistryForTests,
|
||||||
resolveFusionSessionPrincipal,
|
resolveFusionSessionPrincipal,
|
||||||
@@ -1430,7 +1439,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("reaches the pi host-tool identity wrapper from a durable dashboard chat session", async () => {
|
it("reaches the pi host-tool identity wrapper from a durable dashboard chat session", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const {
|
const {
|
||||||
__clearFusionSessionIdentityRegistryForTests,
|
__clearFusionSessionIdentityRegistryForTests,
|
||||||
resolveFusionSessionPrincipal,
|
resolveFusionSessionPrincipal,
|
||||||
@@ -1514,7 +1523,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("assigns distinct fail-closed principals to concurrent anonymous engine sessions", async () => {
|
it("assigns distinct fail-closed principals to concurrent anonymous engine sessions", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const {
|
const {
|
||||||
__clearFusionSessionIdentityRegistryForTests,
|
__clearFusionSessionIdentityRegistryForTests,
|
||||||
resolveFusionSessionPrincipal,
|
resolveFusionSessionPrincipal,
|
||||||
@@ -1558,7 +1567,7 @@ describe("createFnAgent", () => {
|
|||||||
FN-7956 hung AI merge review on extension fn_task_show (second TaskStore boot, no tool timeout).
|
FN-7956 hung AI merge review on extension fn_task_show (second TaskStore boot, no tool timeout).
|
||||||
Merger sessions must not receive host @runfusion/fusion extension paths even with tools:coding.
|
Merger sessions must not receive host @runfusion/fusion extension paths even with tools:coding.
|
||||||
*/
|
*/
|
||||||
const { createFnAgent, setHostExtensionPaths } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent, setHostExtensionPaths } = await import("../pi.js");
|
||||||
setHostExtensionPaths(["/mock/fusion-extension"]);
|
setHostExtensionPaths(["/mock/fusion-extension"]);
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
@@ -1578,7 +1587,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("still injects host extensions for coding non-merger sessions", async () => {
|
it("still injects host extensions for coding non-merger sessions", async () => {
|
||||||
const { createFnAgent, setHostExtensionPaths } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent, setHostExtensionPaths } = await import("../pi.js");
|
||||||
setHostExtensionPaths(["/mock/fusion-extension"]);
|
setHostExtensionPaths(["/mock/fusion-extension"]);
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
@@ -1597,7 +1606,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("passes task-scoped env into bash spawn hook when provided", async () => {
|
it("passes task-scoped env into bash spawn hook when provided", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project",
|
cwd: "/project",
|
||||||
@@ -1636,7 +1645,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps bash tool default behavior when taskEnv is not provided", async () => {
|
it("keeps bash tool default behavior when taskEnv is not provided", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project",
|
cwd: "/project",
|
||||||
@@ -1648,7 +1657,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps spawned env unchanged when taskEnv is empty", async () => {
|
it("keeps spawned env unchanged when taskEnv is empty", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project",
|
cwd: "/project",
|
||||||
@@ -1665,7 +1674,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("adds new task env keys absent from spawned env", async () => {
|
it("adds new task env keys absent from spawned env", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project",
|
cwd: "/project",
|
||||||
@@ -1681,7 +1690,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("preserves undefined task env values explicitly in merged env", async () => {
|
it("preserves undefined task env values explicitly in merged env", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project",
|
cwd: "/project",
|
||||||
@@ -1697,7 +1706,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("injects PATH from task env when spawned env has no PATH", async () => {
|
it("injects PATH from task env when spawned env has no PATH", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project",
|
cwd: "/project",
|
||||||
@@ -1725,7 +1734,7 @@ describe("createFnAgent", () => {
|
|||||||
return "worktree /project\nHEAD abc123\nbranch refs/heads/main\n";
|
return "worktree /project\nHEAD abc123\nbranch refs/heads/main\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await expect(createFnAgent({
|
await expect(createFnAgent({
|
||||||
cwd: "/project/.worktrees/fn-001",
|
cwd: "/project/.worktrees/fn-001",
|
||||||
@@ -1752,7 +1761,7 @@ describe("createFnAgent", () => {
|
|||||||
"worktree /project/.worktrees/fn-001\nHEAD def456\nbranch refs/heads/fusion/fn-001\n";
|
"worktree /project/.worktrees/fn-001\nHEAD def456\nbranch refs/heads/fusion/fn-001\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project/.worktrees/fn-001",
|
cwd: "/project/.worktrees/fn-001",
|
||||||
@@ -1779,7 +1788,7 @@ describe("createFnAgent", () => {
|
|||||||
"worktree /project/.worktrees/task-branch\nHEAD def456\nbranch refs/heads/fusion/fn-001\n";
|
"worktree /project/.worktrees/task-branch\nHEAD def456\nbranch refs/heads/fusion/fn-001\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
// Pass skills parameter with a worktree cwd.
|
// Pass skills parameter with a worktree cwd.
|
||||||
// getProjectRootFromWorktree extracts /project from the .worktrees path,
|
// getProjectRootFromWorktree extracts /project from the .worktrees path,
|
||||||
@@ -1807,7 +1816,7 @@ describe("createFnAgent", () => {
|
|||||||
return value === "/project/.fusion";
|
return value === "/project/.fusion";
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/project/src/components",
|
cwd: "/project/src/components",
|
||||||
@@ -1826,7 +1835,7 @@ describe("createFnAgent", () => {
|
|||||||
// No .fusion directory exists anywhere above cwd.
|
// No .fusion directory exists anywhere above cwd.
|
||||||
existsSyncMock.mockImplementation(() => false);
|
existsSyncMock.mockImplementation(() => false);
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/unrelated/directory",
|
cwd: "/unrelated/directory",
|
||||||
@@ -1850,7 +1859,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("passes a project-trusted settings view through package-manager discovery", async () => {
|
it("passes a project-trusted settings view through package-manager discovery", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -1882,7 +1891,7 @@ describe("createFnAgent", () => {
|
|||||||
errors: [],
|
errors: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -1939,7 +1948,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2034,7 +2043,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2069,7 +2078,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("avoids lock-based SettingsManager.create when loading extension providers", async () => {
|
it("avoids lock-based SettingsManager.create when loading extension providers", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2090,7 +2099,7 @@ describe("createFnAgent", () => {
|
|||||||
provider === "zai" && modelId === "glm-5.1" ? undefined : { provider, id: modelId }
|
provider === "zai" && modelId === "glm-5.1" ? undefined : { provider, id: modelId }
|
||||||
));
|
));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await expect(createFnAgent({
|
await expect(createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2116,7 +2125,7 @@ describe("createFnAgent", () => {
|
|||||||
return { provider, id: modelId };
|
return { provider, id: modelId };
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2139,7 +2148,7 @@ describe("createFnAgent", () => {
|
|||||||
provider === "openai-codex" && modelId === "missing-model" ? undefined : { provider, id: modelId }
|
provider === "openai-codex" && modelId === "missing-model" ? undefined : { provider, id: modelId }
|
||||||
));
|
));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await expect(createFnAgent({
|
await expect(createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2170,7 +2179,7 @@ describe("createFnAgent", () => {
|
|||||||
));
|
));
|
||||||
getAllMock.mockReturnValue([]);
|
getAllMock.mockReturnValue([]);
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await expect(createFnAgent({
|
await expect(createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2209,7 +2218,7 @@ describe("createFnAgent", () => {
|
|||||||
));
|
));
|
||||||
getAllMock.mockReturnValue([{ provider: "grok-cli", id: "grok-4.5", name: "Grok 4.5" }]);
|
getAllMock.mockReturnValue([{ provider: "grok-cli", id: "grok-4.5", name: "Grok 4.5" }]);
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2226,7 +2235,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates a session when configured models resolve successfully", async () => {
|
it("creates a session when configured models resolve successfully", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2249,7 +2258,7 @@ describe("createFnAgent", () => {
|
|||||||
getAllMock.mockReturnValue([]);
|
getAllMock.mockReturnValue([]);
|
||||||
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2282,7 +2291,7 @@ describe("createFnAgent", () => {
|
|||||||
]);
|
]);
|
||||||
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2301,7 +2310,7 @@ describe("createFnAgent", () => {
|
|||||||
getAllMock.mockReturnValue([]);
|
getAllMock.mockReturnValue([]);
|
||||||
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2336,7 +2345,7 @@ describe("createFnAgent", () => {
|
|||||||
getAllMock.mockReturnValue([existingLunaRow]);
|
getAllMock.mockReturnValue([existingLunaRow]);
|
||||||
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2372,7 +2381,7 @@ describe("createFnAgent", () => {
|
|||||||
getAllMock.mockReturnValue([{ provider: "anthropic", id: "claude-opus-4-8", name: "Claude Opus 4.8" }]);
|
getAllMock.mockReturnValue([{ provider: "anthropic", id: "claude-opus-4-8", name: "Claude Opus 4.8" }]);
|
||||||
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2400,7 +2409,7 @@ describe("createFnAgent", () => {
|
|||||||
? { provider, id: modelId }
|
? { provider, id: modelId }
|
||||||
: undefined);
|
: undefined);
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await expect(createFnAgent({
|
await expect(createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2419,7 +2428,7 @@ describe("createFnAgent", () => {
|
|||||||
authStorageGetApiKeyMock.mockResolvedValue(undefined);
|
authStorageGetApiKeyMock.mockResolvedValue(undefined);
|
||||||
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2439,7 +2448,7 @@ describe("createFnAgent", () => {
|
|||||||
provider === "anthropic" ? "sk-ant-api03-direct" : undefined
|
provider === "anthropic" ? "sk-ant-api03-direct" : undefined
|
||||||
));
|
));
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2470,7 +2479,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
getAllMock.mockReturnValue([{ provider: "anthropic", id: "claude-opus-4-8" }]);
|
getAllMock.mockReturnValue([{ provider: "anthropic", id: "claude-opus-4-8" }]);
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2493,7 +2502,7 @@ describe("createFnAgent", () => {
|
|||||||
};
|
};
|
||||||
createAgentSessionMock.mockResolvedValueOnce({ session });
|
createAgentSessionMock.mockResolvedValueOnce({ session });
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const result = await createFnAgent({
|
const result = await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2522,7 +2531,7 @@ describe("createFnAgent", () => {
|
|||||||
execute: vi.fn(),
|
execute: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2543,7 +2552,7 @@ describe("createFnAgent", () => {
|
|||||||
execute: vi.fn(),
|
execute: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2556,7 +2565,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not allow extra builtin tools in readonly sessions by default", async () => {
|
it("does not allow extra builtin tools in readonly sessions by default", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2569,7 +2578,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("intersects readonly builtin allowlist with readonly policy", async () => {
|
it("intersects readonly builtin allowlist with readonly policy", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2590,7 +2599,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("filters coding tools with a case-insensitive toolsAllowlist", async () => {
|
it("filters coding tools with a case-insensitive toolsAllowlist", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2605,7 +2614,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps all coding tools when toolsAllowlist is undefined", async () => {
|
it("keeps all coding tools when toolsAllowlist is undefined", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2619,7 +2628,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("exposes no coding tools when toolsAllowlist is empty", async () => {
|
it("exposes no coding tools when toolsAllowlist is empty", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2643,7 +2652,7 @@ describe("createFnAgent", () => {
|
|||||||
execute: vi.fn(),
|
execute: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -2659,7 +2668,7 @@ describe("createFnAgent", () => {
|
|||||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
|
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
|
||||||
const permanentCreateApproval = vi.fn();
|
const permanentCreateApproval = vi.fn();
|
||||||
const markApprovalCompleted = vi.fn();
|
const markApprovalCompleted = vi.fn();
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2700,7 +2709,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("exposes connected MCP tools in readonly sessions only with the explicit opt-in", async () => {
|
it("exposes connected MCP tools in readonly sessions only with the explicit opt-in", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const close = vi.fn(async () => undefined);
|
const close = vi.fn(async () => undefined);
|
||||||
const mcpClient = {
|
const mcpClient = {
|
||||||
connect: vi.fn(async () => undefined),
|
connect: vi.fn(async () => undefined),
|
||||||
@@ -2764,7 +2773,7 @@ describe("createFnAgent", () => {
|
|||||||
clients.push(client);
|
clients.push(client);
|
||||||
return client;
|
return client;
|
||||||
});
|
});
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expect(createFnAgent({
|
await expect(createFnAgent({
|
||||||
@@ -2794,7 +2803,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps MCP tools out of readonly sessions without the explicit opt-in", async () => {
|
it("keeps MCP tools out of readonly sessions without the explicit opt-in", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const mcpClient = {
|
const mcpClient = {
|
||||||
connect: vi.fn(async () => undefined),
|
connect: vi.fn(async () => undefined),
|
||||||
listTools: vi.fn(async () => ({ tools: [{ name: "lookup" }] })),
|
listTools: vi.fn(async () => ({ tools: [{ name: "lookup" }] })),
|
||||||
@@ -2821,7 +2830,7 @@ describe("createFnAgent", () => {
|
|||||||
const { piLog } = await import("../logger.js");
|
const { piLog } = await import("../logger.js");
|
||||||
// FNXC:EngineDiagnostics 2026-07-26-09:55: entry chatter is debug-gated.
|
// FNXC:EngineDiagnostics 2026-07-26-09:55: entry chatter is debug-gated.
|
||||||
const debugSpy = vi.spyOn(piLog, "debug").mockImplementation(() => {});
|
const debugSpy = vi.spyOn(piLog, "debug").mockImplementation(() => {});
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp/private-worktree",
|
cwd: "/tmp/private-worktree",
|
||||||
@@ -2874,7 +2883,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const { __clearFusionSessionIdentityRegistryForTests } = await import("@fusion/core");
|
const { __clearFusionSessionIdentityRegistryForTests } = await import("@fusion/core");
|
||||||
__clearFusionSessionIdentityRegistryForTests();
|
__clearFusionSessionIdentityRegistryForTests();
|
||||||
|
|
||||||
@@ -2942,7 +2951,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
const { session } = await createFnAgent({
|
const { session } = await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2965,7 +2974,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("enables auto-compaction to prevent context-window overflow", async () => {
|
it("enables auto-compaction to prevent context-window overflow", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -2982,7 +2991,7 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("passes compaction enabled alongside retry settings", async () => {
|
it("passes compaction enabled alongside retry settings", async () => {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -3013,7 +3022,7 @@ describe("createFnAgent", () => {
|
|||||||
};
|
};
|
||||||
createAgentSessionMock.mockResolvedValueOnce({ session });
|
createAgentSessionMock.mockResolvedValueOnce({ session });
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
const { session: guardedSession } = await createFnAgent({
|
const { session: guardedSession } = await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -3044,7 +3053,7 @@ describe("createFnAgent", () => {
|
|||||||
_rewriteFile: rewriteFile,
|
_rewriteFile: rewriteFile,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -3090,7 +3099,7 @@ describe("createFnAgent", () => {
|
|||||||
};
|
};
|
||||||
createAgentSessionMock.mockResolvedValueOnce({ session });
|
createAgentSessionMock.mockResolvedValueOnce({ session });
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -3134,7 +3143,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await expect(createFnAgent({
|
await expect(createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -3164,7 +3173,7 @@ describe("createFnAgent", () => {
|
|||||||
const anyModel = { provider: "anthropic", id: "claude" } as never;
|
const anyModel = { provider: "anthropic", id: "claude" } as never;
|
||||||
|
|
||||||
async function createAndCaptureRuntime(overrides: Record<string, unknown> = {}) {
|
async function createAndCaptureRuntime(overrides: Record<string, unknown> = {}) {
|
||||||
const { createFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js");
|
||||||
await createFnAgent({
|
await createFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
systemPrompt: "test",
|
systemPrompt: "test",
|
||||||
@@ -3283,7 +3292,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { createFnAgent: freshCreateFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: freshCreateFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await freshCreateFnAgent({
|
await freshCreateFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -3374,7 +3383,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { createFnAgent: freshCreateFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: freshCreateFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await freshCreateFnAgent({
|
await freshCreateFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -3396,7 +3405,15 @@ describe("createFnAgent", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("with skillSelection (specific requested names) activates skill filtering", async () => {
|
/*
|
||||||
|
FNXC:SkillResolution 2026-08-16-15:12:
|
||||||
|
FN-9114 changed requested skill names from an allow-list to ensure-present:
|
||||||
|
all enabled discovered skills stay available, and requested names only
|
||||||
|
guarantee inclusion (agent metadata is no longer an availability filter).
|
||||||
|
This test previously asserted the pre-FN-9114 allow-list narrowing and was
|
||||||
|
the one call-site-level assertion missed by that commit's test sweep.
|
||||||
|
*/
|
||||||
|
it("with skillSelection (specific requested names) keeps all discovered skills and ensures requested ones are present", async () => {
|
||||||
let capturedResourceLoaderOptions: any;
|
let capturedResourceLoaderOptions: any;
|
||||||
vi.doMock("@earendil-works/pi-coding-agent", () => ({
|
vi.doMock("@earendil-works/pi-coding-agent", () => ({
|
||||||
LegacyCredentialStorage: {
|
LegacyCredentialStorage: {
|
||||||
@@ -3462,7 +3479,7 @@ describe("createFnAgent", () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { createFnAgent: freshCreateFnAgent } = await import("../pi.js");
|
const { createPiAgentSessionRaw: freshCreateFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
await freshCreateFnAgent({
|
await freshCreateFnAgent({
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
@@ -3487,9 +3504,12 @@ describe("createFnAgent", () => {
|
|||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only paperclip should pass through (matching requested name)
|
// FN-9114: requested names are ensure-present, not an allow-list — every
|
||||||
expect(result.skills).toHaveLength(1);
|
// discovered skill stays available and the requested one is included.
|
||||||
expect(result.skills[0].name).toBe("paperclip");
|
expect(result.skills).toHaveLength(2);
|
||||||
|
expect(result.skills.map((skill: { name: string }) => skill.name)).toEqual(
|
||||||
|
expect.arrayContaining(["paperclip", "lint"]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("diagnostics are logged via structured logger with [skills] context", async () => {
|
it("diagnostics are logged via structured logger with [skills] context", async () => {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
wrapToolsWithRtkRewrite,
|
wrapToolsWithRtkRewrite,
|
||||||
type FallbackModelUsedPayload,
|
type FallbackModelUsedPayload,
|
||||||
} from "../pi.js";
|
} from "../pi.js";
|
||||||
|
import * as piModuleForRegistration from "../pi.js";
|
||||||
import type { RunAuditor } from "../util/run-audit.js";
|
import type { RunAuditor } from "../util/run-audit.js";
|
||||||
import { createFusionAuthStorage, resolveCredentialInstanceRef, type FusionAuthStorage } from "../auth/auth-storage.js";
|
import { createFusionAuthStorage, resolveCredentialInstanceRef, type FusionAuthStorage } from "../auth/auth-storage.js";
|
||||||
import { MockAgentRuntime } from "../providers/mock-provider.js";
|
import { MockAgentRuntime } from "../providers/mock-provider.js";
|
||||||
@@ -1180,3 +1181,28 @@ export async function promptWithAutoRetry(
|
|||||||
export async function describeAgentModel(session: AgentSession): Promise<string> {
|
export async function describeAgentModel(session: AgentSession): Promise<string> {
|
||||||
return describeModel(session);
|
return describeModel(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:CliRuntimeRouting 2026-08-16-14:37:
|
||||||
|
Register this module's createResolvedAgentSession as pi.ts's routed session
|
||||||
|
factory so bare `createFnAgent` callers get CLI runtime routing (cursor/claude/
|
||||||
|
grok/omp/hermes), mock forcing, and runtime-resolved visibility without every
|
||||||
|
lane importing the seam. Late-binding registration (mirrors core's
|
||||||
|
setCreateFnAgent) instead of a static pi.ts -> agent-session-helpers import,
|
||||||
|
which would create an eval-order-sensitive cycle (this module and
|
||||||
|
runtime-resolution both import pi.ts). Runs once at module load; any host that
|
||||||
|
loads @fusion/engine's index (all real hosts) gets it.
|
||||||
|
|
||||||
|
Namespace access + try/catch on purpose: dozens of engine tests replace
|
||||||
|
"../pi.js" with pure vi.mock factories that predate this export, and vitest's
|
||||||
|
mock proxy THROWS on access to a missing export. Under such mocks the
|
||||||
|
registration is skipped and their mocked createFnAgent behaves as before.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
(piModuleForRegistration as { registerRoutedAgentSessionFactory?: (factory: (options: Record<string, unknown>) => Promise<import("../pi.js").AgentResult>) => void })
|
||||||
|
.registerRoutedAgentSessionFactory?.(
|
||||||
|
createResolvedAgentSession as unknown as (options: Record<string, unknown>) => Promise<import("../pi.js").AgentResult>,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Mocked pi.js without the export: leave createFnAgent on its raw fallback.
|
||||||
|
}
|
||||||
|
|||||||
@@ -108,11 +108,13 @@ export class DefaultPiRuntime implements AgentRuntime {
|
|||||||
Forward the resolved session budget explicitly across the default-pi bridge. A project setting of 0 becomes null before this point and must reach createFnAgent so the pi wrapper skips clamping just like plugin runtimes.
|
Forward the resolved session budget explicitly across the default-pi bridge. A project setting of 0 becomes null before this point and must reach createFnAgent so the pi wrapper skips clamping just like plugin runtimes.
|
||||||
*/
|
*/
|
||||||
const { toolOutputMaxChars, mcpServers, ...agentOptions } = options;
|
const { toolOutputMaxChars, mcpServers, ...agentOptions } = options;
|
||||||
|
// FNXC:CliRuntimeRouting 2026-08-16-14:37: `__rawPiSession` short-circuits createFnAgent's routed path — the seam resolved to THIS runtime, so routing the bridge again would recurse. createFnAgent stays the bridge target so tests mocking it keep intercepting pi-session construction.
|
||||||
const result = await createFnAgent({
|
const result = await createFnAgent({
|
||||||
...agentOptions,
|
...agentOptions,
|
||||||
toolOutputMaxChars,
|
toolOutputMaxChars,
|
||||||
mcpServers: normalizeAgentRuntimeMcpServers(mcpServers),
|
mcpServers: normalizeAgentRuntimeMcpServers(mcpServers),
|
||||||
});
|
__rawPiSession: true,
|
||||||
|
} as Parameters<typeof createFnAgent>[0]);
|
||||||
/*
|
/*
|
||||||
FNXC:TriagePlanningRetry 2026-08-03-01:10:
|
FNXC:TriagePlanningRetry 2026-08-03-01:10:
|
||||||
Pi dispatches fallback notification work before its prompt resolves, so it has a known finite
|
Pi dispatches fallback notification work before its prompt resolves, so it has a known finite
|
||||||
|
|||||||
@@ -2313,7 +2313,112 @@ function withMcpPromptOptions(promptOptions: unknown, mcpServers: ResolvedMcpSer
|
|||||||
return { mcpServers };
|
return { mcpServers };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:CliRuntimeRouting 2026-08-16-14:37:
|
||||||
|
`createFnAgent` is now a ROUTED entry point: it delegates to the shared
|
||||||
|
`createResolvedAgentSession` seam (registered below via
|
||||||
|
`registerRoutedAgentSessionFactory` to avoid a static import cycle) so every
|
||||||
|
caller — not just chat/executor/planning — gets CLI runtime routing
|
||||||
|
(cursor-cli/claude-cli/hermes/omp-cli/no-key grok-cli), mock/test-mode forcing,
|
||||||
|
and `session:runtime-resolved` visibility. Bare `createFnAgent` calls used to
|
||||||
|
pin sessions to the default pi runtime, so a CLI-runtime model selection died
|
||||||
|
with "not found in the pi model registry" in any lane that had not been
|
||||||
|
individually migrated to the seam (mission interview was the third such
|
||||||
|
incident after planning's 401 and this cursor one).
|
||||||
|
|
||||||
|
The raw pi implementation lives on as `createPiAgentSessionRaw`, which is what
|
||||||
|
`DefaultPiRuntime` (execution/runtime-resolution.ts) must call — the seam
|
||||||
|
resolves to that runtime, so routing the runtime's own bridge through the seam
|
||||||
|
again would recurse forever. When no routed factory is registered (isolated
|
||||||
|
unit tests importing only pi.ts), `createFnAgent` falls back to the raw path,
|
||||||
|
preserving pre-existing test behavior.
|
||||||
|
|
||||||
|
Callers without an explicit `pluginRunner` resolve one from the host-registered
|
||||||
|
default registry (`registerDefaultAgentPluginRunner`, populated by
|
||||||
|
InProcessRuntime at plugin-system init), keyed by project root and matched by
|
||||||
|
session cwd prefix so multi-project hosts route each session against its own
|
||||||
|
project's plugin set.
|
||||||
|
*/
|
||||||
|
type RoutedAgentSessionFactory = (options: Record<string, unknown>) => Promise<AgentResult>;
|
||||||
|
/*
|
||||||
|
FNXC:CliRuntimeRouting 2026-08-16-14:37:
|
||||||
|
`var` + lazy-init on purpose: agent-session-helpers registers the factory at
|
||||||
|
its own module load, and under the pi.ts <-> helpers import cycle that call can
|
||||||
|
run while pi.ts is still mid-evaluation. A `let`/`const` here sits in its
|
||||||
|
temporal dead zone during that window and made every hoisted createFnAgent
|
||||||
|
call throw "Cannot access before initialization"; `var` is hoisted-initialized
|
||||||
|
and the Map is created on first touch.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var routedAgentSessionFactory: RoutedAgentSessionFactory | undefined;
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var defaultAgentPluginRunners: Map<string, unknown> | undefined;
|
||||||
|
|
||||||
|
function agentPluginRunnerRegistry(): Map<string, unknown> {
|
||||||
|
return (defaultAgentPluginRunners ??= new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Late-binding registration seam (mirrors core's `setCreateFnAgent`): agent-session-helpers registers `createResolvedAgentSession` at module load. */
|
||||||
|
export function registerRoutedAgentSessionFactory(factory: RoutedAgentSessionFactory): void {
|
||||||
|
routedAgentSessionFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register a host PluginRunner as the ambient default for bare `createFnAgent` callers in the project rooted at `rootDir`. */
|
||||||
|
export function registerDefaultAgentPluginRunner(rootDir: string, pluginRunner: unknown): void {
|
||||||
|
agentPluginRunnerRegistry().set(rootDir, pluginRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterDefaultAgentPluginRunner(rootDir: string): void {
|
||||||
|
agentPluginRunnerRegistry().delete(rootDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the ambient PluginRunner for a session cwd: longest registered project-root prefix wins; a sole registered runner matches any cwd. */
|
||||||
|
function resolveDefaultAgentPluginRunner(cwd: string | undefined): unknown {
|
||||||
|
const registry = agentPluginRunnerRegistry();
|
||||||
|
if (registry.size === 0) return undefined;
|
||||||
|
if (cwd) {
|
||||||
|
let best: { rootDir: string; runner: unknown } | undefined;
|
||||||
|
for (const [rootDir, runner] of registry) {
|
||||||
|
if ((cwd === rootDir || cwd.startsWith(`${rootDir}/`)) && (!best || rootDir.length > best.rootDir.length)) {
|
||||||
|
best = { rootDir, runner };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best) return best.runner;
|
||||||
|
}
|
||||||
|
if (registry.size === 1) {
|
||||||
|
return registry.values().next().value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createFnAgent(options: AgentOptions): Promise<AgentResult> {
|
export async function createFnAgent(options: AgentOptions): Promise<AgentResult> {
|
||||||
|
/*
|
||||||
|
FNXC:CliRuntimeRouting 2026-08-16-14:37:
|
||||||
|
`__rawPiSession` is DefaultPiRuntime's re-entry marker: the seam resolves to
|
||||||
|
that runtime, whose bridge calls back into createFnAgent (kept as the bridge
|
||||||
|
target so existing tests that mock createFnAgent still intercept pi-session
|
||||||
|
construction). The marker short-circuits to the raw constructor instead of
|
||||||
|
routing again, which would recurse forever.
|
||||||
|
*/
|
||||||
|
const { __rawPiSession, ...routableOptions } = options as AgentOptions & { __rawPiSession?: boolean };
|
||||||
|
if (!__rawPiSession && routedAgentSessionFactory) {
|
||||||
|
const optionsWithRouting = routableOptions as AgentOptions & { pluginRunner?: unknown; runtimeHint?: string };
|
||||||
|
const pluginRunner = optionsWithRouting.pluginRunner ?? resolveDefaultAgentPluginRunner(routableOptions.cwd);
|
||||||
|
return routedAgentSessionFactory({
|
||||||
|
...routableOptions,
|
||||||
|
sessionPurpose: routableOptions.sessionPurpose ?? "executor",
|
||||||
|
...(pluginRunner ? { pluginRunner } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return createPiAgentSessionRaw(routableOptions as AgentOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw pi-runtime session construction. Internal to the runtime layer: only
|
||||||
|
* `DefaultPiRuntime` (and the unregistered-factory fallback above) may call
|
||||||
|
* this — every product lane goes through `createFnAgent`'s routed path.
|
||||||
|
*/
|
||||||
|
export async function createPiAgentSessionRaw(options: AgentOptions): Promise<AgentResult> {
|
||||||
/*
|
/*
|
||||||
FNXC:EngineDiagnostics 2026-07-26-09:55:
|
FNXC:EngineDiagnostics 2026-07-26-09:55:
|
||||||
createFnAgent is invoked on every agent/session start (executor, triage, chat, heartbeat, etc.). The entry log is steady-state bookkeeping — debug-only (FUSION_DEBUG=pi). Failures and auth issues stay on warn/error.
|
createFnAgent is invoked on every agent/session start (executor, triage, chat, heartbeat, etc.). The entry log is steady-state bookkeeping — debug-only (FUSION_DEBUG=pi). Failures and auth issues stay on warn/error.
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
resolveTaskLifecycleColumns,
|
resolveTaskLifecycleColumns,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import { Scheduler } from "../scheduler.js";
|
import { Scheduler } from "../scheduler.js";
|
||||||
|
import { registerDefaultAgentPluginRunner, unregisterDefaultAgentPluginRunner } from "../pi.js";
|
||||||
import type { PrMonitor, PrComment } from "../merge/pr-monitor.js";
|
import type { PrMonitor, PrComment } from "../merge/pr-monitor.js";
|
||||||
import type { PrInfo } from "@fusion/core";
|
import type { PrInfo } from "@fusion/core";
|
||||||
import { TaskExecutor, type TaskExecutorOptions } from "../executor.js";
|
import { TaskExecutor, type TaskExecutorOptions } from "../executor.js";
|
||||||
@@ -1123,6 +1124,17 @@ export class InProcessRuntime
|
|||||||
rootDir: this.config.workingDirectory,
|
rootDir: this.config.workingDirectory,
|
||||||
});
|
});
|
||||||
await this.pluginRunner.init();
|
await this.pluginRunner.init();
|
||||||
|
/*
|
||||||
|
FNXC:CliRuntimeRouting 2026-08-16-14:37:
|
||||||
|
Publish this project's PluginRunner as the ambient default for bare
|
||||||
|
`createFnAgent` callers (research providers, cron, evaluator, reflection,
|
||||||
|
core DI lanes, dashboard side-lanes) so their sessions route CLI-runtime
|
||||||
|
model selections (cursor-cli etc.) through the plugin runtime instead of
|
||||||
|
dying in pi's model registry. Keyed by project root; createFnAgent matches
|
||||||
|
the session cwd against registered roots so multi-project hosts stay
|
||||||
|
project-scoped.
|
||||||
|
*/
|
||||||
|
registerDefaultAgentPluginRunner(this.config.workingDirectory, this.pluginRunner);
|
||||||
/*
|
/*
|
||||||
* FNXC:PluginMcpServers 2026-07-22-12:00:
|
* FNXC:PluginMcpServers 2026-07-22-12:00:
|
||||||
* FN-8491 installs the sole session-facing provider on the project store.
|
* FN-8491 installs the sole session-facing provider on the project store.
|
||||||
@@ -2343,6 +2355,8 @@ export class InProcessRuntime
|
|||||||
|
|
||||||
// 8. Shutdown plugin runner
|
// 8. Shutdown plugin runner
|
||||||
if (this.pluginRunner) {
|
if (this.pluginRunner) {
|
||||||
|
// FNXC:CliRuntimeRouting 2026-08-16-14:37: retract the ambient default runner published at init so bare createFnAgent callers never resolve a shut-down runner.
|
||||||
|
unregisterDefaultAgentPluginRunner(this.config.workingDirectory);
|
||||||
await this.pluginRunner.shutdown();
|
await this.pluginRunner.shutdown();
|
||||||
runtimeLog.log("PluginRunner shutdown complete");
|
runtimeLog.log("PluginRunner shutdown complete");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user