feat(FN-2617): merge fusion/fn-2617 (auto-resolved)
- test(FN-2617): stabilize workflow remediation timeout under full suite - test(FN-2617): complete Step 5 — add openclaw runtime resolution coverage - feat(FN-2617): complete Step 4 — route step sessions through runtime resolution - fix(FN-2617): thread runtimeHint through merger rebase push flow - feat(FN-2617): complete Step 3 — wire runtimeHint across engine subsystems - feat(FN-2617): complete Step 2 — thread runtimeHint in executor paths - feat(FN-2617): complete Step 1 — add runtimeHint extraction helper
This commit is contained in:
24
packages/engine/src/__tests__/agent-session-helpers.test.ts
Normal file
24
packages/engine/src/__tests__/agent-session-helpers.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractRuntimeHint } from "../agent-session-helpers.js";
|
||||
|
||||
describe("extractRuntimeHint", () => {
|
||||
it("returns undefined for undefined config", () => {
|
||||
expect(extractRuntimeHint(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when runtimeHint key is missing", () => {
|
||||
expect(extractRuntimeHint({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns normalized runtime hint when configured", () => {
|
||||
expect(extractRuntimeHint({ runtimeHint: " openclaw " })).toBe("openclaw");
|
||||
});
|
||||
|
||||
it("returns undefined for whitespace-only runtimeHint", () => {
|
||||
expect(extractRuntimeHint({ runtimeHint: " " })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for non-string runtimeHint", () => {
|
||||
expect(extractRuntimeHint({ runtimeHint: 42 })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -70,6 +70,10 @@ vi.mock("../agent-session-helpers.js", async () => {
|
||||
wasConfigured: false,
|
||||
};
|
||||
},
|
||||
extractRuntimeHint: (runtimeConfig: Record<string, unknown> | undefined) => {
|
||||
const hint = runtimeConfig?.runtimeHint;
|
||||
return typeof hint === "string" && hint.trim().length > 0 ? hint.trim() : undefined;
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("../worktree-names.js", async () => {
|
||||
@@ -8243,7 +8247,7 @@ describe("Workflow Steps Execution", () => {
|
||||
|
||||
vi.useRealTimers();
|
||||
await rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
}, 15_000);
|
||||
|
||||
it("skips script-mode step when scriptName is missing", async () => {
|
||||
const store = createMockStore();
|
||||
|
||||
@@ -42,6 +42,10 @@ vi.mock("../agent-session-helpers.js", async () => {
|
||||
wasConfigured: false,
|
||||
};
|
||||
},
|
||||
extractRuntimeHint: (runtimeConfig: Record<string, unknown> | undefined) => {
|
||||
const hint = runtimeConfig?.runtimeHint;
|
||||
return typeof hint === "string" && hint.trim().length > 0 ? hint.trim() : undefined;
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("node:child_process", () => {
|
||||
|
||||
@@ -244,6 +244,19 @@ describe("runtime-resolution", () => {
|
||||
// Should return the matching runtime
|
||||
expect(result.runtime.id).toBe("unique-id");
|
||||
});
|
||||
|
||||
it("should resolve the openclaw runtime when registered", async () => {
|
||||
const openclawRuntime = createMockPluginRuntime("openclaw", "OpenClaw Runtime");
|
||||
mockPluginRunner.getRuntimeById.mockReturnValue({
|
||||
pluginId: "fusion-plugin-openclaw-runtime",
|
||||
runtime: openclawRuntime,
|
||||
});
|
||||
|
||||
const result = await resolveRuntime(createContext("executor", "openclaw"));
|
||||
|
||||
expect(result.runtimeId).toBe("openclaw");
|
||||
expect(result.wasConfigured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fallback behavior", () => {
|
||||
@@ -258,6 +271,15 @@ describe("runtime-resolution", () => {
|
||||
expect(result.wasConfigured).toBe(false);
|
||||
});
|
||||
|
||||
it("should fall back to pi when openclaw runtime is not registered", async () => {
|
||||
mockPluginRunner.getRuntimeById.mockReturnValue(undefined);
|
||||
|
||||
const result = await resolveRuntime(createContext("executor", "openclaw"));
|
||||
|
||||
expect(result.runtimeId).toBe("pi");
|
||||
expect(result.wasConfigured).toBe(false);
|
||||
});
|
||||
|
||||
it("should fall back to pi when runtime factory throws", async () => {
|
||||
const mockRuntime: PluginRuntimeRegistration = {
|
||||
metadata: {
|
||||
|
||||
@@ -269,5 +269,36 @@ describe("Runtime Selection Regression Tests", () => {
|
||||
expect(result.runtimeId).toBe("code-interpreter");
|
||||
expect(result.wasConfigured).toBe(true);
|
||||
});
|
||||
|
||||
it("should route runtimeHint=openclaw to the openclaw runtime", async () => {
|
||||
mockResolveRuntime.mockResolvedValue({
|
||||
runtime: {
|
||||
id: "openclaw",
|
||||
name: "OpenClaw Runtime",
|
||||
createSession: async () => ({
|
||||
session: {
|
||||
model: { provider: "openclaw", id: "openclaw-agent" },
|
||||
},
|
||||
}),
|
||||
promptWithFallback: vi.fn(),
|
||||
describeModel: () => "openclaw/openclaw-agent",
|
||||
},
|
||||
wasConfigured: true,
|
||||
runtimeId: "openclaw",
|
||||
});
|
||||
|
||||
const { createResolvedAgentSession } = await import("../agent-session-helpers.js");
|
||||
|
||||
const result = await createResolvedAgentSession({
|
||||
sessionPurpose: "executor",
|
||||
pluginRunner: {} as any,
|
||||
runtimeHint: "openclaw",
|
||||
cwd: "/test/path",
|
||||
systemPrompt: "Test prompt",
|
||||
});
|
||||
|
||||
expect(result.runtimeId).toBe("openclaw");
|
||||
expect(result.wasConfigured).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -546,6 +546,25 @@ vi.mock("../pi.js", () => ({
|
||||
compactSessionContext: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../agent-session-helpers.js", async () => {
|
||||
const pi = await import("../pi.js");
|
||||
return {
|
||||
createResolvedAgentSession: vi.fn(async (options: any) => {
|
||||
const result = await pi.createFnAgent(options);
|
||||
return {
|
||||
session: result.session,
|
||||
sessionFile: result.sessionFile,
|
||||
runtimeId: "pi",
|
||||
wasConfigured: false,
|
||||
};
|
||||
}),
|
||||
promptWithAutoRetry: vi.fn(async (session: any, prompt: string, options?: unknown) =>
|
||||
pi.promptWithFallback(session, prompt, options as any),
|
||||
),
|
||||
describeAgentModel: vi.fn(async (session: any) => pi.describeModel(session)),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock logger
|
||||
vi.mock("../logger.js", () => {
|
||||
const createMockLogger = () => ({
|
||||
|
||||
@@ -1075,7 +1075,7 @@ export class HeartbeatMonitor {
|
||||
|
||||
// Lazy-load promptWithFallback
|
||||
const { promptWithFallback } = await import("./pi.js");
|
||||
const { createResolvedAgentSession } = await import("./agent-session-helpers.js");
|
||||
const { createResolvedAgentSession, extractRuntimeHint } = await import("./agent-session-helpers.js");
|
||||
const { buildSessionSkillContextSync } = await import("./session-skill-context.js");
|
||||
|
||||
// Build tools with task creation tracking and run context for mutation correlation
|
||||
@@ -1153,6 +1153,7 @@ export class HeartbeatMonitor {
|
||||
// Create agent session
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "heartbeat",
|
||||
runtimeHint: extractRuntimeHint(agent.runtimeConfig),
|
||||
pluginRunner: this.pluginRunner,
|
||||
cwd: rootDir,
|
||||
systemPrompt,
|
||||
|
||||
@@ -42,6 +42,24 @@ export interface ResolvedSessionResult {
|
||||
wasConfigured: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract runtime hint from untyped runtimeConfig payload.
|
||||
*
|
||||
* @param runtimeConfig - Agent/task runtime configuration
|
||||
* @returns normalized runtime hint or undefined when missing/invalid
|
||||
*/
|
||||
export function extractRuntimeHint(
|
||||
runtimeConfig: Record<string, unknown> | undefined,
|
||||
): string | undefined {
|
||||
const hint = runtimeConfig?.runtimeHint;
|
||||
if (typeof hint !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedHint = hint.trim();
|
||||
return normalizedHint.length > 0 ? normalizedHint : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an agent session using runtime resolution.
|
||||
*
|
||||
|
||||
@@ -11,7 +11,7 @@ import { findWorktreeUser } from "./merger.js";
|
||||
import { generateWorktreeName, slugify } from "./worktree-names.js";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import { describeModel, promptWithFallback, compactSessionContext } from "./pi.js";
|
||||
import { createResolvedAgentSession } from "./agent-session-helpers.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js";
|
||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import { reviewStep, type ReviewVerdict } from "./reviewer.js";
|
||||
import { ModelRegistry, SessionManager, type ToolDefinition, type AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
@@ -1559,6 +1559,11 @@ export class TaskExecutor {
|
||||
// ── Step-Session Path ──────────────────────────────────────────
|
||||
executorLog.log(`${task.id}: using step-session mode (maxParallel=${settings.maxParallelSteps ?? 2})`);
|
||||
|
||||
const stepSessionAgent = detail.assignedAgentId && this.options.agentStore
|
||||
? await this.options.agentStore.getAgent(detail.assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const stepSessionRuntimeHint = extractRuntimeHint(stepSessionAgent?.runtimeConfig);
|
||||
|
||||
const stepExecutor = new StepSessionExecutor({
|
||||
store: this.store,
|
||||
taskDetail: detail,
|
||||
@@ -1568,6 +1573,7 @@ export class TaskExecutor {
|
||||
semaphore: this.options.semaphore,
|
||||
stuckTaskDetector: this.options.stuckTaskDetector,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
runtimeHint: stepSessionRuntimeHint,
|
||||
// Pass skill selection context from the main executor session
|
||||
skillSelection: skillContext.skillSelectionContext,
|
||||
// Pass agentStore and messageStore for delegation and messaging tools
|
||||
@@ -1825,6 +1831,7 @@ export class TaskExecutor {
|
||||
const assignedAgent = assignedAgentId && this.options.agentStore
|
||||
? await this.options.agentStore.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const executorRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
|
||||
// Log fast mode status
|
||||
if (executionMode === "fast") {
|
||||
@@ -1918,6 +1925,7 @@ export class TaskExecutor {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { session, sessionFile } = await createResolvedAgentSession({
|
||||
sessionPurpose: "executor",
|
||||
runtimeHint: executorRuntimeHint,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
cwd: worktreePath,
|
||||
systemPrompt: executorSystemPrompt,
|
||||
@@ -2166,6 +2174,7 @@ export class TaskExecutor {
|
||||
|
||||
const { session: retrySession, sessionFile: retrySessionFile } = await createResolvedAgentSession({
|
||||
sessionPurpose: "executor",
|
||||
runtimeHint: executorRuntimeHint,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
cwd: worktreePath,
|
||||
systemPrompt: executorSystemPrompt,
|
||||
@@ -3809,8 +3818,14 @@ and show an appropriate message to the user.\`
|
||||
projectRootDir: this.rootDir,
|
||||
});
|
||||
|
||||
const workflowAgent = task.assignedAgentId && this.options.agentStore
|
||||
? await this.options.agentStore.getAgent(task.assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const workflowRuntimeHint = extractRuntimeHint(workflowAgent?.runtimeConfig);
|
||||
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "executor",
|
||||
runtimeHint: workflowRuntimeHint,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
cwd: worktreePath,
|
||||
systemPrompt: stepSystemPrompt,
|
||||
@@ -4855,10 +4870,16 @@ and show an appropriate message to the user.\`
|
||||
sessionPurpose: "executor",
|
||||
projectRootDir: this.rootDir,
|
||||
});
|
||||
const parentAgent = childTask.assignedAgentId
|
||||
? await this.options.agentStore.getAgent(childTask.assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const childRuntimeHint = extractRuntimeHint(agent.runtimeConfig)
|
||||
?? extractRuntimeHint(parentAgent?.runtimeConfig);
|
||||
|
||||
// Create child agent session
|
||||
const { session: childSession } = await createResolvedAgentSession({
|
||||
sessionPurpose: "executor",
|
||||
runtimeHint: childRuntimeHint,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
cwd: childWorktreePath,
|
||||
systemPrompt: childSystemPrompt,
|
||||
|
||||
@@ -98,7 +98,7 @@ import { join } from "node:path";
|
||||
import { getTaskMergeBlocker, type TaskStore, type MergeResult, type MergeDetails, type WorkflowStep, type WorkflowStepResult, type Settings, type AgentPromptsConfig } from "@fusion/core";
|
||||
import { resolveAgentPrompt } from "@fusion/core";
|
||||
import { describeModel, promptWithFallback } from "./pi.js";
|
||||
import { createResolvedAgentSession } from "./agent-session-helpers.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js";
|
||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import type { WorktreePool } from "./worktree-pool.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
@@ -800,12 +800,13 @@ async function attemptInMergeVerificationFix(
|
||||
|
||||
// Build skill selection context
|
||||
let skillContext = undefined;
|
||||
let taskForSkillContext: Awaited<ReturnType<typeof store.getTask>> | null = null;
|
||||
if (options.agentStore) {
|
||||
try {
|
||||
const task = await store.getTask(taskId);
|
||||
taskForSkillContext = await store.getTask(taskId);
|
||||
skillContext = await buildSessionSkillContext({
|
||||
agentStore: options.agentStore,
|
||||
task,
|
||||
task: taskForSkillContext,
|
||||
sessionPurpose: "merger",
|
||||
projectRootDir: rootDir,
|
||||
});
|
||||
@@ -816,8 +817,17 @@ async function attemptInMergeVerificationFix(
|
||||
|
||||
// Create the fix agent session
|
||||
throwIfAborted(options.signal, taskId);
|
||||
const assignedAgentId = taskForSkillContext?.assignedAgentId?.trim();
|
||||
const agentStoreWithGetAgent = options.agentStore && typeof (options.agentStore as { getAgent?: unknown }).getAgent === "function"
|
||||
? options.agentStore
|
||||
: null;
|
||||
const assignedAgent = assignedAgentId && agentStoreWithGetAgent
|
||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "merger",
|
||||
runtimeHint: mergerRuntimeHint,
|
||||
pluginRunner: options.pluginRunner,
|
||||
cwd: rootDir, // Runs on the main branch in the project root
|
||||
systemPrompt: `You are a verification fix agent running during a merge on the main branch.
|
||||
@@ -1665,7 +1675,12 @@ async function resolveComplexRebaseConflictsWithAi(
|
||||
taskId: string,
|
||||
settings: Settings,
|
||||
conflictedFiles: string[],
|
||||
options?: { onAgentText?: (delta: string) => void; pluginRunner?: import("./plugin-runner.js").PluginRunner; signal?: AbortSignal },
|
||||
options?: {
|
||||
onAgentText?: (delta: string) => void;
|
||||
pluginRunner?: import("./plugin-runner.js").PluginRunner;
|
||||
signal?: AbortSignal;
|
||||
runtimeHint?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
mergerLog.log(`${taskId}: resolving ${conflictedFiles.length} complex rebase conflict(s) with AI`);
|
||||
|
||||
@@ -1693,6 +1708,7 @@ You are assisting with a paused \`git pull --rebase\`.
|
||||
throwIfAborted(options?.signal, taskId);
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "merger",
|
||||
runtimeHint: options?.runtimeHint,
|
||||
pluginRunner: options?.pluginRunner,
|
||||
cwd: rootDir,
|
||||
systemPrompt,
|
||||
@@ -1738,7 +1754,7 @@ async function resolveRebaseConflictSet(
|
||||
rootDir: string,
|
||||
taskId: string,
|
||||
settings: Settings,
|
||||
options?: { onAgentText?: (delta: string) => void; signal?: AbortSignal },
|
||||
options?: { onAgentText?: (delta: string) => void; signal?: AbortSignal; runtimeHint?: string },
|
||||
): Promise<void> {
|
||||
const conflictedFiles = await getConflictedFiles(rootDir);
|
||||
if (conflictedFiles.length === 0) return;
|
||||
@@ -1781,7 +1797,7 @@ async function pullWithRebaseAndResolveConflicts(
|
||||
settings: Settings,
|
||||
remote: string,
|
||||
branch: string,
|
||||
options?: { onAgentText?: (delta: string) => void; signal?: AbortSignal },
|
||||
options?: { onAgentText?: (delta: string) => void; signal?: AbortSignal; runtimeHint?: string },
|
||||
): Promise<void> {
|
||||
const pullCommand = `git pull --rebase ${quoteArg(remote)} ${quoteArg(branch)}`;
|
||||
try {
|
||||
@@ -1872,7 +1888,7 @@ export async function pushToRemoteAfterMerge(
|
||||
rootDir: string,
|
||||
taskId: string,
|
||||
settings: Settings,
|
||||
options?: { onAgentText?: (delta: string) => void; signal?: AbortSignal },
|
||||
options?: { onAgentText?: (delta: string) => void; signal?: AbortSignal; runtimeHint?: string },
|
||||
): Promise<{ pushed: boolean; error?: string }> {
|
||||
let target: { remote: string; branch: string };
|
||||
|
||||
@@ -2676,7 +2692,20 @@ export async function aiMergeTask(
|
||||
if (settings.pushAfterMerge && settings.mergeStrategy !== "pull-request") {
|
||||
try {
|
||||
throwIfAborted(options.signal, taskId);
|
||||
const pushResult = await pushToRemoteAfterMerge(store, rootDir, taskId, settings, options);
|
||||
const pushTask = await store.getTask(taskId).catch(() => null);
|
||||
const pushAssignedAgentId = pushTask?.assignedAgentId?.trim();
|
||||
const pushAgentStoreWithGetAgent = options.agentStore && typeof (options.agentStore as { getAgent?: unknown }).getAgent === "function"
|
||||
? options.agentStore
|
||||
: null;
|
||||
const pushAssignedAgent = pushAssignedAgentId && pushAgentStoreWithGetAgent
|
||||
? await pushAgentStoreWithGetAgent.getAgent(pushAssignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const pushRuntimeHint = extractRuntimeHint(pushAssignedAgent?.runtimeConfig);
|
||||
const pushResult = await pushToRemoteAfterMerge(store, rootDir, taskId, settings, {
|
||||
onAgentText: options.onAgentText,
|
||||
signal: options.signal,
|
||||
runtimeHint: pushRuntimeHint,
|
||||
});
|
||||
if (pushResult.pushed) {
|
||||
mergerLog.log(`${taskId}: pushed merged result to remote`);
|
||||
} else {
|
||||
@@ -3278,12 +3307,13 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
|
||||
// Build skill selection context (assigned agent skills take precedence over role fallback)
|
||||
let skillContext = undefined;
|
||||
let taskForSkillContext: Awaited<ReturnType<typeof store.getTask>> | null = null;
|
||||
if (options.agentStore) {
|
||||
try {
|
||||
const task = await store.getTask(taskId);
|
||||
taskForSkillContext = await store.getTask(taskId);
|
||||
skillContext = await buildSessionSkillContext({
|
||||
agentStore: options.agentStore,
|
||||
task,
|
||||
task: taskForSkillContext,
|
||||
sessionPurpose: "merger",
|
||||
projectRootDir: rootDir,
|
||||
});
|
||||
@@ -3292,8 +3322,18 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
}
|
||||
}
|
||||
|
||||
const assignedAgentId = taskForSkillContext?.assignedAgentId?.trim();
|
||||
const agentStoreWithGetAgent = options.agentStore && typeof (options.agentStore as { getAgent?: unknown }).getAgent === "function"
|
||||
? options.agentStore
|
||||
: null;
|
||||
const assignedAgent = assignedAgentId && agentStoreWithGetAgent
|
||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "merger",
|
||||
runtimeHint: mergerRuntimeHint,
|
||||
pluginRunner: options.pluginRunner,
|
||||
cwd: rootDir,
|
||||
systemPrompt: mergerSystemPrompt,
|
||||
@@ -3764,12 +3804,13 @@ If issues are found that need attention, describe them clearly.`;
|
||||
|
||||
// Build skill selection context for post-merge session
|
||||
let postMergeSkillContext = undefined;
|
||||
let taskForSkillContext: Awaited<ReturnType<typeof store.getTask>> | null = null;
|
||||
if (mergeOptions.agentStore) {
|
||||
try {
|
||||
const task = await store.getTask(taskId);
|
||||
taskForSkillContext = await store.getTask(taskId);
|
||||
postMergeSkillContext = await buildSessionSkillContext({
|
||||
agentStore: mergeOptions.agentStore,
|
||||
task,
|
||||
task: taskForSkillContext,
|
||||
sessionPurpose: "merger",
|
||||
projectRootDir: rootDir,
|
||||
});
|
||||
@@ -3778,8 +3819,17 @@ If issues are found that need attention, describe them clearly.`;
|
||||
}
|
||||
}
|
||||
|
||||
const assignedAgentId = taskForSkillContext?.assignedAgentId?.trim();
|
||||
const agentStoreWithGetAgent = mergeOptions.agentStore && typeof (mergeOptions.agentStore as { getAgent?: unknown }).getAgent === "function"
|
||||
? mergeOptions.agentStore
|
||||
: null;
|
||||
const assignedAgent = assignedAgentId && agentStoreWithGetAgent
|
||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "merger",
|
||||
runtimeHint: mergerRuntimeHint,
|
||||
pluginRunner: mergeOptions.pluginRunner,
|
||||
cwd,
|
||||
systemPrompt: postMergeSystemPrompt,
|
||||
|
||||
@@ -18,9 +18,10 @@ import type {
|
||||
MissionContractAssertion,
|
||||
MissionFeature,
|
||||
MissionValidatorRun,
|
||||
AgentStore,
|
||||
} from "@fusion/core";
|
||||
import { createFnAgent, promptWithFallback, type AgentResult } from "./pi.js";
|
||||
import { createResolvedAgentSession } from "./agent-session-helpers.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js";
|
||||
import { createLogger } from "./logger.js";
|
||||
|
||||
/** Logger for the mission execution loop subsystem. */
|
||||
@@ -66,6 +67,8 @@ export interface MissionExecutionLoopOptions {
|
||||
maxRetryBudget?: number;
|
||||
/** Plugin runner for runtime selection. When provided, enables plugin runtime lookup. */
|
||||
pluginRunner?: import("./plugin-runner.js").PluginRunner;
|
||||
/** Optional agent store for resolving assigned-agent runtime hints. */
|
||||
agentStore?: AgentStore;
|
||||
}
|
||||
|
||||
export class MissionExecutionLoop extends EventEmitter {
|
||||
@@ -76,6 +79,7 @@ export class MissionExecutionLoop extends EventEmitter {
|
||||
private maxRetryBudget: number;
|
||||
private missionAutopilot?: MissionExecutionLoopOptions["missionAutopilot"];
|
||||
private pluginRunner?: MissionExecutionLoopOptions["pluginRunner"];
|
||||
private agentStore?: MissionExecutionLoopOptions["agentStore"];
|
||||
private activeValidations = new Set<string>(); // feature IDs currently being validated
|
||||
|
||||
constructor(options: MissionExecutionLoopOptions) {
|
||||
@@ -86,6 +90,7 @@ export class MissionExecutionLoop extends EventEmitter {
|
||||
this.maxRetryBudget = options.maxRetryBudget ?? 3;
|
||||
this.missionAutopilot = options.missionAutopilot;
|
||||
this.pluginRunner = options.pluginRunner;
|
||||
this.agentStore = options.agentStore;
|
||||
loopLog.log("MissionExecutionLoop created");
|
||||
}
|
||||
|
||||
@@ -325,6 +330,9 @@ export class MissionExecutionLoop extends EventEmitter {
|
||||
// Get task context for validation
|
||||
const task = feature.taskId ? await this.taskStore.getTask(feature.taskId) : null;
|
||||
const taskContext = task ? this.buildTaskContext(task) : "";
|
||||
const validationRuntimeHint = task?.assignedAgentId && this.agentStore
|
||||
? extractRuntimeHint((await this.agentStore.getAgent(task.assignedAgentId).catch(() => null))?.runtimeConfig)
|
||||
: undefined;
|
||||
|
||||
let session: AgentResult | null = null;
|
||||
|
||||
@@ -332,6 +340,7 @@ export class MissionExecutionLoop extends EventEmitter {
|
||||
// Create validation agent session
|
||||
const sessionResult = await createResolvedAgentSession({
|
||||
sessionPurpose: "validation",
|
||||
runtimeHint: validationRuntimeHint,
|
||||
pluginRunner: this.pluginRunner,
|
||||
cwd: this.rootDir,
|
||||
systemPrompt: this.buildValidationSystemPrompt(feature, assertions, taskContext),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import type { TaskStore, TaskComment, AgentPromptsConfig, Settings } from "@fusion/core";
|
||||
import { buildReviewerMemoryInstructions, resolveAgentPrompt } from "@fusion/core";
|
||||
import { describeModel, promptWithFallback } from "./pi.js";
|
||||
import { createResolvedAgentSession } from "./agent-session-helpers.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js";
|
||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
import { reviewerLog } from "./logger.js";
|
||||
@@ -364,6 +364,7 @@ export async function reviewStep(
|
||||
: undefined;
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "reviewer",
|
||||
runtimeHint: extractRuntimeHint(memoryAgent?.runtimeConfig),
|
||||
pluginRunner: options.pluginRunner,
|
||||
cwd,
|
||||
systemPrompt: reviewerSystemPrompt,
|
||||
|
||||
@@ -256,6 +256,7 @@ export class InProcessRuntime
|
||||
: undefined,
|
||||
rootDir: this.config.workingDirectory,
|
||||
pluginRunner: this.pluginRunner,
|
||||
agentStore: this.agentStore,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ import { join } from "node:path";
|
||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import type { AgentStore, MessageStore, TaskDetail, Settings, TaskStore } from "@fusion/core";
|
||||
|
||||
import { createFnAgent, promptWithFallback, describeModel } from "./pi.js";
|
||||
import {
|
||||
createResolvedAgentSession,
|
||||
describeAgentModel,
|
||||
promptWithAutoRetry,
|
||||
} from "./agent-session-helpers.js";
|
||||
import type { SkillSelectionContext } from "./skill-resolver.js";
|
||||
import { generateWorktreeName } from "./worktree-names.js";
|
||||
import { AgentSemaphore } from "./concurrency.js";
|
||||
@@ -82,6 +86,8 @@ export interface StepSessionExecutorOptions {
|
||||
stuckTaskDetector?: StuckTaskDetector;
|
||||
/** Optional plugin runner for providing plugin tools to step sessions. */
|
||||
pluginRunner?: import("./plugin-runner.js").PluginRunner;
|
||||
/** Optional runtime hint resolved from assigned agent runtimeConfig. */
|
||||
runtimeHint?: string;
|
||||
/** Callback invoked when a step starts executing. */
|
||||
onStepStart?: (stepIndex: number) => void;
|
||||
/** Callback invoked when a step completes (success or failure). */
|
||||
@@ -902,7 +908,10 @@ export class StepSessionExecutor {
|
||||
settings,
|
||||
);
|
||||
|
||||
const createResult = await createFnAgent({
|
||||
const createResult = await createResolvedAgentSession({
|
||||
sessionPurpose: "executor",
|
||||
runtimeHint: this.options.runtimeHint,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
cwd: worktreePath,
|
||||
systemPrompt: `You are an AI agent executing step ${stepIndex} of task ${taskDetail.id}. Follow instructions precisely.`,
|
||||
defaultProvider: executorProvider,
|
||||
@@ -950,13 +959,14 @@ export class StepSessionExecutor {
|
||||
this.activeSessions.set(stepIndex, handle);
|
||||
stuckTaskDetector?.trackTask(trackingKey, { dispose: () => session?.dispose() }, taskDetail.id);
|
||||
|
||||
const sessionModel = await describeAgentModel(session);
|
||||
stepExecLog.log(
|
||||
`Step ${stepIndex} attempt ${attempt + 1} session created ` +
|
||||
`(model=${describeModel(session)}) for task ${taskDetail.id}`,
|
||||
`(model=${sessionModel}) for task ${taskDetail.id}`,
|
||||
);
|
||||
|
||||
// Send prompt
|
||||
await promptWithFallback(session, stepPrompt);
|
||||
await promptWithAutoRetry(session, stepPrompt);
|
||||
|
||||
// Re-raise errors that pi-coding-agent swallowed after exhausting retries.
|
||||
// session.prompt() resolves normally even when retries are exhausted —
|
||||
@@ -990,7 +1000,7 @@ export class StepSessionExecutor {
|
||||
recoveryAttempts++;
|
||||
try {
|
||||
stuckTaskDetector?.recordActivity(trackingKey);
|
||||
await promptWithFallback(session, reducedStepPrompt);
|
||||
await promptWithAutoRetry(session, reducedStepPrompt);
|
||||
checkSessionError(session);
|
||||
stepExecLog.log(`Step ${stepIndex} reduced-prompt recovery succeeded`);
|
||||
await this.store.appendAgentLog(
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
AgentSession,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { describeModel, promptWithFallback } from "./pi.js";
|
||||
import { createResolvedAgentSession } from "./agent-session-helpers.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js";
|
||||
import { reviewStep, type ReviewVerdict } from "./reviewer.js";
|
||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import { PRIORITY_SPECIFY, type AgentSemaphore } from "./concurrency.js";
|
||||
@@ -858,12 +858,18 @@ export class TriageProcessor {
|
||||
),
|
||||
];
|
||||
|
||||
const assignedAgent = task.assignedAgentId && this.options.agentStore
|
||||
? await this.options.agentStore.getAgent(task.assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
let triageRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
|
||||
// Resolve per-agent custom instructions for the triage role
|
||||
let triageInstructions = "";
|
||||
if (this.options.agentStore) {
|
||||
try {
|
||||
const agents = await this.options.agentStore.listAgents({ role: "triage" });
|
||||
for (const agent of agents) {
|
||||
triageRuntimeHint ??= extractRuntimeHint(agent.runtimeConfig);
|
||||
if (agent.instructionsText || agent.instructionsPath) {
|
||||
triageInstructions = await resolveAgentInstructions(agent, this.rootDir);
|
||||
break;
|
||||
@@ -891,6 +897,7 @@ export class TriageProcessor {
|
||||
|
||||
let { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "triage",
|
||||
runtimeHint: triageRuntimeHint,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
cwd: this.rootDir,
|
||||
systemPrompt: triageSystemPrompt,
|
||||
@@ -1111,6 +1118,7 @@ export class TriageProcessor {
|
||||
|
||||
const fallbackResult = await createResolvedAgentSession({
|
||||
sessionPurpose: "triage",
|
||||
runtimeHint: triageRuntimeHint,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
cwd: this.rootDir,
|
||||
systemPrompt: triageSystemPrompt,
|
||||
|
||||
Reference in New Issue
Block a user