feat(FN-3459): enable tool mode for heartbeat agent sessions
This merge enables heartbeat agents to use coding sessions with tool access (FN-3459). It adds heartbeat session prompt tests, updates the heartbeat executor to support tool mode, modifies dashboard node components and hooks, and includes a fix for a typecheck regression. A changeset is included for Fusion-Task-Id: FN-3459
This commit is contained in:
5
.changeset/fn-3459-agent-tool-access.md
Normal file
5
.changeset/fn-3459-agent-tool-access.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Give autonomous heartbeat agent sessions coding-capable workspace tools (read/write/edit/bash within worktree boundaries) while preserving heartbeat-specific custom tools and readonly safety for non-heartbeat readonly flows.
|
||||
@@ -456,6 +456,10 @@ Heartbeat runs are composed from multiple prompt layers so each wake has full id
|
||||
1. **System prompt**
|
||||
- Task-scoped runs use the task heartbeat system prompt.
|
||||
- No-task runs use the ambient/no-task heartbeat system prompt (tool-aligned: no task-scoped tools).
|
||||
2. **Workspace tool mode**
|
||||
- Heartbeat sessions are created with coding-capable workspace tools (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`) inside worktree boundary guards.
|
||||
- Heartbeat behavior still stays lightweight: one concrete action per run, then `fn_heartbeat_done`.
|
||||
- Engine-owned heartbeat tools are still layered on top (task creation/log/docs for task-scoped runs; ambient/delegation/memory tools for no-task runs).
|
||||
2. **Agent identity and instructions bundle**
|
||||
- Inline instructions (`instructionsText`)
|
||||
- File-backed instructions (`instructionsPath`)
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { ContainerStatusInfo, DockerNodeConfigInfo as DockerNodeConfig, ManagedDockerNodeInfo, NodeInfo, NodeUpdateInput, ProjectInfo } from "../api";
|
||||
import type { ContainerStatusInfo, DockerNodeConfigInfo, ManagedDockerNodeInfo, NodeInfo, NodeUpdateInput, ProjectInfo } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { getProjectsForNode } from "../utils/nodeProjectAssignment";
|
||||
import type { ComputedNodeSyncStatus } from "../hooks/useNodeSettingsSync";
|
||||
@@ -595,7 +595,7 @@ export function NodeDetailModal({
|
||||
<details>
|
||||
<summary>Volume Mounts</summary>
|
||||
<div className="node-detail-modal__docker-list">
|
||||
{dockerConfigDraft.volumeMounts.map((mount, index) => (
|
||||
{dockerConfigDraft.volumeMounts.map((mount: DockerNodeConfigInfo["volumeMounts"][number], index: number) => (
|
||||
<div key={`${mount.hostPath}-${mount.containerPath}-${index}`} className="node-detail-modal__docker-row">
|
||||
<input className="input" value={mount.hostPath} placeholder="Host path" onChange={(event) => {
|
||||
const next = [...dockerConfigDraft.volumeMounts];
|
||||
@@ -686,7 +686,7 @@ export function NodeDetailModal({
|
||||
<details>
|
||||
<summary>Extra CLIs</summary>
|
||||
<div className="node-detail-modal__docker-list">
|
||||
{(dockerConfigDraft.extraClis ?? []).map((cli, index) => (
|
||||
{(dockerConfigDraft.extraClis ?? []).map((cli: string, index: number) => (
|
||||
<div key={`${cli}-${index}`} className="node-detail-modal__docker-row">
|
||||
<input className="input" value={cli} onChange={(event) => {
|
||||
const next = [...(dockerConfigDraft.extraClis ?? [])];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import type { DockerNodeConfigInfo as DockerNodeConfig, NodeCreateInput, NodeInfo, NodeUpdateInput } from "../api";
|
||||
import type { DockerNodeConfigInfo, NodeCreateInput, NodeInfo, NodeUpdateInput } from "../api";
|
||||
import {
|
||||
fetchDockerConfigDiff,
|
||||
fetchDockerNodeConfig,
|
||||
|
||||
@@ -486,6 +486,7 @@ describe("executeHeartbeat", () => {
|
||||
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
|
||||
expect(callArgs.tools).toBe("coding");
|
||||
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
|
||||
|
||||
// Should have fn_task_create, fn_list_agents, fn_delegate_task
|
||||
@@ -1793,7 +1794,7 @@ describe("executeHeartbeat", () => {
|
||||
expect(callArgs.systemPrompt).toContain("fn_memory_search");
|
||||
expect(callArgs.systemPrompt).toContain("fn_task_log");
|
||||
expect(callArgs.systemPrompt).toContain("fn_task_document_write");
|
||||
expect(callArgs.tools).toBe("readonly");
|
||||
expect(callArgs.tools).toBe("coding");
|
||||
// Tools: fn_task_create, fn_task_log, fn_task_document_write, fn_task_document_read, fn_list_agents, fn_delegate_task,
|
||||
// fn_get_agent_config, fn_update_agent_config, fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
|
||||
expect(callArgs.customTools).toHaveLength(12);
|
||||
|
||||
@@ -88,6 +88,19 @@ describe("createHeartbeatTools", () => {
|
||||
mockTaskStore = createMockTaskStoreForTools();
|
||||
});
|
||||
|
||||
it("heartbeat task-scoped system prompt documents coding-capable workspace access", () => {
|
||||
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("coding-capable workspace tools");
|
||||
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_log");
|
||||
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_document_write");
|
||||
});
|
||||
|
||||
it("heartbeat no-task system prompt documents coding-capable workspace access without task-scoped tools", () => {
|
||||
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("coding-capable workspace tools");
|
||||
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_document_write");
|
||||
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_document_read");
|
||||
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_log");
|
||||
});
|
||||
|
||||
it("returns task, delegation, and agent-config tools", () => {
|
||||
const store = createMockStore();
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
@@ -171,7 +171,7 @@ Examples of ONE useful action:
|
||||
- DON'T: create vague tasks like "investigate stuff" without actionable scope.
|
||||
|
||||
Keep work lightweight — this is a single-pass check, not a full implementation run.
|
||||
You have readonly file access plus fn_task_create, fn_task_log, and fn_task_document tools.
|
||||
You have coding-capable workspace tools (read/write/edit/bash within worktree boundaries) plus fn_task_create, fn_task_log, and fn_task_document 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.
|
||||
@@ -253,7 +253,7 @@ Examples of ONE useful action:
|
||||
- DON'T: attempt implementation work that requires task-scoped tooling/context.
|
||||
|
||||
Keep work lightweight — this is a single-pass ambient check, not a full implementation run.
|
||||
You have readonly file access plus:
|
||||
You have coding-capable workspace tools (read/write/edit/bash within worktree boundaries) plus:
|
||||
- fn_task_create
|
||||
- fn_list_agents and fn_delegate_task
|
||||
- fn_get_agent_config and fn_update_agent_config (for direct reports only)
|
||||
@@ -934,7 +934,7 @@ export class HeartbeatMonitor {
|
||||
* Implements the Paperclip-style execution model:
|
||||
* 1. Wake — start a heartbeat run record
|
||||
* 2. Check inbox — resolve the agent's assigned task
|
||||
* 3. Work — run a lightweight agent session with readonly tools + fn_task_create/fn_task_log
|
||||
* 3. Work — run a lightweight agent session with coding-capable tools + fn_task_create/fn_task_log
|
||||
* 4. Exit — record results and complete the run
|
||||
*
|
||||
* Budget governance:
|
||||
@@ -1462,7 +1462,7 @@ export class HeartbeatMonitor {
|
||||
pluginRunner: this.pluginRunner,
|
||||
cwd: rootDir,
|
||||
systemPrompt,
|
||||
tools: "readonly",
|
||||
tools: "coding",
|
||||
customTools: heartbeatTools,
|
||||
...(() => {
|
||||
const { provider, modelId } = extractRuntimeModel(agent.runtimeConfig);
|
||||
|
||||
Reference in New Issue
Block a user