Skip to content
228 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 * Where a conversation happens, as the reply loop sees it: read what was
3 * said, show that the agent is typing, post its answer. The loop knows
4 * nothing else about the chat it is in, so the same agent (its definition,
5 * budget and replies) answers wherever it is reached: g1t's own chat today,
6 * another chat app the workspace connected later (docs/WORKSPACE.md,
7 * "Working from another chat app"). Only g1t's adapter exists.
8 */
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.9import type { AgentDelivery, ChatMessage, ConversationForAgent, MessageCard, ServiceBinding } from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to10
11// By path, not the package: it imports only types, so the adapter is tested under Node.
12import { CHAT_MAX_HOPS, chatClient } from "../../../packages/contracts/src/chat.ts";
13
14/** One message, as the reply loop reads it, whatever the surface. */
15export type SurfaceMessage = {
16 id: string;
17 author: { kind: "user" | "agent"; id: string; name: string; display_name: string };
18 body: string;
19 /** A card's one-line summary, when the message is a card. */
20 card: string | null;
21 created_at: string;
22};
23
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.24/** One member of a conversation, as an agent is told about it. */
25export type ConversationMember = {
26 kind: "user" | "agent";
27 id: string;
28 /** What `@` mentions: a person's username, an agent's handle. */
29 name: string;
30 display_name: string;
31 /** An agent's title, or its role when it has none; null for a person. */
32 title: string | null;
33};
34
35/**
36 * Where an agent is answering and who is in it (docs/WORKSPACE.md, "Where
37 * you are"): only these members read what it says there.
38 */
39export type Conversation = {
40 kind: "dm" | "group_dm" | "private_channel" | "public_channel";
41 /** A channel's name; null for a direct message. */
42 name: string | null;
43 /** Every agent, then people up to a cap. */
44 members: ConversationMember[];
45 people: number;
46 agents: number;
47};
48
49/** What handing work to a colleague did. */
50export type HandedOff = { ok: true; where: "here" | "group_dm"; opened: boolean } | { ok: false; message: string };
51
Chat and workspace agents: channels, DMs and named agents you talk to52export interface Surface {
53 /** The latest `limit` messages of the conversation (the thread, or the DM or channel), oldest first. */
54 history(limit: number): Promise<SurfaceMessage[]>;
55 /** Shows the agent typing. Never throws: it is a courtesy. */
56 typing(): Promise<void>;
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)57 /**
58 * Posts as the agent, in the conversation it was asked in, optionally as
59 * a card (a consult, say). Returns the new message's id.
60 */
61 post(body: string, card?: MessageCard | null): Promise<string>;
62 /**
63 * Reacts 👀 to the message that woke the agent, so people see it is on
64 * it. Never throws, and skipped where it adds nothing (see `reactsIn`).
65 */
66 acknowledge(): Promise<void>;
67 /**
68 * Once the reply has posted: 👀 becomes ✅ when it answered (`done`), or
69 * is taken away when it posted a notice or an apology (`withdrawn`).
70 * Never throws.
71 */
72 settle(outcome: "done" | "withdrawn"): Promise<void>;
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.73 /** The conversation and who is in it; null when it can't be read. Never throws. */
74 conversation(): Promise<Conversation | null>;
75 /**
76 * Hands work to a colleague agent for the person who asked: posted here
77 * when the colleague is in this channel or group DM, otherwise in the
78 * group DM of that person, this agent and the colleague, with a card
79 * here saying where it went. Wakes the colleague and nobody else.
80 */
81 handOff(colleagueId: string, brief: string): Promise<HandedOff>;
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)82}
83
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.84/** A conversation as the chat service describes it, as the reply loop reads it. */
85export function conversationFrom(value: ConversationForAgent): Conversation {
86 const { channel } = value;
87 const kind =
88 channel.kind === "dm" ? (value.people + value.agents > 2 ? "group_dm" : "dm") : channel.private ? "private_channel" : "public_channel";
89 return {
90 kind,
91 name: channel.kind === "dm" ? null : channel.name,
92 members: value.members.map((m) => ({
93 kind: m.kind,
94 id: m.id,
95 name: m.name,
96 display_name: m.display_name,
97 title: m.kind === "agent" ? m.title || m.role || null : null,
98 })),
99 people: value.people,
100 agents: value.agents,
101 };
102}
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)104export const SEEN = "👀";
105export const DONE = "✅";
106
107/**
108 * Whether an agent reacts in a conversation: in channels and in direct
109 * messages with more than one person. In a DM with one person, the typing
110 * indicator says enough.
111 */
112export function reactsIn(kind: "channel" | "dm", people: number): boolean {
113 return kind === "channel" || people > 1;
Chat and workspace agents: channels, DMs and named agents you talk to114}
115
116/** A g1t chat message as the loop reads it. */
117export function fromChat(message: ChatMessage): SurfaceMessage {
118 const card = message.card
119 ? [message.card.kind, message.card.title, message.card.detail, message.card.state].filter(Boolean).join(" · ")
120 : null;
121 return {
122 id: message.id,
123 author: {
124 kind: message.author.kind,
125 id: message.author.id,
126 name: message.author.name,
127 display_name: message.author.display_name,
128 },
129 body: message.deleted_at ? "" : message.body,
130 card,
131 created_at: message.created_at,
132 };
133}
134
135/**
136 * g1t's own chat, over the chat service. Replies stay in the thread they
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.137 * were asked in, and carry the delivery's hops, `asked_by` and `asker` on.
138 * A reply wakes nobody, @mentions or not; a hand-off wakes its colleague
139 * one hop further along the same person's request, with that person's
140 * access.
Chat and workspace agents: channels, DMs and named agents you talk to141 */
142export function g1tSurface(chat: ServiceBinding, delivery: AgentDelivery): Surface {
143 const client = chatClient(chat);
144 const { workspace, channel_id: channel, agent_id: agent } = delivery;
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)145 // Whether 👀 went on, so `settle` knows what to take back.
146 let acknowledged: Promise<boolean> = Promise.resolve(false);
Chat and workspace agents: channels, DMs and named agents you talk to147 return {
148 async history(limit) {
149 const page = await client.historyForAgent(workspace, channel, agent, { thread_root: delivery.thread_root, limit });
150 if (!page.ok) throw new Error(`reading the conversation failed: ${page.error.message}`);
151 return page.value.filter((message) => !message.deleted_at).map(fromChat);
152 },
153 async typing() {
154 await client.agentTyping(workspace, channel, agent).catch(() => undefined);
155 },
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)156 async acknowledge() {
157 acknowledged = (async () => {
158 try {
159 // A first message's hello answers nothing; a DM's people decide the rest.
160 if (delivery.message_id.startsWith("hello:")) return false;
161 if (delivery.channel_kind === "dm") {
162 const audience = await client.audience(workspace, channel);
163 if (!audience.ok || !reactsIn("dm", audience.value.member_count)) return false;
164 }
165 const reacted = await client.reactAsAgent(workspace, channel, agent, delivery.message_id, SEEN);
166 return reacted.ok;
167 } catch {
168 return false;
169 }
170 })();
171 await acknowledged;
172 },
173 async settle(outcome) {
174 try {
175 if (!(await acknowledged)) return;
176 await client.reactAsAgent(workspace, channel, agent, delivery.message_id, SEEN, true);
177 if (outcome === "done") await client.reactAsAgent(workspace, channel, agent, delivery.message_id, DONE);
178 } catch {
179 // A reaction is a courtesy: the reply stands without it.
180 }
181 },
182 async post(body, card = null) {
Chat and workspace agents: channels, DMs and named agents you talk to183 const posted = await client.postAsAgent(workspace, channel, agent, {
184 body,
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)185 card,
Chat and workspace agents: channels, DMs and named agents you talk to186 thread_root: delivery.thread_root,
187 hops: Math.min(delivery.hops, CHAT_MAX_HOPS),
188 asked_by: delivery.asked_by,
189 // Agents this reply wakes act for the same person, with their access.
190 asker: delivery.asker ?? null,
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)191 // Chat adds this agent, and never hands the post back to the one that sent it the work.
192 chain: delivery.chain ?? [],
Chat and workspace agents: channels, DMs and named agents you talk to193 });
194 if (!posted.ok) throw new Error(`posting the reply failed: ${posted.error.message}`);
195 return posted.value.id;
196 },
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.197 async conversation() {
198 try {
199 const found = await client.conversationForAgent(workspace, channel, agent, delivery.asked_by);
200 return found.ok ? conversationFrom(found.value) : null;
201 } catch {
202 return null;
203 }
204 },
205 async handOff(colleagueId, brief) {
206 const done = await client.handOffAsAgent(workspace, channel, agent, {
207 colleague_id: colleagueId,
208 brief,
209 thread_root: delivery.thread_root,
210 hops: Math.min(delivery.hops, CHAT_MAX_HOPS),
211 asked_by: delivery.asked_by,
212 // The colleague works for the same person, with their access.
213 asker: delivery.asker ?? null,
214 chain: delivery.chain ?? [],
215 });
216 return done.ok ? { ok: true, where: done.value.where, opened: done.value.opened } : { ok: false, message: done.error.message };
217 },
Chat and workspace agents: channels, DMs and named agents you talk to218 };
219}
220
221/** The surface a delivery came from. Every delivery is g1t's today. */
222export function surfaceFor(chat: ServiceBinding, delivery: AgentDelivery): Surface {
223 switch (delivery.surface ?? "g1t") {
224 case "g1t":
225 default:
226 return g1tSurface(chat, delivery);
227 }
228}

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