@ahpd/server 0.1.0 → 0.3.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 (170) hide show
  1. package/README.md +88 -81
  2. package/dist/config.d.ts +72 -0
  3. package/dist/config.js +75 -0
  4. package/dist/daemon.d.ts +31 -0
  5. package/dist/daemon.js +162 -0
  6. package/dist/main.d.ts +2 -0
  7. package/dist/main.js +335 -0
  8. package/dist/pty.d.ts +12 -0
  9. package/dist/pty.js +56 -0
  10. package/package.json +15 -25
  11. package/dist/automations.d.ts +0 -4
  12. package/dist/automations.d.ts.map +0 -1
  13. package/dist/automations.js +0 -191
  14. package/dist/automations.js.map +0 -1
  15. package/dist/catalog.d.ts +0 -48
  16. package/dist/catalog.d.ts.map +0 -1
  17. package/dist/catalog.js +0 -51
  18. package/dist/catalog.js.map +0 -1
  19. package/dist/changes.d.ts +0 -4
  20. package/dist/changes.d.ts.map +0 -1
  21. package/dist/changes.js +0 -601
  22. package/dist/changes.js.map +0 -1
  23. package/dist/cron.d.ts +0 -43
  24. package/dist/cron.d.ts.map +0 -1
  25. package/dist/cron.js +0 -211
  26. package/dist/cron.js.map +0 -1
  27. package/dist/git.d.ts +0 -16
  28. package/dist/git.d.ts.map +0 -1
  29. package/dist/git.js +0 -126
  30. package/dist/git.js.map +0 -1
  31. package/dist/host.d.ts +0 -24
  32. package/dist/host.d.ts.map +0 -1
  33. package/dist/host.js +0 -5850
  34. package/dist/host.js.map +0 -1
  35. package/dist/index.d.ts +0 -36
  36. package/dist/index.d.ts.map +0 -1
  37. package/dist/index.js +0 -34
  38. package/dist/index.js.map +0 -1
  39. package/dist/listen.d.ts +0 -3
  40. package/dist/listen.d.ts.map +0 -1
  41. package/dist/listen.js +0 -183
  42. package/dist/listen.js.map +0 -1
  43. package/dist/paging.d.ts +0 -38
  44. package/dist/paging.d.ts.map +0 -1
  45. package/dist/paging.js +0 -53
  46. package/dist/paging.js.map +0 -1
  47. package/dist/paths.d.ts +0 -15
  48. package/dist/paths.d.ts.map +0 -1
  49. package/dist/paths.js +0 -19
  50. package/dist/paths.js.map +0 -1
  51. package/dist/resources.d.ts +0 -122
  52. package/dist/resources.d.ts.map +0 -1
  53. package/dist/resources.js +0 -494
  54. package/dist/resources.js.map +0 -1
  55. package/dist/rpc.d.ts +0 -63
  56. package/dist/rpc.d.ts.map +0 -1
  57. package/dist/rpc.js +0 -208
  58. package/dist/rpc.js.map +0 -1
  59. package/dist/scheduled.d.ts +0 -48
  60. package/dist/scheduled.d.ts.map +0 -1
  61. package/dist/scheduled.js +0 -325
  62. package/dist/scheduled.js.map +0 -1
  63. package/dist/terminals.d.ts +0 -29
  64. package/dist/terminals.d.ts.map +0 -1
  65. package/dist/terminals.js +0 -346
  66. package/dist/terminals.js.map +0 -1
  67. package/dist/tools.d.ts +0 -18
  68. package/dist/tools.d.ts.map +0 -1
  69. package/dist/tools.js +0 -65
  70. package/dist/tools.js.map +0 -1
  71. package/dist/types/agent.d.ts +0 -245
  72. package/dist/types/agent.d.ts.map +0 -1
  73. package/dist/types/agent.js +0 -3
  74. package/dist/types/agent.js.map +0 -1
  75. package/dist/types/automations.d.ts +0 -164
  76. package/dist/types/automations.d.ts.map +0 -1
  77. package/dist/types/automations.js +0 -3
  78. package/dist/types/automations.js.map +0 -1
  79. package/dist/types/catalog.d.ts +0 -30
  80. package/dist/types/catalog.d.ts.map +0 -1
  81. package/dist/types/catalog.js +0 -3
  82. package/dist/types/catalog.js.map +0 -1
  83. package/dist/types/changes.d.ts +0 -272
  84. package/dist/types/changes.d.ts.map +0 -1
  85. package/dist/types/changes.js +0 -3
  86. package/dist/types/changes.js.map +0 -1
  87. package/dist/types/common.d.ts +0 -9
  88. package/dist/types/common.d.ts.map +0 -1
  89. package/dist/types/common.js +0 -3
  90. package/dist/types/common.js.map +0 -1
  91. package/dist/types/host.d.ts +0 -372
  92. package/dist/types/host.d.ts.map +0 -1
  93. package/dist/types/host.js +0 -3
  94. package/dist/types/host.js.map +0 -1
  95. package/dist/types/index.d.ts +0 -20
  96. package/dist/types/index.d.ts.map +0 -1
  97. package/dist/types/index.js +0 -8
  98. package/dist/types/index.js.map +0 -1
  99. package/dist/types/listen.d.ts +0 -48
  100. package/dist/types/listen.d.ts.map +0 -1
  101. package/dist/types/listen.js +0 -3
  102. package/dist/types/listen.js.map +0 -1
  103. package/dist/types/paging.d.ts +0 -10
  104. package/dist/types/paging.d.ts.map +0 -1
  105. package/dist/types/paging.js +0 -3
  106. package/dist/types/paging.js.map +0 -1
  107. package/dist/types/probe.d.ts +0 -46
  108. package/dist/types/probe.d.ts.map +0 -1
  109. package/dist/types/probe.js +0 -3
  110. package/dist/types/probe.js.map +0 -1
  111. package/dist/types/resources.d.ts +0 -83
  112. package/dist/types/resources.d.ts.map +0 -1
  113. package/dist/types/resources.js +0 -3
  114. package/dist/types/resources.js.map +0 -1
  115. package/dist/types/rpc.d.ts +0 -65
  116. package/dist/types/rpc.d.ts.map +0 -1
  117. package/dist/types/rpc.js +0 -3
  118. package/dist/types/rpc.js.map +0 -1
  119. package/dist/types/session.d.ts +0 -395
  120. package/dist/types/session.d.ts.map +0 -1
  121. package/dist/types/session.js +0 -3
  122. package/dist/types/session.js.map +0 -1
  123. package/dist/types/terminals.d.ts +0 -131
  124. package/dist/types/terminals.d.ts.map +0 -1
  125. package/dist/types/terminals.js +0 -3
  126. package/dist/types/terminals.js.map +0 -1
  127. package/dist/types/wire.d.ts +0 -62
  128. package/dist/types/wire.d.ts.map +0 -1
  129. package/dist/types/wire.js +0 -13
  130. package/dist/types/wire.js.map +0 -1
  131. package/dist/types/worktrees.d.ts +0 -86
  132. package/dist/types/worktrees.d.ts.map +0 -1
  133. package/dist/types/worktrees.js +0 -3
  134. package/dist/types/worktrees.js.map +0 -1
  135. package/dist/worktrees.d.ts +0 -27
  136. package/dist/worktrees.d.ts.map +0 -1
  137. package/dist/worktrees.js +0 -152
  138. package/dist/worktrees.js.map +0 -1
  139. package/src/automations.ts +0 -204
  140. package/src/catalog.ts +0 -53
  141. package/src/changes.ts +0 -629
  142. package/src/cron.ts +0 -251
  143. package/src/git.ts +0 -132
  144. package/src/host.ts +0 -5947
  145. package/src/index.ts +0 -40
  146. package/src/listen.ts +0 -240
  147. package/src/paging.ts +0 -56
  148. package/src/paths.ts +0 -19
  149. package/src/resources.ts +0 -513
  150. package/src/rpc.ts +0 -222
  151. package/src/scheduled.ts +0 -369
  152. package/src/terminals.ts +0 -353
  153. package/src/tools.ts +0 -64
  154. package/src/types/agent.ts +0 -255
  155. package/src/types/automations.ts +0 -164
  156. package/src/types/catalog.ts +0 -26
  157. package/src/types/changes.ts +0 -266
  158. package/src/types/common.ts +0 -9
  159. package/src/types/host.ts +0 -386
  160. package/src/types/index.ts +0 -27
  161. package/src/types/listen.ts +0 -53
  162. package/src/types/paging.ts +0 -11
  163. package/src/types/probe.ts +0 -42
  164. package/src/types/resources.ts +0 -107
  165. package/src/types/rpc.ts +0 -68
  166. package/src/types/session.ts +0 -415
  167. package/src/types/terminals.ts +0 -133
  168. package/src/types/wire.ts +0 -69
  169. package/src/types/worktrees.ts +0 -91
  170. package/src/worktrees.ts +0 -162
