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 | * 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 | */ | |
| 9 | import type { AgentDelivery, ChatMessage, ServiceBinding } from "@g1t/contracts"; | |
| 10 | ||
| 11 | // By path, not the package: it imports only types, so the adapter is tested under Node. | |
| 12 | import { CHAT_MAX_HOPS, chatClient } from "../../../packages/contracts/src/chat.ts"; | |
| 13 | ||
| 14 | /** One message, as the reply loop reads it, whatever the surface. */ | |
| 15 | export 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 | ||
| 24 | export interface Surface { | |
| 25 | /** The latest `limit` messages of the conversation (the thread, or the DM or channel), oldest first. */ | |
| 26 | history(limit: number): Promise<SurfaceMessage[]>; | |
| 27 | /** Shows the agent typing. Never throws: it is a courtesy. */ | |
| 28 | typing(): Promise<void>; | |
| 29 | /** Posts as the agent, in the conversation it was asked in. Returns the new message's id. */ | |
| 30 | post(body: string): Promise<string>; | |
| 31 | } | |
| 32 | ||
| 33 | /** A g1t chat message as the loop reads it. */ | |
| 34 | export function fromChat(message: ChatMessage): SurfaceMessage { | |
| 35 | const card = message.card | |
| 36 | ? [message.card.kind, message.card.title, message.card.detail, message.card.state].filter(Boolean).join(" · ") | |
| 37 | : null; | |
| 38 | return { | |
| 39 | id: message.id, | |
| 40 | author: { | |
| 41 | kind: message.author.kind, | |
| 42 | id: message.author.id, | |
| 43 | name: message.author.name, | |
| 44 | display_name: message.author.display_name, | |
| 45 | }, | |
| 46 | body: message.deleted_at ? "" : message.body, | |
| 47 | card, | |
| 48 | created_at: message.created_at, | |
| 49 | }; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * g1t's own chat, over the chat service. Replies stay in the thread they | |
| 54 | * were asked in, and carry the delivery's hops, `asked_by` and `asker` on, | |
| 55 | * so an agent the reply @mentions is woken one hop further along the same | |
| 56 | * person's request, with that person's access. | |
| 57 | */ | |
| 58 | export function g1tSurface(chat: ServiceBinding, delivery: AgentDelivery): Surface { | |
| 59 | const client = chatClient(chat); | |
| 60 | const { workspace, channel_id: channel, agent_id: agent } = delivery; | |
| 61 | return { | |
| 62 | async history(limit) { | |
| 63 | const page = await client.historyForAgent(workspace, channel, agent, { thread_root: delivery.thread_root, limit }); | |
| 64 | if (!page.ok) throw new Error(`reading the conversation failed: ${page.error.message}`); | |
| 65 | return page.value.filter((message) => !message.deleted_at).map(fromChat); | |
| 66 | }, | |
| 67 | async typing() { | |
| 68 | await client.agentTyping(workspace, channel, agent).catch(() => undefined); | |
| 69 | }, | |
| 70 | async post(body) { | |
| 71 | const posted = await client.postAsAgent(workspace, channel, agent, { | |
| 72 | body, | |
| 73 | thread_root: delivery.thread_root, | |
| 74 | hops: Math.min(delivery.hops, CHAT_MAX_HOPS), | |
| 75 | asked_by: delivery.asked_by, | |
| 76 | // Agents this reply wakes act for the same person, with their access. | |
| 77 | asker: delivery.asker ?? null, | |
| 78 | }); | |
| 79 | if (!posted.ok) throw new Error(`posting the reply failed: ${posted.error.message}`); | |
| 80 | return posted.value.id; | |
| 81 | }, | |
| 82 | }; | |
| 83 | } | |
| 84 | ||
| 85 | /** The surface a delivery came from. Every delivery is g1t's today. */ | |
| 86 | export function surfaceFor(chat: ServiceBinding, delivery: AgentDelivery): Surface { | |
| 87 | switch (delivery.surface ?? "g1t") { | |
| 88 | case "g1t": | |
| 89 | default: | |
| 90 | return g1tSurface(chat, delivery); | |
| 91 | } | |
| 92 | } |