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 | /** |
| 2 | * Which agents a new message is handed to, and how many agent-to-agent | |
| 3 | * hops along it is. Pure, so it is tested apart from the service. | |
| 4 | * | |
| 5 | * The rules (docs/WORKSPACE.md, "Talking to each other"): | |
| 6 | * - A person's message wakes every agent in a direct message with them, | |
| 7 | * and in a channel only the agent members it @mentions. That starts a | |
| 8 | * chain at hop 0. | |
| 9 | * - An agent's message wakes other agents only when it @mentions them | |
| 10 | * (addressed only, never because they were in the room), one hop further | |
| 11 | * along the chain it was answering. | |
| 12 | * - A chain stops after `MAX_HOPS` hops, and asks a person instead. | |
| 13 | * - An agent is never handed its own message. | |
| 14 | */ | |
| 15 | ||
| 16 | /** The most agent-to-agent hops one person's request may start. Same as CHAT_MAX_HOPS in @g1t/contracts. */ | |
| 17 | export const MAX_HOPS = 6; | |
| 18 | ||
| 19 | export type AgentMember = { id: string; handle: string }; | |
| 20 | ||
| 21 | export type Wake = { agent_id: string; hops: number }; | |
| 22 | ||
| 23 | /** | |
| 24 | * Who a chain was started by, carried along every hop of it: the person's | |
| 25 | * id and what they may do. A person's message starts one with their own | |
| 26 | * access (`askerAccess` in @g1t/contracts); an agent's message carries on | |
| 27 | * the one it was answering, so an agent woken three hops along never does | |
| 28 | * more than the person who asked could. | |
| 29 | */ | |
| 30 | export type Chain<A> = { hops: number; asked_by: string; asker: A | null }; | |
| 31 | ||
| 32 | /** What the agents service is handed for one wake (AgentDelivery), on g1t's own chat. */ | |
| 33 | export function delivery<P extends object, A>( | |
| 34 | place: P, | |
| 35 | wake: Wake, | |
| 36 | message: { id: string; thread_root: string | null }, | |
| 37 | chain: Chain<A>, | |
| 38 | ) { | |
| 39 | return { | |
| 40 | ...place, | |
| 41 | agent_id: wake.agent_id, | |
| 42 | message_id: message.id, | |
| 43 | thread_root: message.thread_root, | |
| 44 | asked_by: chain.asked_by, | |
| 45 | asker: chain.asker, | |
| 46 | hops: wake.hops, | |
| 47 | surface: "g1t" as const, | |
| 48 | }; | |
| 49 | } | |
| 50 | ||
| 51 | export function deliveries(input: { | |
| 52 | /** Who wrote it: `user:<id>` or `agent:<id>`. */ | |
| 53 | author: string; | |
| 54 | /** For an agent's message: the hops of the delivery it answers. */ | |
| 55 | hops: number; | |
| 56 | channelKind: "channel" | "dm"; | |
| 57 | /** The channel's agent members (archived ones left out). */ | |
| 58 | agents: AgentMember[]; | |
| 59 | /** Handles the message @mentions, lowercased. */ | |
| 60 | mentioned: string[]; | |
| 61 | }): Wake[] { | |
| 62 | const mentioned = new Set(input.mentioned.map((h) => h.toLowerCase())); | |
| 63 | const named = (agent: AgentMember) => mentioned.has(agent.handle.toLowerCase()); | |
| 64 | if (input.author.startsWith("user:")) { | |
| 65 | const woken = input.channelKind === "dm" ? input.agents : input.agents.filter(named); | |
| 66 | return woken.map((agent) => ({ agent_id: agent.id, hops: 0 })); | |
| 67 | } | |
| 68 | const hops = Math.max(0, Math.floor(input.hops || 0)) + 1; | |
| 69 | if (hops > MAX_HOPS) return []; | |
| 70 | return input.agents | |
| 71 | .filter((agent) => named(agent) && `agent:${agent.id}` !== input.author) | |
| 72 | .map((agent) => ({ agent_id: agent.id, hops })); | |
| 73 | } |