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:
7
.changeset/fn-7370-thinking-effort-model-logs.md
Normal file
7
.changeset/fn-7370-thinking-effort-model-logs.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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;
|
||||
|
||||
62
packages/engine/src/__tests__/executor-model-marker.test.ts
Normal file
62
packages/engine/src/__tests__/executor-model-marker.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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)",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user