FN-7370: show thinking effort in model log markers

Display configured thinking effort alongside runtime task-log model markers.

- Add a shared formatter for model marker annotations and use it for triage, executor, reviewer, and workflow-step logs.
- Teach dashboard model parsing to ignore parenthesized marker diagnostics while preserving provider/model resolution.
- Cover thinking-effort markers with engine and dashboard tests, docs, and a patch changeset.

Files changed:
 .changeset/fn-7370-thinking-effort-model-logs.md   |  7 +++
 docs/agents.md                                     |  2 +
 packages/dashboard/app/components/TaskChatTab.tsx  |  7 +--
 .../app/components/__tests__/TaskChatTab.test.tsx  |  6 +--
 ...skDetailModal.models-progress-workflow.test.tsx |  6 +--
 .../__tests__/WorkflowResultsTab.test.tsx          |  6 +--
 .../__tests__/effective-model-resolution.test.ts   | 17 ++++--
 .../app/components/effective-model-resolution.ts   | 23 +++++---
 .../src/__tests__/executor-model-marker.test.ts    | 62 ++++++++++++++++++++++
 .../engine/src/__tests__/executor-test-helpers.ts  |  4 ++
 .../__tests__/executor-workflow-step-model.test.ts | 23 +++++++-
 packages/engine/src/__tests__/pi.test.ts           | 12 ++++-
 packages/engine/src/__tests__/reviewer.test.ts     | 37 +++++++++++++
 packages/engine/src/__tests__/triage.test.ts       | 20 ++++++-
 packages/engine/src/executor.ts                    | 17 ++++--
 packages/engine/src/pi.ts                          | 19 +++++++
 packages/engine/src/reviewer.ts                    |  7 +--
 packages/engine/src/triage.ts                      |  4 +-
 18 files changed, 244 insertions(+), 35 deletions(-)

Fusion-Task-Id: FN-7370

Fusion-Task-Lineage: 2c94a20c-77e1-41db-8af0-76239b30e3c4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-01 09:30:00 -07:00
parent 4805182d0c
commit 67f93cedd9
18 changed files with 244 additions and 35 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Show thinking effort on task log model rows when it is configured.
category: fix
dev: Runtime using-model markers append `(thinking effort: <level>)` while dashboard parsers strip suffix annotations for provider icons and effective-model displays.

View File

@@ -374,6 +374,8 @@ The Task Detail Activity → Raw Logs model header prefers runtime provenance ma
- `Reviewer using model: <provider>/<modelId>`
- `Triage using model: <provider>/<modelId>`
When the lane resolves a thinking level, the same row appends ` (thinking effort: <level>)`, for example `Executor using model: openai/gpt-4o (thinking effort: high)`. Dashboard parsers ignore parenthesized diagnostics for provider icons/effective-model headers while Raw Logs and Activity rows keep the full text visible.
This makes the header reflect the model that actually ran. For active runs with no runtime marker yet, the UI can use the currently assigned agent runtime model as a temporary fallback before falling back to task/settings resolution.
### Ephemeral agent terminal cleanup

View File

