@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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/automations.d.ts +4 -0
- package/dist/automations.d.ts.map +1 -0
- package/dist/automations.js +191 -0
- package/dist/automations.js.map +1 -0
- package/dist/catalog.d.ts +48 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +51 -0
- package/dist/catalog.js.map +1 -0
- package/dist/changes.d.ts +4 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +601 -0
- package/dist/changes.js.map +1 -0
- package/dist/cron.d.ts +43 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +211 -0
- package/dist/cron.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +126 -0
- package/dist/git.js.map +1 -0
- package/dist/host.d.ts +24 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +5850 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/listen.d.ts +3 -0
- package/dist/listen.d.ts.map +1 -0
- package/dist/listen.js +183 -0
- package/dist/listen.js.map +1 -0
- package/dist/paging.d.ts +38 -0
- package/dist/paging.d.ts.map +1 -0
- package/dist/paging.js +53 -0
- package/dist/paging.js.map +1 -0
- package/dist/paths.d.ts +15 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/resources.d.ts +122 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +494 -0
- package/dist/resources.js.map +1 -0
- package/dist/rpc.d.ts +63 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +208 -0
- package/dist/rpc.js.map +1 -0
- package/dist/scheduled.d.ts +48 -0
- package/dist/scheduled.d.ts.map +1 -0
- package/dist/scheduled.js +325 -0
- package/dist/scheduled.js.map +1 -0
- package/dist/terminals.d.ts +29 -0
- package/dist/terminals.d.ts.map +1 -0
- package/dist/terminals.js +346 -0
- package/dist/terminals.js.map +1 -0
- package/dist/tools.d.ts +18 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +65 -0
- package/dist/tools.js.map +1 -0
- package/dist/types/agent.d.ts +245 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +3 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/automations.d.ts +164 -0
- package/dist/types/automations.d.ts.map +1 -0
- package/dist/types/automations.js +3 -0
- package/dist/types/automations.js.map +1 -0
- package/dist/types/catalog.d.ts +30 -0
- package/dist/types/catalog.d.ts.map +1 -0
- package/dist/types/catalog.js +3 -0
- package/dist/types/catalog.js.map +1 -0
- package/dist/types/changes.d.ts +272 -0
- package/dist/types/changes.d.ts.map +1 -0
- package/dist/types/changes.js +3 -0
- package/dist/types/changes.js.map +1 -0
- package/dist/types/common.d.ts +9 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +3 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/host.d.ts +372 -0
- package/dist/types/host.d.ts.map +1 -0
- package/dist/types/host.js +3 -0
- package/dist/types/host.js.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/listen.d.ts +48 -0
- package/dist/types/listen.d.ts.map +1 -0
- package/dist/types/listen.js +3 -0
- package/dist/types/listen.js.map +1 -0
- package/dist/types/paging.d.ts +10 -0
- package/dist/types/paging.d.ts.map +1 -0
- package/dist/types/paging.js +3 -0
- package/dist/types/paging.js.map +1 -0
- package/dist/types/probe.d.ts +46 -0
- package/dist/types/probe.d.ts.map +1 -0
- package/dist/types/probe.js +3 -0
- package/dist/types/probe.js.map +1 -0
- package/dist/types/resources.d.ts +83 -0
- package/dist/types/resources.d.ts.map +1 -0
- package/dist/types/resources.js +3 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/rpc.d.ts +65 -0
- package/dist/types/rpc.d.ts.map +1 -0
- package/dist/types/rpc.js +3 -0
- package/dist/types/rpc.js.map +1 -0
- package/dist/types/session.d.ts +395 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +3 -0
- package/dist/types/session.js.map +1 -0
- package/dist/types/terminals.d.ts +131 -0
- package/dist/types/terminals.d.ts.map +1 -0
- package/dist/types/terminals.js +3 -0
- package/dist/types/terminals.js.map +1 -0
- package/dist/types/wire.d.ts +62 -0
- package/dist/types/wire.d.ts.map +1 -0
- package/dist/types/wire.js +13 -0
- package/dist/types/wire.js.map +1 -0
- package/dist/types/worktrees.d.ts +86 -0
- package/dist/types/worktrees.d.ts.map +1 -0
- package/dist/types/worktrees.js +3 -0
- package/dist/types/worktrees.js.map +1 -0
- package/dist/worktrees.d.ts +27 -0
- package/dist/worktrees.d.ts.map +1 -0
- package/dist/worktrees.js +152 -0
- package/dist/worktrees.js.map +1 -0
- package/package.json +61 -0
- package/src/automations.ts +204 -0
- package/src/catalog.ts +53 -0
- package/src/changes.ts +629 -0
- package/src/cron.ts +251 -0
- package/src/git.ts +132 -0
- package/src/host.ts +5947 -0
- package/src/index.ts +40 -0
- package/src/listen.ts +240 -0
- package/src/paging.ts +56 -0
- package/src/paths.ts +19 -0
- package/src/resources.ts +513 -0
- package/src/rpc.ts +222 -0
- package/src/scheduled.ts +369 -0
- package/src/terminals.ts +353 -0
- package/src/tools.ts +64 -0
- package/src/types/agent.ts +255 -0
- package/src/types/automations.ts +164 -0
- package/src/types/catalog.ts +26 -0
- package/src/types/changes.ts +266 -0
- package/src/types/common.ts +9 -0
- package/src/types/host.ts +386 -0
- package/src/types/index.ts +27 -0
- package/src/types/listen.ts +53 -0
- package/src/types/paging.ts +11 -0
- package/src/types/probe.ts +42 -0
- package/src/types/resources.ts +107 -0
- package/src/types/rpc.ts +68 -0
- package/src/types/session.ts +415 -0
- package/src/types/terminals.ts +133 -0
- package/src/types/wire.ts +69 -0
- package/src/types/worktrees.ts +91 -0
- package/src/worktrees.ts +162 -0
package/src/types/rpc.ts
ADDED
|
@@ -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
|
+
*/
|