Give every 1:1 its own direct conversation instead of piling into #general

Agents could only post to named public channels - the messaging tool even
excluded direct spaces - so assignments, reports, and questions all crowded
into general. Now ChatService.ensureDirectSpace finds or creates the private
two-member conversation for any agent/employee pair; send_message takes
to="Name" for 1:1 traffic (exactly one of to/channel), read_messages reads a
conversation by teammate name, and task assignments land in the manager's DM
with the assignee. In a direct conversation the counterpart agent wakes on
every message without needing an @mention - same social contract as any
messenger - still bounded by the autonomous-turn budget, and wake task
briefings tell agents to reply with to= in kind. Covered by six new
direct-message contract tests; the assignment-thread test now encodes the
DM routing contract.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-06 22:49:31 -04:00
1 parent f13ef48a3e
commit cab7b01831
6 files changed
+375 -51

No files matched your search

@@ -0,0 +1,158 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ChatService } from "../../src/main/services/chat-service";
import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools";
function call(name: string, args: Record<string, unknown>) {
return { id: `call_${randomUUID()}`, name, arguments: args };
}
interface Harness {
connection: DatabaseConnection;
organization: OrganizationService;
chat: ChatService;
orgId: string;
employeeId: string;
ceoId: string;
danaId: string;
ctx: AgentToolContext;
}
function harness(options: { enableProvider: boolean }): Harness {
const connection = new DatabaseConnection(join(tmpdir(), `ac-dm-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
let ceoId = "";
let danaId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" });
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
if (options.enableProvider) {
connection.db.prepare("update providers set enabled = 1 where id = ? and org_id = ?").run(providerId, orgId);
}
ceoId = organization.createAgent({
orgId, name: "CEO", role: "CEO", department: "Executive",
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null,
});
danaId = organization.createAgent({
orgId, name: "Dana", role: "Engineer", department: "Engineering",
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: ceoId,
});
});
const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
const chat = new ChatService(connection.db);
return {
connection,
organization,
chat,
orgId,
employeeId,
ceoId,
danaId,
ctx: {
orgId,
agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null,
taskGoalId: null,
workspace: null,
},
};
}
function directSpaces(h: Harness): Array<{ id: string }> {
return h.connection.db.prepare("select id from chat_spaces where org_id = ? and space_type = 'direct'")
.all(h.orgId) as Array<{ id: string }>;
}
describe("agent direct messages", () => {
test("a DM gets its own conversation and later messages reuse it", () => {
const h = harness({ enableProvider: false });
const deps = { db: h.connection.db, organization: h.organization, chat: h.chat };
const first = dispatchAgentTool(deps, h.ctx, call("send_message", { to: "Dana", body: "How is the booking API going?" }));
expect(first).toContain("direct message to Dana");
const threadId = /thread_id="([^"]+)"/.exec(first)?.[1];
expect(threadId).toBeTruthy();
dispatchAgentTool(deps, h.ctx, call("send_message", { to: "Dana", body: "Also, ping me when the spec lands.", thread_id: threadId }));
const spaces = directSpaces(h);
expect(spaces).toHaveLength(1);
const members = h.connection.db.prepare("select principal_type, principal_id from chat_memberships where space_id = ?")
.all(spaces[0].id) as Array<{ principal_type: string; principal_id: string }>;
expect(members).toHaveLength(2);
expect(members.map((m) => m.principal_id).sort()).toEqual([h.ceoId, h.danaId].sort());
const messages = h.connection.db.prepare("select space_id from chat_messages where org_id = ?").all(h.orgId) as Array<{ space_id: string }>;
expect(messages).toHaveLength(2);
expect(messages.every((m) => m.space_id === spaces[0].id)).toBe(true);
});
test("a direct message wakes the counterpart agent without an @mention", () => {
const h = harness({ enableProvider: true });
const deps = { db: h.connection.db, organization: h.organization, chat: h.chat };
dispatchAgentTool(deps, h.ctx, call("send_message", { to: "Dana", body: "Take a look at the failing deploy when you can." }));
const runs = h.connection.db.prepare(
"select count(*) as count from task_runs where org_id = ? and agent_id = ? and trigger = 'chat_mention'",
).get(h.orgId, h.danaId) as { count: number };
expect(runs.count).toBe(1);
});
test("an employee DM wakes the agent like any messenger would", () => {
const h = harness({ enableProvider: true });
const spaceId = h.chat.ensureDirectSpace(h.orgId, { type: "employee", id: h.employeeId }, { type: "agent", id: h.danaId });
h.chat.sendMessage({
orgId: h.orgId,
spaceId,
threadId: null,
senderType: "employee",
senderId: h.employeeId,
bodyMd: "Dana, what is the launch status?",
attachmentFileIds: [],
});
const runs = h.connection.db.prepare(
"select count(*) as count from task_runs where org_id = ? and agent_id = ? and trigger = 'chat_mention'",
).get(h.orgId, h.danaId) as { count: number };
expect(runs.count).toBe(1);
});
test("task assignments go to the manager's direct conversation with the assignee", () => {
const h = harness({ enableProvider: true });
const deps = { db: h.connection.db, organization: h.organization, chat: h.chat };
const result = dispatchAgentTool(deps, h.ctx, call("create_task", {
title: "Ship booking API",
description: "Design and ship the booking API v1.",
assignee_agent_name: "Dana",
}));
expect(result).toContain("assigned to Dana");
const spaces = directSpaces(h);
expect(spaces).toHaveLength(1);
const assignment = h.connection.db.prepare("select space_id, body_md from chat_messages where org_id = ?").get(h.orgId) as { space_id: string; body_md: string };
expect(assignment.space_id).toBe(spaces[0].id);
expect(assignment.body_md).toContain("take task_id=");
});
test("send_message requires exactly one destination", () => {
const h = harness({ enableProvider: false });
const deps = { db: h.connection.db, organization: h.organization, chat: h.chat };
expect(dispatchAgentTool(deps, h.ctx, call("send_message", { body: "hello" }))).toContain("ERROR");
expect(dispatchAgentTool(deps, h.ctx, call("send_message", { to: "Dana", channel: "general", body: "hello" }))).toContain("ERROR");
});
test("read_messages can read a direct conversation by teammate name", () => {
const h = harness({ enableProvider: false });
const deps = { db: h.connection.db, organization: h.organization, chat: h.chat };
dispatchAgentTool(deps, h.ctx, call("send_message", { to: "Dana", body: "Morning - priorities for today are in the doc." }));
const readBack = dispatchAgentTool(deps, h.ctx, call("read_messages", { with: "Dana" }));
expect(readBack).toContain("priorities for today");
expect(readBack).toContain("[CEO]");
});
});
@@ -186,12 +186,17 @@ describe("agent run loop (real tool dispatch)", () => {
const alex = connection.db.prepare("select id from agents where org_id = ? and name = 'Alex Rivera'").get(orgId) as { id: string };
const assignment = connection.db.prepare(`
select m.body_md, m.thread_id, s.name as space_name
select m.body_md, m.thread_id, s.id as space_id, s.space_type
from chat_messages m
join chat_spaces s on s.id = m.space_id
where m.org_id = ?
`).get(orgId) as { body_md: string; thread_id: string; space_name: string } | undefined;
expect(assignment).toMatchObject({ space_name: "engineering" });
`).get(orgId) as { body_md: string; thread_id: string; space_id: string; space_type: string } | undefined;
// Assignments are 1:1 hand-offs, so they live in the manager's direct
// conversation with the assignee instead of a shared channel.
expect(assignment).toMatchObject({ space_type: "direct" });
const dmMembers = connection.db.prepare("select principal_id from chat_memberships where space_id = ?")
.all(assignment?.space_id ?? "") as Array<{ principal_id: string }>;
expect(dmMembers.map((m) => m.principal_id).sort()).toEqual([ceoId, alex.id].sort());
expect(assignment?.body_md).toContain(`@${canonicalMentionHandle("Alex Rivera")}`);
expect(assignment?.body_md).toContain("Build the booking API");