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:
7
.changeset/fn-8477-planning-model-marker.md
Normal file
7
.changeset/fn-8477-planning-model-marker.md
Normal 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.
|
||||
@@ -404,7 +404,7 @@ The Task Detail Activity → Raw Logs model header prefers runtime provenance ma
|
||||
|
||||
- `Executor 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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" }),
|
||||
]);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -64,17 +64,18 @@ function runtimeAgent(runtimeConfig?: Record<string, unknown>): 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" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: <provider>/<modelId>"
|
||||
* Looks for status or text entries with agent role "triage" matching either pattern:
|
||||
* "Planning using model: <provider>/<modelId>"
|
||||
* "Triage using model: <provider>/<modelId>" (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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user