feat(FN-3320): add agent configuration tools for settings management

The merge completes FN-3320 by implementing configuration tools for the agent system (`agent-tools.ts`), wiring them through the executor, and adding test coverage (`agent-tools-config.test.ts`). Documentation was updated in `AGENTS.md` and the engine tools reference, with minor updates to the agent

Fusion-Task-Id: FN-3320
This commit is contained in:
Fusion
2026-05-04 15:21:40 -07:00
committed by gsxdsm
parent 6dfe7c5e09
commit 452c31a085
8 changed files with 486 additions and 13 deletions

View File

@@ -0,0 +1,231 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Agent, AgentStore } from "@fusion/core";
import { createGetAgentConfigTool, createUpdateAgentConfigTool } from "../agent-tools.js";
function createAgent(overrides: Partial<Agent> = {}): Agent {
const now = new Date().toISOString();
return {
id: "manager-1",
name: "Manager",
role: "executor",
state: "idle",
createdAt: now,
updatedAt: now,
metadata: {},
...overrides,
};
}
function createMockAgentStore(overrides: Partial<AgentStore> = {}): AgentStore {
return {
getAgent: vi.fn().mockResolvedValue(null),
updateAgent: vi.fn(),
...overrides,
} as unknown as AgentStore;
}
describe("createGetAgentConfigTool", () => {
let agentStore: AgentStore;
beforeEach(() => {
agentStore = createMockAgentStore();
});
it("returns full configuration for a direct report", async () => {
const report = createAgent({
id: "report-1",
reportsTo: "manager-1",
soul: "Careful and concise",
instructionsText: "Always verify with tests",
instructionsPath: ".fusion/instructions/report.md",
heartbeatProcedurePath: ".fusion/procedure.md",
memory: "Knows release pipeline",
runtimeConfig: {
heartbeatIntervalMs: 30000,
heartbeatTimeoutMs: 120000,
maxConcurrentRuns: 2,
messageResponseMode: "immediate",
budget: { dailyLimitUsd: 10 },
},
});
vi.mocked(agentStore.getAgent).mockResolvedValue(report);
const tool = createGetAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "report-1" }, undefined as never, undefined as never, undefined as never);
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("Agent Config: Manager (report-1)");
expect(text).toContain("Soul:\nCareful and concise");
expect(text).toContain("Instructions Text:\nAlways verify with tests");
expect(text).toContain("heartbeatIntervalMs: 30000");
expect(text).toContain("heartbeatTimeoutMs: 120000");
expect(text).toContain("maxConcurrentRuns: 2");
expect(text).toContain("messageResponseMode: immediate");
expect(text).toContain("Memory:\nKnows release pipeline");
expect(result.details).toEqual({ agent: report });
});
it("returns error when target agent not found", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(null);
const tool = createGetAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "missing" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: Agent missing not found");
});
it("returns error when target is not a direct report", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(createAgent({ id: "other", reportsTo: "another-manager" }));
const tool = createGetAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "other" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: You can only read configuration of agents that report to you");
});
it("returns error when target is the calling agent itself", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(createAgent({ id: "manager-1" }));
const tool = createGetAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "manager-1" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: You can only read configuration of agents that report to you");
});
});
describe("createUpdateAgentConfigTool", () => {
let agentStore: AgentStore;
beforeEach(() => {
agentStore = createMockAgentStore();
});
it("successfully updates soul on a direct report", async () => {
const report = createAgent({ id: "report-1", reportsTo: "manager-1" });
const updated = createAgent({ id: "report-1", reportsTo: "manager-1", soul: "New soul" });
vi.mocked(agentStore.getAgent).mockResolvedValue(report);
vi.mocked(agentStore.updateAgent).mockResolvedValue(updated);
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
await tool.execute("session", { agent_id: "report-1", soul: "New soul" }, undefined as never, undefined as never, undefined as never);
expect(agentStore.updateAgent).toHaveBeenCalledWith("report-1", { soul: "New soul" });
});
it("successfully updates instructionsText on a direct report", async () => {
const report = createAgent({ id: "report-1", reportsTo: "manager-1" });
const updated = createAgent({ id: "report-1", reportsTo: "manager-1", instructionsText: "Do X" });
vi.mocked(agentStore.getAgent).mockResolvedValue(report);
vi.mocked(agentStore.updateAgent).mockResolvedValue(updated);
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
await tool.execute("session", { agent_id: "report-1", instructions_text: "Do X" }, undefined as never, undefined as never, undefined as never);
expect(agentStore.updateAgent).toHaveBeenCalledWith("report-1", { instructionsText: "Do X" });
});
it("successfully updates heartbeat interval by merging runtimeConfig", async () => {
const report = createAgent({ id: "report-1", reportsTo: "manager-1", runtimeConfig: { custom: true } });
const updated = createAgent({ id: "report-1", reportsTo: "manager-1", runtimeConfig: { custom: true, heartbeatIntervalMs: 2000 } });
vi.mocked(agentStore.getAgent).mockResolvedValue(report);
vi.mocked(agentStore.updateAgent).mockResolvedValue(updated);
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
await tool.execute("session", { agent_id: "report-1", heartbeat_interval_ms: 2000 }, undefined as never, undefined as never, undefined as never);
expect(agentStore.updateAgent).toHaveBeenCalledWith("report-1", {
runtimeConfig: { custom: true, heartbeatIntervalMs: 2000 },
});
});
it("successfully updates multiple fields at once", async () => {
const report = createAgent({ id: "report-1", reportsTo: "manager-1" });
const updated = createAgent({ id: "report-1", reportsTo: "manager-1" });
vi.mocked(agentStore.getAgent).mockResolvedValue(report);
vi.mocked(agentStore.updateAgent).mockResolvedValue(updated);
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
await tool.execute("session", {
agent_id: "report-1",
soul: "A",
instructions_text: "B",
heartbeat_timeout_ms: 9000,
message_response_mode: "on-heartbeat",
}, undefined as never, undefined as never, undefined as never);
expect(agentStore.updateAgent).toHaveBeenCalledWith("report-1", {
soul: "A",
instructionsText: "B",
runtimeConfig: {
heartbeatTimeoutMs: 9000,
messageResponseMode: "on-heartbeat",
},
});
});
it("preserves existing runtimeConfig keys when updating heartbeat fields", async () => {
const report = createAgent({ id: "report-1", reportsTo: "manager-1", runtimeConfig: { budget: { cap: 1 }, existing: "keep" } });
const updated = createAgent({ id: "report-1", reportsTo: "manager-1" });
vi.mocked(agentStore.getAgent).mockResolvedValue(report);
vi.mocked(agentStore.updateAgent).mockResolvedValue(updated);
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
await tool.execute("session", { agent_id: "report-1", max_concurrent_runs: 3 }, undefined as never, undefined as never, undefined as never);
expect(agentStore.updateAgent).toHaveBeenCalledWith("report-1", {
runtimeConfig: { budget: { cap: 1 }, existing: "keep", maxConcurrentRuns: 3 },
});
});
it("returns error when target agent not found", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(null);
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "missing", soul: "x" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: Agent missing not found");
expect(agentStore.updateAgent).not.toHaveBeenCalled();
});
it("returns error when target is not a direct report", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(createAgent({ id: "other", reportsTo: "different" }));
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "other", soul: "x" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: You can only update configuration of agents that report to you");
expect(agentStore.updateAgent).not.toHaveBeenCalled();
});
it("returns error when target is ephemeral", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(createAgent({ id: "ephemeral", reportsTo: "manager-1", metadata: { agentKind: "task-worker" } }));
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "ephemeral", soul: "x" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: Cannot update ephemeral/runtime agent ephemeral");
expect(agentStore.updateAgent).not.toHaveBeenCalled();
});
it("returns error when no fields provided to update", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(createAgent({ id: "report-1", reportsTo: "manager-1" }));
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "report-1" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: Provide at least one field to update");
expect(agentStore.updateAgent).not.toHaveBeenCalled();
});
it("validates soul max length", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(createAgent({ id: "report-1", reportsTo: "manager-1" }));
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "report-1", soul: "x".repeat(10001) }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: soul exceeds 10000 character limit");
});
it("validates instructionsText max length", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValue(createAgent({ id: "report-1", reportsTo: "manager-1" }));
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "report-1", instructions_text: "x".repeat(50001) }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: instructions_text exceeds 50000 character limit");
});
it("validates heartbeatIntervalMs minimum", async () => {
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const schema = tool.parameters as { properties: Record<string, { minimum?: number }> };
expect(schema.properties.heartbeat_interval_ms?.minimum).toBe(1000);
});
it("validates heartbeatTimeoutMs minimum", async () => {
const tool = createUpdateAgentConfigTool(agentStore, "manager-1");
const schema = tool.parameters as { properties: Record<string, { minimum?: number }> };
expect(schema.properties.heartbeat_timeout_ms?.minimum).toBe(5000);
});
});