@@ -14,6 +14,7 @@ import { formatRelativeTimeAgo } from "../utils/relativeTimeAgo";
import { ProviderIcon } from "./ProviderIcon";
import { clampChatInputHeight, resolveChatInputOverflowY } from "../utils/chatInputAutosize";
import { markdownComponents } from "./AgentLogViewer";
import { parseRuntimeModelMarker } from "./effective-model-resolution";
import "./TaskChatTab.css";
interface TaskChatTabProps {
@@ -74,9 +75,9 @@ function getRoleLabel(role: AgentLogRole, t: TFunction<"app">): string {
function parseModelMarker(entry: AgentLogEntry): TaskChatModelInfo | null {
if (entry.type !== "text") return null;
const match = entry.text.match(/^(?:Triage|Executor|Reviewer) using model: (.+?)\/(.+)$/);
if (!match) return null;
return { provider: match[1], modelId: match[2] };
const role = entry.agent === "triage" ? "Triage" : entry.agent === "executor" ? "Executor" : entry.agent === "reviewer" ? "Reviewer" : null;
if (!role) return null;
return parseRuntimeModelMarker(entry.text, role);
}
function makeModelInfo(provider: string | undefined, modelId: string | undefined): TaskChatModelInfo | null {

View File

@@ -508,10 +508,10 @@ describe("TaskChatTab", () => {
expect(document.querySelectorAll(".task-chat-provider-icon [data-provider='anthropic']")).toHaveLength(2);
});
it("renders provider icons for task chat roles from runtime model markers", () => {
it("renders provider icons for task chat roles from legacy and suffixed runtime model markers", () => {
mockLogs([
makeEntry({ agent: "triage", text: "Triage using model: google/gemini-pro" }),
makeEntry({ agent: "executor", text: "Executor using model: openai/gpt-4o" }),
makeEntry({ agent: "triage", text: "Triage using model: google/gemini-pro (thinking effort: low)" }),
makeEntry({ agent: "executor", text: "Executor using model: openai/gpt-4o (thinking effort: high)" }),
makeEntry({ agent: "reviewer", text: "Reviewer using model: anthropic/claude-sonnet-4-5" }),
]);

View File

@@ -345,7 +345,7 @@ describe("TaskDetailModal", () => {
vi.mocked(useAgentLogs).mockReturnValue({
entries: [
{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const },
{ timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Triage using model: google/gemini-pro", type: "text" as const, agent: "triage" },
{ timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Triage using model: google/gemini-pro (thinking effort: high)", type: "text" as const, agent: "triage" },
],
loading: false,
clear: vi.fn(),
@@ -565,8 +565,8 @@ describe("TaskDetailModal", () => {
vi.mocked(useAgentLogs).mockReturnValue({
entries: [
{ timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Executor using model: openai/gpt-4o", type: "text" as const, agent: "executor" },
{ timestamp: "2026-01-01T00:00:02Z", taskId: "FN-099", text: "Reviewer using model: google/gemini-2.5-pro", type: "text" as const, agent: "reviewer" },
{ timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Executor using model: openai/gpt-4o (thinking effort: high)", type: "text" as const, agent: "executor" },
{ timestamp: "2026-01-01T00:00:02Z", taskId: "FN-099", text: "Reviewer using model: google/gemini-2.5-pro (thinking effort: medium)", type: "text" as const, agent: "reviewer" },
],
loading: false,
clear: vi.fn(),

View File

@@ -533,21 +533,21 @@ describe("WorkflowResultsTab", () => {
taskId: "FN-001",
agent: "executor",
type: "text",
text: "Executor using model: runtime-executor/runtime-executor-model",
text: "Executor using model: runtime-executor/runtime-executor-model (thinking effort: high)",
},
{
timestamp: "2026-06-25T00:00:01Z",
taskId: "FN-001",
agent: "reviewer",
type: "text",
text: "Reviewer using model: runtime-reviewer/runtime-reviewer-model",
text: "Reviewer using model: runtime-reviewer/runtime-reviewer-model (thinking effort: medium)",
},
{
timestamp: "2026-06-25T00:00:02Z",
taskId: "FN-001",
agent: "triage",
type: "text",
text: "Triage using model: runtime-planning/runtime-planning-model",
text: "Triage using model: runtime-planning/runtime-planning-model (thinking effort: low)",
},
];
const assignedAgent = {

View File

@@ -5,6 +5,7 @@ import {
extractExecutorModelFromLog,
extractPlanningModelFromLog,
extractReviewerModelFromLog,
parseRuntimeModelMarker,
resolveEffectiveExecutor,
resolveEffectivePlanning,
resolveEffectiveValidator,
@@ -63,12 +64,12 @@ function runtimeAgent(runtimeConfig?: Record<string, unknown>): Agent {
}
describe("effective model resolution", () => {
it("extracts the latest role-specific model marker from agent logs", () => {
it("extracts the latest role-specific model marker from legacy and suffixed agent logs", () => {
const entries = [
log("executor", "Executor using model: old-provider/old-model"),
log("reviewer", "Reviewer using model: reviewer-provider/reviewer-model"),
log("triage", "Triage using model: triage-provider/triage-model"),
log("executor", "Executor using model: new-provider/new-model"),
log("reviewer", "Reviewer using model: reviewer-provider/reviewer-model (thinking effort: high)"),
log("triage", "Triage using model: triage-provider/triage-model (thinking effort: low)"),
log("executor", "Executor using model: new-provider/new-model (thinking effort: high)"),
];
expect(extractExecutorModelFromLog(entries)).toEqual({ provider: "new-provider", modelId: "new-model" });
@@ -76,6 +77,14 @@ describe("effective model resolution", () => {
expect(extractPlanningModelFromLog(entries)).toEqual({ provider: "triage-provider", modelId: "triage-model" });
});
it("parses runtime model markers for all roles while ignoring parenthesized diagnostics", () => {
expect(parseRuntimeModelMarker("Triage using model: google/gemini-pro", "Triage")).toEqual({ provider: "google", modelId: "gemini-pro" });
expect(parseRuntimeModelMarker("Executor using model: openai/gpt-4o (thinking effort: high)", "Executor")).toEqual({ provider: "openai", modelId: "gpt-4o" });
expect(parseRuntimeModelMarker("Reviewer using model: anthropic/claude-sonnet-4-5 (thinking effort: high) (fallback after timeout)", "Reviewer")).toEqual({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
expect(parseRuntimeModelMarker("Executor using model: openai/gpt-4o (thinking effort: high)", "Reviewer")).toBeNull();
expect(parseRuntimeModelMarker("Executor using model: unknown model", "Executor")).toBeNull();
});
it("parses assigned-agent runtime models from combined or split fields", () => {
expect(extractAssignedRuntimeModel(runtimeAgent({ model: "runtime-provider/runtime-model" }))).toEqual({ provider: "runtime-provider", modelId: "runtime-model" });
expect(extractAssignedRuntimeModel(runtimeAgent({ modelProvider: "split-provider", modelId: "split-model" }))).toEqual({ provider: "split-provider", modelId: "split-model" });

View File

@@ -14,14 +14,25 @@ function isStringValue(value: unknown): value is string {
/*
FNXC:ModelResolution 2026-06-25-00:00:
FN-7040 requires the Chat tab, Agent Log header, and Workflow tab Model settings to share one effective model resolver so runtime log markers, active assigned-agent runtime models, task overrides, and settings fallbacks never diverge between task-detail surfaces.
FNXC:TaskLogModelThinking 2026-07-01-00:00:
Runtime "using model" markers may append parenthesized diagnostics such as thinking effort, workflow-step overrides, or fallback reasons. Dashboard model resolution strips those suffix annotations while preserving legacy exact markers so provider icons and effective-model headers continue to resolve from the same row operators read in Activity and Raw Logs.
*/
const MODEL_MARKER_PATTERN = /^(Triage|Executor|Reviewer) using model: ([^/\s]+)\/(.+?)(?:\s+\([^)]*\))*$/;
export function parseRuntimeModelMarker(text: string, role: "Triage" | "Executor" | "Reviewer"): { provider: string; modelId: string } | null {
const match = text.match(MODEL_MARKER_PATTERN);
if (!match || match[1] !== role) return null;
return { provider: match[2], modelId: match[3] };
}
export function extractExecutorModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null;
entries.forEach((entry) => {
if (entry.agent !== "executor" || entry.type !== "text") return;
const match = entry.text.match(/^Executor using model: (.+?)\/(.+)$/);
const match = parseRuntimeModelMarker(entry.text, "Executor");
if (match) {
result = { provider: match[1], modelId: match[2] };
result = match;
}
});
return result;
@@ -31,9 +42,9 @@ export function extractReviewerModelFromLog(entries: AgentLogEntry[]): { provide
let result: { provider: string; modelId: string } | null = null;
entries.forEach((entry) => {
if (entry.agent !== "reviewer" || entry.type !== "text") return;
const match = entry.text.match(/^Reviewer using model: (.+?)\/(.+)$/);
const match = parseRuntimeModelMarker(entry.text, "Reviewer");
if (match) {
result = { provider: match[1], modelId: match[2] };
result = match;
}
});
return result;
@@ -119,9 +130,9 @@ export function extractPlanningModelFromLog(entries: AgentLogEntry[]): { provide
let result: { provider: string; modelId: string } | null = null;
entries.forEach((entry) => {
if (entry.agent !== "triage" || entry.type !== "text") return;
const match = entry.text.match(/^Triage using model: (.+?)\/(.+)$/);
const match = parseRuntimeModelMarker(entry.text, "Triage");
if (match) {
result = { provider: match[1], modelId: match[2] };
result = match;
}
});
return result;

View File

@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import { createMockStore, mockedCreateFnAgent, resetExecutorMocks } from "./executor-test-helpers.js";
describe("TaskExecutor model marker logging", () => {
beforeEach(() => {
resetExecutorMocks();
});
it("logs executor model rows with the resolved task thinking effort", async () => {
const store = createMockStore();
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
defaultThinkingLevel: "medium",
});
store.getTask.mockResolvedValue({
id: "FN-7370",
title: "Thinking marker",
description: "Verify executor marker",
column: "in-progress",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
thinkingLevel: "high",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
mockedCreateFnAgent.mockResolvedValue({
session: {
prompt: vi.fn(async () => {
throw new Error("stop after model marker");
}),
dispose: vi.fn(),
},
} as any);
const executor = new TaskExecutor(store as any, "/tmp/test");
await executor.execute(await store.getTask("FN-7370") as any);
expect(store.logEntry).toHaveBeenCalledWith(
"FN-7370",
"Executor using model: mock-provider/mock-model (thinking effort: high)",
undefined,
expect.any(Object),
);
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-7370",
"Executor using model: mock-provider/mock-model (thinking effort: high)",
"text",
undefined,
"executor",
);
});
});

View File

@@ -6,6 +6,10 @@ import { installTaskWorktreeIdentityGuard } from "../worktree-hooks.js";
vi.mock("../pi.js", () => ({
createFnAgent: vi.fn(),
describeModel: vi.fn().mockReturnValue("mock-provider/mock-model"),
formatModelMarkerDetails: vi.fn((model: string, thinking?: string | null, annotations: string[] = []) => {
const suffixes = [thinking ? `thinking effort: ${thinking}` : "", ...annotations].filter(Boolean);
return suffixes.length ? `${model} ${suffixes.map((suffix) => `(${suffix})`).join(" ")}` : model;
}),
compactSessionContext: vi.fn(async (session, instructions) => {
if (typeof (session as any).compact === "function") {
return (session as any).compact(instructions);

View File

@@ -114,7 +114,7 @@ async function runStepWithSettings(
undefined,
);
return captured.last;
return { ...captured.last, logCalls: store.logEntry.mock.calls };
}
describe("executor workflow-step model resolution", () => {
@@ -231,4 +231,25 @@ describe("executor workflow-step model resolution", () => {
defaultModelId: "scripted",
});
});
it("logs workflow-step model rows with thinking effort before override annotations", async () => {
const primary = await runStepWithSettings(
{
defaultThinkingLevel: "high",
executionProvider: "project-exec-provider",
executionModelId: "project-exec-model",
},
{
step: {
modelProvider: "step-provider",
modelId: "step-model",
},
},
);
expect(primary.logCalls).toContainEqual([
"FN-MODEL-1",
"Workflow step 'Model Step' using model: mock-provider/mock-model (thinking effort: high) (workflow step override)",
]);
});
});

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describeModel, compactSessionContext, COMPACTION_FALLBACK_INSTRUCTIONS, createFnAgent, getProjectRootFromWorktree, isModelAuthTierIncompatibilityError, isRetryableModelSelectionError, promptWithFallback, type AgentOptions } from "../pi.js";
import { describeModel, formatModelMarkerDetails, compactSessionContext, COMPACTION_FALLBACK_INSTRUCTIONS, createFnAgent, getProjectRootFromWorktree, isModelAuthTierIncompatibilityError, isRetryableModelSelectionError, promptWithFallback, type AgentOptions } from "../pi.js";
import { createAgentSession, ModelRegistry, type AgentSession } from "@earendil-works/pi-coding-agent";
import { piLog } from "../logger.js";
@@ -147,6 +147,16 @@ describe("describeModel", () => {
});
});
describe("formatModelMarkerDetails", () => {
it("adds thinking effort before workflow annotations and omits empty values", () => {
expect(formatModelMarkerDetails("openai/gpt-4o", "high", ["workflow step override", "fallback after timeout"])).toBe(
"openai/gpt-4o (thinking effort: high) (workflow step override) (fallback after timeout)",
);
expect(formatModelMarkerDetails("openai/gpt-4o", undefined, [""])).toBe("openai/gpt-4o");
expect(formatModelMarkerDetails("openai/gpt-4o", "off")).toBe("openai/gpt-4o (thinking effort: off)");
});
});
describe("COMPACTION_FALLBACK_INSTRUCTIONS", () => {
it("is a non-empty string", () => {
expect(COMPACTION_FALLBACK_INSTRUCTIONS).toBeTruthy();

View File

@@ -3,6 +3,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("../pi.js", () => ({
createFnAgent: vi.fn(),
describeModel: vi.fn().mockReturnValue("mock-provider/mock-model"),
formatModelMarkerDetails: vi.fn((model: string, thinking?: string | null, annotations: string[] = []) => {
const suffixes = [thinking ? `thinking effort: ${thinking}` : "", ...annotations].filter(Boolean);
return suffixes.length ? `${model} ${suffixes.map((suffix) => `(${suffix})`).join(" ")}` : model;
}),
promptWithFallback: vi.fn(async (session, prompt, options) => {
if (typeof session.prompt === "function") {
if (options === undefined) {
@@ -205,6 +209,39 @@ describe("reviewStep — model settings threading", () => {
expect(result.verdict).toBe("APPROVE");
});
it("logs reviewer model rows with default thinking effort", async () => {
mockedCreateFnAgent.mockResolvedValue(
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
);
const store = {
getSettings: vi.fn().mockResolvedValue({}),
logEntry: vi.fn().mockResolvedValue(undefined),
appendAgentLog: vi.fn().mockResolvedValue(undefined),
};
await reviewStep(
"/tmp/worktree", "FN-100", 1, "Test Step", "plan", "# prompt",
undefined,
{
store: store as any,
taskId: "FN-100",
defaultThinkingLevel: "high",
},
);
expect(store.logEntry).toHaveBeenCalledWith(
"FN-100",
"Reviewer using model: mock-provider/mock-model (thinking effort: high)",
);
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-100",
"Reviewer using model: mock-provider/mock-model (thinking effort: high)",
"text",
undefined,
"reviewer",
);
});
it("extracts APPROVE verdict correctly", async () => {
mockedCreateFnAgent.mockResolvedValue(
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),

View File

@@ -32,6 +32,10 @@ vi.mock("../reviewer.js", () => ({
vi.mock("../pi.js", () => ({
createFnAgent: mockCreateFnAgent,
describeModel: vi.fn().mockReturnValue("mock-model"),
formatModelMarkerDetails: vi.fn((model: string, thinking?: string | null, annotations: string[] = []) => {
const suffixes = [thinking ? `thinking effort: ${thinking}` : "", ...annotations].filter(Boolean);
return suffixes.length ? `${model} ${suffixes.map((suffix) => `(${suffix})`).join(" ")}` : model;
}),
promptWithFallback: vi.fn().mockReturnValue("mock-prompt"),
}));
@@ -3638,6 +3642,14 @@ describe("taskCreate tool model inheritance", () => {
const store = createMockStore({
getTask: vi.fn().mockResolvedValue({ ...task, attachments: [] }),
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 10000,
groupOverlappingFiles: false,
autoMerge: true,
defaultThinkingLevel: "high",
} as Settings),
});
// Set up createFnAgent to return a session that immediately throws
@@ -3667,10 +3679,14 @@ describe("taskCreate tool model inheritance", () => {
await processor.specifyTask(task);
// Verify appendAgentLog was called with model info and triage role
// Verify appendAgentLog was called with model and thinking effort info on the same triage row.
expect(store.logEntry).toHaveBeenCalledWith(
"FN-300",
"Triage using model: mock-model (thinking effort: high)",
);
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-300",
"Triage using model: mock-model",
"Triage using model: mock-model (thinking effort: high)",
"text",
undefined,
"triage",

View File

@@ -76,7 +76,7 @@ import {
import { canonicalFusionBranchName, canonicalStepInstanceBranchName, generateWorktreeName, resolveTaskWorkingBranch } from "./worktree-names.js";
import { resolveTaskWorktreePath, resolveWorktreesDir } from "./worktree-paths.js";
import { Type, type Static } from "@earendil-works/pi-ai";
import { describeModel, promptWithFallback, compactSessionContext } from "./pi.js";
import { describeModel, formatModelMarkerDetails, promptWithFallback, compactSessionContext } from "./pi.js";
import { accumulateSessionTokenUsage, mergeTokenUsagePerModel } from "./session-token-usage.js";
import {
createResolvedAgentSession,
@@ -10128,7 +10128,8 @@ export class TaskExecutor {
}
const executorModelDesc = describeModel(session);
const executorModelMarker = `Executor using model: ${executorModelDesc}`;
const executorModelDetails = formatModelMarkerDetails(executorModelDesc, executorThinkingLevel);
const executorModelMarker = `Executor using model: ${executorModelDetails}`;
if (isResuming) {
executorLog.log(`${task.id}: resumed session from ${task.sessionFile}`);
await this.store.logEntry(task.id, `Resumed agent session after unpause (model: ${executorModelDesc})`, undefined, this.getRunContextFor(task.id));
@@ -14516,10 +14517,18 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
...(readonlyCustomTools.allowed.length > 0 ? { customTools: readonlyCustomTools.allowed } : {}),
});
executorLog.log(`${task.id}: workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`);
const workflowModelDetails = formatModelMarkerDetails(
describeModel(session),
settings.defaultThinkingLevel,
[
useOverride && attemptLabel === "primary" ? "workflow step override" : "",
attemptLabel === "fallback" ? "fallback after timeout" : "",
],
);
executorLog.log(`${task.id}: workflow step '${workflowStep.name}' using model ${workflowModelDetails}`);
await this.store.logEntry(
task.id,
`Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`,
`Workflow step '${workflowStep.name}' using model: ${workflowModelDetails}`,
);
this.setActiveWorkflowStepSession(task.id, session, worktreePath, this.createSeenSteeringIds(task));

View File

@@ -517,6 +517,25 @@ export function describeModel(session: AgentSession): string {
return `${model.provider}/${model.id}`;
}
/**
* FNXC:TaskLogModelThinking 2026-07-01-00:00:
* Task-log model markers must keep the historical provider/model prefix stable while appending resolved thinking effort on the same row for operator diagnostics. Extra annotations stay parenthesized after the thinking-effort annotation so dashboard parsers can strip all suffix metadata deterministically.
*/
export function formatModelMarkerDetails(
modelDescription: string,
thinkingLevel?: string | null,
annotations: string[] = [],
): string {
const suffixes: string[] = [];
const normalizedThinkingLevel = typeof thinkingLevel === "string" ? thinkingLevel.trim() : "";
if (normalizedThinkingLevel) {
suffixes.push(`thinking effort: ${normalizedThinkingLevel}`);
}
suffixes.push(...annotations.map((annotation) => annotation.trim()).filter(Boolean));
if (suffixes.length === 0) return modelDescription;
return `${modelDescription} ${suffixes.map((suffix) => `(${suffix})`).join(" ")}`;
}
/**
* Default instructions used when calling `session.compact()` for loop recovery.
* These guide the compaction summary to preserve essential context while

View File

@@ -19,7 +19,7 @@ import {
} from "@fusion/core";
import { recordRetry } from "./retry-burned-logger.js";
import { mergeEffectiveSettings } from "./effective-settings.js";
import { describeModel, promptWithFallback } from "./pi.js";
import { describeModel, formatModelMarkerDetails, promptWithFallback } from "./pi.js";
import { isContextLimitError } from "./context-limit-detector.js";
import { createResolvedAgentSession, extractRuntimeHint, resolveValidatorSessionModel } from "./agent-session-helpers.js";
import { buildSessionSkillContext } from "./session-skill-context.js";
@@ -446,8 +446,9 @@ export async function reviewStep(
});
const reviewerModelDesc = describeModel(session);
const reviewerModelMarker = `Reviewer using model: ${reviewerModelDesc}`;
reviewerLog.log(`${taskId}: reviewer using model ${reviewerModelDesc}`);
const reviewerModelDetails = formatModelMarkerDetails(reviewerModelDesc, options.defaultThinkingLevel);
const reviewerModelMarker = `Reviewer using model: ${reviewerModelDetails}`;
reviewerLog.log(`${taskId}: reviewer using model ${reviewerModelDetails}`);
if (options.store && options.taskId) {
await options.store.logEntry(options.taskId, reviewerModelMarker);
await options.store.appendAgentLog(options.taskId, reviewerModelMarker, "text", undefined, "reviewer").catch(() => undefined);

View File

@@ -80,7 +80,7 @@ import type {
ToolDefinition,
AgentSession,
} from "@earendil-works/pi-coding-agent";
import { describeModel, promptWithFallback } from "./pi.js";
import { describeModel, formatModelMarkerDetails, promptWithFallback } from "./pi.js";
import {
createResolvedAgentSession,
extractRuntimeHint,
@@ -1123,7 +1123,7 @@ export class TriageProcessor {
}),
});
const modelDesc = describeModel(session);
const modelDesc = formatModelMarkerDetails(describeModel(session), settings.defaultThinkingLevel);
planLog.log(`${task.id}: using model ${modelDesc}`);
await this.store.logEntry(task.id, `Triage using model: ${modelDesc}`);
await this.store.appendAgentLog(