/** * The `session` tool, as the extensions that are not `session-bus` call it. * * `board` and `team` are the user's surface for the team; the bus is the transport * underneath, and neither may import it (`test/structure.test.ts`). So the one place * that turns a call into a typed answer lives here, next to the tool's name in * `lib/tool-names.ts` — two copies would be two copies of the details-shape * knowledge, and a bus that renamed a field would be broken in one of them. * * There are two transports, and the difference is the context, not the caller's * choice: `join` travels as an in-process request on `pi.events` because a *command* * context has no `ctx.executeTool` — the same constraint `lib/plan-entry.ts` exists * for — while `peers` and `send` are nested tool calls, which only ever run inside a * tool. One function either way, so a caller never has to know which it is holding. * * Every function returns a result and none throws: "the bus is not loaded", "a plan * is open", and "that label is taken" are all answers a caller reports, not reasons * to abandon a board write or a card operation that has nothing to do with the bus. * * This module is the *caller* side only. Reading the bus's records from disk is * `session-roster.ts`, which is what works when the bus is not loaded at all. */ import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent"; import { type BusAnswer, requestOverBus } from "./bus-request.ts"; import { messageOf } from "./errors.ts"; import { watchingOf } from "./session-roster.ts"; import { SESSION_TOOL } from "./tool-names.ts"; /** * The `session send` text limit, which is also the longest handoff or offer. * * It is copied from the bus's protocol rather than imported — the dependency runs one * way — and it lives here so `team` and `board` clip to one number instead of two. */ export const MAX_MESSAGE_TEXT = 4_000; /** Whether a session is listening for mail, and whether it raises a turn. */ export interface Watching { armed: boolean; wake: boolean; } /** One session the bus lists. */ export interface Peer { id: string; label: string; /** Absent when the record does not say, which is not the same as "not listening". */ listening?: boolean; } /** What a join did: the label it landed on, and whether mail will reach the session. */ export type JoinResult = { ok: true; label: string; watching: Watching } | { ok: false; error: string }; /** EventBus channel on which a caller asks `session-bus` to join the team for it. */ export const TEAM_JOIN_REQUEST = "my-pi-agent/team-join/request"; /** * What a join asks for. * * `label` is the name to be on the team as; without one the bus settles its own. `name` * is the separate question of whether that label also becomes Pi's session name — the * footer and `/resume` — and it is opt-in, because a join is not a rename: only * `/team join