@ahpd/server 0.1.0

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/automations.d.ts +4 -0
  4. package/dist/automations.d.ts.map +1 -0
  5. package/dist/automations.js +191 -0
  6. package/dist/automations.js.map +1 -0
  7. package/dist/catalog.d.ts +48 -0
  8. package/dist/catalog.d.ts.map +1 -0
  9. package/dist/catalog.js +51 -0
  10. package/dist/catalog.js.map +1 -0
  11. package/dist/changes.d.ts +4 -0
  12. package/dist/changes.d.ts.map +1 -0
  13. package/dist/changes.js +601 -0
  14. package/dist/changes.js.map +1 -0
  15. package/dist/cron.d.ts +43 -0
  16. package/dist/cron.d.ts.map +1 -0
  17. package/dist/cron.js +211 -0
  18. package/dist/cron.js.map +1 -0
  19. package/dist/git.d.ts +16 -0
  20. package/dist/git.d.ts.map +1 -0
  21. package/dist/git.js +126 -0
  22. package/dist/git.js.map +1 -0
  23. package/dist/host.d.ts +24 -0
  24. package/dist/host.d.ts.map +1 -0
  25. package/dist/host.js +5850 -0
  26. package/dist/host.js.map +1 -0
  27. package/dist/index.d.ts +36 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +34 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/listen.d.ts +3 -0
  32. package/dist/listen.d.ts.map +1 -0
  33. package/dist/listen.js +183 -0
  34. package/dist/listen.js.map +1 -0
  35. package/dist/paging.d.ts +38 -0
  36. package/dist/paging.d.ts.map +1 -0
  37. package/dist/paging.js +53 -0
  38. package/dist/paging.js.map +1 -0
  39. package/dist/paths.d.ts +15 -0
  40. package/dist/paths.d.ts.map +1 -0
  41. package/dist/paths.js +19 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/resources.d.ts +122 -0
  44. package/dist/resources.d.ts.map +1 -0
  45. package/dist/resources.js +494 -0
  46. package/dist/resources.js.map +1 -0
  47. package/dist/rpc.d.ts +63 -0
  48. package/dist/rpc.d.ts.map +1 -0
  49. package/dist/rpc.js +208 -0
  50. package/dist/rpc.js.map +1 -0
  51. package/dist/scheduled.d.ts +48 -0
  52. package/dist/scheduled.d.ts.map +1 -0
  53. package/dist/scheduled.js +325 -0
  54. package/dist/scheduled.js.map +1 -0
  55. package/dist/terminals.d.ts +29 -0
  56. package/dist/terminals.d.ts.map +1 -0
  57. package/dist/terminals.js +346 -0
  58. package/dist/terminals.js.map +1 -0
  59. package/dist/tools.d.ts +18 -0
  60. package/dist/tools.d.ts.map +1 -0
  61. package/dist/tools.js +65 -0
  62. package/dist/tools.js.map +1 -0
  63. package/dist/types/agent.d.ts +245 -0
  64. package/dist/types/agent.d.ts.map +1 -0
  65. package/dist/types/agent.js +3 -0
  66. package/dist/types/agent.js.map +1 -0
  67. package/dist/types/automations.d.ts +164 -0
  68. package/dist/types/automations.d.ts.map +1 -0
  69. package/dist/types/automations.js +3 -0
  70. package/dist/types/automations.js.map +1 -0
  71. package/dist/types/catalog.d.ts +30 -0
  72. package/dist/types/catalog.d.ts.map +1 -0
  73. package/dist/types/catalog.js +3 -0
  74. package/dist/types/catalog.js.map +1 -0
  75. package/dist/types/changes.d.ts +272 -0
  76. package/dist/types/changes.d.ts.map +1 -0
  77. package/dist/types/changes.js +3 -0
  78. package/dist/types/changes.js.map +1 -0
  79. package/dist/types/common.d.ts +9 -0
  80. package/dist/types/common.d.ts.map +1 -0
  81. package/dist/types/common.js +3 -0
  82. package/dist/types/common.js.map +1 -0
  83. package/dist/types/host.d.ts +372 -0
  84. package/dist/types/host.d.ts.map +1 -0
  85. package/dist/types/host.js +3 -0
  86. package/dist/types/host.js.map +1 -0
  87. package/dist/types/index.d.ts +20 -0
  88. package/dist/types/index.d.ts.map +1 -0
  89. package/dist/types/index.js +8 -0
  90. package/dist/types/index.js.map +1 -0
  91. package/dist/types/listen.d.ts +48 -0
  92. package/dist/types/listen.d.ts.map +1 -0
  93. package/dist/types/listen.js +3 -0
  94. package/dist/types/listen.js.map +1 -0
  95. package/dist/types/paging.d.ts +10 -0
  96. package/dist/types/paging.d.ts.map +1 -0
  97. package/dist/types/paging.js +3 -0
  98. package/dist/types/paging.js.map +1 -0
  99. package/dist/types/probe.d.ts +46 -0
  100. package/dist/types/probe.d.ts.map +1 -0
  101. package/dist/types/probe.js +3 -0
  102. package/dist/types/probe.js.map +1 -0
  103. package/dist/types/resources.d.ts +83 -0
  104. package/dist/types/resources.d.ts.map +1 -0
  105. package/dist/types/resources.js +3 -0
  106. package/dist/types/resources.js.map +1 -0
  107. package/dist/types/rpc.d.ts +65 -0
  108. package/dist/types/rpc.d.ts.map +1 -0
  109. package/dist/types/rpc.js +3 -0
  110. package/dist/types/rpc.js.map +1 -0
  111. package/dist/types/session.d.ts +395 -0
  112. package/dist/types/session.d.ts.map +1 -0
  113. package/dist/types/session.js +3 -0
  114. package/dist/types/session.js.map +1 -0
  115. package/dist/types/terminals.d.ts +131 -0
  116. package/dist/types/terminals.d.ts.map +1 -0
  117. package/dist/types/terminals.js +3 -0
  118. package/dist/types/terminals.js.map +1 -0
  119. package/dist/types/wire.d.ts +62 -0
  120. package/dist/types/wire.d.ts.map +1 -0
  121. package/dist/types/wire.js +13 -0
  122. package/dist/types/wire.js.map +1 -0
  123. package/dist/types/worktrees.d.ts +86 -0
  124. package/dist/types/worktrees.d.ts.map +1 -0
  125. package/dist/types/worktrees.js +3 -0
  126. package/dist/types/worktrees.js.map +1 -0
  127. package/dist/worktrees.d.ts +27 -0
  128. package/dist/worktrees.d.ts.map +1 -0
  129. package/dist/worktrees.js +152 -0
  130. package/dist/worktrees.js.map +1 -0
  131. package/package.json +61 -0
  132. package/src/automations.ts +204 -0
  133. package/src/catalog.ts +53 -0
  134. package/src/changes.ts +629 -0
  135. package/src/cron.ts +251 -0
  136. package/src/git.ts +132 -0
  137. package/src/host.ts +5947 -0
  138. package/src/index.ts +40 -0
  139. package/src/listen.ts +240 -0
  140. package/src/paging.ts +56 -0
  141. package/src/paths.ts +19 -0
  142. package/src/resources.ts +513 -0
  143. package/src/rpc.ts +222 -0
  144. package/src/scheduled.ts +369 -0
  145. package/src/terminals.ts +353 -0
  146. package/src/tools.ts +64 -0
  147. package/src/types/agent.ts +255 -0
  148. package/src/types/automations.ts +164 -0
  149. package/src/types/catalog.ts +26 -0
  150. package/src/types/changes.ts +266 -0
  151. package/src/types/common.ts +9 -0
  152. package/src/types/host.ts +386 -0
  153. package/src/types/index.ts +27 -0
  154. package/src/types/listen.ts +53 -0
  155. package/src/types/paging.ts +11 -0
  156. package/src/types/probe.ts +42 -0
  157. package/src/types/resources.ts +107 -0
  158. package/src/types/rpc.ts +68 -0
  159. package/src/types/session.ts +415 -0
  160. package/src/types/terminals.ts +133 -0
  161. package/src/types/wire.ts +69 -0
  162. package/src/types/worktrees.ts +91 -0
  163. package/src/worktrees.ts +162 -0
