fix(FN-2077): use no-task prompt for ambient heartbeat runs
- Add HEARTBEAT_NO_TASK_SYSTEM_PROMPT for no-task heartbeat sessions with only non-task-scoped tooling guidance - Switch heartbeat session setup to choose the no-task base prompt when executing without task context - Keep agent instruction and memory enrichment on top of the selected base prompt for both task and no-task runs - Extend heartbeat tests to verify no-task prompts exclude task-scoped tool references and are passed into createKbAgent
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import { HeartbeatMonitor, HeartbeatTriggerScheduler, isBlockedStateDuplicate, type AgentSession, type HeartbeatExecutionOptions, HEARTBEAT_SYSTEM_PROMPT } from "./agent-heartbeat.js";
|
import {
|
||||||
|
HeartbeatMonitor,
|
||||||
|
HeartbeatTriggerScheduler,
|
||||||
|
isBlockedStateDuplicate,
|
||||||
|
type AgentSession,
|
||||||
|
type HeartbeatExecutionOptions,
|
||||||
|
HEARTBEAT_SYSTEM_PROMPT,
|
||||||
|
HEARTBEAT_NO_TASK_SYSTEM_PROMPT,
|
||||||
|
} from "./agent-heartbeat.js";
|
||||||
import { AgentLogger } from "./agent-logger.js";
|
import { AgentLogger } from "./agent-logger.js";
|
||||||
import type { AgentStore, AgentHeartbeatRun, TaskStore, TaskDetail, Agent, MessageStore, Message, AgentBudgetStatus } from "@fusion/core";
|
import type { AgentStore, AgentHeartbeatRun, TaskStore, TaskDetail, Agent, MessageStore, Message, AgentBudgetStatus } from "@fusion/core";
|
||||||
|
|
||||||
@@ -1363,6 +1371,14 @@ describe("HeartbeatMonitor", () => {
|
|||||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||||
|
|
||||||
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
expect(mockedCreateKbAgent).toHaveBeenCalledOnce();
|
||||||
|
const callArgs = mockedCreateKbAgent.mock.calls[0]![0]!;
|
||||||
|
const systemPrompt = callArgs.systemPrompt;
|
||||||
|
expect(systemPrompt).not.toContain("task_log");
|
||||||
|
expect(systemPrompt).not.toContain("task_document_write");
|
||||||
|
expect(systemPrompt).not.toContain("task_document_read");
|
||||||
|
expect(systemPrompt).toContain("task_create");
|
||||||
|
expect(systemPrompt).toContain("heartbeat_done");
|
||||||
|
|
||||||
// The execution prompt is passed to session.prompt by promptWithFallback mock
|
// The execution prompt is passed to session.prompt by promptWithFallback mock
|
||||||
const promptCalls = mockSession.prompt.mock.calls;
|
const promptCalls = mockSession.prompt.mock.calls;
|
||||||
expect(promptCalls.length).toBeGreaterThan(0);
|
expect(promptCalls.length).toBeGreaterThan(0);
|
||||||
@@ -2184,6 +2200,13 @@ describe("HeartbeatMonitor", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("execution", () => {
|
describe("execution", () => {
|
||||||
|
it("no-task heartbeat system prompt does not reference task-scoped tools", () => {
|
||||||
|
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_log");
|
||||||
|
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_document_write");
|
||||||
|
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_document_read");
|
||||||
|
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_document");
|
||||||
|
});
|
||||||
|
|
||||||
it("creates session with enriched system prompt and expected tools", async () => {
|
it("creates session with enriched system prompt and expected tools", async () => {
|
||||||
const store = createStoreWithAgentForExec({
|
const store = createStoreWithAgentForExec({
|
||||||
soul: "Act like a practical teammate who prioritizes clarity.",
|
soul: "Act like a practical teammate who prioritizes clarity.",
|
||||||
|
|||||||
@@ -162,6 +162,44 @@ When sending messages:
|
|||||||
- Include relevant context (task IDs, file paths) in metadata when applicable.
|
- Include relevant context (task IDs, file paths) in metadata when applicable.
|
||||||
- Use agent-to-agent for inter-agent communication.`;
|
- Use agent-to-agent for inter-agent communication.`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System prompt for no-task heartbeat agent sessions.
|
||||||
|
* Instructs the agent to perform ambient work only with tools that do not require task context.
|
||||||
|
*/
|
||||||
|
export const HEARTBEAT_NO_TASK_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window.
|
||||||
|
|
||||||
|
Your job:
|
||||||
|
1. Do ONE useful action: analyze, review, create follow-up tasks, or communicate findings.
|
||||||
|
2. Use task_create to spawn follow-up work as needed.
|
||||||
|
3. Use send_message and read_messages to process inter-agent communication.
|
||||||
|
4. Use memory_search and memory_append to leverage and persist useful memory context.
|
||||||
|
5. Call heartbeat_done when finished with an optional summary of what was accomplished.
|
||||||
|
|
||||||
|
Keep work lightweight — this is a single-pass check, not a full implementation run.
|
||||||
|
You have readonly file access plus task_create, list_agents, delegate_task, memory_search, memory_append, send_message, read_messages, and heartbeat_done tools.
|
||||||
|
|
||||||
|
## Memory Boundaries
|
||||||
|
|
||||||
|
You may receive an Agent Memory section and a Project Memory section.
|
||||||
|
- Agent Memory is specific to you, including imported and user-created agents such as CEO-style coordinator agents. It has its own long-term memory, daily notes, dreams, and qmd-backed retrieval under .fusion/agent-memory/{agentId}/.
|
||||||
|
- Project Memory is the workspace memory system under .fusion/memory/ with long-term memory, daily notes, dreams, and qmd-backed retrieval.
|
||||||
|
- Keep these separate: do not copy personal agent operating notes into Project Memory unless they are genuinely useful to every future agent in this workspace.
|
||||||
|
|
||||||
|
## Processing Messages
|
||||||
|
|
||||||
|
When you are woken by an incoming message (source includes "wake-on-message"), you should:
|
||||||
|
1. Use read_messages to check your inbox for unread messages.
|
||||||
|
2. Review each message and determine the appropriate action:
|
||||||
|
- If the message requires a response, use send_message to reply.
|
||||||
|
- If the message is informational, acknowledge it with a brief response via send_message.
|
||||||
|
- If the message requests work, create a follow-up task with task_create or handle it directly.
|
||||||
|
3. After processing messages, continue with your normal heartbeat duties.
|
||||||
|
|
||||||
|
When sending messages:
|
||||||
|
- Be concise and clear about what you need or what you've done.
|
||||||
|
- Include relevant context (task IDs, file paths) in metadata when applicable.
|
||||||
|
- Use agent-to-agent for inter-agent communication.`;
|
||||||
|
|
||||||
/** Parameter schema for the heartbeat_done tool */
|
/** Parameter schema for the heartbeat_done tool */
|
||||||
const heartbeatDoneParams = Type.Object({
|
const heartbeatDoneParams = Type.Object({
|
||||||
summary: Type.Optional(Type.String({ description: "Summary of what was accomplished this heartbeat" })),
|
summary: Type.Optional(Type.String({ description: "Summary of what was accomplished this heartbeat" })),
|
||||||
@@ -1069,14 +1107,17 @@ export class HeartbeatMonitor {
|
|||||||
// Build skill selection context for heartbeat session (uses waking agent's skills, no role fallback)
|
// Build skill selection context for heartbeat session (uses waking agent's skills, no role fallback)
|
||||||
const skillContext = buildSessionSkillContextSync(agent, "heartbeat", rootDir);
|
const skillContext = buildSessionSkillContextSync(agent, "heartbeat", rootDir);
|
||||||
|
|
||||||
let systemPrompt = HEARTBEAT_SYSTEM_PROMPT;
|
const baseHeartbeatSystemPrompt = isNoTaskRun
|
||||||
|
? HEARTBEAT_NO_TASK_SYSTEM_PROMPT
|
||||||
|
: HEARTBEAT_SYSTEM_PROMPT;
|
||||||
|
let systemPrompt = baseHeartbeatSystemPrompt;
|
||||||
try {
|
try {
|
||||||
const agentInstructions = await resolveAgentInstructionsWithRatings(agent, rootDir, this.store);
|
const agentInstructions = await resolveAgentInstructionsWithRatings(agent, rootDir, this.store);
|
||||||
const memoryInstructions = memorySettings?.memoryEnabled === false
|
const memoryInstructions = memorySettings?.memoryEnabled === false
|
||||||
? ""
|
? ""
|
||||||
: buildExecutionMemoryInstructions(rootDir, memorySettings);
|
: buildExecutionMemoryInstructions(rootDir, memorySettings);
|
||||||
systemPrompt = buildSystemPromptWithInstructions(
|
systemPrompt = buildSystemPromptWithInstructions(
|
||||||
HEARTBEAT_SYSTEM_PROMPT,
|
baseHeartbeatSystemPrompt,
|
||||||
[agentInstructions, memoryInstructions].filter((part) => part.trim()).join("\n\n"),
|
[agentInstructions, memoryInstructions].filter((part) => part.trim()).join("\n\n"),
|
||||||
);
|
);
|
||||||
} catch (instructionError) {
|
} catch (instructionError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user