package/src/terminals.ts DELETED
@@ -1,353 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import type { Pty, SpawnPty, Terminal, TerminalOptions } from './types/terminals.js';
3
- import type { TerminalStore } from './types/host.js';
4
-
5
- /**
6
- * A shell on the host machine, as a terminal channel.
7
- *
8
- * Pipes, not a pseudoterminal: a PTY needs a native binding this daemon does
9
- * not depend on, and the protocol has `isPty: false` for exactly this - "output
10
- * is plain text and clients do not need to parse VT sequences". So a command
11
- * runs and its output arrives, and anything that draws itself with cursor
12
- * movement will not look right. Said in the state rather than discovered.
13
- *
14
- * `node:child_process` is used because all three supported runtimes provide it.
15
- */
16
-
17
- /** What runs, when nothing else was asked for. */
18
- const shellOf = (asked?: string): string => asked ?? process.env.SHELL ?? '/bin/sh';
19
-
20
- /**
21
- * What a shell says about itself, in the escape sequences it says it with.
22
- *
23
- * OSC 133 is the command-boundary convention every shell integration script
24
- * writes - `A` before the prompt, `B` where the command starts, `C` where its
25
- * output does, `D;<code>` when it finished - and OSC 7 is the directory. They
26
- * arrive mixed into the output, so this reads them out and leaves the rest
27
- * alone: the bytes still go to the client, which is drawing a terminal and
28
- * needs them.
29
- *
30
- * Only under a pseudoterminal, because only then is there a shell running its
31
- * own prompt to emit them.
32
- */
33
- const MARKS = /\u001b\](133|7);([^\u0007\u001b]*)(?:\u0007|\u001b\\)/g;
34
-
35
- export function createTerminal(options: TerminalOptions, pty?: SpawnPty): Terminal {
36
- const { uri, cwd, emit } = options;
37
- const shell = shellOf(options.shell);
38
- let title = options.name ?? shell.slice(shell.lastIndexOf('/') + 1);
39
- let claim = options.claim;
40
- let cols = options.cols ?? 80;
41
- let rows = options.rows ?? 24;
42
- let exitCode: number | undefined;
43
- /**
44
- * Everything written so far, so a client that subscribes late sees it.
45
- *
46
- * Capped: a terminal left running `tail -f` for a day is a host holding a
47
- * day of output for a client that may never come back.
48
- */
49
- let buffered = '';
50
- const KEEP = 200_000;
51
-
52
- const said = (data: string): void => {
53
- buffered = (buffered + data).slice(-KEEP);
54
- emit('terminal', { type: 'terminal/data', data });
55
- };
56
-
57
- /*
58
- * `-c` when there is a command, and nothing when there is not.
59
- *
60
- * A shell given `-c` runs the one thing and exits. Under pipes that is the
61
- * only completion signal there is; under a pseudoterminal the shell says so
62
- * itself, in OSC 133.
63
- */
64
- const args = options.command === undefined ? [] : ['-c', options.command];
65
- const environment = {
66
- ...process.env,
67
- // A real terminal under a pty, and an honest `dumb` without one.
68
- TERM: pty ? (process.env.TERM ?? 'xterm-256color') : 'dumb',
69
- COLUMNS: String(cols),
70
- LINES: String(rows),
71
- };
72
-
73
- /** Where the shell says it is, once it has said. */
74
- let where = cwd;
75
- /** The command being run, from `C` until `D`. */
76
- let command: { id: string; line: string; at: number } | undefined;
77
- /** What has been typed since the prompt, so the command line can be read back. */
78
- let typed = '';
79
-
80
- /**
81
- * Read the shell's own marks out of a chunk, and say what they meant.
82
- *
83
- * The chunk still reaches the client whole: this is a reader, not a filter,
84
- * and a client drawing a terminal needs the bytes it was sent.
85
- */
86
- const marked = (data: string): void => {
87
- for (const found of data.matchAll(MARKS)) {
88
- const [, kind, body = ''] = found;
89
- if (kind === '7') {
90
- // `file://host/path`, per the convention. The host part is dropped:
91
- // the path is on this machine, and that is what a client opens.
92
- const path = body.replace(/^file:\/\/[^/]*/, '');
93
- if (path !== '' && path !== where) {
94
- where = path;
95
- emit('terminal', { type: 'terminal/cwdChanged', cwd: `file://${path}` });
96
- }
97
- continue;
98
- }
99
- const mark = body.split(';')[0];
100
- if (mark === 'A') { typed = ''; continue; }
101
- if (mark === 'C') {
102
- command = { id: `c${String(Date.now())}`, line: typed.trim(), at: Date.now() };
103
- emit('terminal', {
104
- type: 'terminal/commandExecuted',
105
- commandId: command.id,
106
- commandLine: command.line,
107
- timestamp: command.at,
108
- });
109
- continue;
110
- }
111
- if (mark !== 'D' || command === undefined) continue;
112
- const code = Number(body.split(';')[1]);
113
- emit('terminal', {
114
- type: 'terminal/commandFinished',
115
- commandId: command.id,
116
- ...(Number.isFinite(code) ? { exitCode: code } : {}),
117
- durationMs: Date.now() - command.at,
118
- });
119
- command = undefined;
120
- }
121
- };
122
-
123
- const terminal: Pty | undefined = pty?.(shell, args, {
124
- ...(cwd !== undefined ? { cwd } : {}),
125
- cols,
126
- rows,
127
- env: environment,
128
- });
129
- if (terminal !== undefined) {
130
- terminal.onData((data) => { marked(data); said(data); });
131
- terminal.onExit(({ exitCode: code }) => { exitCode = code; ended(); });
132
- // Said once, at the start. A client MUST check this before relying on
133
- // command boundaries, and the same fact is on the state.
134
- emit('terminal', { type: 'terminal/commandDetectionAvailable' });
135
- }
136
-
137
- const child = terminal !== undefined ? undefined : spawn(shell, args, {
138
- cwd,
139
- /*
140
- * Its own process group, so a signal reaches what it started.
141
- *
142
- * A shell reading from a pipe runs each command as its own child, and a
143
- * signal sent to the shell alone leaves the command running. The group is
144
- * what a terminal driver would have signalled, and this has no driver.
145
- */
146
- detached: true,
147
- // A shell reading commands from a pipe. Without a pseudoterminal there is
148
- // no point asking it to be interactive: it would print a prompt nobody
149
- // can answer the way it expects.
150
- stdio: ['pipe', 'pipe', 'pipe'],
151
- env: environment,
152
- });
153
-
154
- child?.stdout.on('data', (chunk: Buffer) => said(chunk.toString('utf8')));
155
- child?.stderr.on('data', (chunk: Buffer) => said(chunk.toString('utf8')));
156
- /**
157
- * Said once, whichever of the three got here first.
158
- *
159
- * `error` and `close` can both fire for one failed spawn, and a terminal
160
- * that announced its own exit twice would be one every client draws as
161
- * having died, come back, and died again.
162
- */
163
- let announced = false;
164
- const ended = (): void => {
165
- if (announced) return;
166
- announced = true;
167
- emit('terminal', { type: 'terminal/exited', exitCode });
168
- };
169
- child?.on('error', (error: Error) => {
170
- said(`${error.message}\n`);
171
- exitCode = 127;
172
- ended();
173
- });
174
- child?.on('exit', (code: number | null, signal: string | null) => {
175
- // A signal is not an exit code, and 128+n is the shell's own convention
176
- // for one - better than reporting nothing, which reads as still running.
177
- exitCode = code ?? (signal ? 128 : 0);
178
- });
179
- /*
180
- * Announced on `close` rather than on `exit`, which is a race this lost.
181
- *
182
- * `exit` fires when the process goes; `close` fires once its pipes are
183
- * drained. Between the two there is output already written and not yet
184
- * read, so a host that reported the exit on `exit` reported a command's
185
- * result before the result had arrived - which is exactly what a `!`
186
- * command in the composer reads back.
187
- */
188
- child?.on('close', () => {
189
- // A process that closed without an exit event was killed outright.
190
- exitCode ??= 0;
191
- ended();
192
- });
193
-
194
- return {
195
- uri,
196
- title: () => title,
197
- claim: () => claim,
198
- exitCode: () => exitCode,
199
- lifecycle: () => (exitCode === undefined
200
- ? { status: 'running' }
201
- : { status: 'exited', exitCode }),
202
-
203
- state: () => ({
204
- title,
205
- cwd: `file://${cwd}`,
206
- cols,
207
- rows,
208
- // One part, because without command detection there are no boundaries
209
- // to divide the output at. The protocol's shape, not a flat string.
210
- content: buffered === '' ? [] : [{ type: 'unclassified', value: buffered }],
211
- claim,
212
- // Both true only under a pseudoterminal: without one there is no shell
213
- // running its own prompt, so there are no boundaries to report and no
214
- // VT sequences for a client to parse.
215
- supportsCommandDetection: terminal !== undefined,
216
- isPty: terminal !== undefined,
217
- /*
218
- * Both spellings, because this host speaks five versions.
219
- *
220
- * `lifecycle` is what 0.9.0 requires and is not optional there - a
221
- * client reading `lifecycle.status` on a 0.8.0-shaped terminal gets
222
- * `undefined`, which reads as a process that never exits. The flat
223
- * `exitCode` is what every version before it reads, and this daemon
224
- * negotiates down to 0.5.1.
225
- */
226
- lifecycle: exitCode === undefined
227
- ? { status: 'running' }
228
- : { status: 'exited', exitCode },
229
- ...(exitCode !== undefined ? { exitCode } : {}),
230
- }),
231
-
232
- write: (data) => {
233
- if (exitCode !== undefined)
234
- return;
235
- /*
236
- * `^C` is a signal, and there is nothing here to turn it into one.
237
- *
238
- * A pseudoterminal has a line discipline that sees the byte and sends
239
- * SIGINT to the foreground group. Pipes have none, so the byte arrives
240
- * as input and the command runs on - which is a terminal a runaway
241
- * command cannot be stopped in. Sending the signal is what the driver
242
- * would have done.
243
- */
244
- /*
245
- * Under a pseudoterminal the byte is the signal.
246
- *
247
- * A pty has a line discipline: `^C` reaches it as input and it sends
248
- * SIGINT to the foreground group itself, which is the whole point of
249
- * having one. So this writes it through and does nothing clever.
250
- */
251
- if (terminal !== undefined) {
252
- terminal.write(data);
253
- // Kept so a command line can be read back at the next `C` mark; the
254
- // shell echoes what was typed, but the echo arrives as output and
255
- // this is the only place the input itself is seen.
256
- typed += data;
257
- return;
258
- }
259
- const at = data.indexOf('\u0003');
260
- if (at !== -1) {
261
- const rest = data.slice(0, at) + data.slice(at + 1);
262
- if (rest !== '' && child?.stdin.writable) child.stdin.write(rest);
263
- /*
264
- * The group, named by the child's own pid, and only when there is one.
265
- *
266
- * A spawn that failed leaves no pid, and `0` is not a safe stand-in:
267
- * to `kill` it means every process in *this* process group, so a
268
- * terminal whose shell never started would signal the host and
269
- * whatever started the host.
270
- */
271
- const group = child?.pid;
272
- if (group !== undefined) {
273
- try { process.kill(-group, 'SIGINT'); }
274
- // The group is gone, which is the outcome asked for.
275
- catch { /* nothing left to interrupt */ }
276
- }
277
- return;
278
- }
279
- if (child?.stdin.writable) child.stdin.write(data);
280
- },
281
-
282
- // Told, when there is something to tell: a pseudoterminal gets the new
283
- // size and sends SIGWINCH itself. Without one these are kept because the
284
- // state reports them and a client draws to them.
285
- resize: (nextCols, nextRows) => {
286
- terminal?.resize(nextCols, nextRows);
287
- cols = nextCols;
288
- rows = nextRows;
289
- emit('terminal', { type: 'terminal/resized', cols, rows });
290
- },
291
-
292
- // The scrollback and nothing else. `cols`, `rows`, `title` and `claim` are
293
- // untouched, which is what the reference suite checks for.
294
- clear: () => {
295
- buffered = '';
296
- emit('terminal', { type: 'terminal/cleared' });
297
- },
298
-
299
- setTitle: (next) => {
300
- if (next === title) return;
301
- title = next;
302
- emit('terminal', { type: 'terminal/titleChanged', title });
303
- },
304
-
305
- setClaim: (next) => {
306
- claim = next;
307
- emit('terminal', { type: 'terminal/claimed', claim });
308
- },
309
-
310
- close: () => {
311
- if (terminal !== undefined) {
312
- terminal.kill();
313
- return;
314
- }
315
- child?.stdin.end();
316
- // The group, not the shell: detached, its children outlive it otherwise.
317
- // Only when there is a pid to name it - see the interrupt above.
318
- const group = child?.pid;
319
- if (group === undefined) {
320
- child?.kill();
321
- return;
322
- }
323
- try { process.kill(-group, 'SIGKILL'); }
324
- catch { child?.kill(); }
325
- },
326
- };
327
- }
328
-
329
- /**
330
- * A shell on this machine, as a host's `TerminalStore`.
331
- *
332
- * Kept out of `createHost` for the reason above: spawning is the runtime's
333
- * business, and a host that opens no terminal should not have to have one.
334
- *
335
- * ```ts
336
- * createHost({ path, agents, terminals: shellTerminals() });
337
- * ```
338
- *
339
- * Given a `pty` it runs shells under a pseudoterminal instead, which is what
340
- * makes shell integration possible: the shell prints its own OSC 133 marks, so
341
- * command boundaries and the working directory become facts rather than
342
- * guesses. The binding is handed in because it is native code - `node-pty` is
343
- * the daemon's dependency and never this library's, and a host on another
344
- * runtime passes whatever it has.
345
- *
346
- * ```ts
347
- * import { spawn } from 'node-pty';
348
- * createHost({ path, agents, terminals: shellTerminals({ pty: spawn }) });
349
- * ```
350
- */
351
- export const shellTerminals = (options: { pty?: SpawnPty } = {}): TerminalStore => ({
352
- create: (asked) => createTerminal(asked, options.pty),
353
- });
package/src/tools.ts DELETED
@@ -1,64 +0,0 @@
1
- import type { HostTool } from './types/host.js';
2
-
3
- /**
4
- * The tools this package contributes to every session, as a host's `tools`.
5
- *
6
- * The protocol's `serverTools` are the *host's* own - not a backend's and not
7
- * a client's - and what makes one worth contributing is that the host knows
8
- * something the agent inside a session cannot: the other sessions running
9
- * beside it, and the terminals the person is watching. Both are read-only.
10
- *
11
- * ```ts
12
- * createHost({ path, agents, tools: hostTools() });
13
- * ```
14
- *
15
- * A host that wants its own passes its own; a host that passes none
16
- * contributes none, and its sessions report no `serverTools` at all.
17
- */
18
- export const hostTools = (): HostTool[] => [
19
- {
20
- definition: {
21
- name: 'ahp_sessions',
22
- title: 'Sessions on this host',
23
- description: 'The other agent sessions running on this host, with the directories each works in. '
24
- + 'Use it before touching a file to find out whether another agent is already working there.',
25
- inputSchema: { type: 'object', properties: {} },
26
- annotations: { title: 'Sessions on this host', readOnlyHint: true, openWorldHint: false },
27
- },
28
- run: (_input, at) => {
29
- const others = at.sessions().filter((one) => one.uri !== at.session);
30
- if (others.length === 0) return 'No other session is running on this host.';
31
- return others
32
- .map((one) => `${one.uri}\t${one.provider}\t${one.title}\t${one.workingDirectories.join(' ')}`)
33
- .join('\n');
34
- },
35
- },
36
- {
37
- definition: {
38
- name: 'ahp_resource',
39
- title: 'Read a resource this host serves',
40
- description: 'Read a resource by URI, including one a connected client publishes and this machine has no copy of '
41
- + '- a plugin\u2019s virtual files, an editor\u2019s unsaved buffers. Addressed as <scheme>://<client>/<path>.',
42
- inputSchema: { type: 'object', properties: { uri: { type: 'string' } }, required: ['uri'] },
43
- annotations: { title: 'Read a resource this host serves', readOnlyHint: true },
44
- },
45
- run: async (input, at) => await at.read(String(input.uri ?? '')),
46
- },
47
- {
48
- definition: {
49
- name: 'ahp_terminals',
50
- title: 'Terminals on this host',
51
- description: 'The terminals this host has open, with what each is running and where. '
52
- + 'These are the terminals the person can see, which is not the same as a shell run from a tool.',
53
- inputSchema: { type: 'object', properties: {} },
54
- annotations: { title: 'Terminals on this host', readOnlyHint: true, openWorldHint: false },
55
- },
56
- run: (_input, at) => {
57
- const open = at.terminals();
58
- if (open.length === 0) return 'This host has no terminal open.';
59
- return open
60
- .map((one) => `${one.uri}\t${one.title}\t${one.cwd}\t${one.running ? 'running' : 'exited'}`)
61
- .join('\n');
62
- },
63
- },
64
- ];
@@ -1,255 +0,0 @@
1
- /** An agent backend, and everything the host asks one for. */
2
-
3
- import type { Turn } from '@microsoft/agent-host-protocol';
4
- import type { Bag } from './common.js';
5
- import type { WireTurn } from './wire.js';
6
- import type { Emit, Session } from './session.js';
7
- import type { ToolDefinition } from '@microsoft/agent-host-protocol';
8
- import type { Offered } from './probe.js';
9
-
10
- /**
11
- * A tool the host contributes, with the session it was contributed to
12
- * already bound.
13
- *
14
- * The host's `HostTool` takes a `ToolCall` saying where it was called from;
15
- * by the time a backend sees one that is answered, so what is left is a
16
- * definition to offer the model and a function to call.
17
- */
18
- export interface BoundTool {
19
- /** What the model is offered. `name` is the id it calls. */
20
- definition: ToolDefinition;
21
- /**
22
- * What running it does. Absent for a tool a client runs - see `owner`.
23
- */
24
- run?(input: Record<string, unknown>): Promise<string> | string;
25
- /**
26
- * The client that runs this one, when it is a client's rather than the host's.
27
- *
28
- * A client announces what it provides on `SessionActiveClient.tools`, and
29
- * the protocol makes that client responsible for executing the call and
30
- * dispatching its result. So there is nothing to run here: the backend
31
- * offers the tool to the model, reports the call against that client, and
32
- * waits for it to say what happened.
33
- */
34
- owner?: string;
35
- }
36
-
37
- /**
38
- * One session a backend already has, before the host has named it.
39
- *
40
- * Deliberately not a `Summary`: the resource URI, the provider and the status
41
- * bits are the host's to decide - `IsRead` and `IsArchived` in particular are
42
- * kept per client and a backend has never heard of them.
43
- */
44
- export interface Listed {
45
- /** The backend's own id for it. The host serves it as `<provider>:/<id>`. */
46
- id: string;
47
- /** Display title. */
48
- title: string;
49
- /** ISO 8601 timestamp of creation. */
50
- createdAt: string;
51
- /** ISO 8601 timestamp of the last change. */
52
- modifiedAt: string;
53
- /** Directories the agent has tool access to, as `file://` URIs. */
54
- workingDirectories: string[];
55
- }
56
-
57
- /** How the host asks a backend to start a session. */
58
- export interface Start {
59
- /** The session channel URI the client chose. */
60
- uri: string;
61
- /** The chat channel URI beneath it. */
62
- chatUri: string;
63
- /** Config values in force, by key: this agent's defaults with the client's on top. */
64
- settings: Record<string, unknown>;
65
- /**
66
- * The directory the client asked the agent to work in, if it named one.
67
- *
68
- * A path, not a `file://` URI. Absent means the client named none and the
69
- * backend picks. A backend that will not work there should throw saying so:
70
- * a directory accepted and then ignored is a session running somewhere
71
- * nobody asked for, and nothing on screen says which.
72
- */
73
- workingDirectory?: string;
74
- /** Directories beside it the agent may also work in. */
75
- additional?: string[];
76
- /**
77
- * Tools the host contributes to this session, for the backend to offer.
78
- *
79
- * The host's own, not this backend's: a backend that cannot take tools from
80
- * anywhere ignores them, and the host still reports them on the session so
81
- * a client knows they exist.
82
- */
83
- tools?: BoundTool[];
84
- /** The config schema to report on the session channel. This agent's own. */
85
- schema(): Bag;
86
- /** What to report as customizations until the backend reports its own. */
87
- seedCustomizations?: Bag[];
88
- /** Where state actions go. The host routes them to the right channel. */
89
- emit: Emit;
90
- /** A session of this backend's to continue, rather than starting a new one. */
91
- resume?: string;
92
- /** The prompt to resume *at*, so a fork leaves the turns after it behind. */
93
- forkAt?: string;
94
- /**
95
- * The chain entry to resume *at*, keeping the id the session already had.
96
- *
97
- * A truncation rather than a fork: the turns after that point are dropped
98
- * and the conversation carries on as itself, which is what `chat/truncated`
99
- * asks for.
100
- */
101
- rewindAt?: string;
102
- /** Context the first turn carries to the backend without showing it. */
103
- context?: string;
104
- /** Turns already known, so a resumed session does not open empty. */
105
- seed?: Bag[];
106
- /**
107
- * A file a tool is about to change, and the same file once it has.
108
- *
109
- * Optional both ways: a backend that cannot see its own tools does not call
110
- * it, and a host with no changeset source does not pass one.
111
- */
112
- onFileEdit?(turnId: string, path: string, phase: 'before' | 'after'): void;
113
- /** Called once the backend has reported what it can do. */
114
- onHandshake?(): void;
115
- /**
116
- * Tokens for this backend's protected resources, by resource identifier.
117
- *
118
- * Only resources this agent advertised, and only what the connection
119
- * asking for the session pushed - authentication is per connection, so a
120
- * token one client offered is never spent on another's session. Absent for
121
- * a session nobody asked for: an automation firing at nine in the morning
122
- * has no connection behind it and runs on the daemon's own credentials.
123
- *
124
- * What to *do* with one is the backend's business. The host knows a token
125
- * belongs to a resource and nothing else about it.
126
- */
127
- credentials?: Record<string, string>;
128
- }
129
-
130
- /**
131
- * A backend the host can run sessions on.
132
- *
133
- * One host serves several. `provider` is what a client names in
134
- * `createSession` and what every session of this kind reports, so it has to
135
- * be unique across the agents a host was given.
136
- *
137
- * Everything but `provider`, `displayName`, `schema`, `defaults` and `create`
138
- * is optional, and what is left out is a real answer rather than a gap: a
139
- * backend with no `list` has no sessions to browse, one with no `probe`
140
- * offers no models and no commands until a session of its own reports them.
141
- */
142
- export interface Agent {
143
- /** The id clients name. Unique among a host's agents. */
144
- provider: string;
145
- /** What a person reads instead of the id. */
146
- displayName: string;
147
- /** One line about what this backend is. */
148
- description?: string;
149
-
150
- /**
151
- * What a second chat in one session can be made from.
152
- *
153
- * Multi-chat itself is the host's doing - a second chat is `create` called
154
- * twice - but these two are the backend's: a fork continues a conversation
155
- * from one of its turns, and a side chat starts a fresh one that knows what
156
- * a turn elsewhere said. A backend that declares neither still gets several
157
- * chats; it just cannot be asked to make one out of another.
158
- */
159
- chats?: { fork?: boolean; sideChat?: boolean };
160
-
161
- /**
162
- * Whether a session of this backend can work in more than one directory.
163
- *
164
- * The first is the process root and never moves; the rest are its peers.
165
- * A backend that leaves this out gets one directory per session, which is
166
- * what a client is told when the capability is absent.
167
- */
168
- multipleDirectories?: boolean;
169
-
170
- /**
171
- * OAuth protected resources this backend can be given a token for.
172
- *
173
- * RFC 9728 metadata, served on `AgentInfo.protectedResources`, and the only
174
- * thing that makes `authenticate` callable: the protocol says a client's
175
- * `resource` MUST match one the server has itself advertised, so a host that
176
- * advertises none can be sent no token at all.
177
- *
178
- * `required: false` on an entry is a backend saying it works without one -
179
- * this daemon runs as whoever started it and inherits their credentials, so
180
- * a token is an override rather than a precondition.
181
- */
182
- protectedResources?: Bag[];
183
-
184
- /**
185
- * What a session of this kind can be told to do differently.
186
- *
187
- * A JSON-Schema-shaped `{ properties }`. Each property may carry `title`,
188
- * `description`, `enum`, `enumLabels`, `enumDescriptions`, `default`,
189
- * `sessionMutable` and `scope`.
190
- *
191
- * The last two are what the *host* reads, and they are the only two things
192
- * it needs to know about a key it otherwise knows nothing about.
193
- * `sessionMutable: false` is refused on a running session rather than
194
- * accepted and dropped. `scope` is `'session'` - the default - for a key the
195
- * chats of one session share, and `'chat'` for one each chat answers for
196
- * itself: a permission mode is the session's, and a model is the chat's.
197
- * `scope` is this library's, not the protocol's, whose config schema is
198
- * deliberately generic and says nothing about either.
199
- *
200
- * Everything else is between the backend and whatever client draws it.
201
- *
202
- * One schema, used before a session exists and by every session that does.
203
- * Two copies drift, and a composer then offers one set of controls on the
204
- * new-session screen and a different set the moment a session opens.
205
- */
206
- schema(): Bag;
207
- /** What each schema key sits at when nothing has been chosen. */
208
- defaults(): Record<string, unknown>;
209
-
210
- /**
211
- * What the backend offers, asked once at startup.
212
- *
213
- * Before any session exists, because that is when a client asks: the models
214
- * to pick from and the commands behind a slash are what a composer needs to
215
- * draw itself, and waiting for the first session means offering them only
216
- * once the conversation has started.
217
- */
218
- probe?(): Promise<Offered>;
219
-
220
- /**
221
- * The directories this backend will work in.
222
- *
223
- * The host's answer to "may this client read that file": a resource outside
224
- * every backend's directories is refused, because a host that served the
225
- * whole filesystem is one that anybody who can reach the port can read
226
- * `~/.ssh` through.
227
- *
228
- * Left out means this backend claims no directories, and contributes
229
- * nothing to what may be browsed.
230
- */
231
- directories?(): string[];
232
-
233
- /** Sessions this backend already has. Ordering is the host's business. */
234
- list?(): Promise<Listed[]>;
235
-
236
- /**
237
- * One past session's turns, read without starting anything.
238
- *
239
- * What makes a catalogue row openable: the host serves it from here, and
240
- * starts nothing until somebody sends a turn to it. Undefined means this
241
- * backend has no such session.
242
- *
243
- * A `WireTurn` rather than a `Bag[]`: everything around the parts is checked
244
- * here, and each part is checked where it is built - a part is assembled by
245
- * mutation as an agent talks, so the literal is what can be held to a shape
246
- * and the variable after it cannot. This was a `Bag[]`, and inside it a
247
- * rebuilt transcript wrote a tool-call `status` that is not one of the
248
- * seven, left off three fields the completed state requires, and gave its
249
- * content blocks no `type`.
250
- */
251
- transcript?(id: string): Promise<WireTurn<Turn>[] | undefined>;
252
-
253
- /** Start one. */
254
- create(start: Start): Session;
255
- }