import type { ConciergeAnswer, ConciergePolicy, ConciergeStatus, StandingOrder } from "./concierge.js"; import type { RoostEvent } from "./events.js"; import type { PrInfo } from "./pr.js"; import type { UsageInfo } from "./usage.js"; import type { BoardSummary, BoardTask } from "./board.js"; import type { ProfileTerm } from "./profiles.js"; export type JobStateName = "working" | "blocked" | "done" | "failed" | "stopped"; export type JobStatus = null | "busy" | "waiting" | "idle"; export type WaitingFor = null | "dialog" | "question" | "first-prompt"; export type DispatchOrigin = "bg" | "dispatch" | "adopt" | "fork"; /** M8: how full the session's context window is (pi's ctx.getContextUsage()). */ export interface ContextInfo { /** Estimated tokens in context; null right after a compaction, until the next reply. */ tokens: number | null; window: number; /** 0–100; null when tokens is unknown. */ percent: number | null; } export interface JobRow { id: string; cwd: string; kind: "pi" | "shell"; startedAt: number; state: JobStateName; pid: number | null; status: JobStatus; waitingFor: WaitingFor; sessionFile: string; name: string; endedAt: number | null; /** Row text: a status sentence while working, the result or question after. */ summary: string | null; question: string | null; choices: string[]; model: string | null; /** M8: the worker's thinking level (or the one it was started with). */ thinking: string | null; /** M8: context window use as last reported; null for shell jobs and before the first report. */ context: ContextInfo | null; /** M24: what the session spent, as last reported (absent before it reports, or from older workers). */ usage?: UsageInfo | null; error: string | null; /** When the job entered its current state (for "waiting 3m"). */ stateSince: number; /** DM-4: stopped for idleness; attaching or replying starts it again. */ reaped: boolean; /** AV-K7: listed first and never reaped. */ pinned: boolean; /** AV-K13: the user's position within its group, or null when never moved. */ order: number | null; /** The section the order belongs to (a state group or `dir:`); stale elsewhere. */ orderKey: string | null; /** M9: the session that spawned this one (roost_spawn), and its tree's root. */ parentSession?: string | null; rootSession?: string | null; /** M13: someone holds the parent session now (a foreground pi, or a job recorded for it). */ parentOpen?: boolean; /** M18: the agent it runs as. */ agent?: string | null; /** M16: the session's pull requests as gh last reported them. */ prs?: PrInfo[]; /** M10: the session's own git worktree (M26: and the commit it started from). */ worktree?: { path: string; branch: string; base?: string; } | null; /** M26: the sessions started together for one task with several models share this id. */ fanout?: string | null; /** M29: the agent that runs it: "pi", "shell", or (M30) the name of the user's agent profile. */ profile?: string; /** M39: the schedule that started it. */ schedule?: string | null; /** M31: what its agent said it can do in its hello (absent: it never said). */ capabilities?: string[]; /** M44: the hub's own agent (not listed as a session; the hub shows its status line). */ role?: "concierge" | null; /** M45: the concierge's suggested answer for a row that waits for one (the hub sends it with one key). */ suggested?: { text: string; why: string | null; at: number; } | null; /** M48: the task it was started on (one line, at most 200 characters), and the user replies waiting for it. */ task?: string | null; queued?: number; } /** M9: a session an agent may message (roost_agents). */ export interface PeerRow { id: string; name: string; relation: "parent" | "child" | "sibling" | "tree" | "external"; role: "job" | "foreground"; state: JobStateName | "live"; status: JobStatus; waitingFor: WaitingFor; summary: string | null; question: string | null; cwd: string; } export interface SendResult { /** delivered: handed to the running session; queued: it is busy starting; started: it was ended and starts again; duplicate (M43): the same text went to it a moment ago, not sent again */ outcome: "delivered" | "queued" | "started" | "stopped" | "not running" | "duplicate"; id: string; name: string; relation: PeerRow["relation"]; /** The target is waiting on a dialog; the message waits until it is answered. */ dialog?: boolean; } export interface HandbackArgs { state: "done" | "blocked" | "failed"; result: string | null; question: string | null; error: string | null; } /** What a worker reports about itself (full snapshot every time). */ export interface WorkerReport { state: "working" | "blocked" | "done" | "failed"; status: Exclude; waitingFor: WaitingFor; summary: string | null; question: string | null; choices: string[]; tool: string | null; error: string | null; name: string | null; model: string | null; /** M8; absent from workers before 0.1.5. */ thinking?: string | null; context?: ContextInfo | null; /** M24: absent from workers before 0.1.11. */ usage?: UsageInfo | null; /** The worker reacts to the host's SIGUSR2 attach signal (AT-5). */ attachSignal?: boolean; /** M14: on attach the worker sets the terminal up again and repaints itself (read by hosts before M21). */ attachRepaint?: boolean; /** M9: the worker takes agent messages and reports (custom messages, per-item acks). */ agentItems?: boolean; /** M11: when a phone (pi-mobile) last acted on this session (ms epoch): it is in use. */ remoteAt?: number; /** M16: pull request URLs the session linked (gh output, branch lookups). */ prLinks?: string[]; } export interface DispatchArgs { /** DI-6: "shell" runs `command` without pi (no session file). */ kind?: "pi" | "shell"; cwd: string; sessionFile: string; command: string[]; env: Record; name: string; model?: string; thinking?: string; /** M18: the agent (pi-subagents definition) the session runs as. */ agent?: string; /** M20: how the worker's environment follows its workspace (absent: as carried). */ workspaceEnv?: "shell" | "direnv" | "off"; /** M20: names (or `PREFIX*`) a workspace's environment may not set. */ envDeny?: string[]; /** M20: names that pass envDeny (envAllowList). */ envForce?: string[]; origin: DispatchOrigin; piVersion?: string; /** M10: the worktree the dispatching side made for this session (cwd is inside it). */ worktree?: { path: string; branch: string; repo: string; base: string; }; /** M26: one task, several models: the id the sessions share. */ fanout?: string; /** M30: a level 0 agent from this profile (kind "shell"). */ profile?: string; /** M30: how its screen is read and how replies are typed into it. */ term?: ProfileTerm; /** M30: the argv that reopens it, when the profile has one. */ resumeCommand?: string[]; /** M30: the first task is in its argv (it has been given something to do). */ taskInArgv?: boolean; /** M40: a profile with `resume`: a run of it cut short can carry on (autoResume). */ resumable?: boolean; /** M31: a profile's agent that speaks the roost agent protocol. */ protocol?: boolean; /** M36: a resend with the same id returns the job the first one made. */ requestId?: string; } export interface DaemonStatus { pid: number; version: string; socketDir: string; liveJobs: number; totalJobs: number; ptyError: string | null; startedAt: number; } /** M31: the protocol version this daemon and these clients speak (docs/protocol.md). */ export declare const PROTOCOL_VERSION = 1; /** * M31: what an agent can do, said in its hello. Unknown ones are ignored; the list only * grows with a protocol version. */ export declare const AGENT_CAPABILITIES: readonly ["report", "replies", "steer", "usage", "context", "model-switch", "transcript", "peers", "agent-items"]; export type AgentCapability = (typeof AGENT_CAPABILITIES)[number]; export interface HelloArgs { protocol: number; agent?: { name: string; version?: string; }; capabilities?: string[]; } export interface HelloResult { protocol: number; daemon: { name: "roost"; version: string; }; /** What the daemon offers agents (the ops beyond report, ack and hello). */ capabilities: string[]; } /** M39: a schedule as the hub and the CLI show it. */ export interface ScheduleRow { name: string; when: string; task: string | null; /** M40: what each run is: "pi", "shell" (a command) or a profile's name. */ agent: string; cwd: string; model: string | null; paused: boolean; catchUp: boolean; nextAt: number | null; lastRunAt: number | null; lastJob: string | null; lastSkip: { at: number; why: string; } | null; } export interface OpArgs { ping: {}; /** M39: scheduled tasks. */ schedules: {}; "schedule-add": { name: string; when: string; launch: DispatchArgs; task?: string; catchUp?: boolean; }; "schedule-rm": { name: string; }; "schedule-pause": { name: string; paused: boolean; }; "schedule-run": { name: string; }; /** M31: an agent says which protocol and capabilities it has, before it reports. */ hello: HelloArgs; list: { all?: boolean; cwd?: string; }; dispatch: DispatchArgs; stop: { id: string; }; rm: { id: string; }; resolve: { query: string; }; status: {}; shutdown: { keepWorkers?: boolean; }; subscribe: {}; report: { id: string; token: string; report: WorkerReport; }; /** M28: a reply to a working session steers into its current work (pi's Enter) or waits for it to finish (Alt+Enter); absent: waits. */ /** M36: `requestId` makes a resend safe: the same id is taken once (the first answer is given again). */ reply: { id: string; text: string; mode?: ReplyMode; requestId?: string; }; /** Worker → daemon: the reply with this seq was handed to pi (exact: only that agent item). */ ack: { id: string; token: string; seq: number; exact?: boolean; }; /** Start an ended job again from its session file; `restart` also restarts a live one. */ respawn: { id?: string; all?: boolean; restart?: boolean; prompt?: string; }; pin: { id: string; pinned: boolean; }; rename: { id: string; name: string; }; /** AV-K13: these jobs, in this order (the rest of their group keeps its place). */ reorder: { ids: string[]; key: string; }; /** M9: a foreground pi joins as a peer on this connection (pid owns the session's writer lock). */ peer_register: { session?: string; name: string; cwd: string; pid: number; }; /** M9, on a peer connection: the sessions this peer may message. */ peers: {}; send: { to: string; message: string; }; spawn: { launch: DispatchArgs; }; /** M9, on a worker connection: the run it was spawned for (or a user turn) ended. */ handback: HandbackArgs; /** M9, on a foreground peer connection: the report with this ref reached pi. */ peer_ack: { ref: string; }; /** M43: the event feed (what happened), with a cursor: `after` the seq last seen. */ events: { after?: number; limit?: number; id?: string; }; /** M43, on a peer connection: tell me once when that session is next idle (done, or waiting for an answer) or has ended. */ watch: { id: string; when: "idle" | "ended"; }; /** * M44: the concierge: start it (a pi launch the hub built), stop it, or ask how it stands. * M45: set the policy (`answer`, `act`), or undo the last answer it gave a session. The hub's only. */ concierge: { action: "on"; launch?: DispatchArgs; } | { action: "off"; } | { action: "status"; } | { action: "answer"; answer: ConciergeAnswer; } | { action: "act"; act: boolean; } | { action: "undo"; }; /** M44, from the hub: a question for the concierge (`? …`); its answer comes as a `concierge` event. */ "concierge-ask": { text: string; }; /** M44, on the concierge's own connection: what it tells the user (hub_notify), or its answer to a question. */ "concierge-say": { kind: "info" | "need" | "answer"; text: string; }; /** M45, on the concierge's connection: the policy in force (which of its tools it may use). */ "concierge-policy": Record; /** M45, on the concierge's connection: the answer it would give a session that waits for one, and why. */ "concierge-suggest": { id: string; text: string; why?: string; }; /** M45, on the concierge's connection: an answer in the user's place; refused (`policy`) unless the policy allows. */ "concierge-reply": { id: string; text: string; why?: string; }; /** M45, on the concierge's connection: stop, restart or start a session; refused (`policy`) unless `act`. */ "concierge-act": { what: "stop" | "respawn"; id: string; why?: string; } | { what: "dispatch"; launch: DispatchArgs; why?: string; }; /** * M46: the standing orders: keep one (the hub's or the concierge's connection; `when` in M39's * vocabulary puts it on a clock), drop one (the hub's; the concierge's own only), list them, * mark one applied (the concierge's). */ "concierge-order": { action: "add"; text: string; when?: string; } | { action: "rm"; id: number; } | { action: "list"; } | { action: "applied"; id: number; text?: string; }; /** * M47: the shared task board (the hub's, the concierge's and the sessions' connections): list * it, add a task, claim one (a session's; refused `conflict` when another live session holds * it, `blocked` while one it waits on is not done), give one back, mark one done, drop one * (the hub's, the concierge's), hand one to a session (a message there). */ task: { action: "list"; } | { action: "add"; title: string; detail?: string; after?: number[]; } | { action: "claim"; id: number; } | { action: "release"; id: number; } | { action: "done"; id: number; result?: string; } | { action: "drop"; id: number; } | { action: "assign"; id: number; to: string; }; } export interface OpResults { ping: { pid: number; version: string; }; schedules: { schedules: ScheduleRow[]; }; "schedule-add": ScheduleRow; "schedule-rm": { name: string; }; "schedule-pause": ScheduleRow; "schedule-run": { name: string; id: string; }; hello: HelloResult; list: { jobs: JobRow[]; }; dispatch: { id: string; name: string; }; stop: { id: string; }; /** M10: what happened to the session's worktree. */ rm: { id: string; worktree?: { path: string; branch: string; removed: boolean; branchKept: boolean; reason?: string; }; }; resolve: { id: string; }; status: DaemonStatus; shutdown: {}; subscribe: {}; report: {}; /** delivered: handed to the live worker; saved: kept for its next start (PK-5). */ reply: { delivered: boolean; saved: boolean; stopped?: boolean; respawned?: boolean; error?: string; duplicate?: boolean; }; ack: {}; respawn: { results: { id: string; started: boolean; error?: string; }[]; }; pin: { id: string; pinned: boolean; }; rename: { id: string; name: string; }; reorder: {}; peer_register: { id: string; }; peers: { peers: PeerRow[]; }; send: SendResult; spawn: { id: string; name: string; }; handback: { routed: boolean; }; peer_ack: {}; events: { events: RoostEvent[]; cursor: number; }; /** fired: it was already so; the notice went out at once. */ watch: { id: string; name: string; when: "idle" | "ended"; fired: boolean; }; concierge: ConciergeStatus; "concierge-ask": { id: string; }; "concierge-say": {}; "concierge-policy": ConciergePolicy; "concierge-suggest": { id: string; name: string; }; "concierge-reply": { id: string; name: string; }; "concierge-act": { id: string; name: string; }; "concierge-order": { orders: StandingOrder[]; order?: StandingOrder; }; /** M47: the board as it stands (and the task acted on; `sent`: how the hand-over message went). */ task: { tasks: BoardTask[]; task?: BoardTask; sent?: SendResult["outcome"]; }; } export type Op = keyof OpArgs; export type RequestOf = { id: number; op: O; args?: OpArgs[O]; }; export type Request = { [O in Op]: RequestOf; }[Op]; export type ArgsOf = OpArgs[O]; export type ResultOf = OpResults[O]; export type OkResponse = { id: number; ok: true; result: OpResults[O]; }; export type ErrResponse = { id: number; ok: false; error: string; code?: string; }; export type Response = OkResponse | ErrResponse; export type Event = { event: "job"; job: JobRow; } | { event: "removed"; id: string; } /** M44: the concierge's status changed (on, off, a notice, an answer, its budget). */ | { event: "concierge"; status: ConciergeStatus; } /** M47: the task board changed (its counts; `task list` has the tasks). */ | { event: "board"; board: BoardSummary; } /** Daemon → worker, on the connection the worker reports on. */ | { event: "deliver"; id: string; seq: number; text: string; /** M9: absent for user replies; agent items carry an envelope as text (M43: a notice is a watch that fired; M44: a digest wakes the concierge). */ kind?: "reply" | "message" | "report" | "notice" | "digest"; from?: string; relation?: string; /** M9: a report to a foreground peer, acknowledged with peer_ack. */ ref?: string; /** M28: how a user reply joins a running turn. */ mode?: ReplyMode; }; /** M28: "steer" joins the work under way, after its current tool calls; "followUp" waits until it is done. */ export type ReplyMode = "steer" | "followUp"; /** A ContextInfo from untrusted JSON (a report, state.json), or null. */ export declare function contextFrom(v: unknown): ContextInfo | null; /** M49: a line longer than this (in characters) is not a message of this protocol: the reader gives the stream up. */ export declare const LINE_MAX: number; export declare class LineTooLong extends Error { constructor(max: number); } /** * Splits a stream into lines. Only what just arrived is scanned for newlines (a long * line in progress is not read again on every chunk). A line past `max` throws * LineTooLong once; after that the reader takes nothing more. */ export declare class LineReader { private decoder; private partial; private dead; private readonly max; constructor(max?: number); push(chunk: Buffer | string): string[]; private giveUp; } export declare function encodeLine(value: unknown): string;