Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Chat and workspace agents: channels, DMs and named agents you talk to | 1 | import { test } from "node:test"; |
| 2 | import assert from "node:assert/strict"; | |
| 3 | ||
| An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so. | 4 | import { MAX_HOPS, type RecentMessage, addressedAgents, chainFor, deliveries, delivery, handOffPlace, handOffRefusal, personalAgentRefusal, wakeable } from "./delivery.ts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 5 | |
| 6 | const ship = { id: "a1", handle: "ship" }; | |
| 7 | const docs = { id: "a2", handle: "docs" }; | |
| 8 | ||
| In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so. | 9 | test("a person's direct message with nothing said yet wakes every agent in it, at hop 0", () => { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 10 | assert.deepEqual(deliveries({ author: "user:u", hops: 3, channelKind: "dm", agents: [ship, docs], mentioned: [] }), [ |
| 11 | { agent_id: "a1", hops: 0 }, | |
| 12 | { agent_id: "a2", hops: 0 }, | |
| 13 | ]); | |
| In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so. | 14 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: [], recent: [] }), [ |
| 15 | { agent_id: "a1", hops: 0 }, | |
| 16 | { agent_id: "a2", hops: 0 }, | |
| 17 | ]); | |
| 18 | // With one agent there is nobody else it could be for. | |
| 19 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship], mentioned: [], recent: [{ author: "user:u", mentioned: ["docs"] }] }), [{ agent_id: "a1", hops: 0 }]); | |
| 20 | }); | |
| 21 | ||
| 22 | const mike = { id: "a_mike", handle: "mike" }; | |
| 23 | const g1t = { id: "a_g1t", handle: "g1t" }; | |
| 24 | const says = (author: string, ...mentioned: string[]): RecentMessage => ({ author, mentioned }); | |
| 25 | ||
| 26 | test("in a group DM, an unaddressed message goes to the agent the person is talking with", () => { | |
| 27 | // The conversation as it happened: Chase, Mike the recruiter and @g1t in one group message. | |
| 28 | const transcript = [ | |
| 29 | says("user:chase"), // "Are you able to generate PDFs now?" | |
| 30 | says("agent:a_mike"), | |
| 31 | says("agent:a_g1t"), | |
| 32 | says("user:chase", "mike"), // "@Mike Make a PDF of that example resume for me" | |
| 33 | says("agent:a_mike"), | |
| 34 | ]; | |
| 35 | // "Let's take that template and make a fake resume for some fake person": Mike was helping. | |
| 36 | assert.deepEqual(deliveries({ author: "user:chase", hops: 0, channelKind: "dm", agents: [mike, g1t], mentioned: [], recent: transcript }), [{ agent_id: "a_mike", hops: 0 }]); | |
| 37 | // The first question, with nothing before it, went to both; so does a follow-up to both their answers. | |
| 38 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", []), [mike, g1t]); | |
| 39 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", transcript.slice(0, 3)), [mike, g1t]); | |
| 40 | // A mention still goes only to those named, whoever was talking. | |
| 41 | assert.deepEqual(deliveries({ author: "user:chase", hops: 0, channelKind: "dm", agents: [mike, g1t], mentioned: ["g1t"], recent: transcript }), [{ agent_id: "a_g1t", hops: 0 }]); | |
| 42 | }); | |
| 43 | ||
| 44 | test("who is addressed: the last exchange, else the person's last mention, else everyone", () => { | |
| 45 | // Mentioned but not yet answered: the mention stands, through the person's own follow-ups. | |
| 46 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "Mike"), says("user:chase")]), [mike]); | |
| 47 | // Answered by a colleague after the mention (a hand-off here, say): the colleague has the conversation now. | |
| 48 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "mike"), says("agent:a_mike", "g1t"), says("user:chase"), says("agent:a_g1t")]), [g1t]); | |
| 49 | // Both answered the last message: both are in the exchange, in the conversation's order. | |
| 50 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "g1t", "mike"), says("agent:a_g1t"), says("agent:a_mike")]), [mike, g1t]); | |
| 51 | // A thank-you in between changes nothing: the person's last mention still says who. | |
| 52 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "mike"), says("agent:a_mike"), says("user:chase"), says("user:chase")]), [mike]); | |
| 53 | // Another person's message ends the exchange without deciding it; only this person's mentions do. | |
| 54 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "mike"), says("agent:a_mike"), says("user:bo", "g1t")]), [mike]); | |
| 55 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:bo", "g1t"), says("agent:a_g1t")]), [g1t], "whoever answered last, whoever they answered"); | |
| 56 | // A second message before anyone answers the first goes where the first did. | |
| 57 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:bo", "g1t"), says("agent:a_g1t"), says("user:chase")]), [g1t]); | |
| 58 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase"), says("agent:a_mike"), says("user:chase")]), [mike], "only Mike answered the first"); | |
| 59 | // Mentions of agents that aren't here, and messages of agents that left, decide nothing. | |
| 60 | assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "sam"), says("agent:a_gone")]), [mike, g1t]); | |
| 61 | assert.deepEqual(addressedAgents([mike], "user:chase", [says("user:chase", "g1t")]), [mike], "one agent: nothing to decide"); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 62 | }); |
| 63 | ||
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 64 | test("in a group direct message, a person who mentions some of its agents wakes only those", () => { |
| 65 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: ["docs"] }), [{ agent_id: "a2", hops: 0 }]); | |
| 66 | // Mentioning someone who isn't in it is no mention of its agents: all of them answer. | |
| 67 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: ["mike"] }), [ | |
| 68 | { agent_id: "a1", hops: 0 }, | |
| 69 | { agent_id: "a2", hops: 0 }, | |
| 70 | ]); | |
| 71 | }); | |
| 72 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 73 | test("in a channel, a person wakes only the agent members they mention", () => { |
| 74 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents: [ship, docs], mentioned: [] }), []); | |
| 75 | assert.deepEqual( | |
| 76 | deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents: [ship, docs], mentioned: ["Ship", "nobody"] }), | |
| 77 | [{ agent_id: "a1", hops: 0 }], | |
| 78 | ); | |
| 79 | }); | |
| 80 | ||
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 81 | test("an agent's message wakes nobody, even when it @mentions an agent member", () => { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 82 | assert.deepEqual(deliveries({ author: "agent:a1", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: [] }), []); |
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 83 | assert.deepEqual(deliveries({ author: "agent:a1", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: ["docs"] }), []); |
| 84 | assert.deepEqual(deliveries({ author: "agent:a1", hops: 2, channelKind: "channel", agents: [ship, docs], mentioned: ["docs", "ship"] }), []); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 85 | }); |
| 86 | ||
| 87 | test("a delivery carries the chain's asker on g1t's own chat, and the wake's hops", () => { | |
| 88 | const asker = { username: "ada", role: "member", can_write: false }; | |
| 89 | const place = { workspace: "acme", workspace_id: "w1", channel_id: "c1", channel_kind: "channel", channel_name: "ops" }; | |
| 90 | assert.deepEqual( | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 91 | delivery(place, { agent_id: "a2", hops: 3 }, { id: "m1", thread_root: "m0" }, { hops: 2, asked_by: "u1", asker, chain: ["a1"] }), |
| 92 | { ...place, agent_id: "a2", message_id: "m1", thread_root: "m0", asked_by: "u1", asker, chain: ["a1"], hops: 3, surface: "g1t" }, | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 93 | ); |
| 94 | // An agent that passed no asker back hands on none. | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 95 | assert.equal(delivery(place, { agent_id: "a2", hops: 1 }, { id: "m1", thread_root: null }, { hops: 0, asked_by: "u1", asker: null, chain: [] }).asker, null); |
| 96 | }); | |
| 97 | ||
| 98 | test("mentioning @g1t in a channel brings it in once; never into a DM", async () => { | |
| 99 | const { addsOrchestrator } = await import("./delivery.ts"); | |
| 100 | assert.equal(addsOrchestrator({ channelKind: "channel", mentioned: ["G1T"], orchestratorIsMember: false }), true); | |
| 101 | assert.equal(addsOrchestrator({ channelKind: "channel", mentioned: ["g1t"], orchestratorIsMember: true }), false, "already in"); | |
| 102 | assert.equal(addsOrchestrator({ channelKind: "channel", mentioned: ["ship"], orchestratorIsMember: false }), false); | |
| 103 | assert.equal(addsOrchestrator({ channelKind: "dm", mentioned: ["g1t"], orchestratorIsMember: false }), false); | |
| 104 | // Once in, it is woken like any mentioned agent member. | |
| 105 | const g1t = { id: "a9", handle: "g1t" }; | |
| 106 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents: [ship, g1t], mentioned: ["g1t"] }), [{ agent_id: "a9", hops: 0 }]); | |
| 107 | }); | |
| 108 | ||
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 109 | test("a chain is the agents that handled a request, with the poster added", () => { |
| 110 | assert.deepEqual(chainFor(["a9"], "a1"), ["a9", "a1"]); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 111 | assert.deepEqual(chainFor("nonsense", "a1"), ["a1"]); |
| 112 | }); | |
| 113 | ||
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 114 | test("a hand-off goes here when the colleague is in this channel or group DM, else to a group DM", () => { |
| 115 | assert.equal(handOffPlace({ channelKind: "channel", members: 40, colleagueHere: true }), "here"); | |
| 116 | assert.equal(handOffPlace({ channelKind: "dm", members: 3, colleagueHere: true }), "here"); | |
| 117 | assert.equal(handOffPlace({ channelKind: "channel", members: 40, colleagueHere: false }), "group_dm"); | |
| 118 | // The 1:1 DM the user saw: @g1t and the person, Mike not in it. | |
| 119 | assert.equal(handOffPlace({ channelKind: "dm", members: 2, colleagueHere: false }), "group_dm"); | |
| 120 | }); | |
| 121 | ||
| 122 | test("a hand-off is refused to itself, to @g1t, back along the chain, and past the hop limit", () => { | |
| 123 | const ok = { agent: "a9", colleague: { id: "a1" }, chain: [], hops: 0 }; | |
| 124 | assert.equal(handOffRefusal(ok), null); | |
| 125 | assert.match(handOffRefusal({ ...ok, colleague: { id: "a9" } })!, /itself/); | |
| 126 | assert.match(handOffRefusal({ ...ok, agent: "a1", colleague: { id: "a9", builtin: true } })!, /@g1t/); | |
| 127 | assert.match(handOffRefusal({ ...ok, agent: "a2", chain: ["a1", "a2"] })!, /already handled/); | |
| 128 | assert.equal(handOffRefusal({ ...ok, hops: MAX_HOPS - 1 }), null); | |
| 129 | assert.match(handOffRefusal({ ...ok, hops: MAX_HOPS })!, /too many times/); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 130 | }); |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 131 | |
| 132 | test("a personal agent is only in the direct message of it and the person it belongs to", () => { | |
| 133 | const mine = { id: "a9", handle: "pip", scope: "personal", personal_owner_id: "u1" }; | |
| 134 | assert.equal(personalAgentRefusal(mine, { kind: "dm", members: ["user:u1", "agent:a9"] }), null); | |
| 135 | assert.match(personalAgentRefusal(mine, { kind: "dm", members: ["user:u2", "agent:a9"] }) ?? "", /personal agent/); | |
| 136 | assert.match(personalAgentRefusal(mine, { kind: "dm", members: ["user:u1", "user:u2", "agent:a9"] }) ?? "", /personal agent/, "not a group DM"); | |
| 137 | assert.match(personalAgentRefusal(mine, { kind: "channel", members: null }) ?? "", /personal agent/, "never invited to a channel"); | |
| 138 | assert.match(personalAgentRefusal(mine, { kind: "hand_off", members: null }) ?? "", /personal agent/, "never handed work"); | |
| 139 | assert.equal(personalAgentRefusal({ id: "a1", handle: "ship", scope: "workspace" }, { kind: "channel", members: null }), null); | |
| 140 | assert.equal(personalAgentRefusal({ id: "a1", handle: "ship" }, { kind: "hand_off", members: null }), null, "agents from before personal ones are the workspace's"); | |
| 141 | }); | |
| An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so. | 142 | |
| 143 | test("an archived agent stays a member but is never woken; one the agents service no longer knows, or of another workspace, neither", () => { | |
| 144 | const live = { id: "a1", handle: "ship", workspace_id: "wsp_1", archived_at: null }; | |
| 145 | const archived = { id: "a2", handle: "docs", workspace_id: "wsp_1", archived_at: "2026-10-09T10:00:00Z" }; | |
| 146 | const elsewhere = { id: "a3", handle: "ops", workspace_id: "wsp_2", archived_at: null }; | |
| 147 | const found = new Map<string, typeof live | null>([ | |
| 148 | ["a1", live], | |
| 149 | ["a2", archived], | |
| 150 | ["a3", elsewhere], | |
| 151 | // Deleted for good: chat still has its membership row, the agents service has nothing. | |
| 152 | ["a4", null], | |
| 153 | ]); | |
| 154 | assert.deepEqual(wakeable(found.values(), "wsp_1"), [live]); | |
| 155 | // In a direct message with the two of them, an unaddressed message goes to the live one alone. | |
| 156 | const agents = wakeable(found.values(), "wsp_1").map((a) => ({ id: a.id, handle: a.handle })); | |
| 157 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents, mentioned: [] }), [{ agent_id: "a1", hops: 0 }]); | |
| 158 | // Mentioning the archived one by handle wakes nobody: it is not among the wakeable. | |
| 159 | assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents, mentioned: ["docs"] }), []); | |
| 160 | assert.deepEqual(wakeable([], "wsp_1"), []); | |
| 161 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.