| 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 | } |