View File

@@ -1714,19 +1714,21 @@ describe("executeHeartbeat", () => {
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_heartbeat_done
expect(callArgs.customTools).toHaveLength(10);
// 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);
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");
expect(callArgs.customTools![3]!.name).toBe("fn_task_document_read");
expect(callArgs.customTools![4]!.name).toBe("fn_list_agents");
expect(callArgs.customTools![5]!.name).toBe("fn_delegate_task");
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");
expect(callArgs.customTools![6]!.name).toBe("fn_get_agent_config");
expect(callArgs.customTools![7]!.name).toBe("fn_update_agent_config");
expect(callArgs.customTools![8]!.name).toBe("fn_memory_search");
expect(callArgs.customTools![9]!.name).toBe("fn_memory_get");
expect(callArgs.customTools![10]!.name).toBe("fn_memory_append");
// fn_heartbeat_done is last (terminal tool)
expect(callArgs.customTools![9]!.name).toBe("fn_heartbeat_done");
expect(callArgs.customTools![11]!.name).toBe("fn_heartbeat_done");
});
it("includes memory instructions even when agent has no custom instructions", async () => {

View File

@@ -88,19 +88,21 @@ describe("createHeartbeatTools", () => {
mockTaskStore = createMockTaskStoreForTools();
});
it("returns fn_task_create, fn_task_log, fn_task_document_write, and fn_task_document_read tools", () => {
it("returns task, delegation, and agent-config tools", () => {
const store = createMockStore();
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
expect(tools).toHaveLength(6);
expect(tools).toHaveLength(8);
expect(tools[0]!.name).toBe("fn_task_create");
expect(tools[1]!.name).toBe("fn_task_log");
expect(tools[2]!.name).toBe("fn_task_document_write");
expect(tools[3]!.name).toBe("fn_task_document_read");
expect(tools[4]!.name).toBe("fn_list_agents");
expect(tools[5]!.name).toBe("fn_delegate_task");
expect(tools[6]!.name).toBe("fn_get_agent_config");
expect(tools[7]!.name).toBe("fn_update_agent_config");
});
it("fn_task_create tool creates a task in triage via TaskStore", async () => {

View File

@@ -22,7 +22,7 @@ import { buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity }
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { Type, type Static } from "@mariozechner/pi-ai";
import { createHash } from "node:crypto";
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createSendMessageTool, createReadMessagesTool, createMemoryTools, taskCreateParams } from "./agent-tools.js";
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createGetAgentConfigTool, createUpdateAgentConfigTool, 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";
@@ -150,6 +150,7 @@ Your job:
2. Do ONE useful action that changes project clarity or flow.
3. Use fn_task_create to spawn follow-up work, fn_task_log to record observations, and fn_task_document_write for durable artifacts.
4. Use fn_list_agents + fn_delegate_task when work should be assigned to a specific capable agent now.
5. Use fn_get_agent_config and fn_update_agent_config to tune direct reports before delegating recurring work.
5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
Examples of ONE useful action:
@@ -173,6 +174,7 @@ Use this decision rule:
- **Task document (fn_task_document_write):** when findings are structured and likely useful across future sessions for the same task.
- **Create task (fn_task_create):** when someone must do new executable work.
- **Delegate task (fn_delegate_task):** when that new work should go to a specific agent based on role/availability.
- **Manage report config (fn_get_agent_config / fn_update_agent_config):** when direct reports need heartbeat, instruction, or personality tuning.
Prefer fn_task_create when assignment is unclear and scheduler routing is fine.
Prefer fn_delegate_task when immediate ownership by a specific agent materially reduces latency or risk.
@@ -231,6 +233,7 @@ Your job:
2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory.
3. Use fn_task_create to spawn follow-up work.
4. Use fn_list_agents and fn_delegate_task to coordinate with other agents.
5. Use fn_get_agent_config and fn_update_agent_config to read/tune direct-report agents for better routing outcomes.
5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
Examples of ONE useful action:
@@ -244,6 +247,7 @@ Keep work lightweight — this is a single-pass ambient check, not a full implem
You have readonly file access plus:
- fn_task_create
- fn_list_agents and fn_delegate_task
- fn_get_agent_config and fn_update_agent_config (for direct reports only)
- fn_memory_search, fn_memory_get, and fn_memory_append
- fn_heartbeat_done
- fn_send_message and fn_read_messages when messaging is enabled for this run (they may not always be available)
@@ -1351,7 +1355,7 @@ export class HeartbeatMonitor {
// For no-task runs, exclude fn_task_log and document tools (they require a taskId)
let heartbeatTools: ToolDefinition[];
if (isNoTaskRun) {
// No-task runs: fn_task_create, fn_list_agents, fn_delegate_task, messaging, memory, fn_heartbeat_done
// No-task runs: fn_task_create, fn_list_agents, fn_delegate_task, fn_get_agent_config, fn_update_agent_config, messaging, memory, fn_heartbeat_done
heartbeatTools = [];
// fn_task_create tool
@@ -1364,6 +1368,8 @@ export class HeartbeatMonitor {
// Agent delegation tools
heartbeatTools.push(createListAgentsTool(this.store));
heartbeatTools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
heartbeatTools.push(createGetAgentConfigTool(this.store, agentId));
heartbeatTools.push(createUpdateAgentConfigTool(this.store, agentId));
// Messaging tools — when MessageStore is available
if (this.messageStore) {
@@ -1891,6 +1897,8 @@ export class HeartbeatMonitor {
// Agent delegation tools — discover and delegate work to other agents
tools.push(createListAgentsTool(this.store));
tools.push(createDelegateTaskTool(this.store, taskStore, { rootDir: this.rootDir }));
tools.push(createGetAgentConfigTool(this.store, agentId));
tools.push(createUpdateAgentConfigTool(this.store, agentId));
// Messaging tools — when MessageStore is available, agents can send and receive messages
if (messageStore) {

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, TaskCreateInput } from "@fusion/core";
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, 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";
@@ -75,6 +75,25 @@ export const delegateTaskParams = Type.Object({
),
});
export const getAgentConfigParams = Type.Object({
agent_id: Type.String({ description: "The agent ID to read configuration for" }),
});
export const updateAgentConfigParams = Type.Object({
agent_id: Type.String({ description: "The agent ID to update" }),
soul: Type.Optional(Type.String({ description: "Agent personality/identity text", maxLength: 10000 })),
instructions_text: Type.Optional(Type.String({ description: "Inline custom instructions", maxLength: 50000 })),
instructions_path: Type.Optional(Type.String({ description: "Path to instructions markdown file", maxLength: 500 })),
heartbeat_procedure_path: Type.Optional(Type.String({ description: "Path to heartbeat procedure markdown file", maxLength: 500 })),
heartbeat_interval_ms: Type.Optional(Type.Number({ description: "Heartbeat polling interval in ms", minimum: 1000 })),
heartbeat_timeout_ms: Type.Optional(Type.Number({ description: "Heartbeat timeout in ms", minimum: 5000 })),
max_concurrent_runs: Type.Optional(Type.Number({ description: "Max concurrent heartbeat runs", minimum: 1 })),
message_response_mode: Type.Optional(Type.Union([
Type.Literal("immediate"),
Type.Literal("on-heartbeat"),
], { description: "How agent responds to messages" })),
});
export const sendMessageParams = Type.Object({
to_id: Type.String({ description: "Recipient ID (agent ID or user ID, depending on message type)" }),
content: Type.String({ description: "Message body (1-2000 characters)" }),
@@ -981,6 +1000,170 @@ export function createListAgentsTool(agentStore: AgentStore): ToolDefinition {
};
}
/**
* Create a `fn_delegate_task` tool that creates and assigns a task to a specific agent.
*
* @param agentStore - AgentStore for agent lookup
* @param taskStore - TaskStore for task creation
* @returns ToolDefinition for the `fn_delegate_task` tool
*/
export function createGetAgentConfigTool(agentStore: AgentStore, callingAgentId: string): ToolDefinition {
return {
name: "fn_get_agent_config",
label: "Get Agent Config",
description: "Read full configuration for one of your direct-report agents.",
parameters: getAgentConfigParams,
execute: async (_id: string, params: Static<typeof getAgentConfigParams>) => {
const target = await agentStore.getAgent(params.agent_id);
if (!target) {
return {
content: [{ type: "text" as const, text: `ERROR: Agent ${params.agent_id} not found` }],
details: {},
};
}
if (target.reportsTo !== callingAgentId) {
return {
content: [{ type: "text" as const, text: "ERROR: You can only read configuration of agents that report to you" }],
details: {},
};
}
const runtimeConfig = (target.runtimeConfig ?? {}) as Record<string, unknown>;
const lines: string[] = [
`Agent Config: ${target.name} (${target.id})`,
`Role: ${target.role}`,
`State: ${target.state}`,
`Title: ${target.title ?? "(none)"}`,
`Icon: ${target.icon ?? "(none)"}`,
"",
"Soul:",
target.soul ?? "(none)",
"",
"Instructions Text:",
target.instructionsText ?? "(none)",
`Instructions Path: ${target.instructionsPath ?? "(none)"}`,
`Heartbeat Procedure Path: ${target.heartbeatProcedurePath ?? "(none)"}`,
"",
"Runtime Config:",
`heartbeatIntervalMs: ${String(runtimeConfig.heartbeatIntervalMs ?? "(default)")}`,
`heartbeatTimeoutMs: ${String(runtimeConfig.heartbeatTimeoutMs ?? "(default)")}`,
`maxConcurrentRuns: ${String(runtimeConfig.maxConcurrentRuns ?? "(default)")}`,
`messageResponseMode: ${String(runtimeConfig.messageResponseMode ?? "(default)")}`,
`budget: ${JSON.stringify(runtimeConfig.budget ?? null)}`,
"",
"Memory:",
target.memory ?? "(none)",
];
return {
content: [{ type: "text" as const, text: lines.join("\n") }],
details: { agent: target },
};
},
};
}
export function createUpdateAgentConfigTool(agentStore: AgentStore, callingAgentId: string): ToolDefinition {
return {
name: "fn_update_agent_config",
label: "Update Agent Config",
description: "Update configuration for one of your direct-report agents.",
parameters: updateAgentConfigParams,
execute: async (_id: string, params: Static<typeof updateAgentConfigParams>) => {
const target = await agentStore.getAgent(params.agent_id);
if (!target) {
return {
content: [{ type: "text" as const, text: `ERROR: Agent ${params.agent_id} not found` }],
details: {},
};
}
if (target.reportsTo !== callingAgentId) {
return {
content: [{ type: "text" as const, text: "ERROR: You can only update configuration of agents that report to you" }],
details: {},
};
}
if (isEphemeralAgent(target)) {
return {
content: [{ type: "text" as const, text: `ERROR: Cannot update ephemeral/runtime agent ${params.agent_id}` }],
details: {},
};
}
if (params.soul && params.soul.length > 10000) {
return {
content: [{ type: "text" as const, text: "ERROR: soul exceeds 10000 character limit" }],
details: {},
};
}
if (params.instructions_text && params.instructions_text.length > 50000) {
return {
content: [{ type: "text" as const, text: "ERROR: instructions_text exceeds 50000 character limit" }],
details: {},
};
}
if (params.instructions_path && params.instructions_path.length > 500) {
return {
content: [{ type: "text" as const, text: "ERROR: instructions_path exceeds 500 character limit" }],
details: {},
};
}
if (params.heartbeat_procedure_path && params.heartbeat_procedure_path.length > 500) {
return {
content: [{ type: "text" as const, text: "ERROR: heartbeat_procedure_path exceeds 500 character limit" }],
details: {},
};
}
const hasRuntimeConfigUpdates = [
params.heartbeat_interval_ms,
params.heartbeat_timeout_ms,
params.max_concurrent_runs,
params.message_response_mode,
].some((value) => value !== undefined);
const updateInput: AgentUpdateInput = {};
if (params.soul !== undefined) updateInput.soul = params.soul;
if (params.instructions_text !== undefined) updateInput.instructionsText = params.instructions_text;
if (params.instructions_path !== undefined) updateInput.instructionsPath = params.instructions_path;
if (params.heartbeat_procedure_path !== undefined) updateInput.heartbeatProcedurePath = params.heartbeat_procedure_path;
if (hasRuntimeConfigUpdates) {
updateInput.runtimeConfig = {
...((target.runtimeConfig ?? {}) as Record<string, unknown>),
...(params.heartbeat_interval_ms !== undefined ? { heartbeatIntervalMs: params.heartbeat_interval_ms } : {}),
...(params.heartbeat_timeout_ms !== undefined ? { heartbeatTimeoutMs: params.heartbeat_timeout_ms } : {}),
...(params.max_concurrent_runs !== undefined ? { maxConcurrentRuns: params.max_concurrent_runs } : {}),
...(params.message_response_mode !== undefined ? { messageResponseMode: params.message_response_mode } : {}),
};
}
if (Object.keys(updateInput).length === 0) {
return {
content: [{ type: "text" as const, text: "ERROR: Provide at least one field to update" }],
details: {},
};
}
const updated = await agentStore.updateAgent(params.agent_id, updateInput);
const updatedRuntimeConfig = (updated.runtimeConfig ?? {}) as Record<string, unknown>;
return {
content: [{
type: "text" as const,
text: `Updated ${updated.name} (${updated.id})\n` +
`heartbeatIntervalMs: ${String(updatedRuntimeConfig.heartbeatIntervalMs ?? "(default)")}\n` +
`heartbeatTimeoutMs: ${String(updatedRuntimeConfig.heartbeatTimeoutMs ?? "(default)")}\n` +
`maxConcurrentRuns: ${String(updatedRuntimeConfig.maxConcurrentRuns ?? "(default)")}\n` +
`messageResponseMode: ${String(updatedRuntimeConfig.messageResponseMode ?? "(default)")}`,
}],
details: { agent: updated },
};
},
};
}
/**
* Create a `fn_delegate_task` tool that creates and assigns a task to a specific agent.
*

View File

@@ -47,10 +47,12 @@ import { createRunAuditor, generateSyntheticRunId, type EngineRunContext } from
import { evaluateSpecStaleness, getPromptPath } from "./spec-staleness.js";
import {
createDelegateTaskTool,
createGetAgentConfigTool,
createListAgentsTool,
createMemoryTools,
createReadMessagesTool,
createReflectOnPerformanceTool,
createUpdateAgentConfigTool,
createResearchTools,
createSendMessageTool,
createTaskCreateTool as sharedCreateTaskCreateTool,
@@ -67,8 +69,10 @@ import { createFallbackModelObserver } from "./fallback-model-observer.js";
export { summarizeToolArgs } from "./agent-logger.js";
export {
createDelegateTaskTool,
createGetAgentConfigTool,
createListAgentsTool,
createReadMessagesTool,
createUpdateAgentConfigTool,
createSendMessageTool,
createTaskCreateTool,
createTaskDocumentReadTool,
@@ -2766,6 +2770,10 @@ export class TaskExecutor {
...(this.options.agentStore ? [
createListAgentsTool(this.options.agentStore),
createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir }),
...(assignedAgentId ? [
createGetAgentConfigTool(this.options.agentStore, assignedAgentId),
createUpdateAgentConfigTool(this.options.agentStore, assignedAgentId),
] : []),
] : []),
// Messaging tools — allows executor agents to send and receive messages.
...(this.options.messageStore && assignedAgentId ? [