feat(FN-742): add describeModel helper and log model in agent creation sites

- Add describeModel() helper in pi.ts to format provider/model info for logging
- Log resolved model details in executor, reviewer, and triage agent creation
- Update executor and reviewer to call describeModel before session start
- Add unit tests for describeModel covering all input combinations
- Fix test mocks to account for new describeModel dependency
This commit is contained in:
gsxdsm
2026-04-02 19:52:35 -07:00
parent da4ce003cd
commit cb06792766
8 changed files with 69 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ import { AgentSemaphore } from "./concurrency.js";
// Mock external dependencies
vi.mock("./pi.js", () => ({
createKbAgent: vi.fn(),
describeModel: vi.fn().mockReturnValue("mock-provider/mock-model"),
}));
vi.mock("./reviewer.js", () => ({
reviewStep: vi.fn(),

View File

@@ -5,7 +5,7 @@ import type { TaskStore, Task, TaskDetail, StepStatus, Settings, WorkflowStep, M
import { findWorktreeUser } from "./merger.js";
import { generateWorktreeName, slugify } from "./worktree-names.js";
import { Type, type Static } from "@mariozechner/pi-ai";
import { createKbAgent } from "./pi.js";
import { createKbAgent, describeModel } from "./pi.js";
import { reviewStep, type ReviewVerdict } from "./reviewer.js";
import type { ToolDefinition, AgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
import { PRIORITY_EXECUTE, type AgentSemaphore } from "./concurrency.js";
@@ -593,6 +593,9 @@ export class TaskExecutor {
defaultThinkingLevel: settings.defaultThinkingLevel,
});
executorLog.log(`${task.id}: using model ${describeModel(session)}`);
await this.store.logEntry(task.id, `Executor using model: ${describeModel(session)}`);
// Make session available to custom tools (task_update checkpoint capture, review_step rewind)
sessionRef.current = session;
@@ -1369,6 +1372,9 @@ If issues are found that need attention, describe them clearly.`;
defaultThinkingLevel: settings.defaultThinkingLevel,
});
executorLog.log(`${task.id}: workflow step '${workflowStep.name}' using model ${describeModel(session)}`);
await this.store.logEntry(task.id, `Workflow step '${workflowStep.name}' using model: ${describeModel(session)}`);
let output = "";
session.subscribe((event) => {
if (event.type === "message_update") {

View File

@@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest";
import { describeModel } from "./pi.js";
import type { AgentSession } from "@mariozechner/pi-coding-agent";
describe("describeModel", () => {
it('returns "provider/modelId" when session has a model', () => {
const fakeSession = {
model: {
provider: "anthropic",
id: "claude-sonnet-4-5",
name: "Claude Sonnet",
},
} as unknown as AgentSession;
expect(describeModel(fakeSession)).toBe("anthropic/claude-sonnet-4-5");
});
it('returns "unknown model" when session model is undefined', () => {
const fakeSession = {
model: undefined,
} as unknown as AgentSession;
expect(describeModel(fakeSession)).toBe("unknown model");
});
it("handles different providers", () => {
const fakeSession = {
model: {
provider: "openai",
id: "gpt-4o",
name: "GPT-4o",
},
} as unknown as AgentSession;
expect(describeModel(fakeSession)).toBe("openai/gpt-4o");
});
});

View File

@@ -22,6 +22,17 @@ export interface AgentResult {
session: AgentSession;
}
/**
* Extract a human-readable model description from an AgentSession.
* Returns `"<provider>/<modelId>"` (e.g. `"anthropic/claude-sonnet-4-5"`)
* or `"unknown model"` when the session has no model set.
*/
export function describeModel(session: AgentSession): string {
const model = session.model;
if (!model) return "unknown model";
return `${model.provider}/${model.id}`;
}
export interface AgentOptions {
cwd: string;
systemPrompt: string;

View File

@@ -16,6 +16,7 @@ import { AgentSemaphore } from "./concurrency.js";
vi.mock("./pi.js", () => ({
createKbAgent: vi.fn(),
describeModel: vi.fn().mockReturnValue("mock-provider/mock-model"),
}));
vi.mock("./reviewer.js", () => ({
reviewStep: vi.fn(),

View File

@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("./pi.js", () => ({
createKbAgent: vi.fn(),
describeModel: vi.fn().mockReturnValue("mock-provider/mock-model"),
}));
import { reviewStep } from "./reviewer.js";

View File

@@ -9,8 +9,9 @@
*/
import type { TaskStore } from "@fusion/core";
import { createKbAgent } from "./pi.js";
import { createKbAgent, describeModel } from "./pi.js";
import { AgentLogger } from "./agent-logger.js";
import { reviewerLog } from "./logger.js";
import { checkSessionError } from "./usage-limit-detector.js";
const REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
@@ -209,6 +210,11 @@ export async function reviewStep(
defaultThinkingLevel: options.defaultThinkingLevel,
});
reviewerLog.log(`${taskId}: reviewer using model ${describeModel(session)}`);
if (options.store && options.taskId) {
await options.store.logEntry(options.taskId, `Reviewer using model: ${describeModel(session)}`);
}
let reviewText = "";
// Capture the reviewer's full text output (still needed for verdict extraction)

View File

@@ -11,7 +11,7 @@ import type {
ToolDefinition,
AgentSession,
} from "@mariozechner/pi-coding-agent";
import { createKbAgent } from "./pi.js";
import { createKbAgent, describeModel } from "./pi.js";
import { reviewStep, type ReviewVerdict } from "./reviewer.js";
import { PRIORITY_SPECIFY, type AgentSemaphore } from "./concurrency.js";
import { AgentLogger } from "./agent-logger.js";
@@ -477,6 +477,9 @@ export class TriageProcessor {
defaultThinkingLevel: settings.defaultThinkingLevel,
});
triageLog.log(`${task.id}: using model ${describeModel(session)}`);
await this.store.logEntry(task.id, `Triage using model: ${describeModel(session)}`);
// Make session available to review_spec tool (for RETHINK rewind)
sessionRef.current = session;