@anhlp/alp-protocol 0.0.0-stage → 0.0.1-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +7 -0
  3. package/README.md +16 -2
  4. package/conformance/cases.json +951 -0
  5. package/conformance/run.d.mts +32 -0
  6. package/conformance/run.mjs +403 -0
  7. package/dist/connection.d.ts +13 -0
  8. package/dist/connection.js +0 -0
  9. package/dist/contract-v1.d.ts +8783 -0
  10. package/dist/contract-v1.js +5 -0
  11. package/dist/contract.d.ts +25 -0
  12. package/dist/contract.js +28 -0
  13. package/dist/dto.d.ts +104 -0
  14. package/dist/dto.js +0 -0
  15. package/dist/events.d.ts +354 -0
  16. package/dist/events.js +0 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.js +2 -0
  19. package/dist/public.d.ts +1 -0
  20. package/dist/public.js +2 -0
  21. package/dist/rpc.d.ts +1210 -0
  22. package/dist/rpc.js +0 -0
  23. package/dist/schedule.d.ts +43 -0
  24. package/dist/schedule.js +0 -0
  25. package/dist/session.d.ts +285 -0
  26. package/dist/session.js +0 -0
  27. package/dist/validate.d.ts +12 -0
  28. package/dist/validate.js +151 -0
  29. package/dist/wire.d.ts +192 -0
  30. package/dist/wire.js +137 -0
  31. package/fixtures/ipc-v1.jsonl +4 -0
  32. package/fixtures/v1/cases.json +7166 -0
  33. package/package.json +127 -4
  34. package/schema/ipc-v1.json +3271 -0
  35. package/schema/methods-v1.json +8678 -0
  36. package/src/connection.ts +13 -0
  37. package/src/contract-v1.js +2 -0
  38. package/src/contract.js +44 -0
  39. package/src/dto.ts +78 -0
  40. package/src/events.ts +207 -0
  41. package/src/index.ts +7 -0
  42. package/src/public.js +2 -0
  43. package/src/rpc.ts +272 -0
  44. package/src/schedule.ts +43 -0
  45. package/src/session.ts +213 -0
  46. package/src/validate.js +115 -0
  47. package/src/wire.js +213 -0
