feat(FN-3534): surface task provenance in extension outputs

- Add task provenance fields to extension task list/show outputs for better source visibility
- Expand CLI extension tests to cover provenance rendering in task list and task detail responses
- Add a changeset for @runfusion/fusion documenting the provenance output update
- Harden ensure-test-artifacts script and tests to cover incomplete runtime dist tree scenarios

Fusion-Task-Id: FN-3534
This commit is contained in:
Fusion
2026-05-05 23:05:57 -07:00
committed by gsxdsm
parent 9d13295617
commit 12b4a4a007
3 changed files with 128 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Expose task creator provenance in agent-facing task tools by adding source summaries to `fn_task_show` and concise `[via: …]` labels in `fn_task_list`, including agent-name preference from `sourceMetadata.agentName` with `sourceAgentId` fallback.

View File

@@ -562,6 +562,30 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
expect(result.details.count).toBe(2);
});
it("includes concise provenance in list rows", async () => {
const store = new TaskStore(tmpDir);
await store.init();
await store.createTask({
description: "Created by dashboard",
source: { sourceType: "dashboard_ui" },
});
await store.createTask({
description: "Created by agent",
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-123",
sourceMetadata: { agentName: "Reviewer Bot" },
},
});
const listTool = api.tools.get("fn_task_list")!;
const result = await listTool.execute("call-2", {}, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("FN-001 Created by dashboard [via: Dashboard]");
expect(result.content[0].text).toContain("FN-002 Created by agent [via: Agent (Reviewer Bot)]");
});
it("filters by column", async () => {
const createTool = api.tools.get("fn_task_create")!;
await createTool.execute(
@@ -645,9 +669,37 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
expect(result.content[0].text).toContain("FN-001");
expect(result.content[0].text).toContain("Implement caching layer");
expect(result.content[0].text).toContain("Planning");
expect(result.content[0].text).toContain("Created via: API");
expect(result.details.task).toBeDefined();
expect(result.details.task.id).toBe("FN-001");
});
it("shows agent and dashboard provenance", async () => {
const store = new TaskStore(tmpDir);
await store.init();
await store.createTask({
description: "Agent created",
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-999",
sourceMetadata: { agentName: "Scout" },
},
});
await store.createTask({
description: "UI created",
source: { sourceType: "dashboard_ui" },
});
const showTool = api.tools.get("fn_task_show")!;
const agentResult = await showTool.execute("call-2", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
const dashboardResult = await showTool.execute("call-3", { id: "FN-002" }, undefined, undefined, makeCtx(tmpDir));
expect(agentResult.content[0].text).toContain("Created via: Agent (Scout)");
expect(dashboardResult.content[0].text).toContain("Created via: Dashboard");
expect(agentResult.details.task.sourceMetadata?.agentName).toBe("Scout");
expect(agentResult.details.task.sourceAgentId).toBe("agent-999");
});
});
describe("fn_task_attach", () => {

View File

@@ -124,12 +124,78 @@ const INSIGHT_STATUSES: InsightStatus[] = ["generated", "confirmed", "stale", "d
const INSIGHT_RUN_STATUSES: InsightRunStatus[] = ["pending", "running", "completed", "failed", "cancelled"];
const INSIGHT_RUN_TRIGGERS: InsightRunTrigger[] = ["schedule", "manual", "task_completion", "merge_event", "api"];
function getTaskSourceAgentLabel(task: Pick<Task, "sourceMetadata" | "sourceAgentId">): string | undefined {
const metadataAgentName = task.sourceMetadata?.agentName;
if (typeof metadataAgentName === "string" && metadataAgentName.trim().length > 0) {
return metadataAgentName.trim();
}
if (typeof task.sourceAgentId === "string" && task.sourceAgentId.trim().length > 0) {
return task.sourceAgentId.trim();
}
return undefined;
}
function getTaskSourceLabel(task: Pick<Task, "sourceType" | "sourceMetadata" | "sourceAgentId" | "sourceParentTaskId">): string | undefined {
switch (task.sourceType) {
case "dashboard_ui":
return "Dashboard";
case "quick_chat":
return "Quick Chat";
case "chat_session":
return "Chat Session";
case "agent_heartbeat": {
const sourceAgent = getTaskSourceAgentLabel(task);
return sourceAgent ? `Agent (${sourceAgent})` : "Agent";
}
case "automation": {
const sourceAgent = getTaskSourceAgentLabel(task);
return sourceAgent ? `Automation (${sourceAgent})` : "Automation";
}
case "cron":
return "Scheduled Task";
case "workflow_step":
return "Workflow Step";
case "github_import": {
const issueUrl = task.sourceMetadata?.issueUrl;
return typeof issueUrl === "string" && issueUrl.length > 0
? `GitHub Import (${issueUrl})`
: "GitHub Import";
}
case "research": {
const findingLabel = task.sourceMetadata?.findingLabel;
if (typeof findingLabel === "string" && findingLabel.length > 0) {
return `Research (${findingLabel})`;
}
const runId = task.sourceMetadata?.runId;
return typeof runId === "string" && runId.length > 0
? `Research (${runId})`
: "Research";
}
case "task" + "_refine":
return task.sourceParentTaskId ? `Refinement of ${task.sourceParentTaskId}` : "Refinement";
case "task" + "_duplicate":
return task.sourceParentTaskId ? `Duplicate of ${task.sourceParentTaskId}` : "Duplicate";
case "cli":
return "CLI";
case "api":
return "API";
case "recovery":
return "Recovery";
default:
return undefined;
}
}
function formatTaskLine(t: Task): string {
const label =
t.title || t.description.slice(0, 60) + (t.description.length > 60 ? "…" : "");
const source = getTaskSourceLabel(t);
const sourceSuffix = source ? ` [via: ${source}]` : "";
const deps = t.dependencies.length ? ` [deps: ${t.dependencies.join(", ")}]` : "";
const paused = t.paused ? " (paused)" : "";
return `${t.id} ${label}${deps}${paused}`;
return `${t.id} ${label}${sourceSuffix}${deps}${paused}`;
}
async function getResearchAvailability(store: TaskStore): Promise<{ ok: boolean; code?: string; message?: string }> {
@@ -564,6 +630,10 @@ export default function kbExtension(pi: ExtensionAPI) {
if (task.dependencies.length) {
lines.push(`Dependencies: ${task.dependencies.join(", ")}`);
}
const sourceLabel = getTaskSourceLabel(task);
if (sourceLabel) {
lines.push(`Created via: ${sourceLabel}`);
}
if (task.paused) lines.push("Status: PAUSED");
lines.push("");