feat(FN-3059): align provider metadata and documentation
Merged feat(FN-3059) which aligns provider metadata and documentation across the codebase, updating README and getting-started docs plus refinements to the CustomProviderForm and ProviderIcon dashboard components. Fusion-Task-Id: FN-3059
This commit is contained in:
@@ -2886,8 +2886,8 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(callArgs.systemPrompt).toContain("fn_task_document_write");
|
||||
expect(callArgs.tools).toBe("readonly");
|
||||
// Tools: fn_task_create, fn_task_log, fn_task_document_write, fn_task_document_read, fn_list_agents, fn_delegate_task,
|
||||
// fn_memory_search, fn_memory_get, fn_memory_append, fn_identity, fn_heartbeat_done
|
||||
expect(callArgs.customTools).toHaveLength(11);
|
||||
// fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
|
||||
expect(callArgs.customTools).toHaveLength(10);
|
||||
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");
|
||||
@@ -2897,10 +2897,8 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(callArgs.customTools![6]!.name).toBe("fn_memory_search");
|
||||
expect(callArgs.customTools![7]!.name).toBe("fn_memory_get");
|
||||
expect(callArgs.customTools![8]!.name).toBe("fn_memory_append");
|
||||
// fn_identity appears before fn_heartbeat_done
|
||||
expect(callArgs.customTools![9]!.name).toBe("fn_identity");
|
||||
// fn_heartbeat_done is last (terminal tool)
|
||||
expect(callArgs.customTools![10]!.name).toBe("fn_heartbeat_done");
|
||||
expect(callArgs.customTools![9]!.name).toBe("fn_heartbeat_done");
|
||||
});
|
||||
|
||||
it("includes memory instructions even when agent has no custom instructions", async () => {
|
||||
@@ -6053,7 +6051,7 @@ describe("HeartbeatMonitor observability — prompt persistence + run-scoped log
|
||||
expect(savedRun.heartbeatProcedureSource).toBe("default");
|
||||
|
||||
// The execution prompt should contain the procedure text before the no-task action menu
|
||||
expect(savedRun.executionPrompt).toContain("fn_identity");
|
||||
expect(savedRun.executionPrompt).toContain("Identity Snapshot");
|
||||
expect(savedRun.executionPrompt).toContain("Heartbeat Procedure");
|
||||
// The wake delta header should appear before the action menu items
|
||||
const procedureIdx = savedRun.executionPrompt!.indexOf("Heartbeat Procedure");
|
||||
@@ -6114,34 +6112,26 @@ describe("HeartbeatMonitor observability — prompt persistence + run-scoped log
|
||||
expect(taskDescIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(procedureIdx).toBeLessThan(taskDescIdx);
|
||||
|
||||
// fn_identity instruction should appear in the execution prompt
|
||||
expect(savedRun.executionPrompt).toContain("fn_identity");
|
||||
// Identity Snapshot should appear in the execution prompt
|
||||
expect(savedRun.executionPrompt).toContain("## Identity Snapshot");
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
});
|
||||
|
||||
it("fn_identity tool returns correct agent identity information", async () => {
|
||||
it("does not register a fn_identity tool (removed in favor of inline snapshot)", async () => {
|
||||
const store = createStoreWithAgent({ soul: "I am a senior executor.", memory: "Always log blockers." });
|
||||
let capturedIdentityTool: any;
|
||||
let capturedTools: any[] | undefined;
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedIdentityTool = opts.customTools?.find((t: any) => t.name === "fn_identity");
|
||||
capturedTools = opts.customTools;
|
||||
return { session: mockSession as any };
|
||||
});
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
|
||||
|
||||
expect(capturedIdentityTool).toBeDefined();
|
||||
expect(capturedIdentityTool.name).toBe("fn_identity");
|
||||
|
||||
// Call the tool and verify output structure
|
||||
const toolResult = await capturedIdentityTool.execute("call-1", {});
|
||||
expect(toolResult.content[0].text).toContain("agentId: agent-001");
|
||||
expect(toolResult.content[0].text).toContain("name: Test Agent");
|
||||
expect(toolResult.details.soulPresent).toBe(true);
|
||||
expect(toolResult.details.memoryPresent).toBe(true);
|
||||
expect(toolResult.details.soulPreview).toContain("I am a senior executor.");
|
||||
expect(capturedTools).toBeDefined();
|
||||
expect(capturedTools!.find((t) => t.name === "fn_identity")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("inlines the Identity Snapshot block into the execution prompt for runtime-agnostic delivery", async () => {
|
||||
@@ -6167,12 +6157,15 @@ describe("HeartbeatMonitor observability — prompt persistence + run-scoped log
|
||||
|
||||
// Snapshot header + identity fields appear in the execution prompt body itself,
|
||||
// so non-pi runtimes (openclaw/hermes/paperclip) that may not propagate
|
||||
// customTools still see the agent's identity every tick.
|
||||
// customTools still see the agent's identity every tick. Snapshot carries
|
||||
// presence flags + content hashes only — full content lives in the system
|
||||
// prompt's Custom Instructions section.
|
||||
expect(exec).toContain("## Identity Snapshot");
|
||||
expect(exec).toContain("- agentId: agent-001");
|
||||
expect(exec).toContain("- soul: loaded");
|
||||
expect(exec).toContain("- memory: loaded");
|
||||
expect(exec).toContain("I keep momentum across stalled tasks.");
|
||||
expect(exec).toMatch(/- soul: loaded \(\d+ chars, sha256:[0-9a-f]{8}\)/);
|
||||
expect(exec).toMatch(/- memory: loaded \(\d+ chars, sha256:[0-9a-f]{8}\)/);
|
||||
// Snapshot must NOT contain full preview content (that lives in the system prompt)
|
||||
expect(exec).not.toContain("I keep momentum across stalled tasks.");
|
||||
|
||||
// Snapshot must precede the Wake Delta and the Heartbeat Procedure
|
||||
const snapIdx = exec.indexOf("## Identity Snapshot");
|
||||
|
||||
@@ -21,7 +21,8 @@ import type { AgentStore, AgentHeartbeatRun, HeartbeatInvocationSource, AgentHea
|
||||
import { buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity } from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createSendMessageTool, createReadMessagesTool, createMemoryTools, createIdentityTool, taskCreateParams } from "./agent-tools.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createSendMessageTool, createReadMessagesTool, createMemoryTools, taskCreateParams } from "./agent-tools.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
import { resolveAgentInstructionsWithRatings, buildSystemPromptWithInstructions, resolveAgentHeartbeatProcedure } from "./agent-instructions.js";
|
||||
import { heartbeatLog, formatError } from "./logger.js";
|
||||
@@ -295,9 +296,8 @@ export const HEARTBEAT_PROCEDURE = `## Heartbeat Procedure (run every tick, in o
|
||||
1. **Identity & context** — review the **Identity Snapshot** at the top of
|
||||
this prompt. Confirm your role, soul, instructions, and memory match what
|
||||
you expect, and surface any anomalies in your first text output before
|
||||
doing anything else. (If fn_identity is available in your runtime you may
|
||||
also call it for full structured detail; the snapshot above is the
|
||||
authoritative source.)
|
||||
doing anything else. The full content is in the Custom Instructions
|
||||
section of your system prompt.
|
||||
2. **Inbox** — when fn_read_messages is available, call it. Process any pending
|
||||
messages first; reply with reply_to_message_id when answering.
|
||||
3. **Wake delta** — read the Wake Delta block above. The wake reason is the
|
||||
@@ -328,9 +328,8 @@ export const HEARTBEAT_NO_TASK_PROCEDURE = `## Heartbeat Procedure (run every ti
|
||||
1. **Identity & context** — review the **Identity Snapshot** at the top of
|
||||
this prompt. Confirm your role, soul, instructions, and memory match what
|
||||
you expect, and surface any anomalies in your first text output before
|
||||
doing anything else. (If fn_identity is available in your runtime you may
|
||||
also call it for full structured detail; the snapshot above is the
|
||||
authoritative source.)
|
||||
doing anything else. The full content is in the Custom Instructions
|
||||
section of your system prompt.
|
||||
2. **Inbox** — when fn_read_messages is available, call it. Process any pending
|
||||
messages first; reply with reply_to_message_id when answering.
|
||||
3. **Wake delta** — read the Wake Delta block above. The wake reason is the
|
||||
@@ -372,49 +371,43 @@ function truncatePrompt(text: string, maxChars: number): string {
|
||||
* wrap external CLIs and may not propagate JS `customTools` callbacks to the
|
||||
* underlying agent. Embedding the snapshot in the prompt body guarantees the
|
||||
* agent always sees its identity regardless of runtime tool support.
|
||||
* `fn_identity` remains available as a richer optional read for runtimes
|
||||
* that DO support custom tools.
|
||||
*
|
||||
* The full soul/instructions/memory content is already loaded in the system
|
||||
* prompt's Custom Instructions section. The snapshot intentionally carries
|
||||
* only presence flags + 8-char content hashes — enough to detect drift or
|
||||
* misload, without paying a multi-KB preview tax on every tick.
|
||||
*/
|
||||
function shortContentHash(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex").slice(0, 8);
|
||||
}
|
||||
|
||||
function buildIdentitySnapshot(args: {
|
||||
agent: Agent;
|
||||
resolvedInstructions: string;
|
||||
}): string {
|
||||
const { agent, resolvedInstructions } = args;
|
||||
const SOUL_PREVIEW = 500;
|
||||
const INSTR_PREVIEW = 1000;
|
||||
const MEM_PREVIEW = 1000;
|
||||
|
||||
const soulPresent = typeof agent.soul === "string" && agent.soul.trim().length > 0;
|
||||
const instrPresent = resolvedInstructions.trim().length > 0;
|
||||
const memPresent = typeof agent.memory === "string" && agent.memory.trim().length > 0;
|
||||
const soulTrimmed = typeof agent.soul === "string" ? agent.soul.trim() : "";
|
||||
const instrTrimmed = resolvedInstructions.trim();
|
||||
const memTrimmed = typeof agent.memory === "string" ? agent.memory.trim() : "";
|
||||
|
||||
const lines: string[] = [
|
||||
const formatField = (trimmed: string): string => {
|
||||
if (!trimmed) return "absent";
|
||||
return `loaded (${trimmed.length} chars, sha256:${shortContentHash(trimmed)})`;
|
||||
};
|
||||
|
||||
return [
|
||||
"## Identity Snapshot",
|
||||
"",
|
||||
"Verify these match what you expect. Surface any anomalies in your first text output before acting.",
|
||||
"Full content is in the Custom Instructions section of your system prompt. Surface anomalies in your first text output before acting.",
|
||||
"",
|
||||
`- agentId: ${agent.id}`,
|
||||
`- name: ${agent.name}`,
|
||||
`- role: ${agent.role}`,
|
||||
`- soul: ${soulPresent ? "loaded" : "absent"}`,
|
||||
`- instructions: ${instrPresent ? "loaded" : "absent"}`,
|
||||
`- memory: ${memPresent ? "loaded" : "absent"}`,
|
||||
];
|
||||
|
||||
if (soulPresent) {
|
||||
const preview = (agent.soul as string).trim().slice(0, SOUL_PREVIEW);
|
||||
lines.push("", `### Soul (first ${SOUL_PREVIEW} chars)`, preview);
|
||||
}
|
||||
if (instrPresent) {
|
||||
const preview = resolvedInstructions.trim().slice(0, INSTR_PREVIEW);
|
||||
lines.push("", `### Instructions (first ${INSTR_PREVIEW} chars)`, preview);
|
||||
}
|
||||
if (memPresent) {
|
||||
const preview = (agent.memory as string).trim().slice(0, MEM_PREVIEW);
|
||||
lines.push("", `### Memory (first ${MEM_PREVIEW} chars)`, preview);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
`- soul: ${formatField(soulTrimmed)}`,
|
||||
`- instructions: ${formatField(instrTrimmed)}`,
|
||||
`- memory: ${formatField(memTrimmed)}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function getHeartbeatMemorySettings(taskStore: TaskStore): Promise<Settings | undefined> {
|
||||
@@ -1415,9 +1408,6 @@ export class HeartbeatMonitor {
|
||||
[resolvedInstructionsForIdentity, memoryInstructions].filter((part) => part.trim()).join("\n\n"),
|
||||
);
|
||||
|
||||
// Register fn_identity tool before fn_heartbeat_done (which must stay last)
|
||||
heartbeatTools.push(createIdentityTool({ agent, resolvedInstructions: resolvedInstructionsForIdentity }));
|
||||
|
||||
// fn_heartbeat_done must be the last tool in the array (stable terminal signal)
|
||||
heartbeatTools.push(heartbeatDoneTool);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ 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, TaskCreateInput } from "@fusion/core";
|
||||
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, 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";
|
||||
@@ -1359,78 +1359,3 @@ export function createReadMessagesTool(messageStore: MessageStore, agentId: stri
|
||||
};
|
||||
}
|
||||
|
||||
/** Arguments for {@link createIdentityTool}. */
|
||||
export interface CreateIdentityToolArgs {
|
||||
/** The agent record for this heartbeat run. */
|
||||
agent: Agent;
|
||||
/** The resolved instructions string (from resolveAgentInstructionsWithRatings). */
|
||||
resolvedInstructions: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `fn_identity` tool for heartbeat sessions.
|
||||
*
|
||||
* When called, it returns a structured summary of which soul, instructions, and
|
||||
* memory are currently loaded for this tick. The agent is expected to call this
|
||||
* as its FIRST tool action so operators (via dashboard run logs) can verify
|
||||
* correct identity was applied.
|
||||
*/
|
||||
export function createIdentityTool({ agent, resolvedInstructions }: CreateIdentityToolArgs): ToolDefinition {
|
||||
const identityParams = Type.Object({});
|
||||
return {
|
||||
name: "fn_identity",
|
||||
label: "Identity Check",
|
||||
description: "Return a structured summary of which soul, instructions, and memory are loaded for this heartbeat tick. Call this FIRST before any other tool.",
|
||||
parameters: identityParams,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: async (_id: string, _params: Static<typeof identityParams>, _signal?: any, _onUpdate?: any, _ctx?: any) => {
|
||||
const PREVIEW_CHARS = 500;
|
||||
const INSTRUCTIONS_PREVIEW_CHARS = 1000;
|
||||
const MEMORY_PREVIEW_CHARS = 1000;
|
||||
|
||||
const soulPresent = typeof agent.soul === "string" && agent.soul.trim().length > 0;
|
||||
const instructionsPresent = resolvedInstructions.trim().length > 0;
|
||||
const memoryPresent = typeof agent.memory === "string" && agent.memory.trim().length > 0;
|
||||
|
||||
const soulPreview = soulPresent ? (agent.soul as string).slice(0, PREVIEW_CHARS) : "";
|
||||
const instructionsPreview = instructionsPresent ? resolvedInstructions.slice(0, INSTRUCTIONS_PREVIEW_CHARS) : "";
|
||||
const memoryPreview = memoryPresent ? (agent.memory as string).slice(0, MEMORY_PREVIEW_CHARS) : "";
|
||||
|
||||
const result = {
|
||||
agentId: agent.id,
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
soulPresent,
|
||||
instructionsPresent,
|
||||
memoryPresent,
|
||||
soulPreview,
|
||||
instructionsPreview,
|
||||
memoryPreview,
|
||||
};
|
||||
|
||||
const lines = [
|
||||
`agentId: ${result.agentId}`,
|
||||
`name: ${result.name}`,
|
||||
`role: ${result.role}`,
|
||||
`soul: ${result.soulPresent ? "loaded" : "absent"}`,
|
||||
`instructions: ${result.instructionsPresent ? "loaded" : "absent"}`,
|
||||
`memory: ${result.memoryPresent ? "loaded" : "absent"}`,
|
||||
];
|
||||
|
||||
if (result.soulPresent && result.soulPreview) {
|
||||
lines.push(`\nSoul preview (first ${PREVIEW_CHARS} chars):\n${result.soulPreview}`);
|
||||
}
|
||||
if (result.instructionsPresent && result.instructionsPreview) {
|
||||
lines.push(`\nInstructions preview (first ${INSTRUCTIONS_PREVIEW_CHARS} chars):\n${result.instructionsPreview}`);
|
||||
}
|
||||
if (result.memoryPresent && result.memoryPreview) {
|
||||
lines.push(`\nMemory preview (first ${MEMORY_PREVIEW_CHARS} chars):\n${result.memoryPreview}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text: lines.join("\n") }],
|
||||
details: result,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user