Return who is in the room from send_message, not a receipt id
send_message answered the agent with "Posted to #x (message_id=..., thread_id=...)" — a transaction receipt that gives no sense anyone is there. An agent posting into that void has no reason to converse. Now it answers with presence: who was addressed and can reply, who else is in the channel, and the thread_id to stay in the conversation. The message_id noise is gone. The tool's own description now frames it as talking to teammates rather than posting a coordination status.
This commit is contained in:
1 parent
a9049612c5
commit
e80c8487f3
2 files changed
+65
-4
No files matched your search
@@ -88,7 +88,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
name: "send_message",
|
||||
description: "Post a coordination message to a company channel so teammates and the operator can follow the work. Use @mentions for teammates, attach deliverable file ids with attachment_file_ids, and use /handoff @Name when passing a file. Use the channel that matches the work: 'engineering' for engineering, 'product' for product/design, otherwise 'general'. Reply inside a thread by passing its thread_id.",
|
||||
description: "Talk to your teammates in a channel — ask, answer, decide together, or hand off work, the way colleagues do. @mention the people you are addressing, attach deliverable file ids with attachment_file_ids, and use /handoff @Name when passing a file. Use the channel that matches the work: 'engineering' for engineering, 'product' for product/design, otherwise 'general'. Reply inside a thread by passing its thread_id to keep one conversation together.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
@@ -230,7 +230,58 @@ function sendMessageTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonR
|
||||
attachmentFileIds,
|
||||
});
|
||||
const row = deps.db.prepare("select thread_id from chat_messages where id = ?").get(messageId) as { thread_id: string };
|
||||
return `Posted to #${channel} (message_id=${messageId}, thread_id=${row.thread_id}).`;
|
||||
return describeSend(deps.db, ctx.agent.id, channel, space.id, messageId, row.thread_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* What the sending agent hears back: who is in the channel and who they just
|
||||
* addressed — the presence of real colleagues, not a transaction id. Hearing
|
||||
* the room is what lets an agent hold a conversation instead of posting into a
|
||||
* void, while the thread_id keeps the next reply on the same thread.
|
||||
*/
|
||||
function describeSend(db: DatabaseSync, selfAgentId: string, channel: string, spaceId: string, messageId: string, threadId: string): string {
|
||||
const addressed = mentionedTeammates(db, selfAgentId, messageId);
|
||||
const present = channelTeammates(db, selfAgentId, spaceId, addressed);
|
||||
const lines = [`Your message is up in #${channel}.`];
|
||||
if (addressed.length > 0) lines.push(`${joinNames(addressed)} can see it and reply here.`);
|
||||
if (present.length > 0) lines.push(`Also in the room: ${joinNames(present)}.`);
|
||||
lines.push(`To stay in this conversation, reply with thread_id="${threadId}".`);
|
||||
return lines.join(" ");
|
||||
}
|
||||
|
||||
function mentionedTeammates(db: DatabaseSync, selfAgentId: string, messageId: string): string[] {
|
||||
const rows = db.prepare(`
|
||||
select coalesce(a.name, e.display_name) as name
|
||||
from chat_mentions m
|
||||
left join agents a on a.id = m.target_id and m.target_type = 'agent'
|
||||
left join employees e on e.id = m.target_id and m.target_type = 'employee'
|
||||
where m.message_id = ? and m.target_type in ('agent', 'employee') and m.target_id <> ?
|
||||
`).all(messageId, selfAgentId) as Array<{ name: string | null }>;
|
||||
return dedupeNames(rows);
|
||||
}
|
||||
|
||||
function channelTeammates(db: DatabaseSync, selfAgentId: string, spaceId: string, addressed: string[]): string[] {
|
||||
const alreadyNamed = new Set(addressed);
|
||||
const rows = db.prepare(`
|
||||
select coalesce(a.name, e.display_name) as name
|
||||
from chat_memberships cm
|
||||
left join agents a on a.id = cm.principal_id and cm.principal_type = 'agent'
|
||||
left join employees e on e.id = cm.principal_id and cm.principal_type = 'employee'
|
||||
where cm.space_id = ? and cm.principal_id <> ?
|
||||
`).all(spaceId, selfAgentId) as Array<{ name: string | null }>;
|
||||
return dedupeNames(rows).filter((name) => !alreadyNamed.has(name)).slice(0, 6);
|
||||
}
|
||||
|
||||
function dedupeNames(rows: Array<{ name: string | null }>): string[] {
|
||||
const names = new Set<string>();
|
||||
for (const row of rows) if (row.name) names.add(row.name);
|
||||
return [...names];
|
||||
}
|
||||
|
||||
function joinNames(names: string[]): string {
|
||||
if (names.length === 1) return names[0];
|
||||
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
||||
return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
|
||||
}
|
||||
|
||||
function createDocumentTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
|
||||
|
||||
@@ -32,10 +32,10 @@ describe("agent messaging", () => {
|
||||
};
|
||||
|
||||
const first = dispatchAgentTool(deps, ctx, call("send_message", { channel: "engineering", body: "Assigning the booking API to Dana." }));
|
||||
const threadId = /thread_id=([^),\s]+)/.exec(first)?.[1];
|
||||
const threadId = /thread_id="([^"]+)"/.exec(first)?.[1];
|
||||
expect(threadId).toBeTruthy();
|
||||
const second = dispatchAgentTool(deps, ctx, call("send_message", { channel: "engineering", body: "Update: API shipped.", thread_id: threadId }));
|
||||
expect(second).toContain("Posted to #engineering");
|
||||
expect(second).toContain("#engineering");
|
||||
|
||||
const engineering = connection.db.prepare("select id from chat_spaces where org_id = ? and name = 'engineering'").get(orgId) as { id: string };
|
||||
const messages = connection.db.prepare("select space_id, thread_id, sender_type, sender_id from chat_messages where org_id = ?")
|
||||
@@ -45,6 +45,16 @@ describe("agent messaging", () => {
|
||||
expect(messages.every((m) => m.sender_type === "agent" && m.sender_id === ceoId)).toBe(true);
|
||||
// Both messages share the one thread — the reply joined the assignment thread.
|
||||
expect(new Set(messages.map((m) => m.thread_id)).size).toBe(1);
|
||||
|
||||
// Addressing a teammate comes back as presence — who can reply — not a receipt id.
|
||||
connection.transaction(() => organization.createAgent({
|
||||
orgId, name: "Dana", role: "Engineer", department: "Engineering",
|
||||
modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: ceoId,
|
||||
}));
|
||||
const addressed = dispatchAgentTool(deps, ctx, call("send_message", { channel: "engineering", body: "@Dana can you take the booking API?" }));
|
||||
expect(addressed).toContain("Dana can see it and reply here");
|
||||
expect(addressed).not.toContain("message_id=");
|
||||
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user