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.
| Merge branch 'worktree-agent-a1398e81ad1a64c5f' | 1 | /** |
| 2 | * Handing work to a colleague from chat (docs/WORKSPACE.md, "Agents know | |
| 3 | * each other"): the `hand_off` tool's port. Pure apart from what it is | |
| 4 | * given, so its refusals are tested on their own. | |
| 5 | * | |
| 6 | * The agent's words never wake a colleague; this does. It checks who the | |
| 7 | * colleague is here (an agent of the workspace, available, not the agent | |
| 8 | * itself, not @g1t, not one already on this request) and that the person | |
| 9 | * who asked may use the workspace's agents, then asks the surface to hand | |
| 10 | * it over. The chat service checks the same rails again, and decides | |
| 11 | * where the brief goes: here, when the colleague is in this channel or | |
| 12 | * group message, or the group message of the person, the agent and the | |
| 13 | * colleague. The colleague answers through its own reply, paid from its | |
| 14 | * own budget, with the asker's access. | |
| 15 | */ | |
| 16 | import type { AgentStatus, AskerAccess } from "@g1t/contracts"; | |
| 17 | ||
| 18 | import type { HandedOff } from "./surface.ts"; | |
| 19 | ||
| 20 | /** A colleague as the hand-off sees it. */ | |
| 21 | export type Colleague = { id: string; handle: string; display_name: string; builtin: boolean; status: AgentStatus }; | |
| 22 | ||
| 23 | export type HandOffPorts = { | |
| 24 | /** The agent itself. */ | |
| 25 | self: { id: string; handle: string }; | |
| 26 | /** The agents that handled this request before, oldest first. */ | |
| 27 | chain: string[]; | |
| 28 | /** What the person who asked may do; null when the chat service didn't say. */ | |
| 29 | asker: AskerAccess | null; | |
| 30 | /** An agent of the workspace by handle, not archived, or null. */ | |
| 31 | agent(handle: string): Promise<Colleague | null>; | |
| 32 | /** Whether a person of the workspace has this username. */ | |
| 33 | person(handle: string): Promise<boolean>; | |
| 34 | /** Hands the work over (Surface.handOff). */ | |
| 35 | handOff(colleagueId: string, brief: string): Promise<HandedOff>; | |
| 36 | }; | |
| 37 | ||
| 38 | const UNAVAILABLE: Partial<Record<AgentStatus, string>> = { | |
| 39 | paused: "is paused", | |
| 40 | out_of_budget: "is out of budget this month", | |
| 41 | }; | |
| 42 | ||
| 43 | /** Why the work can't go to `colleague`, or null when it can. */ | |
| 44 | export function handOffRefusal(handle: string, colleague: Colleague | null, ports: Pick<HandOffPorts, "self" | "chain" | "asker">, isPerson: boolean): string | null { | |
| 45 | if (!colleague) { | |
| 46 | return isPerson | |
| 47 | ? `@${handle} is a person, not an agent: hand_off is only for agents. Tell them who to ask, by name.` | |
| 48 | : `There is no agent called @${handle} in this workspace. Check the names in your colleagues list.`; | |
| 49 | } | |
| 50 | if (colleague.id === ports.self.id) return "That's you. Do the work yourself, or hand it to someone else."; | |
| 51 | if (colleague.builtin) return "@g1t can't be handed work by an agent. If the person wants g1t, they can ask it themselves."; | |
| 52 | if (ports.chain.includes(colleague.id)) return `@${handle} has already handled this request: don't hand it back. Answer with what you have.`; | |
| 53 | if (ports.asker?.role === "outside") return "The person you're working for isn't a member of this workspace, so its agents can't take work on for them."; | |
| 54 | const why = UNAVAILABLE[colleague.status]; | |
| 55 | if (why) return `@${handle} ${why}, so they can't take this on. Tell the person they're unavailable and why.`; | |
| 56 | return null; | |
| 57 | } | |
| 58 | ||
| 59 | /** The brief as it is posted: addressed to the colleague, by @mention if it isn't already. */ | |
| 60 | export function addressed(handle: string, brief: string): string { | |
| 61 | // Handles are letters, digits, `_` and `-`: nothing to escape. | |
| 62 | const named = new RegExp(`(^|[^a-z0-9_.@-])@${handle}(?![a-z0-9_/-])`, "i"); | |
| 63 | return named.test(brief) ? brief : `@${handle} ${brief}`; | |
| 64 | } | |
| 65 | ||
| 66 | /** What the agent is told once the work is handed over. */ | |
| 67 | export function handedMessage(colleague: Colleague, asker: string | null, done: Extract<HandedOff, { ok: true }>): string { | |
| 68 | const who = asker ? `@${asker}` : "the person who asked"; | |
| 69 | if (done.where === "here") { | |
| 70 | return `Handed to @${colleague.handle}: your brief is posted in this conversation and they're on it; they answer ${who} here. Say so in a sentence; don't repeat the brief.`; | |
| 71 | } | |
| 72 | const dm = done.opened ? "a new group message" : "the group message you three already have"; | |
| 73 | return `Handed to @${colleague.handle} in ${dm} with ${who}, you and them; your brief is there, and a card here links to it. Tell ${who} in a sentence that ${colleague.display_name} has it there. Don't repeat the brief.`; | |
| 74 | } | |
| 75 | ||
| 76 | /** The `hand_off` port (ActionPorts.handOff). */ | |
| 77 | export function handOffPort(ports: HandOffPorts): (handle: string, brief: string) => Promise<{ ok: boolean; message: string }> { | |
| 78 | return async (handle, brief) => { | |
| 79 | const colleague = await ports.agent(handle); | |
| 80 | const refused = handOffRefusal(handle, colleague, ports, colleague ? false : await ports.person(handle).catch(() => false)); | |
| 81 | if (refused) return { ok: false, message: refused }; | |
| 82 | const done = await ports.handOff(colleague!.id, addressed(colleague!.handle, brief)); | |
| 83 | if (!done.ok) return { ok: false, message: `It couldn't be handed over: ${done.message}` }; | |
| 84 | return { ok: true, message: handedMessage(colleague!, ports.asker?.username ?? null, done) }; | |
| 85 | }; | |
| 86 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.