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:
@@ -55,5 +55,4 @@ These tools are **not** part of the pi extension's user-invokable `extension.ts`
|
||||
|
||||
| Tool | Purpose | Parameters |
|
||||
|---|---|---|
|
||||
| `fn_identity` | Return loaded soul/instructions/memory summary for this heartbeat tick (must be called first) | none |
|
||||
| `fn_heartbeat_done` | Signal end of heartbeat run with optional summary | `summary?` (string) |
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useState } from "react";
|
||||
import type { CustomProviderConfig, CustomProviderModelInput } from "../api";
|
||||
import "./CustomProviderForm.css";
|
||||
|
||||
// Keep in sync with BUILT_IN_PROVIDER_IDS in register-custom-provider-routes.ts
|
||||
// Reserved built-in IDs (including hidden/deprecated aliases) to prevent custom-provider collisions.
|
||||
export const BUILT_IN_PROVIDER_IDS = new Set<string>([
|
||||
"anthropic", "claude-cli", "pi-claude-cli", "openai", "openai-codex", "google", "gemini", "google-antigravity",
|
||||
"antigravity", "google-vertex", "vertex", "google-cloud-code", "cloud-code", "google-gemini-cli", "google-generative-ai",
|
||||
|
||||
@@ -26,6 +26,7 @@ import { DroidCliProviderCard } from "./DroidCliProviderCard";
|
||||
import { LoginInstructions } from "./LoginInstructions";
|
||||
import { CustomProviderForm } from "./CustomProviderForm";
|
||||
import { appendTokenQuery } from "../auth";
|
||||
import { filterVisibleOnboardingAndSettingsProviders } from "./providerVisibility";
|
||||
|
||||
const mapLegacyCustomProviderToConfig = (
|
||||
provider: CustomProvider | CustomProviderConfig,
|
||||
@@ -736,12 +737,13 @@ export function ModelOnboardingModal({
|
||||
const loadAuthStatus = useCallback(async () => {
|
||||
try {
|
||||
const { providers, ghCli } = await fetchAuthStatus();
|
||||
setAuthProviders(providers);
|
||||
const visibleProviders = filterVisibleOnboardingAndSettingsProviders(providers);
|
||||
setAuthProviders(visibleProviders);
|
||||
setGhCliStatus(ghCli);
|
||||
setLoginInstructions((prev) => {
|
||||
const next: Record<string, string> = {};
|
||||
for (const [providerId, instructions] of Object.entries(prev)) {
|
||||
const provider = providers.find((candidate) => candidate.id === providerId);
|
||||
const provider = visibleProviders.find((candidate) => candidate.id === providerId);
|
||||
if (provider && !provider.authenticated && provider.loginInProgress) {
|
||||
next[providerId] = instructions;
|
||||
}
|
||||
@@ -755,7 +757,7 @@ export function ModelOnboardingModal({
|
||||
if (outcome !== "pending") {
|
||||
continue;
|
||||
}
|
||||
const provider = providers.find((candidate) => candidate.id === providerId);
|
||||
const provider = visibleProviders.find((candidate) => candidate.id === providerId);
|
||||
if (!provider?.loginInProgress) {
|
||||
delete next[providerId];
|
||||
changed = true;
|
||||
@@ -766,7 +768,7 @@ export function ModelOnboardingModal({
|
||||
// Remove from skippedProviders when a provider becomes authenticated
|
||||
setSkippedProviders((prev) => {
|
||||
const updated = { ...prev };
|
||||
for (const p of providers) {
|
||||
for (const p of visibleProviders) {
|
||||
if (p.authenticated && updated[p.id]) {
|
||||
delete updated[p.id];
|
||||
}
|
||||
@@ -1129,9 +1131,10 @@ export function ModelOnboardingModal({
|
||||
|
||||
try {
|
||||
const { providers, ghCli } = await fetchAuthStatus();
|
||||
setAuthProviders(providers);
|
||||
const visibleProviders = filterVisibleOnboardingAndSettingsProviders(providers);
|
||||
setAuthProviders(visibleProviders);
|
||||
setGhCliStatus(ghCli);
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
const provider = visibleProviders.find((p) => p.id === providerId);
|
||||
if (provider?.authenticated) {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
|
||||
@@ -659,12 +659,14 @@ const providerConfig: Record<
|
||||
|
||||
google: { component: GeminiIcon, color: "var(--provider-gemini)" },
|
||||
gemini: { component: GeminiIcon, color: "var(--provider-gemini)" }, // Gemini alias family
|
||||
// Deprecated upstream in pi-coding-agent 0.71+, retained for legacy usage/auth history rendering.
|
||||
"google-antigravity": { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Gemini" },
|
||||
antigravity: { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Gemini" },
|
||||
"google-vertex": { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Vertex AI" },
|
||||
vertex: { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Vertex AI" },
|
||||
"google-cloud-code": { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Cloud Code" },
|
||||
"cloud-code": { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Cloud Code" },
|
||||
// Deprecated upstream in pi-coding-agent 0.71+, retained for legacy usage/auth history rendering.
|
||||
"google-gemini-cli": { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Gemini CLI" },
|
||||
"google-generative-ai": { component: GeminiIcon, color: "var(--provider-gemini)", label: "Google Generative AI" },
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import { appendTokenQuery } from "../auth";
|
||||
import { useConfirm } from "../hooks/useConfirm";
|
||||
import { useNodes } from "../hooks/useNodes";
|
||||
import { NodeHealthDot } from "./NodeHealthDot";
|
||||
import { filterVisibleOnboardingAndSettingsProviders } from "./providerVisibility";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitHub star count — fetched once per session, cached in localStorage (1 h).
|
||||
@@ -654,11 +655,12 @@ export function SettingsModal({
|
||||
const loadAuthStatus = useCallback(async () => {
|
||||
try {
|
||||
const { providers } = await fetchAuthStatus();
|
||||
setAuthProviders(providers);
|
||||
const visibleProviders = filterVisibleOnboardingAndSettingsProviders(providers);
|
||||
setAuthProviders(visibleProviders);
|
||||
setLoginInstructions((prev) => {
|
||||
const next: Record<string, string> = {};
|
||||
for (const [providerId, instructions] of Object.entries(prev)) {
|
||||
const provider = providers.find((candidate) => candidate.id === providerId);
|
||||
const provider = visibleProviders.find((candidate) => candidate.id === providerId);
|
||||
if (provider && !provider.authenticated) {
|
||||
next[providerId] = instructions;
|
||||
}
|
||||
@@ -910,8 +912,9 @@ export function SettingsModal({
|
||||
pollIntervalRef.current = setInterval(async () => {
|
||||
try {
|
||||
const { providers } = await fetchAuthStatus();
|
||||
setAuthProviders(providers);
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
const visibleProviders = filterVisibleOnboardingAndSettingsProviders(providers);
|
||||
setAuthProviders(visibleProviders);
|
||||
const provider = visibleProviders.find((p) => p.id === providerId);
|
||||
if (provider?.authenticated) {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
|
||||
@@ -249,6 +249,26 @@ describe("ModelOnboardingModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hides deprecated google CLI and antigravity providers while keeping supported Google/Gemini entries", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "google", name: "Google", authenticated: false, type: "api_key" },
|
||||
{ id: "gemini", name: "Gemini", authenticated: false, type: "api_key" },
|
||||
{ id: "google-antigravity", name: "Google Antigravity", authenticated: false, type: "oauth" },
|
||||
{ id: "antigravity", name: "Antigravity", authenticated: false, type: "oauth" },
|
||||
{ id: "google-gemini-cli", name: "Google Gemini CLI", authenticated: false, type: "cli" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
expect(await screen.findByTestId("onboarding-provider-card-google")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("onboarding-provider-card-gemini")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("onboarding-provider-card-google-antigravity")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("onboarding-provider-card-antigravity")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Google Gemini CLI")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Back and Next buttons on middle steps", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
|
||||
@@ -827,6 +827,27 @@ describe("SettingsModal", () => {
|
||||
expect(screen.getByTestId("auth-status-openai")).toHaveTextContent("✗ Not connected");
|
||||
});
|
||||
|
||||
it("hides deprecated Google CLI and antigravity auth providers", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "google", name: "Google", authenticated: false, type: "api_key" },
|
||||
{ id: "gemini", name: "Gemini", authenticated: false, type: "api_key" },
|
||||
{ id: "google-antigravity", name: "Google Antigravity", authenticated: false, type: "oauth" },
|
||||
{ id: "antigravity", name: "Antigravity", authenticated: false, type: "oauth" },
|
||||
{ id: "google-gemini-cli", name: "Google Gemini CLI", authenticated: false, type: "cli" },
|
||||
],
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.getByTestId("auth-provider-icon-google")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-provider-icon-gemini")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("auth-provider-icon-google-antigravity")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("auth-provider-icon-antigravity")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Google Gemini CLI")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("scrolls settings content to top after OAuth login succeeds", async () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
mockLoginProvider.mockResolvedValue({ url: "https://example.com/auth", instructions: "" });
|
||||
|
||||
17
packages/dashboard/app/components/providerVisibility.ts
Normal file
17
packages/dashboard/app/components/providerVisibility.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const HIDDEN_ONBOARDING_AND_SETTINGS_PROVIDER_IDS = new Set([
|
||||
"google-antigravity",
|
||||
"antigravity",
|
||||
"google-gemini-cli",
|
||||
]);
|
||||
|
||||
export function isProviderVisibleInOnboardingAndSettings(providerId: string): boolean {
|
||||
return !HIDDEN_ONBOARDING_AND_SETTINGS_PROVIDER_IDS.has(providerId);
|
||||
}
|
||||
|
||||
export function filterVisibleOnboardingAndSettingsProviders<T extends { id: string }>(
|
||||
providers: T[],
|
||||
): T[] {
|
||||
return providers.filter((provider) => isProviderVisibleInOnboardingAndSettings(provider.id));
|
||||
}
|
||||
|
||||
export { HIDDEN_ONBOARDING_AND_SETTINGS_PROVIDER_IDS };
|
||||
@@ -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