Make chat mentions inline
This commit is contained in:
@@ -370,7 +370,6 @@ export type PostChatMessageOptions = {
|
||||
room: string;
|
||||
body: string;
|
||||
replyToMessageId?: string | null;
|
||||
mentionAgentIds?: string[];
|
||||
requestId?: string;
|
||||
};
|
||||
export type ReadChatMessagesOptions = {
|
||||
@@ -2913,9 +2912,6 @@ export class DaemonClient {
|
||||
room: options.room,
|
||||
body: options.body,
|
||||
...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}),
|
||||
...(options.mentionAgentIds && options.mentionAgentIds.length > 0
|
||||
? { mentionAgentIds: options.mentionAgentIds }
|
||||
: {}),
|
||||
},
|
||||
responseType: "chat/post/response",
|
||||
timeout: 10000,
|
||||
|
||||
@@ -31,7 +31,6 @@ export const ChatPostRequestSchema = z.object({
|
||||
room: z.string(),
|
||||
body: z.string(),
|
||||
replyToMessageId: z.string().optional(),
|
||||
mentionAgentIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const ChatReadRequestSchema = z.object({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import { ChatServiceError, FileBackedChatService } from "./chat-service.js";
|
||||
import { ChatServiceError, FileBackedChatService, parseMentionAgentIds } from "./chat-service.js";
|
||||
|
||||
describe("FileBackedChatService", () => {
|
||||
let paseoHome: string;
|
||||
@@ -48,8 +48,7 @@ describe("FileBackedChatService", () => {
|
||||
const first = await service.postMessage({
|
||||
room: room.name,
|
||||
authorAgentId: "agent-a",
|
||||
body: "first message",
|
||||
mentionAgentIds: ["agent-b", "agent-b", "agent-c"],
|
||||
body: "first message for @agent-b and @agent-c and again @agent-b",
|
||||
});
|
||||
await service.postMessage({
|
||||
room: room.id,
|
||||
@@ -141,4 +140,11 @@ describe("FileBackedChatService", () => {
|
||||
code: "chat_room_not_found",
|
||||
});
|
||||
});
|
||||
|
||||
test("extracts inline mentions from chat bodies", () => {
|
||||
expect(
|
||||
parseMentionAgentIds("Checking with @agent-a, (@agent_b), and duplicate @agent-a again."),
|
||||
).toEqual(["agent-a", "agent_b"]);
|
||||
expect(parseMentionAgentIds("email@example.com is not a mention")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,19 @@ function trimToNull(value: string | null | undefined): string | null {
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
const CHAT_MENTION_PATTERN = /(?:^|[\s(])@([A-Za-z0-9][A-Za-z0-9._-]*)/g;
|
||||
|
||||
export function parseMentionAgentIds(body: string): string[] {
|
||||
const mentionAgentIds = new Set<string>();
|
||||
for (const match of body.matchAll(CHAT_MENTION_PATTERN)) {
|
||||
const agentId = match[1]?.trim();
|
||||
if (agentId) {
|
||||
mentionAgentIds.add(agentId);
|
||||
}
|
||||
}
|
||||
return Array.from(mentionAgentIds).sort();
|
||||
}
|
||||
|
||||
export class ChatServiceError extends Error {
|
||||
readonly code: string;
|
||||
|
||||
@@ -67,7 +80,6 @@ export interface PostChatMessageInput {
|
||||
authorAgentId: string;
|
||||
body: string;
|
||||
replyToMessageId?: string | null;
|
||||
mentionAgentIds?: string[];
|
||||
}
|
||||
|
||||
export interface ReadChatMessagesInput {
|
||||
@@ -195,9 +207,7 @@ export class FileBackedChatService {
|
||||
authorAgentId,
|
||||
body,
|
||||
replyToMessageId,
|
||||
mentionAgentIds: [...new Set((input.mentionAgentIds ?? []).map((value) => value.trim()))]
|
||||
.filter((value) => value.length > 0)
|
||||
.sort(),
|
||||
mentionAgentIds: parseMentionAgentIds(body),
|
||||
createdAt,
|
||||
});
|
||||
|
||||
|
||||
@@ -7730,7 +7730,6 @@ export class Session {
|
||||
authorAgentId: this.clientId,
|
||||
body: request.body,
|
||||
replyToMessageId: request.replyToMessageId,
|
||||
mentionAgentIds: request.mentionAgentIds,
|
||||
});
|
||||
this.emit({
|
||||
type: "chat/post/response",
|
||||
@@ -7740,11 +7739,79 @@ export class Session {
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
void this.notifyMentionedAgents({
|
||||
room: request.room,
|
||||
authorAgentId: this.clientId,
|
||||
body: request.body,
|
||||
mentionAgentIds: message.mentionAgentIds,
|
||||
});
|
||||
} catch (error) {
|
||||
this.emitChatRpcError(request, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyMentionedAgents(input: {
|
||||
room: string;
|
||||
authorAgentId: string;
|
||||
body: string;
|
||||
mentionAgentIds: string[];
|
||||
}): Promise<void> {
|
||||
const mentionAgentIds = input.mentionAgentIds.filter(
|
||||
(agentId) => agentId !== input.authorAgentId,
|
||||
);
|
||||
if (mentionAgentIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = this.buildChatMentionNotification({
|
||||
...input,
|
||||
mentionAgentIds,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
mentionAgentIds.map(async (mentionedAgentId) => {
|
||||
const resolved = await this.resolveAgentIdentifier(mentionedAgentId);
|
||||
if (!resolved.ok) {
|
||||
this.sessionLogger.warn(
|
||||
{ mentionedAgentId, room: input.room, error: resolved.error },
|
||||
"Failed to resolve chat mention target",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.handleSendAgentMessage(resolved.agentId, notification);
|
||||
} catch (error) {
|
||||
this.sessionLogger.warn(
|
||||
{ err: error, mentionedAgentId: resolved.agentId, room: input.room },
|
||||
"Failed to notify mentioned agent about chat message",
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private buildChatMentionNotification(input: {
|
||||
room: string;
|
||||
authorAgentId: string;
|
||||
body: string;
|
||||
mentionAgentIds: string[];
|
||||
}): string {
|
||||
const mentioned = input.mentionAgentIds.map((agentId) => `@${agentId}`).join(", ");
|
||||
const bodyWithoutMentions = input.body
|
||||
.replace(/(^|\s)@[A-Za-z0-9][A-Za-z0-9._-]*/g, "$1")
|
||||
.trim();
|
||||
const body = bodyWithoutMentions.length > 0 ? bodyWithoutMentions : input.body;
|
||||
|
||||
return [
|
||||
`Chat mention from ${input.authorAgentId} in room "${input.room}".`,
|
||||
`Mentioned agents: ${mentioned}.`,
|
||||
"Message:",
|
||||
body,
|
||||
`Read the room with: paseo chat read ${input.room} --limit 20`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
private async handleChatReadRequest(
|
||||
request: Extract<SessionInboundMessage, { type: "chat/read" }>,
|
||||
): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user