@@ -0,0 +1,68 @@
1
+ /** JSON-RPC 2.0 over a single connection. */
2
+
3
+ /** One decoded JSON-RPC call: a method name and its parameters. */
4
+ export interface Request {
5
+ /** The JSON-RPC `method` field, e.g. `subscribe`. */
6
+ readonly method: string;
7
+ /** The `params` object, or an empty object when the call carried none. */
8
+ readonly params: Record<string, unknown>;
9
+ }
10
+
11
+ /**
12
+ * The bytes end of one connection.
13
+ *
14
+ * Implemented per runtime, since Node, Bun and Deno expose different socket
15
+ * objects.
16
+ */
17
+ export interface Wire {
18
+ /** Write one frame. */
19
+ send(text: string): void;
20
+ /** Close the connection. */
21
+ close(): void;
22
+ /** Whether a `send` would still reach the client. */
23
+ isOpen(): boolean;
24
+ }
25
+
26
+ /**
27
+ * The JSON-RPC end of one connection: messages rather than frames.
28
+ *
29
+ * Both directions. AHP is symmetrical - `ServerCommandMap` names ten methods
30
+ * a host may call on a client, and a client publishes resources a host is
31
+ * expected to be able to read - so a peer that could only answer was half a
32
+ * connection.
33
+ */
34
+ export interface Peer {
35
+ /** Send a complete JSON-RPC message. Dropped if the connection has closed. */
36
+ send(message: Record<string, unknown>): void;
37
+ /** Send a server-to-client notification, which carries no id and gets no reply. */
38
+ notify(method: string, params: unknown): void;
39
+ /**
40
+ * Ask the client something, and wait for what it says.
41
+ *
42
+ * Rejects with an `RpcError` the client sent, an `RpcTimeout` when nothing
43
+ * came back inside `timeoutMs`, or an `RpcClosed` when the connection went
44
+ * away with the question still in flight. Three outcomes, three types: a
45
+ * caller that has to tell "the client refused" from "the client is gone"
46
+ * cannot do it by reading a message.
47
+ */
48
+ request(method: string, params: unknown, timeoutMs?: number): Promise<unknown>;
49
+ /**
50
+ * Take one JSON-RPC response off the wire and settle whatever asked for it.
51
+ *
52
+ * Called by `receive` for a message carrying `result` or `error` and no
53
+ * `method`. An id nothing here asked about is dropped: a response is not a
54
+ * request, and answering one is the violation this exists to stop.
55
+ */
56
+ answered(message: Record<string, unknown>): void;
57
+ /** Close the connection, rejecting every question still unanswered. */
58
+ close(): void;
59
+ }
60
+
61
+ /**
62
+ * Answers one request.
63
+ *
64
+ * The resolved value becomes the JSON-RPC `result`. Throwing an `RpcError`
65
+ * produces that error's code; any other throw becomes an internal error.
66
+ * Returning nothing for a notification is correct - notifications get no reply.
67
+ */
68
+ export type Handler = (request: Request, peer: Peer) => Promise<unknown> | unknown;
@@ -0,0 +1,415 @@
1
+ /** One agent session: its lifecycle, its turns, and what it is waiting for. */
2
+
3
+ import type { Bag } from './common.js';
4
+ import type { BoundTool } from './agent.js';
5
+
6
+ /**
7
+ * Emits one state action on a session's channel.
8
+ *
9
+ * `session` addresses the session channel, `chat` the chat channel beneath it,
10
+ * and `terminal` a terminal's own - the emitter knows which of its channels it
11
+ * is talking about and the host knows what each is called.
12
+ */
13
+ export type Emit = (channel: 'session' | 'chat' | 'terminal', action: Bag) => void;
14
+
15
+ /**
16
+ * The model a turn runs on.
17
+ *
18
+ * An object rather than a name, because a model that carries a `configSchema`
19
+ * is chosen by picking a row *and* answering its form: the protocol's
20
+ * `ModelSelection` is `{ id, config }`, and a host that read only the id would
21
+ * accept a form it then ignored.
22
+ */
23
+ export interface Chosen {
24
+ /** The model, spelled as `RootState.agents[].models[].id` spells it. */
25
+ id: string;
26
+ /**
27
+ * What the client filled that model's own `configSchema` in with.
28
+ *
29
+ * JSON primitives, which is what the protocol carries here: most pickers
30
+ * produce strings and a numeric one produces a number.
31
+ */
32
+ config?: Record<string, string | number | boolean | null>;
33
+ }
34
+
35
+ /** How to construct a session. */
36
+ export interface SessionOptions {
37
+ /** The session channel URI, `ahp-session:/<id>`. */
38
+ uri: string;
39
+ /** The chat channel URI beneath it, `ahp-chat:/<id>`. */
40
+ chatUri: string;
41
+ /**
42
+ * Directories beside the first the agent may also read and write.
43
+ *
44
+ * The first is `cwd` and is the process root; these are its peers. The
45
+ * protocol calls the whole set `workingDirectories` and fixes index 0 for
46
+ * the session's lifetime, which is what `immutablePrimary` means.
47
+ */
48
+ additional?: string[];
49
+ /** The directory the agent works in. */
50
+ cwd: string;
51
+ /** Config values chosen at creation, by key. */
52
+ settings?: Record<string, unknown>;
53
+ /** The config schema this session reports, shared with the root channel. */
54
+ schema?: () => Bag;
55
+ /** Customizations to report until the agent reports its own. */
56
+ seedCustomizations?: Bag[];
57
+ /** Where state actions go. */
58
+ emit: Emit;
59
+ /**
60
+ * Environment for the agent's own process.
61
+ *
62
+ * Merged over `process.env` by the session, never handed to the SDK alone:
63
+ * the SDK's `env` *replaces* the subprocess environment rather than adding
64
+ * to it, so passing only a credential is a subprocess with no `PATH`.
65
+ */
66
+ env?: Record<string, string>;
67
+ /** An existing agent session to continue, rather than starting a new one. */
68
+ resume?: string;
69
+ /**
70
+ * The prompt this session is resumed *at*, so the rest is left behind.
71
+ *
72
+ * A fork: the conversation continues from that turn as though the ones
73
+ * after it had not happened, under a new id of its own so the original is
74
+ * untouched. Meaningless without `resume`, which names what is being forked.
75
+ */
76
+ forkAt?: string;
77
+ /**
78
+ * The chain entry this session is resumed *at*, keeping it and everything
79
+ * before it.
80
+ *
81
+ * A rewind rather than a fork: the conversation carries on under the id it
82
+ * already had, with the turns after that point dropped. What
83
+ * `chat/truncated` asks for, and the id is the difference - a fork leaves
84
+ * the original for somebody else to find, a truncation means there is
85
+ * nothing left to find. Meaningless without `resume`, and ignored beside
86
+ * `forkAt`, which asks for the other thing.
87
+ */
88
+ rewindAt?: string;
89
+ /**
90
+ * Context the first turn carries without showing it.
91
+ *
92
+ * A side chat is started from a turn somewhere else and needs to know what
93
+ * that turn said, but the protocol is explicit that the source transcript is
94
+ * not copied into this chat's visible history - so this reaches the model
95
+ * and never the wire.
96
+ */
97
+ context?: string;
98
+
99
+ /**
100
+ * MCP servers this session declares, by name.
101
+ *
102
+ * Declared by the host rather than left to the backend's own discovery,
103
+ * because a server the SDK was given is one it can be told about again -
104
+ * which is what applying a token a client signed in with requires.
105
+ */
106
+ mcpServers?: Record<string, Bag>;
107
+ /**
108
+ * Tools the host contributes to this session.
109
+ *
110
+ * Offered to the model as an MCP server that runs in this process, so a
111
+ * call is a function call rather than a subprocess. What they are is the
112
+ * host's business; this only runs them.
113
+ */
114
+ tools?: BoundTool[];
115
+ /** Turns already known, so a resumed session does not open empty. */
116
+ seed?: Bag[];
117
+ /**
118
+ * A file a tool is about to change, and the same file once it has.
119
+ *
120
+ * Off the agent's own message stream rather than out of a hook: the SDK's
121
+ * `PreToolUse` and `PostToolUse` are bypassable from a person's settings,
122
+ * and the stream is the signal that cannot be turned off. Called with
123
+ * `before` as the tool is announced and `after` when its result arrives,
124
+ * which is what makes a turn's changeset the turn's rather than the
125
+ * working tree's at the time somebody asked.
126
+ *
127
+ * The path only. Reading it is the host's business, because reading a file
128
+ * is a filesystem and a session has none.
129
+ */
130
+ onFileEdit?(turnId: string, path: string, phase: 'before' | 'after'): void;
131
+ /** Called once the agent has reported what it can do. */
132
+ onHandshake?(): void;
133
+ }
134
+
135
+ /** What a host-run command did, once it has finished doing it. */
136
+ export interface Ran {
137
+ /** Whether it exited cleanly. */
138
+ success: boolean;
139
+ /** One line about how it went, in the past tense, for the finished call. */
140
+ said: string;
141
+ /** Everything it printed. Empty when it printed nothing. */
142
+ output: string;
143
+ /** The terminal it ran in, so a client can watch it while it runs. */
144
+ terminal?: string;
145
+ /** What it exited with, where the runtime reported one. */
146
+ code?: number;
147
+ }
148
+
149
+ /** A live session. */
150
+ export interface Session {
151
+ /** The session channel URI. */
152
+ readonly uri: string;
153
+ /** The chat channel URI. */
154
+ readonly chatUri: string;
155
+
156
+ /** Models this session can run a turn on. Empty until the agent has answered. */
157
+ models(): { id: string; name: string }[];
158
+ /**
159
+ * The id the agent gave this session, if it has said one yet.
160
+ *
161
+ * Not the same as `uri`: the client names the channel, the agent names the
162
+ * transcript it writes. The catalogue needs both to tell that the row on
163
+ * disk and this session are one conversation.
164
+ */
165
+ agentId(): string | undefined;
166
+
167
+ /**
168
+ * The backend's own name for the prompt that began a turn, if it has one.
169
+ *
170
+ * What a fork is cut at. A turn has an id this host chose and the backend
171
+ * has an id of its own for the same prompt, and only the backend's means
172
+ * anything when it is asked to resume at one.
173
+ *
174
+ * Optional, and its absence is what makes forking unavailable: a backend
175
+ * that cannot name a prompt cannot be asked to continue from one, and the
176
+ * host advertises no `fork` capability for it rather than offering a control
177
+ * that fails when it is used.
178
+ */
179
+ forkPoint?(turnId: string): string | undefined;
180
+ /**
181
+ * The backend's own name for the *last* thing a turn did, if it has one.
182
+ *
183
+ * Where a rewind cuts. `forkPoint` names the prompt a turn began with and
184
+ * `endPoint` names the last entry it left behind, and the two are different
185
+ * questions: a fork re-asks the turn, a truncation keeps it whole and drops
186
+ * what came after.
187
+ *
188
+ * Optional and, like `forkPoint`, only ever answered for a turn this process
189
+ * watched run: the backend's names for a turn read back off a transcript are
190
+ * not recorded, so a session resumed from disk can be truncated no further
191
+ * back than its own first turn.
192
+ */
193
+ endPoint?(turnId: string): string | undefined;
194
+ /** Skills, commands, subagents and MCP servers this session was given. */
195
+ customizations(): Bag[];
196
+ /** Every completed turn. Snapshots carry only the newest page of these. */
197
+ allTurns(): Bag[];
198
+
199
+ /** `SessionStatus` bitset, derived from what the session is doing now. */
200
+ status(): number;
201
+ /** What it is doing now, in one line, or nothing when it is idle. */
202
+ activity(): string | undefined;
203
+ /** Display title. */
204
+ title(): string;
205
+ /** ISO 8601 timestamp of the last change. */
206
+ modifiedAt(): string;
207
+ /**
208
+ * Where the agent is actually working, as `file://` URIs.
209
+ *
210
+ * Asked rather than assumed: the host knows what directory it was started
211
+ * in and a session may have been created in another, so a host that
212
+ * answered with its own reported the wrong one for exactly the sessions
213
+ * somebody chose a directory for.
214
+ */
215
+ workingDirectories(): string[];
216
+
217
+ /** The session channel's state, for a subscription snapshot. */
218
+ sessionState(): Bag;
219
+ /** The chat channel's state, for a subscription snapshot. */
220
+ chatState(): Bag;
221
+
222
+ /**
223
+ * Run one command as a turn of this chat's, without asking the agent.
224
+ *
225
+ * What the composer's `!` prefix means: the person typed a command rather
226
+ * than a question, and the answer is a shell's. The turn is still the
227
+ * chat's - a host that emitted one this session did not know about would
228
+ * serve a snapshot without it the moment anybody re-subscribed - so the
229
+ * session opens it, reports the tool call, and closes it when `run`
230
+ * settles.
231
+ *
232
+ * `run` is the host's half: it is handed the id of the tool call this turn
233
+ * is about and answers with what happened. The terminal is the host's
234
+ * because the shell is - a session has no port to spawn one through - and
235
+ * naming it back is what lets a client watch the output arrive rather than
236
+ * only read it afterwards.
237
+ *
238
+ * Optional. A backend that leaves it out is one this host advertises no
239
+ * `terminalCommandPrefix` for, which is the protocol's own way of saying the
240
+ * shorthand is unavailable.
241
+ */
242
+ ran?(turnId: string, command: string, run: (toolCallId: string) => Promise<Ran>): void;
243
+
244
+ /**
245
+ * Put a message into the turn that is already running.
246
+ *
247
+ * Steering, in the protocol's word: somebody correcting an agent halfway
248
+ * rather than waiting for it to finish doing the thing they are trying to
249
+ * stop. Answers whether there was a turn to steer - a chat with nothing
250
+ * running has nothing to inject into, and the caller says so rather than
251
+ * quietly turning it into an ordinary message.
252
+ *
253
+ * Optional. A backend that cannot take a message mid-turn leaves it out,
254
+ * and this host refuses steering for that backend with that as the reason.
255
+ */
256
+ steer?(id: string, text: string): boolean;
257
+
258
+ /** Start a turn with what the person said, optionally naming a model. */
259
+ begin(turnId: string, text: string, model?: Chosen): void;
260
+ /**
261
+ * Run the latest turn again, without adding a message.
262
+ *
263
+ * What `chat/turnResume` asks for: the turn errored, its message and its
264
+ * parts are intact, and the client wants the same prompt tried again rather
265
+ * than typed again. Answers whether there was such a turn to resume.
266
+ *
267
+ * Optional. A backend that cannot re-run a turn leaves it out, and the host
268
+ * refuses the action with that as the reason.
269
+ */
270
+ resume?(turnId: string): boolean;
271
+
272
+ /** Stop the running turn, and answer anything it was blocked on. */
273
+ cancel(turnId: string): void;
274
+
275
+ /**
276
+ * Hold a message, and make it the next turn when the running one ends.
277
+ *
278
+ * The queue is the session's, not a client's: a client that held a message
279
+ * would be the only thing that could ever send it, and nothing in a client
280
+ * watches for a turn to end. The same `id` twice edits what is waiting.
281
+ */
282
+ queue(id: string, text: string, model?: Chosen): void;
283
+ /** Take one back, while it is still waiting. */
284
+ unqueue(id: string): void;
285
+ /**
286
+ * What somebody is part-way through typing.
287
+ *
288
+ * Held by the session so two people on one chat see each other's - a client
289
+ * that kept its own would need nothing from a host for this.
290
+ */
291
+ setDraft(draft: Bag | undefined): void;
292
+ /** Reorder what is waiting. Anything not named keeps its place behind what is. */
293
+ reorder(order: string[]): void;
294
+
295
+ /** Answer a tool call the agent is waiting on. */
296
+ confirm(toolCallId: string, approved: boolean): void;
297
+
298
+ /**
299
+ * Replace the tools this session offers the model.
300
+ *
301
+ * The host's own are fixed at creation; a client's come and go with the
302
+ * client, which is what this is for. Replaces rather than merges, because
303
+ * a tool taken away has to be able to go.
304
+ *
305
+ * False when the backend could not re-declare them - a session whose agent
306
+ * has gone, or one whose backend cannot change its tools once it is running.
307
+ *
308
+ * Optional. A backend that leaves it out is one this host offers no
309
+ * client-provided tools through, and it says so rather than accepting an
310
+ * announcement it will not act on.
311
+ */
312
+ setTools?(tools: BoundTool[]): Promise<boolean>;
313
+ /**
314
+ * The client running a tool call, for a call that is one client's to run.
315
+ *
316
+ * Nothing for a call the agent is running itself, which is what makes this
317
+ * the check for whether a client may write into one: the protocol says a
318
+ * host should refuse `chat/toolCallContentChanged` from anybody but the
319
+ * call's own contributor.
320
+ */
321
+ toolCallOwner?(toolCallId: string): string | undefined;
322
+ /**
323
+ * What a client says one of its own tool calls did.
324
+ *
325
+ * False when no call by that id is waiting, or when it is waiting on a
326
+ * different client - both are a client out of step rather than a no-op, and
327
+ * the host refuses rather than dropping it.
328
+ */
329
+ completeToolCall?(
330
+ toolCallId: string,
331
+ clientId: string,
332
+ result: { text: string; ok: boolean },
333
+ ): boolean;
334
+ /**
335
+ * A client that was running tool calls here has gone.
336
+ *
337
+ * Its outstanding calls are failed rather than left open: the agent is
338
+ * waiting on a promise that nothing can settle any more, and a turn that
339
+ * hangs for ever is worse than a tool that says it could not run.
340
+ */
341
+ clientGone?(clientId: string): void;
342
+ /** Answer a question the agent asked, keyed by question id. */
343
+ answer(requestId: string, accepted: boolean, answers: Bag): void;
344
+ /**
345
+ * One question of an open request, as somebody types the answer.
346
+ *
347
+ * The protocol calls the result the request's synced answer state, and it is
348
+ * what a `chat/inputCompleted` carrying no answers of its own is completed
349
+ * with. Held by the session for the same reason a draft message is: two
350
+ * people on one chat are answering one form.
351
+ *
352
+ * False when nothing here is waiting on that request, or when what is
353
+ * waiting is a tool confirmation rather than a question.
354
+ *
355
+ * Optional. A backend that keeps no drafts leaves it out, and the host
356
+ * refuses the action with that as the reason.
357
+ */
358
+ setAnswer?(requestId: string, questionId: string, answer: Bag | undefined): boolean;
359
+
360
+ /**
361
+ * Take one config value, or say why not.
362
+ *
363
+ * Every key a client sets goes through here, including the ones a backend
364
+ * advertises in its own schema: the host reads that schema for how a key
365
+ * behaves - whether it can move on a running session, whether it belongs to
366
+ * the session or to one chat - and knows nothing about what any key means.
367
+ *
368
+ * `true` when it was taken. A string is the refusal, in the backend's own
369
+ * words, and it is a string rather than `false` because only the backend
370
+ * knows which of the two things went wrong: a key it does not serve, or a
371
+ * value it will not take for a key it does. A host that answered both with
372
+ * one sentence would be telling a client its control does not exist when
373
+ * the truth is that the value was wrong.
374
+ */
375
+ setConfig?(key: string, value: unknown): true | string | Promise<true | string>;
376
+ /** The config in force, by key. */
377
+ settings(): Record<string, unknown>;
378
+
379
+ /**
380
+ * Turn a customization on or off. False when this backend cannot.
381
+ *
382
+ * False is a real answer and the one to give for anything with no runtime
383
+ * switch: a control that reports success and changes nothing is worse than
384
+ * one that refuses.
385
+ */
386
+ setCustomizationEnabled(id: string, enabled: boolean): Promise<boolean>;
387
+ /** Start an MCP server, which is also how one that needs signing into is. */
388
+ startMcpServer(id: string): Promise<boolean>;
389
+ /** Stop one. */
390
+ stopMcpServer(id: string): Promise<boolean>;
391
+
392
+ /**
393
+ * A token for one of this session's MCP servers, as the client signed in.
394
+ *
395
+ * Answers whether the server it names is one of this session's. The token
396
+ * becomes that server's `Authorization` header and the server is asked to
397
+ * connect again; nothing else in the session is told.
398
+ *
399
+ * Optional: a backend that cannot re-declare a server leaves it out, and the
400
+ * host then advertises no resource for one to be signed into.
401
+ */
402
+ authenticated?(resource: string, token: string): Promise<boolean>;
403
+
404
+ /**
405
+ * The OAuth resources this session's MCP servers need signing into.
406
+ *
407
+ * What `authenticate` is checked against: the protocol says a client's
408
+ * `resource` MUST match one the server advertised, and these are advertised
409
+ * on the servers' own `authRequired` states.
410
+ */
411
+ awaiting?(): string[];
412
+
413
+ /** End the session and stop its agent. */
414
+ close(): void;
415
+ }
@@ -0,0 +1,133 @@
1
+ /** A shell on the host machine, as a terminal channel. */
2
+
3
+ import type { TerminalClaim, TerminalLifecycleState, TerminalState } from '@microsoft/agent-host-protocol';
4
+ import type { OnWire } from './wire.js';
5
+ import type { Bag } from './common.js';
6
+ import type { Emit } from './session.js';
7
+
8
+ /** Who currently holds a terminal: a connected client, or a session. */
9
+ /**
10
+ * Who is holding a terminal, in the protocol's own type.
11
+ *
12
+ * A client, or a session and the chat inside it. Was `Bag`, which meant a
13
+ * claim missing the fields its own kind requires compiled perfectly.
14
+ */
15
+ export type Claim = OnWire<TerminalClaim>;
16
+
17
+ /** How to start one. */
18
+ export interface TerminalOptions {
19
+ /** The terminal channel URI, chosen by the client that asked for it. */
20
+ uri: string;
21
+ /** The directory it starts in. Already checked against what the host serves. */
22
+ cwd: string;
23
+ /** Who is holding it. */
24
+ claim: Claim;
25
+ /** Display name. The shell's own if none is given. */
26
+ name?: string;
27
+ /**
28
+ * The shell to run, absolute. The store's own choice if none is given.
29
+ *
30
+ * A connected client pushes this: VS Code sends `defaultShell` on the root
31
+ * channel out of `terminal.integrated.agentHostProfile.<os>`, because the
32
+ * shell somebody wants a host-managed terminal to open is a preference of
33
+ * theirs rather than a fact about the machine.
34
+ */
35
+ shell?: string;
36
+ /**
37
+ * One command to run instead of a shell to sit in.
38
+ *
39
+ * The terminal runs it and exits, so `terminal/exited` is what says the
40
+ * command finished and its code is what says how. Without this the shell
41
+ * reads from a pipe and stays open, which is right for a terminal somebody
42
+ * is typing into and wrong for one opened to answer a single question -
43
+ * there is no shell integration here to tell where one command ends.
44
+ */
45
+ command?: string;
46
+ /** Width in columns, as the client draws it. */
47
+ cols?: number;
48
+ /** Height in rows. */
49
+ rows?: number;
50
+ /** Where state actions go. The host routes them to the terminal's channel. */
51
+ emit: Emit;
52
+ }
53
+
54
+ /** A running terminal. */
55
+ export interface Terminal {
56
+ /** Its channel URI. */
57
+ readonly uri: string;
58
+ /** Display title. */
59
+ title(): string;
60
+ /** Who is holding it. */
61
+ claim(): Claim;
62
+ /** The process's exit code, once it has one. Undefined while it runs. */
63
+ exitCode(): number | undefined;
64
+ /**
65
+ * Running, or exited and with what.
66
+ *
67
+ * 0.9.0 moved the exit code in here and made this required, so a terminal
68
+ * described without it is a terminal a client cannot ask about. `exitCode`
69
+ * above stays for the versions this host still negotiates down to.
70
+ */
71
+ lifecycle(): OnWire<TerminalLifecycleState>;
72
+ /** The channel's state, for a subscription snapshot. */
73
+ /**
74
+ * The terminal channel's snapshot, in the protocol's own type.
75
+ *
76
+ * Typed against the package rather than as a `Bag`, which is the point:
77
+ * `Bag` is why 0.9.0 moved the exit code inside `lifecycle` and this host
78
+ * went on sending the old shape with a clean compile. A field removed or
79
+ * renamed upstream is a type error here now, at the one place the payload
80
+ * is built.
81
+ *
82
+ * The intersection is the deliberate part. This host negotiates down to
83
+ * 0.5.1 and every version before 0.9.0 reads a flat `exitCode`, so it sends
84
+ * both - and saying so in the type is the difference between a divergence
85
+ * somebody chose and one nobody noticed.
86
+ */
87
+ state(): OnWire<TerminalState> & { exitCode?: number };
88
+
89
+ /** Send input. Ignored once the process has exited. */
90
+ write(data: string): void;
91
+ /** Record a new size. Nothing is signalled: there is no pseudoterminal. */
92
+ resize(cols: number, rows: number): void;
93
+ /**
94
+ * Throw away the scrollback, keeping everything else.
95
+ *
96
+ * The size, the title and the claim survive: a client clears a terminal to
97
+ * stop reading what is already there, not to give it up. Nothing reaches
98
+ * the process - there is no pseudoterminal to send anything to, and a shell
99
+ * has no notion of its own output having been discarded.
100
+ */
101
+ clear(): void;
102
+ /** Rename it. */
103
+ setTitle(title: string): void;
104
+ /** Hand it to somebody else. */
105
+ setClaim(claim: Claim): void;
106
+ /** Kill the process and let go. */
107
+ close(): void;
108
+ }
109
+
110
+ /**
111
+ * A pseudoterminal, as this host needs one.
112
+ *
113
+ * Handed in rather than imported: a PTY is a native binding, and a library
114
+ * that depended on one would not load under a runtime it was not built for.
115
+ * The daemon passes `node-pty` when it has it; a host on another runtime
116
+ * passes its own, and one that passes none keeps pipes and says `isPty: false`.
117
+ */
118
+ export interface Pty {
119
+ /** Everything the terminal writes, VT sequences included. */
120
+ onData(listen: (data: string) => void): void;
121
+ /** Called once, when the process goes. */
122
+ onExit(listen: (exit: { exitCode: number }) => void): void;
123
+ write(data: string): void;
124
+ resize(cols: number, rows: number): void;
125
+ kill(signal?: string): void;
126
+ }
127
+
128
+ /** Start one. The shape `node-pty`'s own `spawn` already has. */
129
+ export type SpawnPty = (
130
+ file: string,
131
+ args: string[],
132
+ options: { cwd?: string; cols: number; rows: number; env: Record<string, string | undefined> },
133
+ ) => Pty;
@@ -0,0 +1,69 @@
1
+ /** The protocol's own shapes, as they actually go down the socket. */
2
+
3
+ import type { Bag } from './common.js';
4
+
5
+ /**
6
+ * Why this exists at all.
7
+ *
8
+ * Every payload this host builds used to be a `Bag`, and that is how 0.9.0
9
+ * moved a terminal's exit code inside `lifecycle`, removed `Turn.error` and
10
+ * required an `ErrorInfo` on a failed MCP server without a single compile
11
+ * error here. Three shape defects in one afternoon's audit, each one a client
12
+ * behaving correctly and getting it wrong. Typing the construction sites
13
+ * against the package turns the next one into a build failure.
14
+ *
15
+ * The conversion is needed because the protocol types its discriminants as
16
+ * `const enum`, and this project compiles with `verbatimModuleSyntax`, which
17
+ * refuses to import an ambient const enum as a value. That is not a problem
18
+ * worth casting past: what travels on the wire *is* the string, and
19
+ * `` `${SomeEnum}` `` is the set of strings that enum can be - checked, so a
20
+ * value renamed upstream stops matching here exactly as a field would.
21
+ */
22
+
23
+ /** One value, with string enums widened to the strings they are. */
24
+ type WireValue<V> =
25
+ // A function is not a payload. Left alone so a shape carrying one - which
26
+ // none of these do - does not silently become something else.
27
+ V extends (...args: never[]) => unknown ? V
28
+ : V extends string ? `${V}`
29
+ : V extends readonly (infer E)[] ? WireValue<E>[]
30
+ : V extends object ? OnWire<V>
31
+ : V;
32
+
33
+ /**
34
+ * A protocol shape as this host builds it.
35
+ *
36
+ * Optional keys stay optional, which matters under `exactOptionalPropertyTypes`:
37
+ * a field the protocol allows to be absent must not become one that has to be
38
+ * present and `undefined`.
39
+ */
40
+ export type OnWire<T> = { [K in keyof T]: WireValue<T[K]> };
41
+
42
+ /**
43
+ * A turn, whose parts are checked where they are built rather than here.
44
+ *
45
+ * `responseParts` is seven kinds and an eight-state tool call, assembled piece
46
+ * by piece as an agent talks - so the array cannot carry a union the way a
47
+ * finished value can: a call is `running` when it is pushed and `completed`
48
+ * three frames later, and a variable declared as either fights the other.
49
+ *
50
+ * The gap this used to leave is where the worst of it hid. A rebuilt
51
+ * transcript wrote a tool-call `status` that is not one of the seven, left off
52
+ * three fields the completed state requires, and gave its content blocks no
53
+ * `type` - four defects in one object, none of them a compile error, and every
54
+ * one of them a conversation that would not draw. So each part is now checked
55
+ * against the state it claims, at the moment it is built, with `satisfies` on
56
+ * the literal; what stays loose here is only the mutation that follows.
57
+ */
58
+ export type WireTurn<T> = Omit<OnWire<T>, 'responseParts'> & { responseParts: Bag[] };
59
+
60
+ /**
61
+ * On `satisfies` rather than annotation, where you see it.
62
+ *
63
+ * A turn is built as an `ActiveTurn` and then *mutated* into a `Turn` - a
64
+ * state and a duration are set on the same object when it ends. Declaring the
65
+ * variable as either one fights the other, so the literal is checked with
66
+ * `satisfies` at the moment it is built and the variable stays a `Bag` for the
67
+ * mutation that follows. The construction is what the protocol changes under,
68
+ * and the construction is what is checked.
69
+ */