feat(FN-3060): add agent task auto-summarization and droid CLI path reconci

This merge adds droid CLI path reconciliation as a new pi extension, wires agent task auto-summarization for the agent tools layer, makes the UsageIndicator component resizable with improved styling, and updates related tests and documentation. The core changes include a new `reconcile-droid-cli-pat

Fusion-Task-Id: FN-3060
This commit is contained in:
Fusion
2026-05-01 12:40:25 -07:00
committed by gsxdsm
parent e6c79f1a41
commit 95cd107523
11 changed files with 94 additions and 25 deletions

View File

@@ -3350,7 +3350,7 @@ describe("HeartbeatMonitor", () => {
sourceAgentId: "agent-001",
sourceRunId: undefined,
},
});
}, expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
});
@@ -3870,7 +3870,7 @@ describe("HeartbeatMonitor", () => {
sourceAgentId: "agent-001",
sourceRunId: undefined,
},
});
}, expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
const responseText = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
expect(responseText).toContain("Created FN-100");

View File

@@ -12,6 +12,7 @@ function createMockAgentStore(overrides: Partial<AgentStore> = {}): AgentStore {
function createMockTaskStore(overrides: Partial<TaskStore> = {}): TaskStore {
return {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({
id: "FN-001",
description: "",
@@ -188,7 +189,7 @@ describe("createDelegateTaskTool", () => {
column: "todo",
assignedAgentId: "agent-001",
source: { sourceType: "api" },
});
}, expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Delegated to Bob (agent-001)");
@@ -283,7 +284,7 @@ describe("createDelegateTaskTool", () => {
column: "todo",
assignedAgentId: "agent-001",
source: { sourceType: "api" },
});
}, expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("depends on: FN-010");
@@ -312,6 +313,7 @@ describe("createDelegateTaskTool", () => {
expect(taskStore.createTask).toHaveBeenCalledWith(
expect.objectContaining({ dependencies: undefined }),
expect.objectContaining({ settings: { autoSummarizeTitles: false } }),
);
const text = (result.content[0] as { text: string }).text;

View File

@@ -17,6 +17,7 @@ import {
sendMessageParams,
readMessagesParams,
} from "../agent-tools.js";
import * as core from "@fusion/core";
import type { MessageStore, Message } from "@fusion/core";
const loggerSpies = vi.hoisted(() => ({
@@ -62,6 +63,7 @@ vi.mock("node:child_process", async () => {
describe("createTaskCreateTool", () => {
it("returns details.taskId and keeps Created <id> response text", async () => {
const store = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({
id: "PROJ-042",
description: "Follow-up task",
@@ -87,6 +89,9 @@ describe("createTaskCreateTool", () => {
dependencies: ["PROJ-001"],
column: "triage",
source: undefined,
}, {
settings: { autoSummarizeTitles: false },
onSummarize: undefined,
});
expect(result.details).toEqual({ taskId: "PROJ-042" });
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
@@ -96,6 +101,7 @@ describe("createTaskCreateTool", () => {
it("passes explicit provenance to store.createTask", async () => {
const store = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "PROJ-099", description: "Test", dependencies: [], column: "triage" }),
};
@@ -108,7 +114,7 @@ describe("createTaskCreateTool", () => {
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-123", sourceRunId: undefined },
}));
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
});
@@ -118,6 +124,7 @@ describe("createDelegateTaskTool", () => {
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
};
const taskStore = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "FN-100", dependencies: [], description: "Delegated" }),
};
@@ -126,8 +133,34 @@ describe("createDelegateTaskTool", () => {
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: { sourceType: "api" },
}), expect.objectContaining({
settings: { autoSummarizeTitles: false },
}));
});
it("wires title summarization callback when rootDir is provided", async () => {
const summarizeSpy = vi.spyOn(core, "summarizeTitle").mockResolvedValue("Short title");
const agentStore = {
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
};
const taskStore = {
getSettings: vi.fn().mockResolvedValue({
autoSummarizeTitles: true,
titleSummarizerProvider: "openai",
titleSummarizerModelId: "gpt-4o-mini",
}),
createTask: vi.fn().mockResolvedValue({ id: "FN-101", dependencies: [], description: "Delegated" }),
};
const tool = createDelegateTaskTool(agentStore as any, taskStore as any, { rootDir: "/repo" });
await tool.execute("call-1", { agent_id: "agent-1", description: "Delegated" } as any, undefined, undefined, {} as any);
const options = vi.mocked(taskStore.createTask).mock.calls[0]?.[1] as { onSummarize?: (description: string) => Promise<string | null> };
expect(options.onSummarize).toBeTypeOf("function");
await options.onSummarize?.("Long description");
expect(summarizeSpy).toHaveBeenCalledWith("Long description", "/repo", "openai", "gpt-4o-mini");
summarizeSpy.mockRestore();
});
});
describe("createTaskLogTool", () => {

View File

@@ -1534,6 +1534,8 @@ describe("taskCreate tool model inheritance", () => {
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
title: "Child Task",
description: "Child task description",
}), expect.objectContaining({
settings: { autoSummarizeTitles: false },
}));
});
@@ -1632,6 +1634,7 @@ describe("taskCreate tool model inheritance", () => {
// The second createTask call should have the resolved sibling id preserved.
expect(createTaskMock).toHaveBeenLastCalledWith(
expect.objectContaining({ dependencies: ["FN-701"] }),
expect.objectContaining({ settings: { autoSummarizeTitles: false } }),
);
expect(createdSubtasksRef.current).toEqual(["FN-701", "FN-702"]);
});

View File

@@ -1355,11 +1355,11 @@ export class HeartbeatMonitor {
sourceType: "agent_heartbeat",
sourceAgentId: agentId,
sourceRunId: runContext?.runId,
}));
}, { rootDir: this.rootDir }));
// Agent delegation tools
heartbeatTools.push(createListAgentsTool(this.store));
heartbeatTools.push(createDelegateTaskTool(this.store, taskStore));
heartbeatTools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
// Messaging tools — when MessageStore is available
if (this.messageStore) {
@@ -1845,7 +1845,7 @@ export class HeartbeatMonitor {
const baseCreateTool = createTaskCreateTool(taskStore, {
sourceType: "agent_heartbeat",
sourceAgentId: agentId,
});
}, { rootDir: this.rootDir });
const trackedCreateTool: ToolDefinition = {
...baseCreateTool,
execute: async (id: string, params: Static<typeof taskCreateParams>, signal, onUpdate, ctx) => {
@@ -1885,7 +1885,7 @@ export class HeartbeatMonitor {
tools.push(createTaskDocumentReadTool(taskStore, taskId));
// Agent delegation tools — discover and delegate work to other agents
tools.push(createListAgentsTool(this.store));
tools.push(createDelegateTaskTool(this.store, taskStore));
tools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
// Messaging tools — when MessageStore is available, agents can send and receive messages
if (messageStore) {

View File

@@ -11,8 +11,8 @@ 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, SourceType, Settings, ResearchRun, ResearchRunStatus, Agent } from "@fusion/core";
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, resolveResearchSettings, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh } from "@fusion/core";
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, Agent, TaskCreateInput } from "@fusion/core";
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
import { ResearchOrchestrator } from "./research-orchestrator.js";
import { ResearchProviderRegistry } from "./research/provider-registry.js";
import { ResearchStepRunner } from "./research-step-runner.js";
@@ -442,6 +442,31 @@ async function getAgentMemoryWindow(rootDir: string, agentMemory: AgentMemoryCon
// ── Tool factory functions ────────────────────────────────────────────────
type AgentTaskCreationOptions = {
rootDir?: string;
};
export async function createAgentTask(
store: TaskStore,
input: TaskCreateInput,
options?: AgentTaskCreationOptions,
): Promise<Awaited<ReturnType<TaskStore["createTask"]>>> {
const settings = typeof (store as { getSettings?: unknown }).getSettings === "function"
? await store.getSettings()
: {} as Settings;
const rootDir = options?.rootDir;
return store.createTask(input, {
settings: { autoSummarizeTitles: settings.autoSummarizeTitles === true },
onSummarize: rootDir
? async (description: string) => {
const resolved = resolveTitleSummarizerSettingsModel(settings);
return summarizeTitle(description, rootDir, resolved.provider, resolved.modelId);
}
: undefined,
});
}
/**
* Create a `fn_task_create` tool that creates a new task in triage.
*
@@ -451,6 +476,7 @@ async function getAgentMemoryWindow(rootDir: string, agentMemory: AgentMemoryCon
export function createTaskCreateTool(
store: TaskStore,
provenance?: { sourceType: SourceType; sourceAgentId?: string; sourceRunId?: string },
options?: AgentTaskCreationOptions,
): ToolDefinition {
return {
name: "fn_task_create",
@@ -462,7 +488,7 @@ export function createTaskCreateTool(
"or the current task should wait for the new one).",
parameters: taskCreateParams,
execute: async (_id: string, params: Static<typeof taskCreateParams>) => {
const task = await store.createTask({
const task = await createAgentTask(store, {
description: params.description,
dependencies: params.dependencies,
column: "triage",
@@ -471,7 +497,7 @@ export function createTaskCreateTool(
sourceAgentId: provenance.sourceAgentId,
sourceRunId: provenance.sourceRunId,
} : undefined,
});
}, options);
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
return {
content: [{
@@ -926,7 +952,11 @@ export function createListAgentsTool(agentStore: AgentStore): ToolDefinition {
* @param taskStore - TaskStore for task creation
* @returns ToolDefinition for the `fn_delegate_task` tool
*/
export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskStore): ToolDefinition {
export function createDelegateTaskTool(
agentStore: AgentStore,
taskStore: TaskStore,
options?: AgentTaskCreationOptions,
): ToolDefinition {
return {
name: "fn_delegate_task",
label: "Delegate Task",
@@ -954,13 +984,13 @@ export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskSt
}
// Create task assigned to the target agent
const task = await taskStore.createTask({
const task = await createAgentTask(taskStore, {
description: params.description,
dependencies: params.dependencies,
column: "todo",
assignedAgentId: params.agent_id,
source: { sourceType: "api" },
});
}, options);
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
return {

View File

@@ -2426,7 +2426,7 @@ export class TaskExecutor {
// Agent delegation tools — discover and delegate work to other agents.
...(this.options.agentStore ? [
createListAgentsTool(this.options.agentStore),
createDelegateTaskTool(this.options.agentStore, this.store),
createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir }),
] : []),
// Messaging tools — allows executor agents to send and receive messages.
...(this.options.messageStore && assignedAgentId ? [
@@ -3350,7 +3350,7 @@ export class TaskExecutor {
}
private createTaskCreateTool(): ToolDefinition {
return sharedCreateTaskCreateTool(this.store, { sourceType: "api" });
return sharedCreateTaskCreateTool(this.store, { sourceType: "api" }, { rootDir: this.rootDir });
}
private createTaskDocumentWriteTool(taskId: string): ToolDefinition {

View File

@@ -932,14 +932,14 @@ export class StepSessionExecutor {
? [createTaskLogTool(this.options.store, taskDetail.id)]
: [];
const taskCreateTool = this.options.store
? [createTaskCreateTool(this.options.store)]
? [createTaskCreateTool(this.options.store, undefined, { rootDir: this.options.rootDir })]
: [];
// Agent delegation tools — discover and delegate work to other agents.
const delegationTools = this.options.agentStore
? [
createListAgentsTool(this.options.agentStore),
createDelegateTaskTool(this.options.agentStore, this.options.store!),
createDelegateTaskTool(this.options.agentStore, this.options.store!, { rootDir: this.options.rootDir }),
]
: [];

View File

@@ -37,6 +37,7 @@ import type { StuckTaskDetector } from "./stuck-task-detector.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import {
createAgentTask,
createDelegateTaskTool,
createListAgentsTool,
createMemoryTools,
@@ -930,7 +931,7 @@ export class TriageProcessor {
// Agent delegation tools — discover and delegate work to other agents.
...(this.options.agentStore ? [
createListAgentsTool(this.options.agentStore),
createDelegateTaskTool(this.options.agentStore, this.store),
createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir }),
] : []),
this.createReviewSpecTool(
task.id,
@@ -1642,7 +1643,7 @@ export class TriageProcessor {
parentTask = undefined;
}
const newTask = await store.createTask({
const newTask = await createAgentTask(store, {
title: params.title,
description: params.description,
dependencies: validDeps,
@@ -1656,7 +1657,7 @@ export class TriageProcessor {
sourceType: "agent_heartbeat",
sourceParentTaskId: options.parentTaskId,
},
});
}, { rootDir: this.rootDir });
// Track the created subtask
options.createdSubtasksRef.current.push(newTask.id);