feat(FN-1976): forward mailbox message events end to end
- Wire MessageStore into the in-process executor runtime and project engine - Forward MessageStore events through dashboard SSE infrastructure for mailbox updates - Close mailbox pipeline gaps across API routes, server wiring, and mailbox UI components - Add regression coverage for messaging routes, SSE forwarding, and agent tool behavior - Document the MessageStore SSE wiring pattern in .fusion/memory.md
This commit is contained in:
@@ -104,8 +104,8 @@ describe("createSendMessageTool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses provided type when specified", async () => {
|
||||
const mockMessage = createMessage();
|
||||
it("uses provided type when specified and maps recipient type for agent-to-user", async () => {
|
||||
const mockMessage = createMessage({ toType: "user", type: "agent-to-user" });
|
||||
vi.mocked(messageStore.sendMessage).mockReturnValue(mockMessage);
|
||||
|
||||
await executeTool(tool, {
|
||||
@@ -115,7 +115,22 @@ describe("createSendMessageTool", () => {
|
||||
});
|
||||
|
||||
expect(messageStore.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: "agent-to-user" })
|
||||
expect.objectContaining({ type: "agent-to-user", toType: "user" })
|
||||
);
|
||||
});
|
||||
|
||||
it("maps recipient type to agent for agent-to-agent messages", async () => {
|
||||
const mockMessage = createMessage({ toType: "agent", type: "agent-to-agent" });
|
||||
vi.mocked(messageStore.sendMessage).mockReturnValue(mockMessage);
|
||||
|
||||
await executeTool(tool, {
|
||||
to_id: "agent-2",
|
||||
content: "Test",
|
||||
type: "agent-to-agent",
|
||||
});
|
||||
|
||||
expect(messageStore.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: "agent-to-agent", toType: "agent" })
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export const delegateTaskParams = Type.Object({
|
||||
});
|
||||
|
||||
export const sendMessageParams = Type.Object({
|
||||
to_id: Type.String({ description: "Recipient agent ID (e.g. 'agent-abc123')" }),
|
||||
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)" }),
|
||||
type: Type.Optional(Type.Union([
|
||||
Type.Literal("agent-to-agent"),
|
||||
@@ -474,13 +474,16 @@ export function createSendMessageTool(messageStore: MessageStore, fromAgentId: s
|
||||
}
|
||||
|
||||
try {
|
||||
const messageType = params.type ?? "agent-to-agent";
|
||||
const recipientType = messageType === "agent-to-user" ? "user" : "agent";
|
||||
|
||||
const message = messageStore.sendMessage({
|
||||
fromId: fromAgentId,
|
||||
fromType: "agent",
|
||||
toId: params.to_id,
|
||||
toType: "agent",
|
||||
toType: recipientType,
|
||||
content,
|
||||
type: params.type ?? "agent-to-agent",
|
||||
type: messageType,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -268,6 +268,11 @@ export class ProjectEngine {
|
||||
return this.runtime.getAgentStore();
|
||||
}
|
||||
|
||||
/** Get the MessageStore (if initialized). Returns undefined before start(). */
|
||||
getMessageStore(): import("@fusion/core").MessageStore | undefined {
|
||||
return this.runtime.getMessageStore();
|
||||
}
|
||||
|
||||
/** Get the HeartbeatMonitor (if initialized). */
|
||||
getHeartbeatMonitor() {
|
||||
return this.runtime.getHeartbeatMonitor();
|
||||
|
||||
@@ -131,7 +131,12 @@ export class InProcessRuntime
|
||||
|
||||
try {
|
||||
// 1. Initialize TaskStore (use external if provided, otherwise create new)
|
||||
const { TaskStore, PluginStore: PluginStoreClass, PluginLoader: PluginLoaderClass } = await import("@fusion/core");
|
||||
const {
|
||||
TaskStore,
|
||||
PluginStore: PluginStoreClass,
|
||||
PluginLoader: PluginLoaderClass,
|
||||
MessageStore: MessageStoreClass,
|
||||
} = await import("@fusion/core");
|
||||
if (this.config.externalTaskStore) {
|
||||
this.taskStore = this.config.externalTaskStore;
|
||||
runtimeLog.log(`TaskStore provided externally for project ${this.config.projectId}`);
|
||||
@@ -141,6 +146,9 @@ export class InProcessRuntime
|
||||
runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
|
||||
}
|
||||
|
||||
// Initialize MessageStore early so TaskExecutor receives send_message capability.
|
||||
this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
|
||||
|
||||
// 2. Initialize Plugin system (PluginStore + PluginLoader + PluginRunner)
|
||||
this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
|
||||
await this.pluginStore.init();
|
||||
@@ -263,6 +271,7 @@ export class InProcessRuntime
|
||||
usageLimitPauser: this.usageLimitPauser,
|
||||
stuckTaskDetector: this.stuckTaskDetector,
|
||||
pluginRunner: this.pluginRunner,
|
||||
messageStore: this.messageStore,
|
||||
missionStore,
|
||||
onSliceComplete: (slice) => {
|
||||
void this.scheduler.onSliceComplete(slice);
|
||||
@@ -353,13 +362,10 @@ export class InProcessRuntime
|
||||
|
||||
// 6. Initialize AgentStore and HeartbeatMonitor
|
||||
try {
|
||||
const { AgentStore: AgentStoreClass, MessageStore: MessageStoreClass } = await import("@fusion/core");
|
||||
const { AgentStore: AgentStoreClass } = await import("@fusion/core");
|
||||
this.agentStore = new AgentStoreClass({ rootDir: this.taskStore.getFusionDir() });
|
||||
await this.agentStore.init();
|
||||
|
||||
// Initialize MessageStore for wake-on-message behavior
|
||||
this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
|
||||
|
||||
this.heartbeatMonitor = new HeartbeatMonitor({
|
||||
store: this.agentStore,
|
||||
agentStore: this.agentStore, // enables per-agent config resolution
|
||||
@@ -718,6 +724,14 @@ export class InProcessRuntime
|
||||
return this.agentStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MessageStore instance (if initialized).
|
||||
* Returns undefined before start() or if initialization fails.
|
||||
*/
|
||||
getMessageStore(): import("@fusion/core").MessageStore | undefined {
|
||||
return this.messageStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project's Scheduler instance.
|
||||
* @throws Error if runtime has not been started
|
||||
|
||||
Reference in New Issue
Block a user