From 0ade15481dbc4b946a742ed084d60e2be1d914ee Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 22 Jul 2026 00:30:34 -0700 Subject: [PATCH] FN-8477: label planning model activity logs Planning activity consistently identifies its model provenance while preserving legacy log compatibility. - Emit Planning using model markers from the planning lane. - Accept Planning and legacy Triage markers in dashboard model resolution. - Update documentation, regression coverage, and release metadata. Files changed: .changeset/fn-8477-planning-model-marker.md | 7 +++++++ docs/agents.md | 2 +- packages/dashboard/app/components/TaskChatTab.tsx | 2 +- .../app/components/__tests__/TaskChatTab.test.tsx | 2 +- ...skDetailModal.models-progress-workflow.test.tsx | 4 ++-- .../__tests__/WorkflowResultsTab.test.tsx | 2 +- .../__tests__/effective-model-resolution.test.ts | 24 ++++++++++++++-------- .../app/components/effective-model-resolution.ts | 20 ++++++++++++------ packages/engine/src/__tests__/triage.test.ts | 8 ++++---- packages/engine/src/triage.ts | 8 ++++++-- 10 files changed, 52 insertions(+), 27 deletions(-) Fusion-Task-Id: FN-8477 Fusion-Task-Lineage: 7ddce64a-d378-4d08-ad63-ade1b20d4b87 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8477-planning-model-marker.md | 7 ++++++ docs/agents.md | 2 +- .../dashboard/app/components/TaskChatTab.tsx | 2 +- .../components/__tests__/TaskChatTab.test.tsx | 2 +- ...ailModal.models-progress-workflow.test.tsx | 4 ++-- .../__tests__/WorkflowResultsTab.test.tsx | 2 +- .../effective-model-resolution.test.ts | 24 ++++++++++++------- .../components/effective-model-resolution.ts | 20 +++++++++++----- packages/engine/src/__tests__/triage.test.ts | 8 +++---- packages/engine/src/triage.ts | 8 +++++-- 10 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 .changeset/fn-8477-planning-model-marker.md diff --git a/.changeset/fn-8477-planning-model-marker.md b/.changeset/fn-8477-planning-model-marker.md new file mode 100644 index 0000000000..e5990ccd09 --- /dev/null +++ b/.changeset/fn-8477-planning-model-marker.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show Planning (not Triage) in task activity model-using logs. +category: fix +dev: Engine planning lane emits Planning using model:; dashboard parsers dual-accept legacy Triage using model: rows. diff --git a/docs/agents.md b/docs/agents.md index 81a2cc3a8a..8be40b905e 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -404,7 +404,7 @@ The Task Detail Activity → Raw Logs model header prefers runtime provenance ma - `Executor using model: /` - `Reviewer using model: /` -- `Triage using model: /` +- `Planning using model: /` (legacy `Triage using model: /` rows remain parseable) When the lane resolves a thinking level, the same row appends ` (thinking effort: )`, 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. diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx index 1b45b610cc..3f569c5b19 100644 --- a/packages/dashboard/app/components/TaskChatTab.tsx +++ b/packages/dashboard/app/components/TaskChatTab.tsx @@ -76,7 +76,7 @@ function getRoleLabel(role: AgentLogRole, t: TFunction<"app">): string { function parseModelMarker(entry: AgentLogEntry): TaskChatModelInfo | null { if (entry.type !== "status" && entry.type !== "text") return null; - const role = entry.agent === "triage" ? "Triage" : entry.agent === "executor" ? "Executor" : entry.agent === "reviewer" ? "Reviewer" : null; + const role = entry.agent === "triage" ? "Planning" : entry.agent === "executor" ? "Executor" : entry.agent === "reviewer" ? "Reviewer" : null; if (!role) return null; return parseRuntimeModelMarker(entry.text, role); } diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx index 9bbb94bc24..dd6fb633d6 100644 --- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx @@ -555,7 +555,7 @@ describe("TaskChatTab", () => { it("uses status and text runtime markers before static overrides for reviewer, executor, and planner icons", () => { mockLogs([ - makeEntry({ agent: "triage", type: "text", text: "Triage using model: openai/gpt-4o" }), + makeEntry({ agent: "triage", type: "text", text: "Planning using model: openai/gpt-4o" }), makeEntry({ agent: "executor", type: "status", text: "Executor using model: openai/gpt-4o" }), makeEntry({ agent: "reviewer", type: "status", text: "Reviewer using model: openai-codex/gpt-5.6-terra" }), ]); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx index 73c9cb97d0..14537d0478 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx @@ -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 (thinking effort: high)", type: "text" as const, agent: "triage" }, + { timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Planning using model: google/gemini-pro (thinking effort: high)", type: "text" as const, agent: "triage" }, ], loading: false, clear: vi.fn(), @@ -519,7 +519,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: "Planning using model: google/gemini-pro", type: "text" as const, agent: "triage" }, ], loading: false, clear: vi.fn(), diff --git a/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx index a2a8f5fe29..216051ad71 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx @@ -581,7 +581,7 @@ describe("WorkflowResultsTab", () => { taskId: "FN-001", agent: "triage", type: "text", - text: "Triage using model: runtime-planning/runtime-planning-model (thinking effort: low)", + text: "Planning using model: runtime-planning/runtime-planning-model (thinking effort: low)", }, ]; const assignedAgent = { diff --git a/packages/dashboard/app/components/__tests__/effective-model-resolution.test.ts b/packages/dashboard/app/components/__tests__/effective-model-resolution.test.ts index 161b317064..4d5eb56e9c 100644 --- a/packages/dashboard/app/components/__tests__/effective-model-resolution.test.ts +++ b/packages/dashboard/app/components/__tests__/effective-model-resolution.test.ts @@ -64,17 +64,18 @@ function runtimeAgent(runtimeConfig?: Record): Agent { } describe("effective model resolution", () => { - it("extracts the latest role-specific model marker from legacy and suffixed agent logs", () => { + it("extracts the latest role-specific model marker from Planning and legacy Triage agent logs", () => { const entries = [ log("executor", "Executor using model: old-provider/old-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("triage", "Triage using model: legacy-planning/legacy-planning-model"), + log("triage", "Planning using model: planning-provider/planning-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" }); expect(extractReviewerModelFromLog(entries)).toEqual({ provider: "reviewer-provider", modelId: "reviewer-model" }); - expect(extractPlanningModelFromLog(entries)).toEqual({ provider: "triage-provider", modelId: "triage-model" }); + expect(extractPlanningModelFromLog(entries)).toEqual({ provider: "planning-provider", modelId: "planning-model" }); }); /* @@ -88,15 +89,17 @@ describe("effective model resolution", () => { const statusEntries = [ { ...log("executor", "Executor using model: status-provider/status-model"), type: "status" as const }, { ...log("reviewer", "Reviewer using model: status-reviewer/status-reviewer-model"), type: "status" as const }, - { ...log("triage", "Triage using model: status-triage/status-triage-model"), type: "status" as const }, + { ...log("triage", "Planning using model: status-planning/status-planning-model"), type: "status" as const }, ]; expect(extractExecutorModelFromLog(statusEntries)).toEqual({ provider: "status-provider", modelId: "status-model" }); expect(extractReviewerModelFromLog(statusEntries)).toEqual({ provider: "status-reviewer", modelId: "status-reviewer-model" }); - expect(extractPlanningModelFromLog(statusEntries)).toEqual({ provider: "status-triage", modelId: "status-triage-model" }); + expect(extractPlanningModelFromLog(statusEntries)).toEqual({ provider: "status-planning", modelId: "status-planning-model" }); - // Legacy rows written before the `status` type existed still resolve. + // Legacy text rows, including the former Triage planning prefix, still resolve. const legacyEntries = [log("executor", "Executor using model: legacy-provider/legacy-model")]; + const legacyPlanningEntries = [log("triage", "Triage using model: legacy-planning/legacy-planning-model")]; expect(extractExecutorModelFromLog(legacyEntries)).toEqual({ provider: "legacy-provider", modelId: "legacy-model" }); + expect(extractPlanningModelFromLog(legacyPlanningEntries)).toEqual({ provider: "legacy-planning", modelId: "legacy-planning-model" }); // A tool row is still never a model marker, whatever its text says. const toolEntries = [{ ...log("executor", "Executor using model: tool-provider/tool-model"), type: "tool" as const }]; @@ -104,10 +107,13 @@ describe("effective model resolution", () => { }); 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("Planning using model: google/gemini-pro (thinking effort: low)", "Planning")).toEqual({ provider: "google", modelId: "gemini-pro" }); + expect(parseRuntimeModelMarker("Triage using model: google/gemini-pro", "Planning")).toEqual({ provider: "google", modelId: "gemini-pro" }); + expect(parseRuntimeModelMarker("Planning 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("Planning using model: google/gemini-pro", "Executor")).toBeNull(); expect(parseRuntimeModelMarker("Executor using model: unknown model", "Executor")).toBeNull(); }); @@ -139,8 +145,8 @@ describe("effective model resolution", () => { it("resolves planning from task override before triage log marker and settings fallback", () => { const task = { ...baseTask, planningModelProvider: "task-planning", planningModelId: "task-planning-model" } as Task; - expect(resolveEffectivePlanning(task, [log("triage", "Triage using model: log-planning/log-planning-model")], settings)).toEqual({ provider: "task-planning", modelId: "task-planning-model" }); - expect(resolveEffectivePlanning({ ...baseTask, planningModelProvider: null, planningModelId: null } as Task, [log("triage", "Triage using model: log-planning/log-planning-model")], settings)).toEqual({ provider: "log-planning", modelId: "log-planning-model" }); + expect(resolveEffectivePlanning(task, [log("triage", "Planning using model: log-planning/log-planning-model")], settings)).toEqual({ provider: "task-planning", modelId: "task-planning-model" }); + expect(resolveEffectivePlanning({ ...baseTask, planningModelProvider: null, planningModelId: null } as Task, [log("triage", "Planning using model: log-planning/log-planning-model")], settings)).toEqual({ provider: "log-planning", modelId: "log-planning-model" }); expect(resolveEffectivePlanning({ ...baseTask, planningModelProvider: null, planningModelId: null } as Task, [], settings)).toEqual({ provider: "settings-planning", modelId: "settings-planning-model" }); }); }); diff --git a/packages/dashboard/app/components/effective-model-resolution.ts b/packages/dashboard/app/components/effective-model-resolution.ts index cb2d269e13..a2e4eb8dc3 100644 --- a/packages/dashboard/app/components/effective-model-resolution.ts +++ b/packages/dashboard/app/components/effective-model-resolution.ts @@ -17,8 +17,11 @@ FN-7040 requires the Chat tab, Agent Log header, and Workflow tab Model settings 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. + +FNXC:PlanningModelMarker 2026-07-21-12:00: +New planning sessions identify the operator-facing lane as Planning, while historical rows retain Triage. Treat both prefixes as one planning lane so stored logs continue to resolve provider icons and effective-model headers. */ -const MODEL_MARKER_PATTERN = /^(Triage|Executor|Reviewer) using model: ([^/\s]+)\/(.+?)(?:\s+\([^)]*\))*$/; +const MODEL_MARKER_PATTERN = /^(Planning|Triage|Executor|Reviewer) using model: ([^/\s]+)\/(.+?)(?:\s+\([^)]*\))*$/; /* FNXC:TaskLogModelThinking 2026-07-15-11:20: @@ -28,9 +31,13 @@ function isEngineMarkerEntryType(type: AgentLogEntry["type"]): boolean { return type === "status" || type === "text"; } -export function parseRuntimeModelMarker(text: string, role: "Triage" | "Executor" | "Reviewer"): { provider: string; modelId: string } | null { +export function parseRuntimeModelMarker(text: string, role: "Planning" | "Triage" | "Executor" | "Reviewer"): { provider: string; modelId: string } | null { const match = text.match(MODEL_MARKER_PATTERN); - if (!match || match[1] !== role) return null; + const isPlanningRole = role === "Planning" || role === "Triage"; + const matchesRole = isPlanningRole + ? match?.[1] === "Planning" || match?.[1] === "Triage" + : match?.[1] === role; + if (!match || !matchesRole) return null; return { provider: match[2], modelId: match[3] }; } @@ -130,15 +137,16 @@ export function resolveEffectiveValidator( /** * Extract planning model from agent log entries. - * Looks for text entries with agent role "triage" matching the pattern: - * "Triage using model: /" + * Looks for status or text entries with agent role "triage" matching either pattern: + * "Planning using model: /" + * "Triage using model: /" (legacy) * Returns the latest match, or null if none found. */ export function extractPlanningModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null { let result: { provider: string; modelId: string } | null = null; entries.forEach((entry) => { if (entry.agent !== "triage" || !isEngineMarkerEntryType(entry.type)) return; - const match = parseRuntimeModelMarker(entry.text, "Triage"); + const match = parseRuntimeModelMarker(entry.text, "Planning"); if (match) { result = match; } diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 5ded60c73b..60aeaed0aa 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -4266,11 +4266,11 @@ describe("taskCreate tool model inheritance", () => { expect(store.logEntry).toHaveBeenCalledWith( "FN-7437", - "Triage using model: mock-model (thinking effort: low)", + "Planning using model: mock-model (thinking effort: low)", ); expect(store.appendAgentLog).toHaveBeenCalledWith( "FN-7437", - "Triage using model: mock-model (thinking effort: low)", + "Planning using model: mock-model (thinking effort: low)", "status", undefined, "triage", @@ -5241,11 +5241,11 @@ describe("taskCreate tool model inheritance", () => { // 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)", + "Planning using model: mock-model (thinking effort: high)", ); expect(store.appendAgentLog).toHaveBeenCalledWith( "FN-300", - "Triage using model: mock-model (thinking effort: high)", + "Planning using model: mock-model (thinking effort: high)", "status", undefined, "triage", diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 7b7587928b..f4519c2f49 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -1674,11 +1674,15 @@ export class TriageProcessor { }); const modelDesc = formatModelMarkerDetails(describeModel(session), resolvePlanningThinkingLevel(settings, task.planningThinkingLevel ?? task.thinkingLevel)); + /* + FNXC:PlanningModelMarker 2026-07-21-12:00: + Planning-lane provenance is operator-facing, so its task activity marker uses the board's Planning name while the persisted agent role remains the internal `triage` identifier. + */ planLog.log(`${task.id}: using model ${modelDesc}`); - await this.store.logEntry(task.id, `Triage using model: ${modelDesc}`); + await this.store.logEntry(task.id, `Planning using model: ${modelDesc}`); await this.store.appendAgentLog( task.id, - `Triage using model: ${modelDesc}`, + `Planning using model: ${modelDesc}`, "status", undefined, "triage",