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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-22 00:30:34 -07:00
parent fee920d366
commit 0ade15481d
10 changed files with 52 additions and 27 deletions

View File

@@ -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.

View File

@@ -404,7 +404,7 @@ The Task Detail Activity → Raw Logs model header prefers runtime provenance ma
- `Executor using model: <provider>/<modelId>` - `Executor using model: <provider>/<modelId>`
- `Reviewer using model: <provider>/<modelId>` - `Reviewer using model: <provider>/<modelId>`
- `Triage using model: <provider>/<modelId>` - `Planning using model: <provider>/<modelId>` (legacy `Triage using model: <provider>/<modelId>` rows remain parseable)
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. 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.

View File

@@ -76,7 +76,7 @@ function getRoleLabel(role: AgentLogRole, t: TFunction<"app">): string {
function parseModelMarker(entry: AgentLogEntry): TaskChatModelInfo | null { function parseModelMarker(entry: AgentLogEntry): TaskChatModelInfo | null {
if (entry.type !== "status" && entry.type !== "text") return 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; if (!role) return null;
return parseRuntimeModelMarker(entry.text, role); return parseRuntimeModelMarker(entry.text, role);
} }

View File

@@ -555,7 +555,7 @@ describe("TaskChatTab", () => {
it("uses status and text runtime markers before static overrides for reviewer, executor, and planner icons", () => { it("uses status and text runtime markers before static overrides for reviewer, executor, and planner icons", () => {
mockLogs([ 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: "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" }), makeEntry({ agent: "reviewer", type: "status", text: "Reviewer using model: openai-codex/gpt-5.6-terra" }),
]); ]);

View File

@@ -345,7 +345,7 @@ describe("TaskDetailModal", () => {
vi.mocked(useAgentLogs).mockReturnValue({ vi.mocked(useAgentLogs).mockReturnValue({
entries: [ entries: [
{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const }, { 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, loading: false,
clear: vi.fn(), clear: vi.fn(),
@@ -519,7 +519,7 @@ describe("TaskDetailModal", () => {
vi.mocked(useAgentLogs).mockReturnValue({ vi.mocked(useAgentLogs).mockReturnValue({
entries: [ entries: [
{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const }, { 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, loading: false,
clear: vi.fn(), clear: vi.fn(),

View File

@@ -581,7 +581,7 @@ describe("WorkflowResultsTab", () => {
taskId: "FN-001", taskId: "FN-001",
agent: "triage", agent: "triage",
type: "text", 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 = { const assignedAgent = {

View File

@@ -64,17 +64,18 @@ function runtimeAgent(runtimeConfig?: Record<string, unknown>): Agent {
} }
describe("effective model resolution", () => { 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 = [ const entries = [
log("executor", "Executor using model: old-provider/old-model"), log("executor", "Executor using model: old-provider/old-model"),
log("reviewer", "Reviewer using model: reviewer-provider/reviewer-model (thinking effort: high)"), 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)"), log("executor", "Executor using model: new-provider/new-model (thinking effort: high)"),
]; ];
expect(extractExecutorModelFromLog(entries)).toEqual({ provider: "new-provider", modelId: "new-model" }); expect(extractExecutorModelFromLog(entries)).toEqual({ provider: "new-provider", modelId: "new-model" });
expect(extractReviewerModelFromLog(entries)).toEqual({ provider: "reviewer-provider", modelId: "reviewer-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 = [ const statusEntries = [
{ ...log("executor", "Executor using model: status-provider/status-model"), type: "status" as const }, { ...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("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(extractExecutorModelFromLog(statusEntries)).toEqual({ provider: "status-provider", modelId: "status-model" });
expect(extractReviewerModelFromLog(statusEntries)).toEqual({ provider: "status-reviewer", modelId: "status-reviewer-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 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(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. // 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 }]; 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", () => { 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("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("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: 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(); 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", () => { 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; 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(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", "Triage 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, [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" }); expect(resolveEffectivePlanning({ ...baseTask, planningModelProvider: null, planningModelId: null } as Task, [], settings)).toEqual({ provider: "settings-planning", modelId: "settings-planning-model" });
}); });
}); });

View File

@@ -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: 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. 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: FNXC:TaskLogModelThinking 2026-07-15-11:20:
@@ -28,9 +31,13 @@ function isEngineMarkerEntryType(type: AgentLogEntry["type"]): boolean {
return type === "status" || type === "text"; 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); 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] }; return { provider: match[2], modelId: match[3] };
} }
@@ -130,15 +137,16 @@ export function resolveEffectiveValidator(
/** /**
* Extract planning model from agent log entries. * Extract planning model from agent log entries.
* Looks for text entries with agent role "triage" matching the pattern: * Looks for status or text entries with agent role "triage" matching either pattern:
* "Triage using model: <provider>/<modelId>" * "Planning using model: <provider>/<modelId>"
* "Triage using model: <provider>/<modelId>" (legacy)
* Returns the latest match, or null if none found. * Returns the latest match, or null if none found.
*/ */
export function extractPlanningModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null { export function extractPlanningModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null; let result: { provider: string; modelId: string } | null = null;
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.agent !== "triage" || !isEngineMarkerEntryType(entry.type)) return; if (entry.agent !== "triage" || !isEngineMarkerEntryType(entry.type)) return;
const match = parseRuntimeModelMarker(entry.text, "Triage"); const match = parseRuntimeModelMarker(entry.text, "Planning");
if (match) { if (match) {
result = match; result = match;
} }

View File

@@ -4266,11 +4266,11 @@ describe("taskCreate tool model inheritance", () => {
expect(store.logEntry).toHaveBeenCalledWith( expect(store.logEntry).toHaveBeenCalledWith(
"FN-7437", "FN-7437",
"Triage using model: mock-model (thinking effort: low)", "Planning using model: mock-model (thinking effort: low)",
); );
expect(store.appendAgentLog).toHaveBeenCalledWith( expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-7437", "FN-7437",
"Triage using model: mock-model (thinking effort: low)", "Planning using model: mock-model (thinking effort: low)",
"status", "status",
undefined, undefined,
"triage", "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. // Verify appendAgentLog was called with model and thinking effort info on the same triage row.
expect(store.logEntry).toHaveBeenCalledWith( expect(store.logEntry).toHaveBeenCalledWith(
"FN-300", "FN-300",
"Triage using model: mock-model (thinking effort: high)", "Planning using model: mock-model (thinking effort: high)",
); );
expect(store.appendAgentLog).toHaveBeenCalledWith( expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-300", "FN-300",
"Triage using model: mock-model (thinking effort: high)", "Planning using model: mock-model (thinking effort: high)",
"status", "status",
undefined, undefined,
"triage", "triage",

View File

@@ -1674,11 +1674,15 @@ export class TriageProcessor {
}); });
const modelDesc = formatModelMarkerDetails(describeModel(session), resolvePlanningThinkingLevel(settings, task.planningThinkingLevel ?? task.thinkingLevel)); 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}`); 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( await this.store.appendAgentLog(
task.id, task.id,
`Triage using model: ${modelDesc}`, `Planning using model: ${modelDesc}`,
"status", "status",
undefined, undefined,
"triage", "triage",