feat(FN-2923): merge fusion/fn-2923
- Add task `source` provenance field tracking how tasks enter the system (e.g. `"manual"`, `"heartbeat"`, `"executor"`, `"triage"`, `"cron"`, `"routine"`) - Wire `source` into shared create-task tools, executor agent, triage agent, cron runner, and routine runner - Add `source` to heartbeat task creation in `agent-heartbeat.ts` and mission execution loop in `mission-execution-loop.ts` - Wire `source` field into dashboard routes (`register-planning-subtask-routes.ts`, `register-task-workflow-routes.ts`) and GitHub integration (`register-git-github.ts`) - Add `source` to the API layer and task store handlers in the dashboard - Expose `source` in the `fn` CLI task command and `task-plan` extension tool - Add integration tests for provenance across engine (agent-tools, agent-heartbeat, cron-runner, triage, pr-comment-handler) and dashboard routes - Update existing tests to assert the correct source values - Bulk-revert stale changeset files that were superseded by the v0.9.1 release commit Commits merged: - test(FN-2923): complete Step 14 — align tests with task source provenance - test(FN-2923): complete Steps 12-13 — update and add provenance tests - feat(FN-2923): complete Step 10 — wire CLI task provenance - feat(FN-2923): complete Step 9 — wire dashboard UI and API source fields - feat(FN-2923): complete Step 8 — wire dashboard route provenance - feat(FN-2923): complete Step 7 — wire remaining engine provenance - feat(FN-2923): complete Step 6 — add routine create-task provenance - feat(FN-2923): complete Step 5 — add cron create-task provenance - feat(FN-2923): complete Step 4 — add triage subtask source - feat(FN-2923): complete Step 3 — add executor create-task source - feat(FN-2923): complete Step 2 — add heartbeat task provenance - feat(FN-2923): complete Step 1 — add provenance to shared create-task tools - feat(FN-2945): merge fusion/fn-2945 - chore(release): v0.9.1 Files changed: .changeset/active-agents-no-stuck-connecting.md | 13 --- .changeset/active-agents-panel-hoist-heartbeat.md | 13 --- .changeset/fix-agent-heartbeat-terminal-links.md | 5 -- .changeset/fix-experimental-defaults.md | 5 -- .changeset/improve-git-manager-diff-pane.md | 5 -- .changeset/insights-two-pane-layout.md | 5 -- .changeset/show-planning-tasks-immediately.md | 5 -- CHANGELOG.md | 92 ++++++++++++++++++++++ package.json | 2 +- packages/cli-alias/CHANGELOG.md | 15 ++++ packages/cli-alias/package.json | 2 +- packages/cli/CHANGELOG.md | 12 +++ packages/cli/package.json | 2 +- packages/cli/src/__tests__/task-plan.test.ts | 1 + packages/cli/src/commands/__tests__/task.test.ts | 15 +++- packages/cli/src/commands/task.ts | 15 +++- packages/cli/src/extension.ts | 9 +++ packages/core/CHANGELOG.md | 7 ++ packages/core/package.json | 2 +- packages/dashboard/CHANGELOG.md | 14 ++++ .../app/__tests__/agent-css-classes.test.ts | 3 +- packages/dashboard/app/__tests__/api.test.ts | 13 +++ packages/dashboard/app/api/legacy.ts | 2 + packages/dashboard/app/components/AgentsView.css | 22 +++--- .../app/components/ModelOnboardingModal.tsx | 5 +- packages/dashboard/app/components/TodoView.tsx | 2 + .../__tests__/ModelOnboardingModal.test.tsx | 4 +- .../app/components/__tests__/QuickChatFAB.test.tsx | 14 +--- .../app/components/__tests__/TodoView.test.tsx | 4 +- .../__tests__/agents-view-mobile.test.tsx | 5 +- .../components/__tests__/onboarding-flow.test.tsx | 2 +- .../app/hooks/__tests__/useTaskHandlers.test.ts | 4 +- packages/dashboard/app/hooks/useTaskHandlers.ts | 4 +- packages/dashboard/package.json | 2 +- packages/dashboard/src/__tests__/routes.test.ts | 14 ++++ packages/dashboard/src/routes.ts | 4 + .../dashboard/src/routes/register-git-github.ts | 12 +++ .../src/routes/register-planning-subtask-routes.ts | 3 + .../src/routes/register-task-workflow-routes.ts | 7 ++ packages/desktop/CHANGELOG.md | 7 ++ packages/desktop/package.json | 2 +- packages/engine/CHANGELOG.md | 11 +++ packages/engine/package.json | 2 +- .../engine/src/__tests__/agent-heartbeat.test.ts | 10 +++ .../src/__tests__/agent-tools-delegation.test.ts | 2 + packages/engine/src/__tests__/agent-tools.test.ts | 37 +++++++++ packages/engine/src/__tests__/cron-runner.test.ts | 4 + .../src/__tests__/pr-comment-handler.test.ts | 8 ++ packages/engine/src/__tests__/triage.test.ts | 2 + packages/engine/src/agent-heartbeat.ts | 13 ++- packages/engine/src/agent-tools.ts | 13 ++- packages/engine/src/cron-runner.ts | 7 +- packages/engine/src/executor.ts | 2 +- packages/engine/src/mission-execution-loop.ts | 8 ++ packages/engine/src/pr-comment-handler.ts | 5 ++ packages/engine/src/project-engine.ts | 8 ++ packages/engine/src/routine-runner.ts | 4 + packages/engine/src/triage.ts | 4 + packages/mobile/CHANGELOG.md | 7 ++ packages/mobile/package.json | 2 +- packages/pi-claude-cli/CHANGELOG.md | 7 ++ packages/pi-claude-cli/package.json | 2 +- packages/plugin-sdk/CHANGELOG.md | 10 +++ packages/plugin-sdk/package.json | 2 +- .../examples/fusion-plugin-auto-label/CHANGELOG.md | 8 ++ .../examples/fusion-plugin-auto-label/package.json | 2 +- .../examples/fusion-plugin-ci-status/CHANGELOG.md | 8 ++ .../examples/fusion-plugin-ci-status/package.json | 2 +- .../fusion-plugin-notification/CHANGELOG.md | 8 ++ .../fusion-plugin-notification/package.json | 2 +- .../fusion-plugin-settings-demo/CHANGELOG.md | 8 ++ .../fusion-plugin-settings-demo/package.json | 2 +- plugins/fusion-plugin-hermes-runtime/CHANGELOG.md | 8 ++ plugins/fusion-plugin-hermes-runtime/package.json | 2 +- .../fusion-plugin-openclaw-runtime/CHANGELOG.md | 8 ++ .../fusion-plugin-openclaw-runtime/package.json | 2 +- .../fusion-plugin-paperclip-runtime/CHANGELOG.md | 8 ++ .../fusion-plugin-paperclip-runtime/package.json | 2 +- 78 files changed, 499 insertions(+), 115 deletions(-) Fusion-Task-Id: FN-2923
This commit is contained in:
@@ -3229,6 +3229,11 @@ describe("HeartbeatMonitor", () => {
|
||||
description: "Follow-up task",
|
||||
dependencies: undefined,
|
||||
column: "triage",
|
||||
source: {
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceAgentId: "agent-001",
|
||||
sourceRunId: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3744,6 +3749,11 @@ describe("HeartbeatMonitor", () => {
|
||||
description: "Follow-up task",
|
||||
dependencies: undefined,
|
||||
column: "triage",
|
||||
source: {
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceAgentId: "agent-001",
|
||||
sourceRunId: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const responseText = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
|
||||
|
||||
@@ -187,6 +187,7 @@ describe("createDelegateTaskTool", () => {
|
||||
dependencies: undefined,
|
||||
column: "todo",
|
||||
assignedAgentId: "agent-001",
|
||||
source: { sourceType: "api" },
|
||||
});
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
@@ -281,6 +282,7 @@ describe("createDelegateTaskTool", () => {
|
||||
dependencies: ["FN-010"],
|
||||
column: "todo",
|
||||
assignedAgentId: "agent-001",
|
||||
source: { sourceType: "api" },
|
||||
});
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
buildQmdAgentMemorySearchArgs,
|
||||
createMemoryTools,
|
||||
createTaskCreateTool,
|
||||
createDelegateTaskTool,
|
||||
createSendMessageTool,
|
||||
createReadMessagesTool,
|
||||
qmdAgentMemoryCollectionName,
|
||||
@@ -82,12 +83,48 @@ describe("createTaskCreateTool", () => {
|
||||
description: "Follow-up task",
|
||||
dependencies: ["PROJ-001"],
|
||||
column: "triage",
|
||||
source: undefined,
|
||||
});
|
||||
expect(result.details).toEqual({ taskId: "PROJ-042" });
|
||||
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
||||
expect(responseText).toContain("Created PROJ-042: Follow-up task");
|
||||
expect(responseText).toContain("(depends on: PROJ-001)");
|
||||
});
|
||||
|
||||
it("passes explicit provenance to store.createTask", async () => {
|
||||
const store = {
|
||||
createTask: vi.fn().mockResolvedValue({ id: "PROJ-099", description: "Test", dependencies: [], column: "triage" }),
|
||||
};
|
||||
|
||||
const tool = createTaskCreateTool(store as any, {
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceAgentId: "agent-123",
|
||||
});
|
||||
|
||||
await tool.execute("call-1", { description: "Test" } as any, undefined, undefined, {} as any);
|
||||
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-123", sourceRunId: undefined },
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDelegateTaskTool", () => {
|
||||
it("creates delegated tasks with api source provenance", async () => {
|
||||
const agentStore = {
|
||||
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
|
||||
};
|
||||
const taskStore = {
|
||||
createTask: vi.fn().mockResolvedValue({ id: "FN-100", dependencies: [], description: "Delegated" }),
|
||||
};
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore as any, taskStore as any);
|
||||
await tool.execute("call-1", { agent_id: "agent-1", description: "Delegated" } as any, undefined, undefined, {} as any);
|
||||
|
||||
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
source: { sourceType: "api" },
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("createMemoryTools", () => {
|
||||
|
||||
@@ -1252,6 +1252,10 @@ describe("CronRunner", () => {
|
||||
column: "todo",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
source: {
|
||||
sourceType: "cron",
|
||||
sourceMetadata: { scheduleId: "test-schedule-id", stepId: expect.any(String) },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -213,6 +213,14 @@ describe("PrCommentHandler", () => {
|
||||
description: expect.stringContaining("FN-001"),
|
||||
column: "triage",
|
||||
dependencies: ["FN-001"],
|
||||
source: {
|
||||
sourceType: "api",
|
||||
sourceParentTaskId: "FN-001",
|
||||
sourceMetadata: {
|
||||
prNumber: 42,
|
||||
prUrl: "https://github.com/owner/repo/pull/42",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1387,6 +1387,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
modelId: parentTaskResult?.modelId,
|
||||
validatorModelProvider: parentTaskResult?.validatorModelProvider,
|
||||
validatorModelId: parentTaskResult?.validatorModelId,
|
||||
source: { sourceType: "agent_heartbeat", sourceParentTaskId: parentTaskId },
|
||||
});
|
||||
|
||||
expect(store.getTask).toHaveBeenCalledWith("FN-001");
|
||||
@@ -1435,6 +1436,7 @@ describe("taskCreate tool model inheritance", () => {
|
||||
modelId: parentTask?.modelId,
|
||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||
validatorModelId: parentTask?.validatorModelId,
|
||||
source: { sourceType: "agent_heartbeat", sourceParentTaskId: parentTaskId },
|
||||
});
|
||||
|
||||
expect(store.getTask).toHaveBeenCalledWith("FN-NONEXISTENT");
|
||||
|
||||
@@ -1157,8 +1157,12 @@ export class HeartbeatMonitor {
|
||||
// No-task runs: fn_task_create, fn_list_agents, fn_delegate_task, messaging, memory, fn_heartbeat_done
|
||||
heartbeatTools = [];
|
||||
|
||||
// fn_task_create tool (no tracking needed for no-task runs)
|
||||
heartbeatTools.push(createTaskCreateTool(taskStore));
|
||||
// fn_task_create tool
|
||||
heartbeatTools.push(createTaskCreateTool(taskStore, {
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceAgentId: agentId,
|
||||
sourceRunId: runContext?.runId,
|
||||
}));
|
||||
|
||||
// Agent delegation tools
|
||||
heartbeatTools.push(createListAgentsTool(this.store));
|
||||
@@ -1551,7 +1555,10 @@ export class HeartbeatMonitor {
|
||||
const tools: ToolDefinition[] = [];
|
||||
|
||||
// Wrap createTaskCreateTool with tracking and agent-link logging
|
||||
const baseCreateTool = createTaskCreateTool(taskStore);
|
||||
const baseCreateTool = createTaskCreateTool(taskStore, {
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceAgentId: agentId,
|
||||
});
|
||||
const trackedCreateTool: ToolDefinition = {
|
||||
...baseCreateTool,
|
||||
execute: async (id: string, params: Static<typeof taskCreateParams>, signal, onUpdate, ctx) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/p
|
||||
import { existsSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message } from "@fusion/core";
|
||||
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType } from "@fusion/core";
|
||||
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh } from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
@@ -420,7 +420,10 @@ async function getAgentMemoryWindow(rootDir: string, agentMemory: AgentMemoryCon
|
||||
* @param store - TaskStore for task persistence
|
||||
* @returns ToolDefinition for the `fn_task_create` tool
|
||||
*/
|
||||
export function createTaskCreateTool(store: TaskStore): ToolDefinition {
|
||||
export function createTaskCreateTool(
|
||||
store: TaskStore,
|
||||
provenance?: { sourceType: SourceType; sourceAgentId?: string; sourceRunId?: string },
|
||||
): ToolDefinition {
|
||||
return {
|
||||
name: "fn_task_create",
|
||||
label: "Create Task",
|
||||
@@ -435,6 +438,11 @@ export function createTaskCreateTool(store: TaskStore): ToolDefinition {
|
||||
description: params.description,
|
||||
dependencies: params.dependencies,
|
||||
column: "triage",
|
||||
source: provenance ? {
|
||||
sourceType: provenance.sourceType,
|
||||
sourceAgentId: provenance.sourceAgentId,
|
||||
sourceRunId: provenance.sourceRunId,
|
||||
} : undefined,
|
||||
});
|
||||
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
|
||||
return {
|
||||
@@ -899,6 +907,7 @@ export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskSt
|
||||
dependencies: params.dependencies,
|
||||
column: "todo",
|
||||
assignedAgentId: params.agent_id,
|
||||
source: { sourceType: "api" },
|
||||
});
|
||||
|
||||
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
|
||||
|
||||
@@ -388,7 +388,7 @@ export class CronRunner {
|
||||
} else if (step.type === "ai-prompt") {
|
||||
return this.executeAiPromptStep(step, stepIndex, timeoutMs, stepStartedAt);
|
||||
} else if (step.type === "create-task") {
|
||||
return this.executeCreateTaskStep(step, stepIndex, stepStartedAt);
|
||||
return this.executeCreateTaskStep(step, stepIndex, stepStartedAt, schedule.id);
|
||||
}
|
||||
|
||||
// Unknown step type
|
||||
@@ -564,6 +564,7 @@ export class CronRunner {
|
||||
step: AutomationStep,
|
||||
stepIndex: number,
|
||||
startedAt: string,
|
||||
scheduleId: string,
|
||||
): Promise<AutomationStepResult> {
|
||||
// Validate that taskDescription is present and non-empty
|
||||
if (!step.taskDescription?.trim()) {
|
||||
@@ -586,6 +587,10 @@ export class CronRunner {
|
||||
column: (step.taskColumn as Column) || "triage",
|
||||
modelProvider: step.modelProvider?.trim() || undefined,
|
||||
modelId: step.modelId?.trim() || undefined,
|
||||
source: {
|
||||
sourceType: "cron",
|
||||
sourceMetadata: { scheduleId, stepId: step.id },
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -3083,7 +3083,7 @@ export class TaskExecutor {
|
||||
}
|
||||
|
||||
private createTaskCreateTool(): ToolDefinition {
|
||||
return sharedCreateTaskCreateTool(this.store);
|
||||
return sharedCreateTaskCreateTool(this.store, { sourceType: "api" });
|
||||
}
|
||||
|
||||
private createTaskDocumentWriteTool(taskId: string): ToolDefinition {
|
||||
|
||||
@@ -265,6 +265,14 @@ export class MissionExecutionLoop extends EventEmitter {
|
||||
column: "in-progress",
|
||||
missionId,
|
||||
sliceId: feature.sliceId,
|
||||
source: {
|
||||
sourceType: "automation",
|
||||
sourceMetadata: {
|
||||
missionId,
|
||||
featureId: feature.id,
|
||||
sliceId: feature.sliceId,
|
||||
},
|
||||
},
|
||||
});
|
||||
validationTaskId = validationTask.id;
|
||||
|
||||
|
||||
@@ -219,6 +219,11 @@ Please review the PR comments and address any remaining issues.`;
|
||||
description,
|
||||
column: "triage",
|
||||
dependencies: [originalTaskId],
|
||||
source: {
|
||||
sourceType: "api",
|
||||
sourceParentTaskId: originalTaskId,
|
||||
sourceMetadata: { prNumber: prInfo.number, prUrl: prInfo.url },
|
||||
},
|
||||
});
|
||||
|
||||
prMonitorLog.log(`Created follow-up task ${task.id} for PR #${prInfo.number}`);
|
||||
|
||||
@@ -1233,6 +1233,10 @@ export class ProjectEngine {
|
||||
description: followUpDescription,
|
||||
column: "triage",
|
||||
priority: "high",
|
||||
source: {
|
||||
sourceType: "recovery",
|
||||
sourceParentTaskId: taskId,
|
||||
},
|
||||
});
|
||||
await store.addTaskComment(
|
||||
taskId,
|
||||
@@ -1368,6 +1372,10 @@ export class ProjectEngine {
|
||||
`Last merge error: ${errorMsg}`,
|
||||
column: "triage",
|
||||
priority: "high",
|
||||
source: {
|
||||
sourceType: "recovery",
|
||||
sourceParentTaskId: taskId,
|
||||
},
|
||||
});
|
||||
await store.addTaskComment(
|
||||
taskId,
|
||||
|
||||
@@ -385,6 +385,10 @@ export class RoutineRunner {
|
||||
column: (step.taskColumn as Column) || "triage",
|
||||
modelProvider: step.modelProvider?.trim() || undefined,
|
||||
modelId: step.modelId?.trim() || undefined,
|
||||
source: {
|
||||
sourceType: "automation",
|
||||
sourceMetadata: { routineId: routine.id, stepId: step.id },
|
||||
},
|
||||
};
|
||||
try {
|
||||
const task = await this.options.taskStore.createTask(taskInput);
|
||||
|
||||
@@ -1573,6 +1573,10 @@ export class TriageProcessor {
|
||||
modelId: parentTask?.modelId,
|
||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||
validatorModelId: parentTask?.validatorModelId,
|
||||
source: {
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceParentTaskId: options.parentTaskId,
|
||||
},
|
||||
});
|
||||
|
||||
// Track the created subtask
|
||||
|
||||
Reference in New Issue
Block a user