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 | */ | |
| 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) | 30 | export type Chain<A> = { |
| 31 | hops: number; | |
| 32 | asked_by: string; | |
| 33 | asker: A | null; | |
| 34 | /** The agents that handled the request so far, by id, oldest first: for an agent's message, ending with its author. */ | |
| 35 | chain: string[]; | |
| 36 | }; | |
| 37 | ||
| 38 | /** The most agent ids a chain carries: the hop limit's worth, and some. */ | |
| 39 | const MAX_CHAIN = 16; | |
| 40 | ||
| 41 | /** | |
| 42 | * An agent's post's chain: the one it was answering, as the agents service | |
| 43 | * passed it back, with the agent itself added. Anything that is not a list | |
| 44 | * of ids is no chain. | |
| 45 | */ | |
| 46 | export function chainFor(given: unknown, author: string): string[] { | |
| 47 | const before = Array.isArray(given) ? given.filter((id): id is string => typeof id === "string" && !!id).slice(-MAX_CHAIN) : []; | |
| 48 | return [...before, author]; | |
| 49 | } | |
| 50 | ||
| 51 | /** | |
| 52 | * The agent that sent the author its work: the one before the author in | |
| 53 | * the chain. The author's message never goes back to it (no ping-pong). | |
| 54 | */ | |
| 55 | export function sender(chain: string[]): string | null { | |
| 56 | return chain.length >= 2 ? chain[chain.length - 2] : null; | |
| 57 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 58 | |
| 59 | /** What the agents service is handed for one wake (AgentDelivery), on g1t's own chat. */ | |
| 60 | export function delivery<P extends object, A>( | |
| 61 | place: P, | |
| 62 | wake: Wake, | |
| 63 | message: { id: string; thread_root: string | null }, | |
| 64 | chain: Chain<A>, | |
| 65 | ) { | |
| 66 | return { | |
| 67 | ...place, | |
| 68 | agent_id: wake.agent_id, | |
| 69 | message_id: message.id, | |
| 70 | thread_root: message.thread_root, | |
| 71 | asked_by: chain.asked_by, | |
| 72 | asker: chain.asker, | |
| 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) | 73 | chain: chain.chain, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 74 | hops: wake.hops, |
| 75 | surface: "g1t" as const, | |
| 76 | }; | |
| 77 | } | |
| 78 | ||
| 79 | export function deliveries(input: { | |
| 80 | /** Who wrote it: `user:<id>` or `agent:<id>`. */ | |
| 81 | author: string; | |
| 82 | /** For an agent's message: the hops of the delivery it answers. */ | |
| 83 | hops: number; | |
| 84 | channelKind: "channel" | "dm"; | |
| 85 | /** The channel's agent members (archived ones left out). */ | |
| 86 | agents: AgentMember[]; | |
| 87 | /** Handles the message @mentions, lowercased. */ | |
| 88 | mentioned: string[]; | |
| 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) | 89 | /** For an agent's message: the agent that sent it the work, never handed it back. */ |
| 90 | notTo?: string | null; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 91 | }): Wake[] { |
| 92 | const mentioned = new Set(input.mentioned.map((h) => h.toLowerCase())); | |
| 93 | const named = (agent: AgentMember) => mentioned.has(agent.handle.toLowerCase()); | |
| 94 | if (input.author.startsWith("user:")) { | |
| 95 | const woken = input.channelKind === "dm" ? input.agents : input.agents.filter(named); | |
| 96 | return woken.map((agent) => ({ agent_id: agent.id, hops: 0 })); | |
| 97 | } | |
| 98 | const hops = Math.max(0, Math.floor(input.hops || 0)) + 1; | |
| 99 | if (hops > MAX_HOPS) return []; | |
| 100 | return input.agents | |
| 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) | 101 | .filter((agent) => named(agent) && `agent:${agent.id}` !== input.author && agent.id !== input.notTo) |
| Chat and workspace agents: channels, DMs and named agents you talk to | 102 | .map((agent) => ({ agent_id: agent.id, hops })); |
| 103 | } | |
| 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) | 104 | |
| 105 | /** The built-in orchestrator's handle. Same as BUILTIN_AGENT_HANDLE in @g1t/contracts. */ | |
| 106 | export const ORCHESTRATOR = "g1t"; | |
| 107 | ||
| 108 | /** | |
| 109 | * Whether a message brings @g1t into the conversation: every workspace has | |
| 110 | * it, so mentioning it in a channel adds it as a member the first time, | |
| 111 | * with no invite. A direct message is made with its members and never | |
| 112 | * gains one; to talk to @g1t alone, open a DM with it. | |
| 113 | */ | |
| 114 | export function addsOrchestrator(input: { channelKind: "channel" | "dm"; mentioned: string[]; orchestratorIsMember: boolean }): boolean { | |
| 115 | if (input.channelKind !== "channel" || input.orchestratorIsMember) return false; | |
| 116 | return input.mentioned.some((handle) => handle.toLowerCase() === ORCHESTRATOR); | |
| 117 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.