feat(FN-4177): add heartbeat messaging support for chat rooms
The merge delivers room heartbeat messaging for agents, allowing heartbeat-triggered agents to send messages into chat rooms and subscribe to room activity. Core changes include new chat-store room APIs, engine heartbeat room message tools, and corresponding tests, with a small dashboard ChatView up Fusion-Task-Id: FN-4177
This commit is contained in:
@@ -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, relative, resolve } from "node:path";
|
||||
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore, ApprovalRequestStore, ProjectSettings } from "@fusion/core";
|
||||
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore, ApprovalRequestStore, ProjectSettings, ChatStore } from "@fusion/core";
|
||||
import { DASHBOARD_USER_ID, canAgentTakeImplementationTaskForExplicitRouting, dailyMemoryPath, ensureOpenClawMemoryFiles, extractAgentProvisioningRequest, formatRoleMismatchReason, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
|
||||
import { ResearchOrchestrator } from "./research-orchestrator.js";
|
||||
import { ResearchProviderRegistry } from "./research/provider-registry.js";
|
||||
@@ -159,6 +159,13 @@ export const readMessagesParams = Type.Object({
|
||||
limit: Type.Optional(Type.Number({ description: "Max messages to return (default: 20)" })),
|
||||
});
|
||||
|
||||
export const postRoomMessageParams = Type.Object({
|
||||
roomId: Type.String({ description: "Room ID to post into" }),
|
||||
content: Type.String({ description: "Room message body (1-2000 characters)" }),
|
||||
replyToMessageId: Type.Optional(Type.String({ description: "Optional ID of the room message you are replying to" })),
|
||||
mentions: Type.Optional(Type.Array(Type.String(), { description: "Optional agent IDs to mention in the room message" })),
|
||||
});
|
||||
|
||||
export const memorySearchParams = Type.Object({
|
||||
query: Type.String({ description: "Search terms for durable project memory. Use focused keywords, not a full prompt." }),
|
||||
limit: Type.Optional(Type.Number({ description: "Maximum snippets to return (default: 5, max: 20)" })),
|
||||
@@ -2125,6 +2132,71 @@ export function createResearchTools(options: ResearchToolsOptions): ToolDefiniti
|
||||
return [runTool, listTool, getTool, cancelTool];
|
||||
}
|
||||
|
||||
export function createPostRoomMessageTool(chatStore: ChatStore, fromAgentId: string): ToolDefinition {
|
||||
return {
|
||||
name: "fn_post_room_message",
|
||||
label: "Post Room Message",
|
||||
description:
|
||||
"Post a message to a room you are a member of. Room membership is enforced before posting, " +
|
||||
"so only reply when the room content is relevant to your role or identity.",
|
||||
parameters: postRoomMessageParams,
|
||||
execute: async (_id: string, params: Static<typeof postRoomMessageParams>) => {
|
||||
const content = params.content.trim();
|
||||
if (content.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "ERROR: Message content cannot be empty" }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
if (content.length > 2000) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "ERROR: Message content exceeds 2000 character limit" }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
const replyToMessageId = params.replyToMessageId?.trim();
|
||||
if (params.replyToMessageId !== undefined && !replyToMessageId) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "ERROR: replyToMessageId must be a non-empty string" }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const isMember = chatStore.listRoomMembers(params.roomId).some((member) => member.agentId === fromAgentId);
|
||||
if (!isMember) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `ERROR: Agent ${fromAgentId} is not a member of room ${params.roomId}` }],
|
||||
details: {},
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const message = chatStore.addRoomMessage(params.roomId, {
|
||||
role: "assistant",
|
||||
senderAgentId: fromAgentId,
|
||||
content,
|
||||
mentions: params.mentions ?? [],
|
||||
...(replyToMessageId ? { metadata: { replyToMessageId } } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Room message posted to ${params.roomId} (ID: ${message.id})` }],
|
||||
details: { messageId: message.id },
|
||||
};
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `ERROR: Failed to post room message: ${errorMessage}` }],
|
||||
details: {},
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createReadMessagesTool(messageStore: MessageStore, agentId: string): ToolDefinition {
|
||||
const REPLY_CONTEXT_CONTENT_MAX_CHARS = 400;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user