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:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }),
|
||||
]
|
||||
: [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user