feat(FN-3581): honor runtime model precedence for assigned agents
This merge delivers five major feature clusters: a fully rebuilt dependency graph plugin with draggable nodes, position persistence, modular architecture, highlighting and selection states, toolbar navigation, and keyboard controls; a new roadmap plugin with domain store, ordering logic, and compreh Fusion-Task-Id: FN-3581
This commit is contained in:
@@ -74,6 +74,32 @@ vi.mock("../agent-session-helpers.js", async () => {
|
||||
const hint = runtimeConfig?.runtimeHint;
|
||||
return typeof hint === "string" && hint.trim().length > 0 ? hint.trim() : undefined;
|
||||
},
|
||||
resolveExecutorSessionModel: (
|
||||
taskModelProvider: string | undefined,
|
||||
taskModelId: string | undefined,
|
||||
settings: Record<string, unknown> | undefined,
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||
) => {
|
||||
const model = typeof assignedAgentRuntimeConfig?.model === "string" ? assignedAgentRuntimeConfig.model : "";
|
||||
const slash = model.indexOf("/");
|
||||
if (slash > 0 && slash < model.length - 1) {
|
||||
return { provider: model.slice(0, slash), modelId: model.slice(slash + 1) };
|
||||
}
|
||||
if (taskModelProvider && taskModelId) return { provider: taskModelProvider, modelId: taskModelId };
|
||||
if (typeof settings?.executionProvider === "string" && typeof settings?.executionModelId === "string") {
|
||||
return { provider: settings.executionProvider as string, modelId: settings.executionModelId as string };
|
||||
}
|
||||
if (typeof settings?.executionGlobalProvider === "string" && typeof settings?.executionGlobalModelId === "string") {
|
||||
return { provider: settings.executionGlobalProvider as string, modelId: settings.executionGlobalModelId as string };
|
||||
}
|
||||
if (typeof settings?.defaultProviderOverride === "string" && typeof settings?.defaultModelIdOverride === "string") {
|
||||
return { provider: settings.defaultProviderOverride as string, modelId: settings.defaultModelIdOverride as string };
|
||||
}
|
||||
if (typeof settings?.defaultProvider === "string" && typeof settings?.defaultModelId === "string") {
|
||||
return { provider: settings.defaultProvider as string, modelId: settings.defaultModelId as string };
|
||||
}
|
||||
return { provider: undefined, modelId: undefined };
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("../worktree-names.js", async () => {
|
||||
@@ -3919,8 +3945,11 @@ describe("TaskExecutor executor model hot-swap", () => {
|
||||
(executor as any).activeSessions.set("FN-001", {
|
||||
session: { setModel, dispose: vi.fn() },
|
||||
seenSteeringIds: new Set(),
|
||||
lastModelProvider: "anthropic",
|
||||
lastModelId: "claude-sonnet-4-5",
|
||||
lastResolvedModelProvider: "anthropic",
|
||||
lastResolvedModelId: "claude-sonnet-4-5",
|
||||
lastTaskModelProvider: "anthropic",
|
||||
lastTaskModelId: "claude-sonnet-4-5",
|
||||
lastAssignedAgentId: null,
|
||||
});
|
||||
|
||||
store._trigger("task:updated", buildUpdatedTask({
|
||||
@@ -3964,8 +3993,11 @@ describe("TaskExecutor executor model hot-swap", () => {
|
||||
(executor as any).activeSessions.set("FN-001", {
|
||||
session: { setModel, dispose: vi.fn() },
|
||||
seenSteeringIds: new Set(),
|
||||
lastModelProvider: "anthropic",
|
||||
lastModelId: "claude-sonnet-4-5",
|
||||
lastResolvedModelProvider: "anthropic",
|
||||
lastResolvedModelId: "claude-sonnet-4-5",
|
||||
lastTaskModelProvider: "anthropic",
|
||||
lastTaskModelId: "claude-sonnet-4-5",
|
||||
lastAssignedAgentId: null,
|
||||
});
|
||||
|
||||
store._trigger("task:updated", buildUpdatedTask({
|
||||
@@ -4004,8 +4036,11 @@ describe("TaskExecutor executor model hot-swap", () => {
|
||||
(executor as any).activeSessions.set("FN-001", {
|
||||
session: { setModel, dispose: vi.fn() },
|
||||
seenSteeringIds: new Set(),
|
||||
lastModelProvider: "anthropic",
|
||||
lastModelId: "claude-sonnet-4-5",
|
||||
lastResolvedModelProvider: "anthropic",
|
||||
lastResolvedModelId: "claude-sonnet-4-5",
|
||||
lastTaskModelProvider: "anthropic",
|
||||
lastTaskModelId: "claude-sonnet-4-5",
|
||||
lastAssignedAgentId: null,
|
||||
});
|
||||
|
||||
store._trigger("task:updated", buildUpdatedTask({
|
||||
@@ -4044,8 +4079,11 @@ describe("TaskExecutor executor model hot-swap", () => {
|
||||
(executor as any).activeSessions.set("FN-001", {
|
||||
session: { setModel, dispose: vi.fn() },
|
||||
seenSteeringIds: new Set(),
|
||||
lastModelProvider: "openai",
|
||||
lastModelId: "gpt-4o",
|
||||
lastResolvedModelProvider: "openai",
|
||||
lastResolvedModelId: "gpt-4o",
|
||||
lastTaskModelProvider: "openai",
|
||||
lastTaskModelId: "gpt-4o",
|
||||
lastAssignedAgentId: null,
|
||||
});
|
||||
|
||||
store._trigger("task:updated", buildUpdatedTask({
|
||||
@@ -4073,8 +4111,11 @@ describe("TaskExecutor executor model hot-swap", () => {
|
||||
(executor as any).activeSessions.set("FN-001", {
|
||||
session: { setModel, dispose: vi.fn() },
|
||||
seenSteeringIds: new Set(),
|
||||
lastModelProvider: "anthropic",
|
||||
lastModelId: "claude-sonnet-4-5",
|
||||
lastResolvedModelProvider: "anthropic",
|
||||
lastResolvedModelId: "claude-sonnet-4-5",
|
||||
lastTaskModelProvider: "anthropic",
|
||||
lastTaskModelId: "claude-sonnet-4-5",
|
||||
lastAssignedAgentId: null,
|
||||
});
|
||||
|
||||
store._trigger("task:updated", buildUpdatedTask({
|
||||
@@ -4099,8 +4140,11 @@ describe("TaskExecutor executor model hot-swap", () => {
|
||||
(executor as any).activeSessions.set("FN-001", {
|
||||
session: { setModel, dispose },
|
||||
seenSteeringIds: new Set(),
|
||||
lastModelProvider: "anthropic",
|
||||
lastModelId: "claude-sonnet-4-5",
|
||||
lastResolvedModelProvider: "anthropic",
|
||||
lastResolvedModelId: "claude-sonnet-4-5",
|
||||
lastTaskModelProvider: "anthropic",
|
||||
lastTaskModelId: "claude-sonnet-4-5",
|
||||
lastAssignedAgentId: null,
|
||||
});
|
||||
|
||||
store._trigger("task:updated", buildUpdatedTask({
|
||||
|
||||
@@ -46,6 +46,32 @@ vi.mock("../agent-session-helpers.js", async () => {
|
||||
const hint = runtimeConfig?.runtimeHint;
|
||||
return typeof hint === "string" && hint.trim().length > 0 ? hint.trim() : undefined;
|
||||
},
|
||||
resolveExecutorSessionModel: (
|
||||
taskModelProvider: string | undefined,
|
||||
taskModelId: string | undefined,
|
||||
settings: Record<string, unknown> | undefined,
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||
) => {
|
||||
const model = typeof assignedAgentRuntimeConfig?.model === "string" ? assignedAgentRuntimeConfig.model : "";
|
||||
const slash = model.indexOf("/");
|
||||
if (slash > 0 && slash < model.length - 1) {
|
||||
return { provider: model.slice(0, slash), modelId: model.slice(slash + 1) };
|
||||
}
|
||||
if (taskModelProvider && taskModelId) return { provider: taskModelProvider, modelId: taskModelId };
|
||||
if (typeof settings?.executionProvider === "string" && typeof settings?.executionModelId === "string") {
|
||||
return { provider: settings.executionProvider as string, modelId: settings.executionModelId as string };
|
||||
}
|
||||
if (typeof settings?.executionGlobalProvider === "string" && typeof settings?.executionGlobalModelId === "string") {
|
||||
return { provider: settings.executionGlobalProvider as string, modelId: settings.executionGlobalModelId as string };
|
||||
}
|
||||
if (typeof settings?.defaultProviderOverride === "string" && typeof settings?.defaultModelIdOverride === "string") {
|
||||
return { provider: settings.defaultProviderOverride as string, modelId: settings.defaultModelIdOverride as string };
|
||||
}
|
||||
if (typeof settings?.defaultProvider === "string" && typeof settings?.defaultModelId === "string") {
|
||||
return { provider: settings.defaultProvider as string, modelId: settings.defaultModelId as string };
|
||||
}
|
||||
return { provider: undefined, modelId: undefined };
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("node:child_process", () => {
|
||||
|
||||
@@ -562,6 +562,27 @@ vi.mock("../agent-session-helpers.js", async () => {
|
||||
pi.promptWithFallback(session, prompt, options as any),
|
||||
),
|
||||
describeAgentModel: vi.fn(async (session: any) => pi.describeModel(session)),
|
||||
resolveExecutorSessionModel: vi.fn((taskModelProvider?: string, taskModelId?: string, settings?: any, assignedAgentRuntimeConfig?: Record<string, unknown>) => {
|
||||
const model = typeof assignedAgentRuntimeConfig?.model === "string" ? assignedAgentRuntimeConfig.model : "";
|
||||
const slash = model.indexOf("/");
|
||||
if (slash > 0 && slash < model.length - 1) {
|
||||
return { provider: model.slice(0, slash), modelId: model.slice(slash + 1) };
|
||||
}
|
||||
if (taskModelProvider && taskModelId) return { provider: taskModelProvider, modelId: taskModelId };
|
||||
if (settings?.executionProvider && settings?.executionModelId) {
|
||||
return { provider: settings.executionProvider, modelId: settings.executionModelId };
|
||||
}
|
||||
if (settings?.executionGlobalProvider && settings?.executionGlobalModelId) {
|
||||
return { provider: settings.executionGlobalProvider, modelId: settings.executionGlobalModelId };
|
||||
}
|
||||
if (settings?.defaultProviderOverride && settings?.defaultModelIdOverride) {
|
||||
return { provider: settings.defaultProviderOverride, modelId: settings.defaultModelIdOverride };
|
||||
}
|
||||
if (settings?.defaultProvider && settings?.defaultModelId) {
|
||||
return { provider: settings.defaultProvider, modelId: settings.defaultModelId };
|
||||
}
|
||||
return { provider: undefined, modelId: undefined };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { AgentRuntimeOptions } from "./agent-runtime.js";
|
||||
import type { SkillSelectionContext } from "./skill-resolver.js";
|
||||
import type { PluginRunner } from "./plugin-runner.js";
|
||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveTaskExecutionModel, type Settings } from "@fusion/core";
|
||||
import { resolveRuntime, buildRuntimeResolutionContext, type SessionPurpose } from "./runtime-resolution.js";
|
||||
import { createLogger } from "./logger.js";
|
||||
import { promptWithFallback, describeModel } from "./pi.js";
|
||||
@@ -112,6 +113,31 @@ export function extractRuntimeModel(
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveExecutorSessionModel(
|
||||
taskModelProvider: string | undefined,
|
||||
taskModelId: string | undefined,
|
||||
settings: Partial<Settings> | undefined,
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||
): { provider: string | undefined; modelId: string | undefined } {
|
||||
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig);
|
||||
if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) {
|
||||
return assignedRuntimeModel;
|
||||
}
|
||||
|
||||
const resolvedTaskModel = resolveTaskExecutionModel(
|
||||
{
|
||||
modelProvider: taskModelProvider,
|
||||
modelId: taskModelId,
|
||||
},
|
||||
settings,
|
||||
);
|
||||
|
||||
return {
|
||||
provider: resolvedTaskModel.provider,
|
||||
modelId: resolvedTaskModel.modelId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an agent session using runtime resolution.
|
||||
*
|
||||
|
||||
@@ -25,7 +25,11 @@ import { generateWorktreeName, slugify } from "./worktree-names.js";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import { describeModel, promptWithFallback, compactSessionContext } from "./pi.js";
|
||||
import { accumulateSessionTokenUsage } from "./session-token-usage.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js";
|
||||
import {
|
||||
createResolvedAgentSession,
|
||||
extractRuntimeHint,
|
||||
resolveExecutorSessionModel,
|
||||
} 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";
|
||||
@@ -498,40 +502,6 @@ function getExecutorSystemPrompt(settings: Settings): string {
|
||||
return customPrompt || EXECUTOR_SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
function resolveExecutorModelPair(
|
||||
taskModelProvider: string | undefined,
|
||||
taskModelId: string | undefined,
|
||||
settings: Partial<Settings> | undefined,
|
||||
): { provider: string | undefined; modelId: string | undefined } {
|
||||
if (taskModelProvider && taskModelId) {
|
||||
return { provider: taskModelProvider, modelId: taskModelId };
|
||||
}
|
||||
if (settings?.executionProvider && settings?.executionModelId) {
|
||||
return {
|
||||
provider: settings.executionProvider,
|
||||
modelId: settings.executionModelId,
|
||||
};
|
||||
}
|
||||
if (settings?.executionGlobalProvider && settings?.executionGlobalModelId) {
|
||||
return {
|
||||
provider: settings.executionGlobalProvider,
|
||||
modelId: settings.executionGlobalModelId,
|
||||
};
|
||||
}
|
||||
if (settings?.defaultProviderOverride && settings?.defaultModelIdOverride) {
|
||||
return {
|
||||
provider: settings.defaultProviderOverride,
|
||||
modelId: settings.defaultModelIdOverride,
|
||||
};
|
||||
}
|
||||
if (settings?.defaultProvider && settings?.defaultModelId) {
|
||||
return {
|
||||
provider: settings.defaultProvider,
|
||||
modelId: settings.defaultModelId,
|
||||
};
|
||||
}
|
||||
return { provider: undefined, modelId: undefined };
|
||||
}
|
||||
|
||||
export interface TaskExecutorOptions {
|
||||
semaphore?: AgentSemaphore;
|
||||
@@ -573,8 +543,11 @@ export class TaskExecutor {
|
||||
private activeSessions = new Map<string, {
|
||||
session: AgentSession;
|
||||
seenSteeringIds: Set<string>;
|
||||
lastModelProvider?: string | null;
|
||||
lastModelId?: string | null;
|
||||
lastResolvedModelProvider?: string;
|
||||
lastResolvedModelId?: string;
|
||||
lastTaskModelProvider?: string | null;
|
||||
lastTaskModelId?: string | null;
|
||||
lastAssignedAgentId?: string | null;
|
||||
}>();
|
||||
/** Active step-session executors per task (mutually exclusive with activeSessions). */
|
||||
private activeStepExecutors = new Map<string, StepSessionExecutor>();
|
||||
@@ -1030,21 +1003,32 @@ export class TaskExecutor {
|
||||
// Handle executor model hot-swap on active single-session executions
|
||||
if (this.activeSessions.has(task.id) && !task.paused) {
|
||||
const activeEntry = this.activeSessions.get(task.id)!;
|
||||
const providerChanged = task.modelProvider !== activeEntry.lastModelProvider;
|
||||
const modelIdChanged = task.modelId !== activeEntry.lastModelId;
|
||||
const taskModelProviderChanged = task.modelProvider !== activeEntry.lastTaskModelProvider;
|
||||
const taskModelIdChanged = task.modelId !== activeEntry.lastTaskModelId;
|
||||
const assignedAgentChanged = (task.assignedAgentId ?? null) !== (activeEntry.lastAssignedAgentId ?? null);
|
||||
|
||||
if (providerChanged || modelIdChanged) {
|
||||
activeEntry.lastModelProvider = task.modelProvider;
|
||||
activeEntry.lastModelId = task.modelId;
|
||||
if (taskModelProviderChanged || taskModelIdChanged || assignedAgentChanged) {
|
||||
activeEntry.lastTaskModelProvider = task.modelProvider;
|
||||
activeEntry.lastTaskModelId = task.modelId;
|
||||
activeEntry.lastAssignedAgentId = task.assignedAgentId ?? null;
|
||||
|
||||
const settings = await this.store.getSettings();
|
||||
// Resolve model using canonical lane hierarchy for hot-swap
|
||||
const { provider: newProvider, modelId: newModelId } = resolveExecutorModelPair(
|
||||
const assignedRuntimeConfig = await this.getAssignedAgentRuntimeConfig(task.assignedAgentId);
|
||||
const { provider: newProvider, modelId: newModelId } = resolveExecutorSessionModel(
|
||||
task.modelProvider,
|
||||
task.modelId,
|
||||
settings,
|
||||
assignedRuntimeConfig,
|
||||
);
|
||||
|
||||
const providerChanged = newProvider !== activeEntry.lastResolvedModelProvider;
|
||||
const modelIdChanged = newModelId !== activeEntry.lastResolvedModelId;
|
||||
if (!providerChanged && !modelIdChanged) {
|
||||
return;
|
||||
}
|
||||
activeEntry.lastResolvedModelProvider = newProvider;
|
||||
activeEntry.lastResolvedModelId = newModelId;
|
||||
|
||||
if (newProvider && newModelId) {
|
||||
try {
|
||||
const model = this.modelRegistry.find(newProvider, newModelId);
|
||||
@@ -1685,7 +1669,7 @@ export class TaskExecutor {
|
||||
private async executeReviewHandoff(
|
||||
task: Task,
|
||||
_session: AgentSession,
|
||||
_sessionEntry: { session: AgentSession; seenSteeringIds: Set<string>; lastModelProvider?: string | null; lastModelId?: string | null },
|
||||
_sessionEntry: { session: AgentSession; seenSteeringIds: Set<string>; lastResolvedModelProvider?: string; lastResolvedModelId?: string; lastTaskModelProvider?: string | null; lastTaskModelId?: string | null; lastAssignedAgentId?: string | null },
|
||||
): Promise<void> {
|
||||
try {
|
||||
executorLog.log(`Executing review handoff for ${task.id}`);
|
||||
@@ -1875,6 +1859,15 @@ export class TaskExecutor {
|
||||
return activeRun !== null;
|
||||
}
|
||||
|
||||
private async getAssignedAgentRuntimeConfig(
|
||||
assignedAgentId: string | null | undefined,
|
||||
): Promise<Record<string, unknown> | undefined> {
|
||||
const normalizedId = assignedAgentId?.trim();
|
||||
if (!normalizedId || !this.options.agentStore) return undefined;
|
||||
const agent = await this.options.agentStore.getAgent(normalizedId).catch(() => null);
|
||||
return (agent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-dispatch execute() for any unstarted in-progress task belonging to the
|
||||
* given agent. Called after a heartbeat run completes to unblock tasks that
|
||||
@@ -2398,6 +2391,7 @@ export class TaskExecutor {
|
||||
stuckTaskDetector: this.options.stuckTaskDetector,
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
runtimeHint: stepSessionRuntimeHint,
|
||||
assignedAgentRuntimeConfig: (stepSessionAgent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined,
|
||||
// Pass skill selection context from the main executor session
|
||||
skillSelection: skillContext.skillSelectionContext,
|
||||
// Pass agentStore and messageStore for delegation and messaging tools
|
||||
@@ -2892,10 +2886,11 @@ export class TaskExecutor {
|
||||
// 3. Global execution lane pair (executionGlobalProvider + executionGlobalModelId)
|
||||
// 4. Project default override pair (defaultProviderOverride + defaultModelIdOverride)
|
||||
// 5. Global default pair (defaultProvider + defaultModelId)
|
||||
const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair(
|
||||
const { provider: executorProvider, modelId: executorModelId } = resolveExecutorSessionModel(
|
||||
detail.modelProvider,
|
||||
detail.modelId,
|
||||
settings,
|
||||
(assignedAgent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined,
|
||||
);
|
||||
const executorFallbackProvider = settings.fallbackProvider;
|
||||
const executorFallbackModelId = settings.fallbackModelId;
|
||||
@@ -2962,17 +2957,20 @@ export class TaskExecutor {
|
||||
}),
|
||||
});
|
||||
|
||||
const executorModelDesc = describeModel(session);
|
||||
const executorModelMarker = `Executor using model: ${executorModelDesc}`;
|
||||
if (isResuming) {
|
||||
executorLog.log(`${task.id}: resumed session from ${task.sessionFile}`);
|
||||
await this.store.logEntry(task.id, `Resumed agent session after unpause (model: ${describeModel(session)})`, undefined, this.currentRunContext);
|
||||
await this.store.logEntry(task.id, `Resumed agent session after unpause (model: ${executorModelDesc})`, undefined, this.currentRunContext);
|
||||
} else {
|
||||
executorLog.log(`${task.id}: using model ${describeModel(session)}`);
|
||||
await this.store.logEntry(task.id, `Executor using model: ${describeModel(session)}`, undefined, this.currentRunContext);
|
||||
executorLog.log(`${task.id}: using model ${executorModelDesc}`);
|
||||
await this.store.logEntry(task.id, executorModelMarker, undefined, this.currentRunContext);
|
||||
// Persist session file path so pause/resume can reopen it
|
||||
if (sessionFile) {
|
||||
await this.store.updateTask(task.id, { sessionFile });
|
||||
}
|
||||
}
|
||||
await this.store.appendAgentLog(task.id, executorModelMarker, "text", undefined, "executor");
|
||||
|
||||
// Make session available to custom tools (fn_task_update checkpoint capture, fn_review_step rewind)
|
||||
sessionRef.current = session;
|
||||
@@ -2988,8 +2986,11 @@ export class TaskExecutor {
|
||||
this.activeSessions.set(task.id, {
|
||||
session,
|
||||
seenSteeringIds,
|
||||
lastModelProvider: detail.modelProvider,
|
||||
lastModelId: detail.modelId,
|
||||
lastResolvedModelProvider: executorProvider,
|
||||
lastResolvedModelId: executorModelId,
|
||||
lastTaskModelProvider: detail.modelProvider,
|
||||
lastTaskModelId: detail.modelId,
|
||||
lastAssignedAgentId: detail.assignedAgentId ?? null,
|
||||
});
|
||||
|
||||
// Register with stuck task detector for heartbeat monitoring
|
||||
@@ -3274,8 +3275,11 @@ export class TaskExecutor {
|
||||
this.activeSessions.set(task.id, {
|
||||
session: retrySession,
|
||||
seenSteeringIds,
|
||||
lastModelProvider: detail.modelProvider,
|
||||
lastModelId: detail.modelId,
|
||||
lastResolvedModelProvider: executorProvider,
|
||||
lastResolvedModelId: executorModelId,
|
||||
lastTaskModelProvider: detail.modelProvider,
|
||||
lastTaskModelId: detail.modelId,
|
||||
lastAssignedAgentId: detail.assignedAgentId ?? null,
|
||||
});
|
||||
stuckDetector?.trackTask(task.id, retrySession);
|
||||
|
||||
@@ -4563,10 +4567,12 @@ ${feedback}
|
||||
}
|
||||
|
||||
// Resolve model using the executor's model hierarchy
|
||||
const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair(
|
||||
const assignedRuntimeConfig = await this.getAssignedAgentRuntimeConfig(task.assignedAgentId);
|
||||
const { provider: executorProvider, modelId: executorModelId } = resolveExecutorSessionModel(
|
||||
task.modelProvider,
|
||||
task.modelId,
|
||||
settings,
|
||||
assignedRuntimeConfig,
|
||||
);
|
||||
|
||||
// Create the fix agent session
|
||||
@@ -6919,7 +6925,7 @@ Child agent: ${agent.id} (${name})`;
|
||||
// honor project executionProvider/executionModelId overrides (parity
|
||||
// with main executor at the top of agentWork()).
|
||||
const { provider: childExecutorProvider, modelId: childExecutorModelId } =
|
||||
resolveExecutorModelPair(undefined, undefined, settings);
|
||||
resolveExecutorSessionModel(undefined, undefined, settings, agent.runtimeConfig as Record<string, unknown> | undefined);
|
||||
|
||||
// Create child agent session
|
||||
const { session: childSession } = await createResolvedAgentSession({
|
||||
|
||||
@@ -553,9 +553,12 @@ export async function reviewStep(
|
||||
throw err;
|
||||
}
|
||||
|
||||
reviewerLog.log(`${taskId}: reviewer using model ${describeModel(session)}`);
|
||||
const reviewerModelDesc = describeModel(session);
|
||||
const reviewerModelMarker = `Reviewer using model: ${reviewerModelDesc}`;
|
||||
reviewerLog.log(`${taskId}: reviewer using model ${reviewerModelDesc}`);
|
||||
if (options.store && options.taskId) {
|
||||
await options.store.logEntry(options.taskId, `Reviewer using model: ${describeModel(session)}`);
|
||||
await options.store.logEntry(options.taskId, reviewerModelMarker);
|
||||
await options.store.appendAgentLog(options.taskId, reviewerModelMarker, "text", undefined, "reviewer").catch(() => undefined);
|
||||
}
|
||||
|
||||
// Notify the caller so it can track this session in a per-task subagent map.
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
createResolvedAgentSession,
|
||||
describeAgentModel,
|
||||
promptWithAutoRetry,
|
||||
resolveExecutorSessionModel,
|
||||
} from "./agent-session-helpers.js";
|
||||
import type { SkillSelectionContext } from "./skill-resolver.js";
|
||||
import { generateWorktreeName } from "./worktree-names.js";
|
||||
@@ -96,6 +97,8 @@ export interface StepSessionExecutorOptions {
|
||||
pluginRunner?: import("./plugin-runner.js").PluginRunner;
|
||||
/** Optional runtime hint resolved from assigned agent runtimeConfig. */
|
||||
runtimeHint?: string;
|
||||
/** Optional assigned-agent runtime config for model override precedence. */
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>;
|
||||
/** Callback invoked when a step starts executing. */
|
||||
onStepStart?: (stepIndex: number) => void;
|
||||
/** Callback invoked when a step completes (success or failure). */
|
||||
@@ -564,40 +567,6 @@ interface SessionHandle {
|
||||
abortBash: () => void;
|
||||
}
|
||||
|
||||
function resolveExecutorModelPair(
|
||||
taskModelProvider: string | undefined,
|
||||
taskModelId: string | undefined,
|
||||
settings: Partial<Settings> | undefined,
|
||||
): { provider: string | undefined; modelId: string | undefined } {
|
||||
if (taskModelProvider && taskModelId) {
|
||||
return { provider: taskModelProvider, modelId: taskModelId };
|
||||
}
|
||||
if (settings?.executionProvider && settings?.executionModelId) {
|
||||
return {
|
||||
provider: settings.executionProvider,
|
||||
modelId: settings.executionModelId,
|
||||
};
|
||||
}
|
||||
if (settings?.executionGlobalProvider && settings?.executionGlobalModelId) {
|
||||
return {
|
||||
provider: settings.executionGlobalProvider,
|
||||
modelId: settings.executionGlobalModelId,
|
||||
};
|
||||
}
|
||||
if (settings?.defaultProviderOverride && settings?.defaultModelIdOverride) {
|
||||
return {
|
||||
provider: settings.defaultProviderOverride,
|
||||
modelId: settings.defaultModelIdOverride,
|
||||
};
|
||||
}
|
||||
if (settings?.defaultProvider && settings?.defaultModelId) {
|
||||
return {
|
||||
provider: settings.defaultProvider,
|
||||
modelId: settings.defaultModelId,
|
||||
};
|
||||
}
|
||||
return { provider: undefined, modelId: undefined };
|
||||
}
|
||||
|
||||
/** Fallback store used when step logging persistence is not configured. */
|
||||
const NOOP_TASK_STORE: Pick<TaskStore, "appendAgentLog"> = {
|
||||
@@ -961,10 +930,11 @@ export class StepSessionExecutor {
|
||||
// 3. Global execution lane pair (settings.executionGlobalProvider + settings.executionGlobalModelId)
|
||||
// 4. Project default override pair (settings.defaultProviderOverride + settings.defaultModelIdOverride)
|
||||
// 5. Global default pair (settings.defaultProvider + settings.defaultModelId)
|
||||
const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair(
|
||||
const { provider: executorProvider, modelId: executorModelId } = resolveExecutorSessionModel(
|
||||
taskDetail.modelProvider,
|
||||
taskDetail.modelId,
|
||||
settings,
|
||||
this.options.assignedAgentRuntimeConfig,
|
||||
);
|
||||
|
||||
const createResult = await createResolvedAgentSession({
|
||||
|
||||
Reference in New Issue
Block a user