package/src/events.ts ADDED
@@ -0,0 +1,207 @@
1
+ import type { RuntimeKind, MailEvent, Pin } from './dto.js';
2
+
3
+ /** Provider-neutral session events (plans/reference/ALPD.md §4). Viewers project them; the runtime never imports a viewer. */
4
+
5
+ /** `code: 'outcome_unknown'` when the provider may or may not have received the request (ALPD §66). */
6
+ export type AlpError = { message: string; code?: string };
7
+
8
+ export type TimelineItem =
9
+ | {
10
+ kind: 'user_message'; id: string; text: string; clientMessageId?: string; /** How many images the message carried; their data is not replayed. */ images?: number;
11
+ /** Files attached to the message (capability `attachments`, §12.6): ids, display names, types and sizes, never a host path. */
12
+ attachments?: Array<{ attachmentId: string; name: string; mimeType: string; size: number }>;
13
+ /** Who wrote it: the user, or ALP itself (mail, a brief, a resume); absent when the runtime cannot tell. Only the user's words name a root (ALPD §15). */
14
+ origin?: TurnOrigin;
15
+ }
16
+ | { kind: 'assistant_message'; id: string; text: string }
17
+ /** Something about ALP as a whole the user should see, such as a usage limit or a pause (ALPD §29). */
18
+ | { kind: 'notice'; id: string; level: 'info' | 'warning' | 'error'; text: string }
19
+ /** The runtime compacting the session's context (ALPD §57). */
20
+ | { kind: 'compaction'; id: string; status: 'running' | 'completed' | 'failed'; trigger?: 'auto' | 'manual'; preTokens?: number; postTokens?: number }
21
+ /** The tasks a root's tree created or worked on (plans/reference/ALPD.md §21). */
22
+ | { kind: 'todo'; id: string; items: Array<{ id: string; text: string; status: 'pending' | 'in_progress' | 'completed' }> }
23
+ | {
24
+ kind: 'tool_call';
25
+ id: string;
26
+ callId: string;
27
+ name: string;
28
+ status: 'running' | 'completed' | 'failed';
29
+ error?: string;
30
+ detail: ToolCallDetail;
31
+ };
32
+
33
+ /**
34
+ * What a tool call did, typed by kind (ALPD §4.2), in the shapes of Paseo's ToolCallDetail.
35
+ * `shell` is a command; `unknown` the fallback with the tool's raw input and output. The typed
36
+ * kinds between come from the runtime's own tools; filePath is the path the runtime reported.
37
+ * The runtime clips each string field of a typed detail (not shell, not unknown) to 65,536
38
+ * characters, filePaths to 200 entries and webResults to 50. A receiver shows a detail whose
39
+ * type it does not know as unknown: new types may be added within v1.
40
+ */
41
+ export type ToolCallDetail =
42
+ | { type: 'shell'; command: string; cwd?: string; output: string; exitCode?: number | null }
43
+ | { type: 'read'; filePath: string; content?: string; offset?: number; limit?: number }
44
+ | { type: 'edit'; filePath: string; oldString?: string; newString?: string; unifiedDiff?: string }
45
+ | { type: 'write'; filePath: string; content?: string }
46
+ | {
47
+ type: 'search';
48
+ query: string;
49
+ toolName?: 'search' | 'grep' | 'glob' | 'web_search';
50
+ content?: string;
51
+ filePaths?: string[];
52
+ webResults?: Array<{ title: string; url: string }>;
53
+ numFiles?: number;
54
+ numMatches?: number;
55
+ truncated?: boolean;
56
+ }
57
+ | { type: 'fetch'; url: string; prompt?: string; result?: string; code?: number }
58
+ | { type: 'sub_agent'; subAgentType?: string; description?: string; log?: string }
59
+ | { type: 'plan'; text: string }
60
+ | { type: 'plain_text'; label?: string; text?: string }
61
+ | { type: 'unknown'; input: unknown; output: unknown };
62
+
63
+ /**
64
+ * How much a session has used, from its runtime's latest token report (ALPD §57), as Paseo's
65
+ * AgentUsage: the context window's fill, the latest model response's tokens, and the session's
66
+ * cost when the runtime says it. A field the runtime does not report is absent; a later report
67
+ * replaces the fields it carries and keeps the others.
68
+ */
69
+ export type SessionUsage = {
70
+ /** Tokens the context holds now: the latest model call's input and output. */
71
+ contextWindowUsedTokens?: number;
72
+ /** The context window, once the runtime has said what it is. */
73
+ contextWindowMaxTokens?: number;
74
+ /** The latest model response's input tokens, of which cachedInputTokens came from cache, and output tokens. */
75
+ inputTokens?: number;
76
+ cachedInputTokens?: number;
77
+ outputTokens?: number;
78
+ /** The session's cost in US dollars so far, as the runtime counts it (Claude, or ACP in USD). */
79
+ totalCostUsd?: number;
80
+ };
81
+
82
+ export type SessionSnapshot = {
83
+ id: string;
84
+ projectRoot: string;
85
+ agent: string;
86
+ runtime: RuntimeKind;
87
+ model: string;
88
+ mode: string;
89
+ thinking: string;
90
+ workflow: { mode: string; maxPeers: number; supervisor: boolean };
91
+ /** The label of the session's team, such as Phở (ALPD §42); absent for a custom graph. */
92
+ teamLabel?: string;
93
+ threadId: string;
94
+ /** The native thread is kept and the session can be resumed. */
95
+ persistent: boolean;
96
+ parentId?: string;
97
+ /** The requester's tool call that started this assignment. */
98
+ toolCallId?: string;
99
+ activeTurnId?: string;
100
+ /** A turn, an assignment, a child, or undelivered mail keeps the session working. */
101
+ busy: boolean;
102
+ /** Why a usage limit or a pause parked this assignment (ALPD §29). */
103
+ parked?: string;
104
+ /** A digest of the instructions the session runs with (ALPD §34). */
105
+ instructionsSha?: string;
106
+ /** The session's runtime takes images in session.prompt (capability `prompt.images`); absent means it does not. */
107
+ promptImages?: boolean;
108
+ /**
109
+ * How the session's runtime can use files attached in session.prompt (§12.6): `file.path` (its tools
110
+ * read a staged read-only copy), `document.native`, `text.extract`. Absent: it takes no attachment parts.
111
+ */
112
+ promptAttachments?: Array<'file.path' | 'document.native' | 'text.extract'>;
113
+ /** The latest usage the session's runtime reported (ALPD §57); absent before its first report. */
114
+ usage?: SessionUsage;
115
+ };
116
+
117
+ export type AssignmentSnapshot = {
118
+ id: string;
119
+ agent: string;
120
+ mode: string;
121
+ status: string;
122
+ startedAt: string;
123
+ /** An isolated assignment's branch and working directory. */
124
+ worktree?: { branch: string; path: string };
125
+ };
126
+
127
+ /** runtime: a turn the native runtime started by itself, such as Claude after a background task ended. */
128
+ export type TurnOrigin = 'user' | 'wake' | 'assignment' | 'runtime';
129
+
130
+ /**
131
+ * A root's turn that ended while nobody looked (Paseo's attention): `finished` for a completed turn,
132
+ * `error` for a failed one. alpd keeps it for every client until one acknowledges it or the root
133
+ * starts another turn (docs/protocol/alp-json-rpc-v1.md §12.2).
134
+ */
135
+ export type TurnAttention = { reason: 'finished' | 'error'; at: string; turnId?: string; message?: string };
136
+
137
+ /** A question an agent asked the user with alp_ask to: "user". */
138
+ export type UserQuestion = {
139
+ id: string;
140
+ sessionId: string;
141
+ rootId: string;
142
+ agent: string;
143
+ body: string;
144
+ options?: string[];
145
+ askedAt: string;
146
+ };
147
+
148
+ export type SessionState = 'running' | 'waiting' | 'waiting_parent' | 'waiting_user' | 'idle';
149
+
150
+ /** A live tree at one moment, for dashboards (plans/reference/ALPD.md §17). */
151
+ export type TreeStatus = {
152
+ rootId: string;
153
+ sessions: Array<SessionSnapshot & { state: SessionState; idleMs: number; workdir: string; unreadMail: number }>;
154
+ assignments: Array<AssignmentSnapshot & { requester: string; isolation: 'shared' | 'worktree'; idleMs: number }>;
155
+ questions: UserQuestion[];
156
+ /** Finished worktree assignments waiting for alp_merge or alp_discard. */
157
+ worktrees: Array<{ assignmentId: string; requester: string; agent: string; branch: string; files: string[]; stat: string }>;
158
+ leases: Array<{ checkout: string; assignmentId: string; agent: string }>;
159
+ /** Live claims on the project board held by this tree's sessions. */
160
+ claims: Pin[];
161
+ };
162
+
163
+ export type AlpEvent =
164
+ | { type: 'session.opened'; session: SessionSnapshot; cwd: string; effective: { model: string; thinking: string } }
165
+ | { type: 'session.ready' }
166
+ /** `title` comes with alpd's announcement of a root's name (its first prompt, or `session.rename`). */
167
+ | { type: 'session.updated'; session: SessionSnapshot & { title?: string } }
168
+ | { type: 'session.closed' }
169
+ | { type: 'session.failed'; error: AlpError }
170
+ | { type: 'prompt.accepted'; clientMessageId: string; result: 'turn' | 'steer'; turnId: string }
171
+ | { type: 'prompt.failed'; clientMessageId: string; error: AlpError }
172
+ | { type: 'turn.started'; turnId: string; origin: TurnOrigin }
173
+ | { type: 'turn.ended'; turnId: string; state: 'completed' | 'failed' | 'canceled'; error?: AlpError }
174
+ | { type: 'item'; item: TimelineItem }
175
+ /** The session's usage changed: the whole latest SessionUsage, not a delta (ALPD §57). */
176
+ | { type: 'usage'; usage: SessionUsage }
177
+ | { type: 'mail'; mail: Omit<MailEvent, 'passive' | 'deliveredTurn'> }
178
+ | { type: 'assignment'; assignment: AssignmentSnapshot }
179
+ | { type: 'question'; question: UserQuestion }
180
+ | { type: 'question.resolved'; questionId: string; outcome: 'answered' | 'dismissed' | 'timeout' | 'canceled'; answer?: string }
181
+ /** Emitted on the session that pinned it (plans/reference/ALPD.md §18). */
182
+ | { type: 'pin'; pin: Pin }
183
+ | { type: 'unpin'; pinId: string; reason: 'unpinned' | 'session_ended' }
184
+ /** A root's attention set, or cleared when absent: sent only to `question.watch { attention: true }` connections, never kept. */
185
+ | { type: 'attention'; attention?: TurnAttention }
186
+ /**
187
+ * The root's conversation was rewound (capability `session.rewind`, §12.7): history pages and cursors of
188
+ * the earlier generation are stale. Sent to attached connections only, never kept: drop the cached
189
+ * history, fetch a new tail and attach again with this generation.
190
+ */
191
+ | { type: 'history.reset'; generation: string; reason: 'rewind'; branchId: string };
192
+
193
+ export type Envelope = {
194
+ sessionId: string;
195
+ /** Random per runtime instance; a cursor from another epoch is stale. */
196
+ epoch: string;
197
+ /** Strictly increasing per session. */
198
+ seq: number;
199
+ /**
200
+ * Position in its tree's stored timeline, set by alpd on the events it keeps: strictly
201
+ * increasing per root across restarts, so `session.attach { after }` replays only what
202
+ * a reconnecting client missed (ALPD §66).
203
+ */
204
+ cursor?: number;
205
+ ts: string;
206
+ event: AlpEvent;
207
+ };
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './wire.js';
2
+ export type * from './dto.js';
3
+ export type * from './events.js';
4
+ export type * from './connection.js';
5
+ export type * from './session.js';
6
+ export type * from './schedule.js';
7
+ export type * from './rpc.js';
package/src/public.js ADDED
@@ -0,0 +1,2 @@
1
+ // Runtime API is plain ESM so source clients also work before the first build.
2
+ export * from './wire.js';
package/src/rpc.ts ADDED
@@ -0,0 +1,272 @@
1
+ /** Portable JSON-RPC structures; implementations and provider SDKs stay outside protocol. */
2
+ import type { RuntimeKind, Pin } from './dto.js';
3
+ import type { AlpError, Envelope, SessionSnapshot, TreeStatus, TurnAttention, UserQuestion } from './events.js';
4
+ import type { ConfigureOutcome, ConfigurePreview, ConversationBranch, ForkProvenance, PauseState, PromptContent, RecallAnswer, ResumeCommand, SessionPreview, SessionSpec, SessionSummary } from './session.js';
5
+ import type { EntryName, LanguageSetting, LibraryEntryRow, LibraryKind, LibraryScope, LibrarySource, TaskRow, TeamRow } from './dto.js';
6
+ import type { ScheduleJob, ScheduleStatus } from './schedule.js';
7
+
8
+ export type RpcId = string | number | null;
9
+ export type RpcErrorData = { code: number; message: string; data?: unknown };
10
+ export type RpcRequest = { jsonrpc: '2.0'; id?: RpcId; method: string; params?: Record<string, unknown> };
11
+ export type RpcResponse = { jsonrpc: '2.0'; id: RpcId } & ({ result: unknown; error?: never } | { error: RpcErrorData; result?: never });
12
+ export type EventNotification = { jsonrpc: '2.0'; method: 'event'; params: Envelope };
13
+ /** Why a terminal ended (§12.10). */
14
+ export type TerminalEndReason = 'exited' | 'closed' | 'idle' | 'lifetime' | 'detached' | 'revoked' | 'disabled' | 'shutdown';
15
+ /** A terminal as clients see it (capability `terminal`, §12.10): never a process id, shell path or host path. */
16
+ export type TerminalInfo = {
17
+ terminalId: string; workspaceId: string; title: string; cols: number; rows: number; state: 'running' | 'exited';
18
+ attached: boolean; createdAt: string; lastActivityAt: string; expiresAt: string;
19
+ exitCode?: number | null; signal?: number | null; endReason?: TerminalEndReason;
20
+ };
21
+ /** Effective terminal limits a daemon.hello advertises with capability `terminal`. */
22
+ export type TerminalLimits = {
23
+ maxPerPrincipal: number; maxPerDaemon: number; maxInputBytes: number; maxOutputFrameBytes: number;
24
+ replayBytes: number; idleMs: number; maxLifetimeMs: number; reconnectMs: number;
25
+ };
26
+ /** params of a terminal.event notification, sent only to the connection attached to the terminal. */
27
+ export type TerminalEvent =
28
+ | { terminalId: string; type: 'output'; seq: number; data: string }
29
+ | { terminalId: string; type: 'exit'; seq: number; exitCode: number | null; signal: number | null; reason: TerminalEndReason }
30
+ | { terminalId: string; type: 'detached'; reason: 'replaced' | 'slow_consumer' };
31
+ export type TerminalNotification = { jsonrpc: '2.0'; method: 'terminal.event'; params: TerminalEvent };
32
+
33
+ /** Names a daemon may list in its daemon.hello result (schema/methods-v1.json `capabilities`). */
34
+ export type HistoryItem = Envelope | { cursor: number; oversized: { bytes: number } };
35
+ export type HistoryPage = { generation: string; items: HistoryItem[]; olderCursor?: string; hasOlder: boolean; tipCursor: number };
36
+
37
+ export type Capability = 'session.interrupt.turn' | 'session.fork' | 'session.rewind' | 'session.reload' | 'session.resumeCommand' | 'session.configure.live' | 'terminal' | 'attachments' | 'workspace.git' | 'workspace.worktree' | 'workspace.registry' | 'session.history' | 'attach.after' | 'prompt.receipts' | 'extension.methods' | 'team' | 'task' | 'library' | 'library.project.write' | 'settings.language' | 'session.list.page' | 'question.watch' | 'question.watch.turns' | 'session.attention' | 'workspace.files' | 'workspace.files.read' | 'prompt.images' | 'schedule';
38
+
39
+ /** `data.reason` values alpd sets on errors (docs/protocol/alp-json-rpc-v1.md §5.3). */
40
+ export type ErrorReason =
41
+ | 'hello_required' | 'protocol_version' | 'invalid_params' | 'invalid_id' | 'batch_unsupported' | 'frame_too_large'
42
+ | 'invalid_cursor' | 'stale_cursor' | 'history_gap'
43
+ | 'stale_revision' | 'request_conflict' | 'cwd_mismatch' | 'workspace_mismatch' | 'corrupt_registry' | 'identity_changed'
44
+ | 'response_too_large' | 'persistence_failed' | 'outcome_unknown' | 'key_conflict'
45
+ // Project, task and library methods (docs/protocol/alp-json-rpc-v1.md §5.3).
46
+ | 'invalid_path' | 'invalid_scope' | 'invalid_entry' | 'secret_marker' | 'not_alp_project' | 'unknown_task' | 'not_found'
47
+ | 'library_policy' | 'unsafe_path' | 'revision_conflict' | 'task_state' | 'in_use' | 'busy' | 'unreadable'
48
+ // workspace.files.search (§12.3); workspace.files.list and read (§12.4).
49
+ | 'not_directory' | 'blocked_path' | 'ignored' | 'linked' | 'binary' | 'too_large'
50
+ // workspace.git.status and workspace.git.diff (§12.5).
51
+ | 'not_repository' | 'unknown_ref' | 'invalid_compare' | 'stale_snapshot' | 'git_unavailable' | 'git_failed'
52
+ // Attachments (§12.6).
53
+ | 'invalid_name' | 'invalid_data' | 'attachment_too_large' | 'offset_mismatch' | 'chunk_conflict' | 'upload_incomplete'
54
+ | 'hash_mismatch' | 'upload_expired' | 'quota_exceeded' | 'attachment_referenced' | 'attachment_limit'
55
+ | 'attachment_unavailable' | 'attachment_unsupported'
56
+ // session.fork and session.rewind (§12.7).
57
+ | 'session_busy' | 'stale_generation' | 'turn_not_found' | 'turn_in_progress' | 'empty_prefix' | 'boundary_unmapped'
58
+ | 'runtime_unsupported' | 'native_refused' | 'branch_not_found' | 'attachment_forbidden'
59
+ // session.reload (§12.8).
60
+ | 'session_closed' | 'no_history'
61
+ // session.configure with model, thinking or team (§12.9).
62
+ | 'runtime_mismatch' | 'agent_mismatch' | 'permission_escalation' | 'team_not_found'
63
+ // Terminal (§12.10).
64
+ | 'terminal_disabled' | 'terminal_unsupported' | 'terminal_limit' | 'not_attached' | 'terminal_exited' | 'input_too_large';
65
+
66
+ type Empty = Record<string, never>;
67
+
68
+ /**
69
+ * Params and result of every v1 method, as schema/methods-v1.json defines them; the protocol tests
70
+ * type-check the golden fixtures against this map, so the two cannot drift. Results may carry
71
+ * properties this map does not name: clients ignore them.
72
+ */
73
+ export type RegistryProject = { id: string; root: string; canonical?: string; customName?: string | null; addedAt: string; removedAt?: string | null; revision?: number };
74
+ export type RegistryWorkspace = { branch?: string | null; managedWorktree?: boolean; id: string; projectId: string; directory: string; canonical?: string; title?: string | null; pinnedAt?: string | null; archivedAt?: string | null; labels: string[]; createdAt: string; revision?: number; idempotencyKey?: string };
75
+ /** workspace.git.status and workspace.git.diff (capability `workspace.git`, §12.5): paths are relative to the workspace, never host paths. */
76
+ export type GitChange = 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'typechange' | 'untracked' | 'conflicted';
77
+ export type GitCompare = { mode: 'uncommitted' | 'staged' | 'unstaged' | 'base'; baseRef?: string; ignoreWhitespace?: boolean };
78
+ export type GitStatusFile = { path: string; oldPath?: string; change: GitChange; staged: boolean; unstaged: boolean };
79
+ export type GitStatus = {
80
+ state: 'repository' | 'not_repository' | 'unsupported'; reason?: string;
81
+ branch: string | null; detached: boolean; head: string | null;
82
+ upstream: { ref: string; ahead: number; behind: number } | null;
83
+ baseRef: string | null; aheadBehind: { ahead: number; behind: number } | null;
84
+ hasRemote: boolean; remoteUrl: string | null; dirty: boolean;
85
+ files: GitStatusFile[]; hiddenCount: number; truncated: boolean; snapshot: string;
86
+ };
87
+ export type GitDiffLine = { type: 'add' | 'remove' | 'context'; content: string; noNewline?: true };
88
+ export type GitDiffHunk = { oldStart: number; oldCount: number; newStart: number; newCount: number; context?: string; lines: GitDiffLine[] };
89
+ export type GitDiffFile = { path: string; oldPath?: string; change: GitChange; kind: 'text' | 'binary' | 'too_large' | 'withheld' | 'symlink' | 'submodule'; additions: number; deletions: number; hunks: GitDiffHunk[] };
90
+ export type GitDiffPage = { compare: { mode: GitCompare['mode']; baseRef?: string; ignoreWhitespace: boolean }; snapshot: string; files: GitDiffFile[]; hiddenCount: number; truncated: boolean; nextCursor?: string };
91
+ /**
92
+ * Capability `attachments` (§12.6). Principal context is set only by a trusted gateway on its own
93
+ * connection; a local client leaves it out and acts as the owner.
94
+ */
95
+ export type PrincipalContext = { principalId?: string; principalEpoch?: string; authorizedRoots?: string[] };
96
+ /** A committed attachment as clients see it: ids and display metadata, never a host path or URL. */
97
+ export type AttachmentInfo = {
98
+ attachmentId: string; sessionId: string; name: string; mimeType: string; detectedMimeType: string;
99
+ size: number; sha256: string; createdAt: string; committedAt: string; referenced: boolean;
100
+ };
101
+ /** Effective attachment limits a daemon.hello advertises with capability `attachments`. */
102
+ export type AttachmentLimits = {
103
+ maxObjectBytes: number; maxPerPrompt: number; maxPromptBytes: number; maxChunkBytes: number; maxActiveUploads: number;
104
+ maxPrincipalBytes: number; maxDaemonBytes: number; uploadTimeoutMs: number; unreferencedRetentionMs: number; releasedGraceMs: number;
105
+ };
106
+ /** Where a fork cuts the source conversation (§12.7): after a completed turn, or before a turn. */
107
+ export type ForkBoundary = { throughTurnId: string } | { beforeTurnId: string };
108
+ export type RegistryMutation = { requestId: string; ifRevision: number };
109
+ export type RegistryPage = { revision: number; total: number; nextCursor: string | null };
110
+ export type RpcMethodMap = {
111
+ 'terminal.create': { params: PrincipalContext & { workspaceId: string; cols: number; rows: number; requestId: string }; result: { terminal: TerminalInfo } };
112
+ 'terminal.list': { params: PrincipalContext & { workspaceId?: string }; result: { terminals: TerminalInfo[] } };
113
+ 'terminal.attach': { params: PrincipalContext & { terminalId: string; after?: number }; result: { terminal: TerminalInfo; nextSeq: number; gap?: { fromSeq: number; toSeq: number } } };
114
+ 'terminal.input': { params: PrincipalContext & { terminalId: string; data: string }; result: { bytes: number } };
115
+ 'terminal.resize': { params: PrincipalContext & { terminalId: string; cols: number; rows: number }; result: { cols: number; rows: number } };
116
+ 'terminal.close': { params: PrincipalContext & { terminalId: string }; result: { closed: true } };
117
+ 'terminal.revoke': { params: { principalId: string }; result: { closed: number } };
118
+ 'attachment.begin': { params: PrincipalContext & { sessionId: string; name: string; size: number; mimeType: string; sha256?: string; requestId: string }; result: { uploadId: string; state: 'uploading' | 'committed'; received: number; size: number; maxChunkBytes: number; expiresAt?: string; attachmentId?: string } };
119
+ 'attachment.chunk': { params: PrincipalContext & { uploadId: string; offset: number; data: string }; result: { received: number; size: number } };
120
+ 'attachment.commit': { params: PrincipalContext & { uploadId: string; sha256: string }; result: { attachment: AttachmentInfo } };
121
+ 'attachment.abort': { params: PrincipalContext & { uploadId: string }; result: { aborted: true } };
122
+ 'attachment.get': { params: PrincipalContext & { attachmentId: string }; result: { attachment: AttachmentInfo } };
123
+ 'attachment.delete': { params: PrincipalContext & { attachmentId: string }; result: { deleted: true } };
124
+ 'workspace.git.status': { params: { workspaceId: string }; result: GitStatus };
125
+ 'workspace.git.diff': { params: { workspaceId: string; compare: GitCompare; path?: string; cursor?: string }; result: GitDiffPage };
126
+ 'workspace.worktree.options': { params: { workspaceId: string }; result: { mappingIds: string[] } };
127
+ 'workspace.worktree.create': { params: RegistryMutation & { workspaceId: string; mappingId: string }; result: { workspace: RegistryWorkspace; revision: number } };
128
+ 'workspace.worktree.remove': { params: RegistryMutation & { workspaceId: string }; result: { workspace: RegistryWorkspace; revision: number; filesRemoved: true; branchRetained: true } };
129
+ 'registry.worktree': { params: { principalId?: string; principalEpoch?: string; mappingIds?: string[]; authorizedRoots?: string[] }; result: { mappings: Array<{id:string;source:string}>; workspaces: unknown[] } };
130
+ 'project.list': { params: { cursor?: string; limit?: number; projectRoots?: string[] }; result: RegistryPage & { projects: RegistryProject[] } };
131
+ 'workspace.list': { params: { cursor?: string; limit?: number; projectRoots?: string[] }; result: RegistryPage & { workspaces: RegistryWorkspace[] } };
132
+ 'workspace.get': { params: { workspaceId: string }; result: { workspace: RegistryWorkspace; revision: number } };
133
+ 'project.register': { params: RegistryMutation & { directory: string }; result: { project: RegistryProject; revision: number } };
134
+ 'project.remove': { params: RegistryMutation & { projectId: string }; result: { project: RegistryProject; revision: number } };
135
+ 'workspace.update': { params: RegistryMutation & { workspaceId: string; title?: string | null; pinned?: boolean; archived?: boolean }; result: { workspace: RegistryWorkspace; revision: number } };
136
+ 'registry.owner': { params: { operation?: string; args?: Record<string, unknown>; requestId?: string; ifRevision?: number }; result: { registry: Record<string, unknown> } };
137
+
138
+ 'daemon.hello': { params: { protocolVersion: 1; client?: { name: string; version: string }; capabilities?: string[] }; result: { protocolVersion: 1; daemonVersion?: string; pid?: number; /** The daemon host's machine name (os.hostname()), a label for clients. */ hostname?: string; promptImageLimits?: { maxCount: number; maxBytes: number; maxTotalBytes: number }; attachmentLimits?: AttachmentLimits; terminalLimits?: TerminalLimits; capabilities?: string[] } };
139
+ 'daemon.status': {
140
+ params: Empty;
141
+ result: {
142
+ pid: number; startedAt: string; version: string; protocolVersion: number; sessions: number; questions: number;
143
+ previousExit?: { kind: 'clean' | 'crash'; at?: string };
144
+ persistence?: Record<string, unknown>;
145
+ health: { live: boolean; ready: boolean; checks: Array<{ id: string; ok: boolean; code?: string; message?: string }> };
146
+ };
147
+ };
148
+ 'daemon.shutdown': { params: Empty; result: Empty };
149
+ 'daemon.pause': { params: { runtime?: RuntimeKind; now?: boolean; reason?: string }; result: PauseState };
150
+ 'daemon.resume': { params: { runtime?: RuntimeKind }; result: PauseState };
151
+ 'daemon.pauses': { params: Empty; result: PauseState };
152
+ 'daemon.usage': { params: { refresh?: boolean }; result: { providers: Array<Record<string, unknown>> } };
153
+ 'catalog.get': { params: Empty; result: { models: Array<Record<string, unknown>>; modes: Array<Record<string, unknown>>; thinkingOptions?: unknown; defaultModel: string } };
154
+ 'assignment.recall': { params: { assignmentId?: string; taskId?: string; projectRoot?: string; question: string }; result: RecallAnswer };
155
+ 'board.list': { params: { projectRoot: string }; result: { pins: Pin[] } };
156
+ 'question.list': { params: { projectRoot?: string }; result: { questions: UserQuestion[] } };
157
+ /** Capability `question.watch`: the waiting questions now; then this connection is told of every tree's `question` and `question.resolved` (of `projectRoot` when given). `turns` (capability `question.watch.turns`): also every root's `turn.ended`. `attention` (capability `session.attention`): also each root's attention, now and as it changes. */
158
+ 'question.watch': { params: { projectRoot?: string; turns?: boolean; attention?: boolean }; result: { questions: UserQuestion[]; attention?: Array<{ sessionId: string; attention: TurnAttention }> } };
159
+ /** Capability `session.attention`: acknowledges a root's attention for every client; with `at`, only that one (a later turn's stays). */
160
+ 'session.attention.clear': { params: { sessionId: string; at?: string }; result: { cleared: boolean } };
161
+ /** Capability `workspace.files`: files under `projectRoot` whose relative path matches `query`, best first; read-only, links not followed. */
162
+ 'workspace.files.search': { params: { projectRoot: string; query?: string; limit?: number }; result: { files: Array<{ path: string }>; truncated?: boolean } };
163
+ /** Capability `workspace.files.read`: the entries of a directory under `projectRoot` (its root when `path` is left out); links, blocked and (unless `includeIgnored`) .gitignore'd entries left out. */
164
+ 'workspace.files.list': { params: { projectRoot: string; path?: string; includeIgnored?: boolean }; result: { entries: Array<{ name: string; path: string; kind: 'file' | 'directory'; size?: number; modifiedAt?: string }>; truncated?: boolean } };
165
+ /** Capability `workspace.files.read`: a UTF-8 text file under `projectRoot`, at most 1 MiB; read-only, links not followed. */
166
+ 'workspace.files.read': { params: { projectRoot: string; path: string; includeIgnored?: boolean }; result: { path: string; content: string; size: number; modifiedAt?: string } };
167
+ 'question.answer': { params: { questionId: string; text?: string; dismiss?: boolean; reason?: string }; result: { questionId: string } };
168
+ 'session.create': {
169
+ params: { sessionId?: string; history?: 'skip' | 'replay'; delegation?: boolean; resume?: boolean } & ({ workspaceId: string; spec: Omit<SessionSpec, 'cwd'> & { cwd?: string } } | { workspaceId?: undefined; spec: SessionSpec });
170
+ result: { session: SessionSnapshot; attached: boolean; resumed?: true; durable?: boolean };
171
+ };
172
+ 'session.preview': { params: { spec: SessionSpec }; result: { preview: SessionPreview } };
173
+ 'session.history': { params: { sessionId: string; before?: string; limit?: number; maxBytes?: number }; result: HistoryPage };
174
+ 'session.attach': { params: { sessionId: string; replay?: boolean; after?: number; generation?: string }; result: { session: SessionSummary; cursor: number; reset?: true } };
175
+ 'session.release': { params: { sessionId: string }; result: { closed: boolean } };
176
+ 'session.prompt': {
177
+ params: PrincipalContext & { sessionId: string; clientMessageId: string; delivery?: 'auto' | 'steer'; content: PromptContent };
178
+ /** `{}` from a daemon without prompt.receipts. */
179
+ result: { outcome?: 'accepted' | 'failed'; error?: AlpError; duplicate?: boolean; durable?: boolean };
180
+ };
181
+ 'session.interrupt': { params: { sessionId: string; /** With session.interrupt.turn, cancel only this turn. */ turnId?: string }; result: { /** Conditional request only: false means the named turn is no longer active. */ interrupted?: boolean } };
182
+ /**
183
+ * Changes a session's permissions mode while idle; with capability `session.configure.live`, also a live
184
+ * root's model, effort or team (§12.9): those need requestId and ifGeneration, apply at the idle barrier
185
+ * through a restart of its native process on the same conversation, and answer `configured`. Answered
186
+ * once per (sessionId, requestId): a repeat answers the first result with `duplicate: true`.
187
+ */
188
+ 'session.configure': {
189
+ params: { sessionId: string; mode?: string; model?: string; thinking?: string; team?: string; requestId?: string; ifGeneration?: string };
190
+ result: { session: SessionSnapshot | SessionSummary; configured?: ConfigureOutcome; duplicate?: true };
191
+ };
192
+ /** What session.configure would run a live root with, and whether it would take it now (capability `session.configure.live`, §12.9). */
193
+ 'session.configure.preview': {
194
+ params: { sessionId: string; mode?: string; model?: string; thinking?: string; team?: string };
195
+ result: { preview: ConfigurePreview };
196
+ };
197
+ 'session.close': { params: { sessionId: string }; result: Empty };
198
+ 'session.rename': { params: { sessionId: string; title: string }; result: { session: SessionSummary } };
199
+ /**
200
+ * A new root whose conversation is the source's up to a completed turn (capability `session.fork`, §12.7).
201
+ * Answered once per (sessionId, requestId): a repeat answers the first result with `duplicate: true`.
202
+ */
203
+ 'session.fork': {
204
+ params: PrincipalContext & { sessionId: string; requestId: string; ifGeneration: string; boundary?: ForkBoundary; branchId?: string; workspaceId?: string; newSessionId?: string; title?: string };
205
+ result: { session: SessionSummary; forkedFrom: ForkProvenance; duplicate?: true };
206
+ };
207
+ /**
208
+ * The root's conversation from before a turn on, under the same session id; the later turns stay as a
209
+ * recoverable branch and no file is changed (capability `session.rewind`, §12.7).
210
+ */
211
+ 'session.rewind': {
212
+ params: { sessionId: string; requestId: string; ifGeneration: string; beforeTurnId: string };
213
+ result: { session: SessionSummary; generation: string; branch: ConversationBranch; duplicate?: true };
214
+ };
215
+ /**
216
+ * Restarts a live root's native process on its own conversation and configuration at the idle
217
+ * barrier, keeping its session id, history and receipts (capability `session.reload`, §12.8).
218
+ * Answered once per (sessionId, requestId): a repeat answers the first result with `duplicate: true`.
219
+ */
220
+ 'session.reload': {
221
+ params: { sessionId: string; requestId: string; ifGeneration: string };
222
+ result: { session: SessionSummary; duplicate?: true };
223
+ };
224
+ /**
225
+ * The ALP CLI command that continues a session in a terminal, built by alpd (capability
226
+ * `session.resumeCommand`, §12.8): argv and its POSIX and Windows command lines, for the clipboard only.
227
+ */
228
+ 'session.resumeCommand': {
229
+ params: { sessionId: string };
230
+ result: ResumeCommand;
231
+ };
232
+ 'session.archive': { params: { sessionId: string; archived?: boolean }; result: { session: SessionSummary } };
233
+ 'session.get': { params: { sessionId: string }; result: { session: SessionSummary } };
234
+ /** `limit`, `cursor`, `query` and `projectRoots` with capability `session.list.page`: a page, and `nextCursor` while more follow. */
235
+ 'session.list': { params: { projectRoot?: string; projectRoots?: string[]; rootsOnly?: boolean; includeClosed?: boolean; includeArchived?: boolean; limit?: number; cursor?: string; query?: string }; result: { sessions: SessionSummary[]; nextCursor?: string } };
236
+ 'session.status': { params: { sessionId: string }; result: { status: TreeStatus } };
237
+ 'session.message': { params: { sessionId: string; text: string }; result: Empty };
238
+ 'session.log': { params: { sessionId: string }; result: { rootId: string; entries: Array<Record<string, unknown>> } };
239
+ 'extension.register': { params: { method: string }; result: { registered: string } };
240
+ 'extension.unregister': { params: { method: string }; result: { unregistered: string } };
241
+ 'team.list': { params: { projectRoot?: string }; result: { projectRoot: string | null; teams: TeamRow[] } };
242
+ 'task.list': { params: { projectRoot: string }; result: { projectRoot: string; tasks: TaskRow[]; unreadable: Array<{ file: string; error: string }>; revision: string } };
243
+ 'task.create': { params: { projectRoot: string; title: string; priority?: number; requestId?: string }; result: { id: string; duplicate?: true } };
244
+ 'task.update': { params: { projectRoot: string; id: string; action: 'close' | 'reopen'; note?: string; ifRev?: number }; result: { id: string; status: string; rev: number; landed?: string } };
245
+ 'task.gate.resolve': { params: { projectRoot: string; id: string; gate: string; note?: string; ifRev?: number }; result: { id: string; status: string; rev: number } };
246
+ 'library.list': { params: { kind: LibraryKind; projectRoot?: string }; result: { projectRoot: string | null; entries: LibraryEntryRow[] } };
247
+ 'library.get': {
248
+ params: { kind: LibraryKind; name: EntryName; projectRoot?: string; scope?: LibraryScope };
249
+ /** `content` carries `{ $secret: 'keep' }` in place of each secret value; `secrets` lists their JSON Pointers. */
250
+ result: { kind: LibraryKind; name: string; source: LibrarySource; overrides?: LibrarySource; content: Record<string, unknown>; revision: string | null; usedBy: string[]; librarySkills?: string[]; secrets?: string[] };
251
+ };
252
+ 'library.save': {
253
+ params: { kind: LibraryKind; name: EntryName; scope: LibraryScope; projectRoot?: string; policy?: 'project-data'; content: Record<string, unknown>; revision?: string | null; sync?: 'none' | 'known-projects' };
254
+ result: { revision: string | null; syncedProjects?: number; syncErrors?: string[] };
255
+ };
256
+ 'library.delete': { params: { kind: LibraryKind; name: EntryName; scope: LibraryScope; projectRoot?: string; policy?: 'project-data'; revision?: string }; result: { removed: true; now?: 'builtin' | 'library' } };
257
+ 'library.duplicate': { params: { kind: LibraryKind; from: EntryName; to: EntryName; scope: LibraryScope; projectRoot?: string; policy?: 'project-data' }; result: { revision: string | null } };
258
+ 'library.rename': { params: { kind: LibraryKind; from: EntryName; to: EntryName; scope: LibraryScope; projectRoot?: string; policy?: 'project-data' }; result: { revision: string | null } };
259
+ 'library.skills.set': { params: { agent: EntryName; skills: EntryName[] }; result: { skills: string[] } };
260
+ 'library.models': { params: Empty; result: { models: Array<{ id: string; label: string; efforts: string[]; effort?: string }> } };
261
+ 'settings.language.get': { params: Empty; result: LanguageSetting };
262
+ 'settings.language.set': { params: { language: string | null }; result: LanguageSetting };
263
+ /** Capability `schedule`: a one-shot continuation of a root session, due at `dueAt` (UTC, ISO 8601). */
264
+ 'schedule.create': { params: { projectRoot: string; sessionId: string; dueAt: string; prompt: string }; result: { job: ScheduleJob } };
265
+ 'schedule.list': { params: { projectRoot: string; status?: ScheduleStatus }; result: { jobs: ScheduleJob[] } };
266
+ 'schedule.get': { params: { id: string; projectRoot: string }; result: { job: ScheduleJob } };
267
+ 'schedule.cancel': { params: { id: string; projectRoot: string }; result: { job: ScheduleJob } };
268
+ };
269
+
270
+ export type RpcMethod = keyof RpcMethodMap;
271
+ export type RpcParams<M extends RpcMethod> = RpcMethodMap[M]['params'];
272
+ export type RpcResult<M extends RpcMethod> = RpcMethodMap[M]['result'];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A scheduled continuation: a one-shot job that opens a new turn on a root session at a
3
+ * given time, without a user message. The protocol owns the shape so every
4
+ * client (the CLI, a viewer, an agent tool) reads the same state.
5
+ */
6
+
7
+ /** Where a job stands: waiting for its time, delivered, failed, or cancelled before it fired. */
8
+ export type ScheduleStatus = 'pending' | 'delivered' | 'failed' | 'cancelled';
9
+
10
+ /** Why a job failed or still waits; failed may mean execution is unknown. */
11
+ export type ScheduleError = { message: string; code?: string };
12
+
13
+ /** One job as `schedule.list` and `schedule.get` answer it. */
14
+ export type ScheduleJob = {
15
+ /** The job's own id, `sch_` and sixteen hex digits. */
16
+ id: string;
17
+ /** The project whose root session the job continues. */
18
+ projectRoot: string;
19
+ /** The root session the job opens a turn on. */
20
+ sessionId: string;
21
+ /** The continuation the job delivers. */
22
+ prompt: string;
23
+ /** When the job is due, in UTC (ISO 8601). */
24
+ dueAt: string;
25
+ status: ScheduleStatus;
26
+ /**
27
+ * How many times alpd tried to deliver the job. A job delivered once may have been
28
+ * tried twice: a retry after a crash reads the receipt of the first delivery.
29
+ */
30
+ attempts: number;
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ /** When the job was delivered, failed or cancelled. */
34
+ deliveredAt?: string;
35
+ failedAt?: string;
36
+ cancelledAt?: string;
37
+ /** When the first delivery was tried. */
38
+ attemptedAt?: string;
39
+ /** How late the delivery was, in milliseconds; 0 when it was on time. */
40
+ lateByMs?: number;
41
+ /** The last error of the job: why it failed, or why it still waits. */
42
+ lastError?: ScheduleError;
43
+ };