Skip to content
73 linesCodeBlameRaw
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. */
17export const MAX_HOPS = 6;
18
19export type AgentMember = { id: string; handle: string };
20
21export 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 */
30export 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. */
33export 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
51export 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}