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:
Fusion
2026-05-01 17:39:47 -07:00
committed by gsxdsm
parent da19aa2634
commit 4d13ba01ea
15 changed files with 138 additions and 155 deletions

View File

@@ -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");

View File

@@ -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);

View File

@@ -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,
};
},
};
}