FN-7115: expose governed heartbeat workflow tools
Expose the missing governed workflow and promotion tools to heartbeat agent sessions.\n\n- Add workflow authoring/settings, research cancellation, and task promotion/select tools to the shared heartbeat work surface.\n- Keep task-read discovery and executor-only worktree tools correctly scoped while documenting the heartbeat lane surface.\n- Cover tool injection, permission-gate behavior, and workflow factory exposure with regression tests.\n\nFiles changed:\n .changeset/FN-7115-heartbeat-tool-injection.md | 7 ++\n docs/agents.md | 6 +-\n .../engine/src/__tests__/agent-action-gate.test.ts | 86 +++++++++++++++++++++-\n .../agent-workflow-tools-exposure.test.ts | 16 ++--\n .../src/__tests__/heartbeat-executor.test.ts | 31 +++++---\n .../src/__tests__/heartbeat-session-prompt.test.ts | 69 ++++++++++++++++-\n packages/engine/src/agent-heartbeat.ts | 22 ++++--\n 7 files changed, 207 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-7115 Fusion-Task-Lineage: 0883767b-7982-4f10-a466-1467b867dea1 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/FN-7115-heartbeat-tool-injection.md
Normal file
7
.changeset/FN-7115-heartbeat-tool-injection.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Permanent/custom agents can use governed workflow and task-promotion tools.
|
||||
category: feature
|
||||
dev: Injects the FN-7111-classified mutating tools (fn_workflow_create/update/delete/settings/select, fn_task_promote) into the heartbeat agent-work lane (packages/engine/src/agent-heartbeat.ts), governed by AgentPermissionPolicy via wrapToolsWithActionGate. Executor-only tools requiring worktree/workspace context (fn_run_verification, fn_acquire_repo_worktree) remain intentionally excluded from the ambient lane. Hermetic readonly lanes and automation allowedTools are unchanged.
|
||||
@@ -34,7 +34,7 @@ fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>]
|
||||
- Dashboard chat and planning sessions with a scoped task store expose `fn_task_document_write` and `fn_task_document_read`; because neither lane has an ambient task, both tools require an explicit `task_id`.
|
||||
- Agent workflow-routing tools follow an intent boundary: agents may select or change a task workflow only when the user explicitly requested that workflow or when the agent created the task. Executors must not call `fn_workflow_select` to reroute the task they are executing unless the task instructions or a user steering comment explicitly asks for the workflow change.
|
||||
- Executor, heartbeat, and dashboard chat sessions expose artifact registry tools so agents can publish and inspect multi-type deliverables without relying on the dashboard gallery. Planning sessions intentionally exclude artifact tools until they can thread the existing `MessageStore` dependency.
|
||||
- Permanent/custom heartbeat agents receive the broad coordination and work-discovery tool surface instead of a narrowly curated subset: read-only task discovery (`fn_task_list`, `fn_task_show`, `fn_task_search`) for work discovery and duplicate avoidance, workflow discovery (`fn_workflow_list`, `fn_workflow_get`, `fn_trait_list`), bounded research (`fn_research_run`, `fn_research_list`, `fn_research_get`), structured clarification (`fn_ask_question`), artifact, memory, messaging, goal, evaluation, identity, and delegation tools. The task read tools are store-backed, text-only, and action-gate-recognized as read-only; dangerous actions are controlled at invocation time by each agent's `AgentPermissionPolicy` through the action gate (allow / require approval / block), not by withholding safe tools from the session.
|
||||
- Permanent/custom heartbeat agents receive the broad coordination and work-discovery tool surface instead of a narrowly curated subset: read-only task discovery (`fn_task_list`, `fn_task_show`, `fn_task_search`) for work discovery and duplicate avoidance, workflow discovery and authoring (`fn_workflow_list`, `fn_workflow_get`, `fn_workflow_create`, `fn_workflow_update`, `fn_workflow_delete`, `fn_workflow_settings`, `fn_trait_list`), governed research (`fn_research_run`, `fn_research_list`, `fn_research_get`, `fn_research_cancel`), structured clarification (`fn_ask_question`), artifact, memory, messaging, goal, evaluation, identity, and delegation tools. Task-scoped heartbeat sessions also expose current-task workflow selection and promotion (`fn_workflow_select`, `fn_task_promote`); no-task heartbeats omit those because they have no ambient task. Executor-only worktree/workspace tools such as `fn_run_verification` and `fn_acquire_repo_worktree` remain out of the ambient heartbeat lane until that lane owns the required worktree/workspace context. The task read tools are store-backed, text-only, and action-gate-recognized as read-only; dangerous actions are controlled at invocation time by each agent's `AgentPermissionPolicy` through the action gate (allow / require approval / block), not by withholding governed tools from the session.
|
||||
|
||||
### Artifact registry tools
|
||||
|
||||
@@ -499,12 +499,12 @@ This layered behavior is shared by heartbeat agents and task-scoped sessions tha
|
||||
|
||||
## Research Tools in Planning/Execution Sessions
|
||||
|
||||
Triage and executor runtime sessions include a bounded research tool surface only when `experimentalFeatures.researchView` is enabled for the project. Permanent/custom heartbeat sessions always register the safe research discovery/run tools (`fn_research_run`, `fn_research_list`, `fn_research_get`) so permission policy and runtime setup responses govern use; `fn_research_run` remains `network_api`-gated by `AgentPermissionPolicy`, and disabled or misconfigured research returns an actionable setup result instead of removing the tool.
|
||||
Triage and executor runtime sessions include a bounded research tool surface only when `experimentalFeatures.researchView` is enabled for the project. Permanent/custom heartbeat sessions always register the governed research discovery/run/cancel tools (`fn_research_run`, `fn_research_list`, `fn_research_get`, `fn_research_cancel`) so permission policy and runtime setup responses govern use; `fn_research_run` and `fn_research_cancel` remain `network_api`-gated by `AgentPermissionPolicy`, and disabled or misconfigured research returns an actionable setup result instead of removing the tool.
|
||||
|
||||
- `fn_research_run` — create/start a bounded research run for a focused query
|
||||
- `fn_research_list` — list recent runs and statuses
|
||||
- `fn_research_get` — fetch one run's structured findings payload
|
||||
- `fn_research_cancel` — cancel an active run (triage/executor research surface only; heartbeat withholds cancel until it has explicit action-gate classification)
|
||||
- `fn_research_cancel` — cancel an active run
|
||||
|
||||
These tools return structured metadata (`runId`, `status`, `summary`, `findings`, `citations`, `error`, `setup`) in addition to concise text so downstream model steps can consume results deterministically.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
addToExemptTools,
|
||||
computeApprovalDedupeKey,
|
||||
@@ -135,6 +135,90 @@ describe("agent-action-gate", () => {
|
||||
expect(blockedDecision.disposition).toBe("block");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"fn_workflow_create",
|
||||
"fn_workflow_update",
|
||||
"fn_workflow_delete",
|
||||
"fn_workflow_settings",
|
||||
"fn_workflow_select",
|
||||
"fn_task_promote",
|
||||
] as const)("governs newly injected heartbeat mutating tool %s by task-agent policy", (toolName) => {
|
||||
const allowedDecision = evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: unrestrictedPolicy });
|
||||
const approvalDecision = evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: approvalPolicy });
|
||||
const blockedDecision = evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: lockedDownPolicy });
|
||||
|
||||
expect(allowedDecision.category).toBe("task_agent_mutation");
|
||||
expect(allowedDecision.disposition).toBe("allow");
|
||||
expect(approvalDecision.category).toBe("task_agent_mutation");
|
||||
expect(approvalDecision.disposition).toBe("require-approval");
|
||||
expect(blockedDecision.category).toBe("task_agent_mutation");
|
||||
expect(blockedDecision.disposition).toBe("block");
|
||||
});
|
||||
|
||||
it("executes or withholds newly injected workflow tools according to AgentPermissionPolicy", async () => {
|
||||
const execute = vi.fn().mockResolvedValue({ ok: true });
|
||||
const tool = { name: "fn_workflow_delete", label: "Delete Workflow", description: "", parameters: {}, execute };
|
||||
const { wrapToolsWithActionGate } = await import("../pi.js");
|
||||
|
||||
const unrestricted = wrapToolsWithActionGate([tool as any], {
|
||||
agentId: "agent-1",
|
||||
agentName: "Agent",
|
||||
isEphemeral: false,
|
||||
taskId: "FN-1",
|
||||
permissionPolicy: unrestrictedPolicy,
|
||||
createApprovalRequest: vi.fn(),
|
||||
findApprovalByDedupeKey: vi.fn(),
|
||||
});
|
||||
await expect((unrestricted[0] as any).execute("delete-allow", { workflow_id: "WF-1" })).resolves.toEqual({ ok: true });
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
|
||||
const locked = wrapToolsWithActionGate([tool as any], {
|
||||
agentId: "agent-1",
|
||||
agentName: "Agent",
|
||||
isEphemeral: false,
|
||||
taskId: "FN-1",
|
||||
permissionPolicy: lockedDownPolicy,
|
||||
createApprovalRequest: vi.fn(),
|
||||
findApprovalByDedupeKey: vi.fn(),
|
||||
});
|
||||
const blocked = await (locked[0] as any).execute("delete-block", { workflow_id: "WF-1" });
|
||||
expect(blocked).toEqual(expect.objectContaining({
|
||||
isError: true,
|
||||
decision: expect.objectContaining({
|
||||
category: "task_agent_mutation",
|
||||
disposition: "block",
|
||||
toolName: "fn_workflow_delete",
|
||||
}),
|
||||
}));
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
|
||||
const createApprovalRequest = vi.fn().mockResolvedValue({ id: "apr-workflow-1" });
|
||||
const pauseForApproval = vi.fn();
|
||||
const approval = wrapToolsWithActionGate([tool as any], {
|
||||
agentId: "agent-1",
|
||||
agentName: "Agent",
|
||||
isEphemeral: false,
|
||||
taskId: "FN-1",
|
||||
permissionPolicy: approvalPolicy,
|
||||
createApprovalRequest,
|
||||
findApprovalByDedupeKey: vi.fn().mockResolvedValue(null),
|
||||
pauseForApproval,
|
||||
});
|
||||
const pending = await (approval[0] as any).execute("delete-approval", { workflow_id: "WF-1" });
|
||||
expect(pending).toEqual(expect.objectContaining({
|
||||
isError: true,
|
||||
decision: expect.objectContaining({
|
||||
category: "task_agent_mutation",
|
||||
disposition: "require-approval",
|
||||
toolName: "fn_workflow_delete",
|
||||
metadata: expect.objectContaining({ approvalRequestId: "apr-workflow-1" }),
|
||||
}),
|
||||
}));
|
||||
expect(createApprovalRequest).toHaveBeenCalledTimes(1);
|
||||
expect(pauseForApproval).toHaveBeenCalledTimes(1);
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(FN_3548_COORDINATION_TOOLS)("always allows newly exempt internal tool %s under locked-down policies", (toolName) => {
|
||||
const decision = evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: lockedDownPolicy });
|
||||
expect(decision.disposition).toBe("allow");
|
||||
|
||||
@@ -8,16 +8,19 @@ import {
|
||||
createWorkflowUpdateTool,
|
||||
createWorkflowDeleteTool,
|
||||
} from "../index.js";
|
||||
import { createWorkflowSettingsTool } from "../agent-tools.js";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
/**
|
||||
* U11 / R12 drift guard (engine half): the workflow-authoring tool surface that
|
||||
* chat, planning, and the task executor all share must always expose the six
|
||||
* `fn_workflow_*` tools. The lanes assemble their toolset from
|
||||
* `createWorkflowAuthoringTools` (chat/planning) and the executor mirrors the
|
||||
* same factories — so asserting factory completeness here guards every lane's
|
||||
* source of truth. Lane-wiring (that chat/planning actually pass these to
|
||||
* createFnAgent) is asserted in packages/dashboard's exposure test.
|
||||
* chat, planning, and the task executor all share must always expose the core
|
||||
* `fn_workflow_*` authoring tools. The lanes assemble most of their toolset
|
||||
* from `createWorkflowAuthoringTools` (chat/planning) and the executor mirrors
|
||||
* the same factories — so asserting factory completeness here guards every
|
||||
* lane's source of truth. `fn_workflow_settings` remains a standalone factory
|
||||
* that heartbeat/executor wire explicitly. Lane-wiring (that chat/planning
|
||||
* actually pass these to createFnAgent) is asserted in packages/dashboard's
|
||||
* exposure test.
|
||||
*
|
||||
* We invoke the REAL factories with a fake store — never mock the factories
|
||||
* themselves — so a renamed/removed tool name is caught.
|
||||
@@ -52,6 +55,7 @@ describe("workflow tool exposure (engine factories)", () => {
|
||||
expect(createWorkflowCreateTool(fakeStore).name).toBe("fn_workflow_create");
|
||||
expect(createWorkflowUpdateTool(fakeStore).name).toBe("fn_workflow_update");
|
||||
expect(createWorkflowDeleteTool(fakeStore).name).toBe("fn_workflow_delete");
|
||||
expect(createWorkflowSettingsTool(fakeStore).name).toBe("fn_workflow_settings");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3030,8 +3030,8 @@ describe("executeHeartbeat", () => {
|
||||
expect(callArgs.systemPrompt).toContain("fn_task_document_write");
|
||||
expect(callArgs.tools).toBe("coding");
|
||||
// fn_artifact_register/list/view, agent config/provisioning, goals/evaluations/identity,
|
||||
// task read discovery, workflow discovery, bounded research, clarification, web fetch, memory, and fn_heartbeat_done.
|
||||
expect(callArgs.customTools).toHaveLength(32);
|
||||
// task read discovery, workflow discovery/authoring, task promotion, bounded research, clarification, web fetch, memory, and fn_heartbeat_done.
|
||||
expect(callArgs.customTools).toHaveLength(39);
|
||||
expect(callArgs.customTools![0]!.name).toBe("fn_task_create");
|
||||
expect(callArgs.customTools![1]!.name).toBe("fn_task_log");
|
||||
expect(callArgs.customTools![2]!.name).toBe("fn_task_document_write");
|
||||
@@ -3054,17 +3054,24 @@ describe("executeHeartbeat", () => {
|
||||
expect(callArgs.customTools![19]!.name).toBe("fn_task_search");
|
||||
expect(callArgs.customTools![20]!.name).toBe("fn_workflow_list");
|
||||
expect(callArgs.customTools![21]!.name).toBe("fn_workflow_get");
|
||||
expect(callArgs.customTools![22]!.name).toBe("fn_trait_list");
|
||||
expect(callArgs.customTools![23]!.name).toBe("fn_ask_question");
|
||||
expect(callArgs.customTools![24]!.name).toBe("fn_research_run");
|
||||
expect(callArgs.customTools![25]!.name).toBe("fn_research_list");
|
||||
expect(callArgs.customTools![26]!.name).toBe("fn_research_get");
|
||||
expect(callArgs.customTools![27]!.name).toBe("fn_web_fetch");
|
||||
expect(callArgs.customTools![28]!.name).toBe("fn_memory_search");
|
||||
expect(callArgs.customTools![29]!.name).toBe("fn_memory_get");
|
||||
expect(callArgs.customTools![30]!.name).toBe("fn_memory_append");
|
||||
expect(callArgs.customTools![22]!.name).toBe("fn_workflow_create");
|
||||
expect(callArgs.customTools![23]!.name).toBe("fn_workflow_update");
|
||||
expect(callArgs.customTools![24]!.name).toBe("fn_workflow_delete");
|
||||
expect(callArgs.customTools![25]!.name).toBe("fn_workflow_settings");
|
||||
expect(callArgs.customTools![26]!.name).toBe("fn_trait_list");
|
||||
expect(callArgs.customTools![27]!.name).toBe("fn_ask_question");
|
||||
expect(callArgs.customTools![28]!.name).toBe("fn_research_run");
|
||||
expect(callArgs.customTools![29]!.name).toBe("fn_research_list");
|
||||
expect(callArgs.customTools![30]!.name).toBe("fn_research_get");
|
||||
expect(callArgs.customTools![31]!.name).toBe("fn_research_cancel");
|
||||
expect(callArgs.customTools![32]!.name).toBe("fn_workflow_select");
|
||||
expect(callArgs.customTools![33]!.name).toBe("fn_task_promote");
|
||||
expect(callArgs.customTools![34]!.name).toBe("fn_web_fetch");
|
||||
expect(callArgs.customTools![35]!.name).toBe("fn_memory_search");
|
||||
expect(callArgs.customTools![36]!.name).toBe("fn_memory_get");
|
||||
expect(callArgs.customTools![37]!.name).toBe("fn_memory_append");
|
||||
// fn_heartbeat_done is last (terminal tool)
|
||||
expect(callArgs.customTools![31]!.name).toBe("fn_heartbeat_done");
|
||||
expect(callArgs.customTools![38]!.name).toBe("fn_heartbeat_done");
|
||||
});
|
||||
|
||||
it("loads workspace memory into system prompt and identity snapshot when inline memory is empty", async () => {
|
||||
|
||||
@@ -137,10 +137,15 @@ describe("createHeartbeatTools", () => {
|
||||
"fn_reflect_on_performance",
|
||||
"fn_workflow_list",
|
||||
"fn_workflow_get",
|
||||
"fn_workflow_create",
|
||||
"fn_workflow_update",
|
||||
"fn_workflow_delete",
|
||||
"fn_workflow_settings",
|
||||
"fn_trait_list",
|
||||
"fn_research_run",
|
||||
"fn_research_list",
|
||||
"fn_research_get",
|
||||
"fn_research_cancel",
|
||||
"fn_ask_question",
|
||||
"fn_heartbeat_done",
|
||||
] as const;
|
||||
@@ -183,7 +188,7 @@ describe("createHeartbeatTools", () => {
|
||||
|
||||
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
|
||||
|
||||
expect(tools).toHaveLength(27);
|
||||
expect(tools).toHaveLength(34);
|
||||
expect(tools[0]!.name).toBe("fn_task_create");
|
||||
expect(tools[1]!.name).toBe("fn_task_log");
|
||||
expect(tools[2]!.name).toBe("fn_task_document_write");
|
||||
@@ -207,14 +212,67 @@ describe("createHeartbeatTools", () => {
|
||||
"fn_task_search",
|
||||
"fn_workflow_list",
|
||||
"fn_workflow_get",
|
||||
"fn_workflow_create",
|
||||
"fn_workflow_update",
|
||||
"fn_workflow_delete",
|
||||
"fn_workflow_settings",
|
||||
"fn_trait_list",
|
||||
"fn_ask_question",
|
||||
"fn_research_run",
|
||||
"fn_research_list",
|
||||
"fn_research_get",
|
||||
"fn_research_cancel",
|
||||
"fn_workflow_select",
|
||||
"fn_task_promote",
|
||||
]);
|
||||
expect(tools.map((tool) => tool.name)).not.toContain("fn_research_cancel");
|
||||
expect(tools.map((tool) => tool.name)).not.toContain("fn_run_verification");
|
||||
expect(tools.map((tool) => tool.name)).not.toContain("fn_acquire_repo_worktree");
|
||||
});
|
||||
|
||||
it("heartbeat workflow create/update tools strip approval-bypass flags", async () => {
|
||||
const store = createMockStore();
|
||||
const captured: { createIr?: any; updateIr?: any } = {};
|
||||
const taskStore = createMockTaskStoreForTools({
|
||||
createWorkflowDefinition: vi.fn().mockImplementation(async (input: any) => {
|
||||
captured.createIr = input.ir;
|
||||
return { id: "WF-001", name: input.name };
|
||||
}),
|
||||
updateWorkflowDefinition: vi.fn().mockImplementation(async (_id: string, input: any) => {
|
||||
captured.updateIr = input.ir;
|
||||
return { id: "WF-001", name: input.name ?? "wf" };
|
||||
}),
|
||||
} as Partial<TaskStore>);
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore, rootDir: "/tmp" });
|
||||
|
||||
const tools = monitor.createHeartbeatTools("agent-001", taskStore, "FN-001");
|
||||
const createTool = tools.find((tool) => tool.name === "fn_workflow_create")!;
|
||||
const updateTool = tools.find((tool) => tool.name === "fn_workflow_update")!;
|
||||
const irWithFlags = {
|
||||
version: "v1",
|
||||
name: "wf",
|
||||
nodes: [
|
||||
{ id: "prompt", kind: "prompt", config: { cliSkipApproval: true } },
|
||||
{
|
||||
id: "foreach",
|
||||
kind: "foreach",
|
||||
config: {
|
||||
template: {
|
||||
nodes: [{ id: "inner", kind: "step-execute", config: { autoApprove: true } }],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
await createTool.execute("call-create", { name: "wf", ir: irWithFlags }, undefined as any, undefined as any, undefined as any);
|
||||
await updateTool.execute("call-update", { workflow_id: "WF-001", ir: irWithFlags }, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(captured.createIr.nodes[0].config.cliSkipApproval).toBeUndefined();
|
||||
expect(captured.createIr.nodes[1].config.template.nodes[0].config.autoApprove).toBeUndefined();
|
||||
expect(captured.updateIr.nodes[0].config.cliSkipApproval).toBeUndefined();
|
||||
expect(captured.updateIr.nodes[1].config.template.nodes[0].config.autoApprove).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fn_task_create tool creates a task in triage via TaskStore", async () => {
|
||||
@@ -810,18 +868,23 @@ describe("no-task heartbeat tool surface", () => {
|
||||
"fn_artifact_view",
|
||||
"fn_workflow_list",
|
||||
"fn_workflow_get",
|
||||
"fn_workflow_create",
|
||||
"fn_workflow_update",
|
||||
"fn_workflow_delete",
|
||||
"fn_workflow_settings",
|
||||
"fn_trait_list",
|
||||
"fn_ask_question",
|
||||
"fn_research_run",
|
||||
"fn_research_list",
|
||||
"fn_research_get",
|
||||
"fn_research_cancel",
|
||||
]));
|
||||
expect(capturedCustomTools).not.toEqual(expect.arrayContaining([
|
||||
"fn_task_log",
|
||||
"fn_task_document_write",
|
||||
"fn_task_document_read",
|
||||
"fn_research_cancel",
|
||||
"fn_run_verification",
|
||||
"fn_acquire_repo_worktree",
|
||||
"fn_workflow_select",
|
||||
"fn_task_promote",
|
||||
]));
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgen
|
||||
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
||||
import { Type, type Static } from "@earendil-works/pi-ai";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createTaskReadTools, createArtifactRegisterTool, createArtifactListTool, createArtifactViewTool, createListAgentsTool, createDelegateTaskTool, createGetAgentConfigTool, createUpdateAgentConfigTool, createAgentCreateTool, createAgentDeleteTool, createSendMessageTool, createReadMessagesTool, createPostRoomMessageTool, createMemoryTools, createGoalRetrievalTools, createReadEvaluationsTool, createUpdateIdentityTool, createReflectOnPerformanceTool, createWebFetchTool, createWorkflowListTool, createWorkflowGetTool, createTraitListTool, createAskQuestionTool, createResearchTools, readAgentMemoryWorkspaceLongTerm, taskCreateParams } from "./agent-tools.js";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createTaskReadTools, createArtifactRegisterTool, createArtifactListTool, createArtifactViewTool, createListAgentsTool, createDelegateTaskTool, createGetAgentConfigTool, createUpdateAgentConfigTool, createAgentCreateTool, createAgentDeleteTool, createSendMessageTool, createReadMessagesTool, createPostRoomMessageTool, createMemoryTools, createGoalRetrievalTools, createReadEvaluationsTool, createUpdateIdentityTool, createReflectOnPerformanceTool, createWebFetchTool, createWorkflowListTool, createWorkflowGetTool, createWorkflowSelectTool, createTaskPromoteTool, createWorkflowCreateTool, createWorkflowUpdateTool, createWorkflowDeleteTool, createWorkflowSettingsTool, createTraitListTool, createAskQuestionTool, createResearchTools, readAgentMemoryWorkspaceLongTerm, taskCreateParams } from "./agent-tools.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
import {
|
||||
resolveAgentInstructionsWithRatings,
|
||||
@@ -409,7 +409,7 @@ Examples of ONE useful coordination action:
|
||||
|
||||
Keep work lightweight — this is a single-pass coordination check, not an implementation run.
|
||||
You have workspace read tools (for context gathering) plus fn_task_create, fn_task_log, fn_task_document tools,
|
||||
fn_send_message, fn_read_messages, fn_post_room_message, fn_list_agents, fn_delegate_task, workflow discovery, bounded research, fn_ask_question, and memory tools.
|
||||
fn_send_message, fn_read_messages, fn_post_room_message, fn_list_agents, fn_delegate_task, workflow discovery/authoring, task promotion, bounded research, fn_ask_question, and memory tools.
|
||||
|
||||
**Task Documents:** Save important findings with fn_task_document_write(key="...", content="...").
|
||||
Documents persist across sessions and are visible in the dashboard's Documents tab.
|
||||
@@ -507,8 +507,8 @@ You have coding-capable workspace tools (read/write/edit/bash within worktree bo
|
||||
- fn_artifact_register, fn_artifact_list, and fn_artifact_view
|
||||
- fn_read_evaluations and fn_update_identity (available in no-task runs)
|
||||
- fn_reflect_on_performance when reflection is enabled for this run
|
||||
- fn_workflow_list, fn_workflow_get, and fn_trait_list for workflow discovery
|
||||
- fn_research_run, fn_research_list, and fn_research_get for bounded research when configured
|
||||
- fn_workflow_list, fn_workflow_get, fn_workflow_create, fn_workflow_update, fn_workflow_delete, fn_workflow_settings, and fn_trait_list for workflow discovery/authoring
|
||||
- fn_research_run, fn_research_list, fn_research_get, and fn_research_cancel for bounded research when configured
|
||||
- fn_ask_question to ask the dashboard user for structured clarification
|
||||
- fn_web_fetch
|
||||
- fn_memory_search, fn_memory_get, and fn_memory_append
|
||||
@@ -3423,6 +3423,12 @@ export class HeartbeatMonitor {
|
||||
*
|
||||
* FNXC:AgentTooling 2026-06-27-14:21:
|
||||
* Read-only task discovery tools are part of this shared heartbeat-safe surface so both no-task and task-scoped permanent/custom heartbeat runs can list, show, and search tasks for duplicate avoidance without bespoke tool copies.
|
||||
*
|
||||
* FNXC:AgentTooling 2026-06-27-15:30:
|
||||
* FN-7115 requires classified mutating workflow tools and governed research cancellation to be injected into the heartbeat lane instead of being withheld. Executor-only tools that need a task worktree or workspace task stay excluded because this ambient lane cannot supply that context safely.
|
||||
*
|
||||
* FNXC:AgentTooling 2026-06-27-23:04:
|
||||
* Heartbeat agents are autonomous and prompt-injectable, so workflow create/update tools must strip embedded approval-bypass flags before persisting IR. Permission policy governs whether the tool call may happen; stripApprovalFlags prevents the resulting workflow from weakening future approval gates.
|
||||
*/
|
||||
private createSharedHeartbeatWorkTools(taskStore: TaskStore): ToolDefinition[] {
|
||||
const rootDir = this.rootDir ?? process.cwd();
|
||||
@@ -3430,12 +3436,16 @@ export class HeartbeatMonitor {
|
||||
store: taskStore,
|
||||
rootDir,
|
||||
getSettings: () => taskStore.getSettings(),
|
||||
}).filter((tool) => tool.name !== "fn_research_cancel");
|
||||
});
|
||||
|
||||
return [
|
||||
...createTaskReadTools(taskStore),
|
||||
createWorkflowListTool(taskStore),
|
||||
createWorkflowGetTool(taskStore),
|
||||
createWorkflowCreateTool(taskStore, { stripApprovalFlags: true }),
|
||||
createWorkflowUpdateTool(taskStore, { stripApprovalFlags: true }),
|
||||
createWorkflowDeleteTool(taskStore),
|
||||
createWorkflowSettingsTool(taskStore),
|
||||
createTraitListTool(),
|
||||
createAskQuestionTool(),
|
||||
...researchTools,
|
||||
@@ -3541,6 +3551,8 @@ export class HeartbeatMonitor {
|
||||
}
|
||||
|
||||
tools.push(...this.createSharedHeartbeatWorkTools(taskStore));
|
||||
tools.push(createWorkflowSelectTool(taskStore, taskId));
|
||||
tools.push(createTaskPromoteTool(taskStore, taskId));
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user