Skip to content
135 linesCodeBlameRaw

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 to1/**
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"):
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.6 * - A person's message in a channel wakes the agent members it @mentions.
7 * In a direct message it wakes the agents in it that it @mentions, or
8 * all of them when it mentions none. That starts a chain at hop 0.
9 * - An agent's message wakes nobody, @mentions or not. An agent gets a
10 * colleague working only by handing off (`handOffPlace`), which wakes
11 * that one colleague, one hop further along the chain it was answering.
12 * No loops, and no agent summoned because its name came up.
Chat and workspace agents: channels, DMs and named agents you talk to13 * - A chain stops after `MAX_HOPS` hops, and asks a person instead.
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 */
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)30export 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[];
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.36 /**
37 * Set for a message written with a workflow job's token (`G1T_TOKEN`):
38 * it wakes no agent, or a workflow that posts on a failing check could
39 * start one whose push runs the workflow again, without end.
40 */
41 quiet?: boolean;
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)42};
43
44/** The most agent ids a chain carries: the hop limit's worth, and some. */
45const MAX_CHAIN = 16;
46
47/**
48 * An agent's post's chain: the one it was answering, as the agents service
49 * passed it back, with the agent itself added. Anything that is not a list
50 * of ids is no chain.
51 */
52export function chainFor(given: unknown, author: string): string[] {
53 const before = Array.isArray(given) ? given.filter((id): id is string => typeof id === "string" && !!id).slice(-MAX_CHAIN) : [];
54 return [...before, author];
55}
Chat and workspace agents: channels, DMs and named agents you talk to56
57/** What the agents service is handed for one wake (AgentDelivery), on g1t's own chat. */
58export function delivery<P extends object, A>(
59 place: P,
60 wake: Wake,
61 message: { id: string; thread_root: string | null },
62 chain: Chain<A>,
63) {
64 return {
65 ...place,
66 agent_id: wake.agent_id,
67 message_id: message.id,
68 thread_root: message.thread_root,
69 asked_by: chain.asked_by,
70 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)71 chain: chain.chain,
Chat and workspace agents: channels, DMs and named agents you talk to72 hops: wake.hops,
73 surface: "g1t" as const,
74 };
75}
76
77export function deliveries(input: {
78 /** Who wrote it: `user:<id>` or `agent:<id>`. */
79 author: string;
80 /** For an agent's message: the hops of the delivery it answers. */
81 hops: number;
82 channelKind: "channel" | "dm";
83 /** The channel's agent members (archived ones left out). */
84 agents: AgentMember[];
85 /** Handles the message @mentions, lowercased. */
86 mentioned: string[];
87}): Wake[] {
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.88 // Only a person's message wakes anyone; agents reach each other by hand-off.
89 if (!input.author.startsWith("user:")) return [];
Chat and workspace agents: channels, DMs and named agents you talk to90 const mentioned = new Set(input.mentioned.map((h) => h.toLowerCase()));
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.91 const named = input.agents.filter((agent) => mentioned.has(agent.handle.toLowerCase()));
92 const woken = input.channelKind === "dm" && !named.length ? input.agents : named;
93 return woken.map((agent) => ({ agent_id: agent.id, hops: 0 }));
94}
95
96/**
97 * Where a hand-off's brief goes: here, when the colleague is already a
98 * member of this channel or group direct message (everyone here sees the
99 * work move); otherwise a group direct message of the person who asked,
100 * the agent and the colleague, so the colleague reads only what it was
101 * handed and works for that person, with their access.
102 */
103export function handOffPlace(input: { channelKind: "channel" | "dm"; members: number; colleagueHere: boolean }): "here" | "group_dm" {
104 const shared = input.channelKind === "channel" || input.members > 2;
105 return input.colleagueHere && shared ? "here" : "group_dm";
106}
107
108/**
109 * Why an agent may not hand work to `colleague`, or null when it may. The
110 * colleague is already of the workspace and not archived; this decides the
111 * rest of the rails: not itself, never @g1t (no agent puts g1t to work),
112 * never an agent already on this request (no ping-pong), and within the
113 * hop limit.
114 */
115export function handOffRefusal(input: { agent: string; colleague: { id: string; builtin?: boolean }; chain: string[]; hops: number }): string | null {
116 if (input.colleague.id === input.agent) return "An agent can't hand work to itself.";
117 if (input.colleague.builtin) return "An agent can't hand work to @g1t. The person can ask @g1t themselves.";
118 if (input.chain.includes(input.colleague.id)) return "That agent has already handled this request: no handing work back.";
119 if (Math.max(0, Math.floor(input.hops || 0)) + 1 > MAX_HOPS) return "This request has been passed along too many times. Ask a person to step in.";
120 return null;
Chat and workspace agents: channels, DMs and named agents you talk to121}
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)122
123/** The built-in orchestrator's handle. Same as BUILTIN_AGENT_HANDLE in @g1t/contracts. */
124export const ORCHESTRATOR = "g1t";
125
126/**
127 * Whether a message brings @g1t into the conversation: every workspace has
128 * it, so mentioning it in a channel adds it as a member the first time,
129 * with no invite. A direct message is made with its members and never
130 * gains one; to talk to @g1t alone, open a DM with it.
131 */
132export function addsOrchestrator(input: { channelKind: "channel" | "dm"; mentioned: string[]; orchestratorIsMember: boolean }): boolean {
133 if (input.channelKind !== "channel" || input.orchestratorIsMember) return false;
134 return input.mentioned.some((handle) => handle.toLowerCase() === ORCHESTRATOR);
135}

This file's history is long; its oldest lines are credited to the oldest commit read.