@softov/ahpc 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 +296 -0
- package/dist/src/ahp/channels.d.ts +146 -0
- package/dist/src/ahp/channels.js +423 -0
- package/dist/src/ahp/connection.d.ts +656 -0
- package/dist/src/ahp/connection.js +1 -0
- package/dist/src/ahp/fake.d.ts +69 -0
- package/dist/src/ahp/fake.js +1996 -0
- package/dist/src/ahp/live.d.ts +160 -0
- package/dist/src/ahp/live.js +2641 -0
- package/dist/src/ahp/operate.d.ts +36 -0
- package/dist/src/ahp/operate.js +56 -0
- package/dist/src/ahp/publish.d.ts +39 -0
- package/dist/src/ahp/publish.js +370 -0
- package/dist/src/ahp/status.d.ts +23 -0
- package/dist/src/ahp/status.js +23 -0
- package/dist/src/ahp/types.d.ts +745 -0
- package/dist/src/ahp/types.js +27 -0
- package/dist/src/app.d.ts +54 -0
- package/dist/src/app.js +369 -0
- package/dist/src/blocks.d.ts +88 -0
- package/dist/src/blocks.js +102 -0
- package/dist/src/cli/main.d.ts +13 -0
- package/dist/src/cli/main.js +1537 -0
- package/dist/src/cli/render.d.ts +43 -0
- package/dist/src/cli/render.js +91 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +45 -0
- package/dist/src/connect.d.ts +53 -0
- package/dist/src/connect.js +69 -0
- package/dist/src/control.d.ts +189 -0
- package/dist/src/control.js +2241 -0
- package/dist/src/diff.d.ts +44 -0
- package/dist/src/diff.js +111 -0
- package/dist/src/flags.d.ts +28 -0
- package/dist/src/flags.js +76 -0
- package/dist/src/main.d.ts +10 -0
- package/dist/src/main.js +54 -0
- package/dist/src/schedule.d.ts +46 -0
- package/dist/src/schedule.js +140 -0
- package/dist/src/screens.d.ts +79 -0
- package/dist/src/screens.js +1322 -0
- package/dist/src/state.d.ts +355 -0
- package/dist/src/state.js +531 -0
- package/dist/src/terminal.d.ts +44 -0
- package/dist/src/terminal.js +101 -0
- package/dist/src/tui.d.ts +92 -0
- package/dist/src/tui.js +372 -0
- package/dist/src/view/automations.d.ts +31 -0
- package/dist/src/view/automations.js +76 -0
- package/dist/src/view/bood/art.d.ts +90 -0
- package/dist/src/view/bood/art.js +152 -0
- package/dist/src/view/bood/beetle.d.ts +7 -0
- package/dist/src/view/bood/beetle.js +123 -0
- package/dist/src/view/bood/bunny.d.ts +8 -0
- package/dist/src/view/bood/bunny.js +228 -0
- package/dist/src/view/bood/cat.d.ts +9 -0
- package/dist/src/view/bood/cat.js +358 -0
- package/dist/src/view/bood/crab.d.ts +7 -0
- package/dist/src/view/bood/crab.js +121 -0
- package/dist/src/view/bood/index.d.ts +29 -0
- package/dist/src/view/bood/index.js +25 -0
- package/dist/src/view/bood/motion.d.ts +178 -0
- package/dist/src/view/bood/motion.js +298 -0
- package/dist/src/view/bood/owl.d.ts +9 -0
- package/dist/src/view/bood/owl.js +285 -0
- package/dist/src/view/bood/registry.d.ts +36 -0
- package/dist/src/view/bood/registry.js +218 -0
- package/dist/src/view/bood/render.d.ts +31 -0
- package/dist/src/view/bood/render.js +196 -0
- package/dist/src/view/bood/sprite.d.ts +18 -0
- package/dist/src/view/bood/sprite.js +130 -0
- package/dist/src/view/bood/sprout.d.ts +8 -0
- package/dist/src/view/bood/sprout.js +121 -0
- package/dist/src/view/bood/types.d.ts +158 -0
- package/dist/src/view/bood/types.js +57 -0
- package/dist/src/view/bubble.d.ts +75 -0
- package/dist/src/view/bubble.js +86 -0
- package/dist/src/view/changes.d.ts +35 -0
- package/dist/src/view/changes.js +22 -0
- package/dist/src/view/composer.d.ts +64 -0
- package/dist/src/view/composer.js +192 -0
- package/dist/src/view/controls.d.ts +44 -0
- package/dist/src/view/controls.js +49 -0
- package/dist/src/view/creature.d.ts +69 -0
- package/dist/src/view/creature.js +83 -0
- package/dist/src/view/customizations.d.ts +28 -0
- package/dist/src/view/customizations.js +53 -0
- package/dist/src/view/details.d.ts +65 -0
- package/dist/src/view/details.js +65 -0
- package/dist/src/view/filediff.d.ts +29 -0
- package/dist/src/view/filediff.js +24 -0
- package/dist/src/view/files.d.ts +31 -0
- package/dist/src/view/files.js +39 -0
- package/dist/src/view/hitl.d.ts +43 -0
- package/dist/src/view/hitl.js +171 -0
- package/dist/src/view/icons.d.ts +13 -0
- package/dist/src/view/icons.js +71 -0
- package/dist/src/view/picker.d.ts +42 -0
- package/dist/src/view/picker.js +71 -0
- package/dist/src/view/sessionhead.d.ts +41 -0
- package/dist/src/view/sessionhead.js +60 -0
- package/dist/src/view/sessions.d.ts +34 -0
- package/dist/src/view/sessions.js +61 -0
- package/dist/src/view/terminal.d.ts +30 -0
- package/dist/src/view/terminal.js +32 -0
- package/dist/src/view/toolcall.d.ts +27 -0
- package/dist/src/view/toolcall.js +48 -0
- package/dist/src/view/transcript.d.ts +50 -0
- package/dist/src/view/transcript.js +60 -0
- package/package.json +72 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import type { Agent, Answer, Automation, AutomationRun, Changeset, Completion, ContentRef, Customization, FileContent, PendingInput, QueuedMessage, ChangesetOperationTarget, ChangesetScope, ChatSource, ModelSelection, ResourceEntry, SessionConfig, SessionDetail, SessionSummary, SessionUri, TerminalRow, TerminalState, ToolCall, Turn } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a chat client needs from an agent host.
|
|
4
|
+
*
|
|
5
|
+
* A host is a *sessions server*: several clients watch and drive the same
|
|
6
|
+
* sessions and none of them owns the process running the agent. So everything
|
|
7
|
+
* here is either a question about state the host owns, or a fire-and-forget
|
|
8
|
+
* dispatch - there is no local "send and append what I sent". The turn appears
|
|
9
|
+
* when the host has reduced it, which is why the UI re-reads rather than
|
|
10
|
+
* echoing.
|
|
11
|
+
*
|
|
12
|
+
* This is the seam. `fakeHost` implements it with a scripted agent so the
|
|
13
|
+
* example runs, and is checked, with nothing installed; a real client
|
|
14
|
+
* implements the same shape over a WebSocket and changes nothing above it.
|
|
15
|
+
*/
|
|
16
|
+
export interface HostConnection {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly url: string;
|
|
19
|
+
state(): 'connecting' | 'connected' | 'offline';
|
|
20
|
+
/** The catalogue. Deliberately thin - everything else is on the channel. */
|
|
21
|
+
listSessions(): Promise<SessionSummary[]>;
|
|
22
|
+
/** The harnesses the host advertises, and the models each offers. */
|
|
23
|
+
agents(): Promise<Agent[]>;
|
|
24
|
+
/**
|
|
25
|
+
* The configuration schema for a session that does not exist yet.
|
|
26
|
+
*
|
|
27
|
+
* `resolveSessionConfig`, which is the whole reason it is separate from
|
|
28
|
+
* `config`: the permission modes a harness offers have to be offerable
|
|
29
|
+
* *before* anything has been created, and they differ by provider. It is
|
|
30
|
+
* iterative on a real host - an answer can bring new questions, a git
|
|
31
|
+
* workspace is what makes a host offer a worktree - so it takes what has
|
|
32
|
+
* been chosen so far rather than only the provider.
|
|
33
|
+
*/
|
|
34
|
+
resolveConfig(options: {
|
|
35
|
+
provider: string;
|
|
36
|
+
workingDirectory?: string;
|
|
37
|
+
/** What has been chosen so far. The host echoes it back with defaults applied. */
|
|
38
|
+
values?: Record<string, string>;
|
|
39
|
+
}): Promise<SessionConfig>;
|
|
40
|
+
/**
|
|
41
|
+
* Create one.
|
|
42
|
+
*
|
|
43
|
+
* `config` is what the composer's control row was set to. It belongs here
|
|
44
|
+
* rather than in a `setConfig` after the fact: most of what the schema
|
|
45
|
+
* offers is not `sessionMutable`, so a session created without it is a
|
|
46
|
+
* session that can never be given it.
|
|
47
|
+
*/
|
|
48
|
+
createSession(options: {
|
|
49
|
+
provider: string;
|
|
50
|
+
workingDirectory?: string;
|
|
51
|
+
config?: Record<string, string>;
|
|
52
|
+
}): Promise<SessionUri>;
|
|
53
|
+
disposeSession(uri: SessionUri): Promise<void>;
|
|
54
|
+
setArchived(uri: SessionUri, archived: boolean): void;
|
|
55
|
+
/**
|
|
56
|
+
* Mark read, or put the bold back.
|
|
57
|
+
*
|
|
58
|
+
* A client flag, not activity: `IsRead` says a person has looked since the
|
|
59
|
+
* last change, and the host tells every other client that one of them has.
|
|
60
|
+
*/
|
|
61
|
+
setRead(uri: SessionUri, read: boolean): void;
|
|
62
|
+
/**
|
|
63
|
+
* Subscribe to a session: a snapshot, then an ordered stream of what happens.
|
|
64
|
+
*
|
|
65
|
+
* Closing the returned handle drops *this consumer*, and a second subscribe
|
|
66
|
+
* on a channel already being drained must not unsubscribe it - that is
|
|
67
|
+
* channel-wide, and silently kills the stream everything else is reading.
|
|
68
|
+
*/
|
|
69
|
+
subscribe(uri: SessionUri, observer: (event: HostEvent) => void, chat?: string): {
|
|
70
|
+
close(): void;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Pull the page of history that sits before the turns already loaded.
|
|
74
|
+
*
|
|
75
|
+
* The turns do not come back from this. A host inserts them into the chat's
|
|
76
|
+
* own state and dispatches `chat/turnsLoaded` before it answers, so anything
|
|
77
|
+
* subscribed to that chat sees them arrive the way it sees everything else -
|
|
78
|
+
* which is why there is no return value carrying turns and no second path
|
|
79
|
+
* from a turn to the screen.
|
|
80
|
+
*
|
|
81
|
+
* What comes back is whether there is still more behind it, so a caller that
|
|
82
|
+
* wants the whole conversation can ask again and one that wants a screenful
|
|
83
|
+
* can stop.
|
|
84
|
+
*
|
|
85
|
+
* How much history a snapshot arrives with is the host's business and the
|
|
86
|
+
* two that exist disagree: one sends a tail window, and one sends none at
|
|
87
|
+
* all and expects to be asked. So this is not only how a long conversation
|
|
88
|
+
* is read to its beginning - against some hosts it is how it is read at all.
|
|
89
|
+
*/
|
|
90
|
+
loadOlderTurns(uri: SessionUri, chat?: string): Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Open a second conversation in the same session.
|
|
93
|
+
*
|
|
94
|
+
* A chat belongs to a session, and a session may hold several - the session
|
|
95
|
+
* is a container, not the conversation. The URI is chosen here, as a
|
|
96
|
+
* session's is, so it can be subscribed to without a round trip in between.
|
|
97
|
+
*
|
|
98
|
+
* Only where the agent advertises it: a host that does not is one where
|
|
99
|
+
* `createChat` MUST NOT be called at all.
|
|
100
|
+
*/
|
|
101
|
+
createChat(uri: SessionUri, first?: string, source?: ChatSource): Promise<string>;
|
|
102
|
+
/**
|
|
103
|
+
* The terminals the host is running.
|
|
104
|
+
*
|
|
105
|
+
* The host's rather than a session's: one outlives the turn that opened it,
|
|
106
|
+
* several clients watch one, and the protocol lists them on the root
|
|
107
|
+
* channel - which is where something owned by no session belongs.
|
|
108
|
+
*/
|
|
109
|
+
terminals(): Promise<TerminalRow[]>;
|
|
110
|
+
/** Open one. The URI is chosen here, so it can be watched without a round trip. */
|
|
111
|
+
createTerminal(options?: {
|
|
112
|
+
cwd?: string;
|
|
113
|
+
name?: string;
|
|
114
|
+
}): Promise<string>;
|
|
115
|
+
/** Kill it and let go. */
|
|
116
|
+
disposeTerminal(uri: string): Promise<void>;
|
|
117
|
+
/** Watch one: its state, and then everything that happens to it. */
|
|
118
|
+
watchTerminal(uri: string, observer: (state: TerminalState) => void): {
|
|
119
|
+
close(): void;
|
|
120
|
+
};
|
|
121
|
+
/** Send input. Nothing comes back but what the shell says. */
|
|
122
|
+
writeTerminal(uri: string, data: string): void;
|
|
123
|
+
/**
|
|
124
|
+
* Tell the host how big this client is drawing the terminal.
|
|
125
|
+
*
|
|
126
|
+
* `terminal-channel.md` lists `terminal/resized` among the client-dispatched
|
|
127
|
+
* actions and its reducer sets `cols` and `rows`. A host never told wraps
|
|
128
|
+
* its output at a width nobody chose.
|
|
129
|
+
*/
|
|
130
|
+
resizeTerminal(uri: string, cols: number, rows: number): void;
|
|
131
|
+
/** Empty the scrollback. `terminal/cleared` resets `content` to nothing. */
|
|
132
|
+
clearTerminal(uri: string): void;
|
|
133
|
+
/** Rename it. `terminal/titleChanged` sets `title`. */
|
|
134
|
+
renameTerminal(uri: string, title: string): void;
|
|
135
|
+
/**
|
|
136
|
+
* Take it for this client.
|
|
137
|
+
*
|
|
138
|
+
* `TerminalClaimedAction.claim` is required and is a `TerminalClaim` - a
|
|
139
|
+
* client claim carrying this connection's `clientId`, or a session claim.
|
|
140
|
+
* There is no release: the protocol declares no action for giving one up,
|
|
141
|
+
* so this client takes and never pretends to hand back.
|
|
142
|
+
*/
|
|
143
|
+
claimTerminal(uri: string): void;
|
|
144
|
+
/**
|
|
145
|
+
* What the host offers to complete what is being typed.
|
|
146
|
+
*
|
|
147
|
+
* The host's question, not this client's: a path is a path on *its*
|
|
148
|
+
* filesystem, and a skill is one it contributed. Asked with the whole draft
|
|
149
|
+
* and where the caret is, because what is being completed depends on the
|
|
150
|
+
* word the caret is in - an at-sign mid-word is an address and a slash
|
|
151
|
+
* mid-sentence is a path.
|
|
152
|
+
*
|
|
153
|
+
* Nothing is a real answer: a host that completes neither is one whose
|
|
154
|
+
* composer offers no menu, which is what it did before either was served.
|
|
155
|
+
*/
|
|
156
|
+
completions(options: {
|
|
157
|
+
channel: string;
|
|
158
|
+
text: string;
|
|
159
|
+
offset?: number;
|
|
160
|
+
}): Promise<Completion[]>;
|
|
161
|
+
/**
|
|
162
|
+
* Send one action verbatim, without this client knowing what it means.
|
|
163
|
+
*
|
|
164
|
+
* The escape hatch, and optional because only a real host has one: the
|
|
165
|
+
* protocol has far more client-dispatchable actions than a chat client has
|
|
166
|
+
* controls for, and a way to send an arbitrary one is what makes the rest of
|
|
167
|
+
* them testable at all. `chat` targets the chat channel rather than the
|
|
168
|
+
* session's.
|
|
169
|
+
*/
|
|
170
|
+
dispatch?(uri: SessionUri, action: Record<string, unknown>, chat?: boolean): void;
|
|
171
|
+
/**
|
|
172
|
+
* One directory of the host's filesystem, as far as it lets this client see.
|
|
173
|
+
*
|
|
174
|
+
* Optional, because a host may serve none: `createHost` takes its filesystem
|
|
175
|
+
* as a port, and one given none answers `-32601`. Absent here means the same
|
|
176
|
+
* thing a layer up - there is nothing to browse, rather than nothing there.
|
|
177
|
+
*/
|
|
178
|
+
resourceList?(uri: string): Promise<ResourceEntry[]>;
|
|
179
|
+
/**
|
|
180
|
+
* One file's bytes, by `file://` URI on the *host's* machine.
|
|
181
|
+
*
|
|
182
|
+
* `encoding` is reported rather than assumed, because a host serves whatever
|
|
183
|
+
* is on its disk: a caller that treated every answer as text would print a
|
|
184
|
+
* PNG to a terminal.
|
|
185
|
+
*/
|
|
186
|
+
resourceRead?(uri: string): Promise<{
|
|
187
|
+
data: string;
|
|
188
|
+
encoding: string;
|
|
189
|
+
contentType?: string;
|
|
190
|
+
}>;
|
|
191
|
+
/**
|
|
192
|
+
* What the host knows about one path without reading it.
|
|
193
|
+
*
|
|
194
|
+
* `type` is the host's own `ResourceType` rather than a boolean: a symlink
|
|
195
|
+
* is neither a file nor a directory, and narrowing it here would be this
|
|
196
|
+
* client deciding something the host already answered.
|
|
197
|
+
*/
|
|
198
|
+
resourceResolve?(uri: string): Promise<{
|
|
199
|
+
uri: string;
|
|
200
|
+
type: string;
|
|
201
|
+
size?: number;
|
|
202
|
+
mtime?: string;
|
|
203
|
+
/**
|
|
204
|
+
* An opaque version token, where the host keeps one.
|
|
205
|
+
*
|
|
206
|
+
* Carried back as `ifMatch` on a write: the host MUST fail with
|
|
207
|
+
* `-32011 Conflict` when its copy has moved on, which is what stops a
|
|
208
|
+
* read-modify-write losing whatever changed in between. Absent for a
|
|
209
|
+
* directory, which has no bytes to have been changed under anyone.
|
|
210
|
+
*/
|
|
211
|
+
etag?: string;
|
|
212
|
+
}>;
|
|
213
|
+
/**
|
|
214
|
+
* Write one file.
|
|
215
|
+
*
|
|
216
|
+
* The whole `resource*` family is symmetrical and optional: a host that
|
|
217
|
+
* serves no filesystem answers `-32601`, which is why these are optional
|
|
218
|
+
* here too. `-32009` is a refusal naming the grant that would lift it, and
|
|
219
|
+
* `createOnly` is the protocol's guard against replacing something that is
|
|
220
|
+
* already there.
|
|
221
|
+
*/
|
|
222
|
+
/**
|
|
223
|
+
* Write one file.
|
|
224
|
+
*
|
|
225
|
+
* `ifMatch` is the etag a `resourceResolve` returned, and is the guard on a
|
|
226
|
+
* read-modify-write: without it, a write lands on whatever is there now.
|
|
227
|
+
* `createOnly` is a different guarantee and not a substitute - it refuses a
|
|
228
|
+
* file that has appeared, not one that moved between the resolve and the
|
|
229
|
+
* write.
|
|
230
|
+
*/
|
|
231
|
+
resourceWrite?(uri: string, data: string, options?: {
|
|
232
|
+
encoding?: string;
|
|
233
|
+
createOnly?: boolean;
|
|
234
|
+
ifMatch?: string;
|
|
235
|
+
}): Promise<void>;
|
|
236
|
+
resourceDelete?(uri: string, options?: {
|
|
237
|
+
recursive?: boolean;
|
|
238
|
+
}): Promise<void>;
|
|
239
|
+
resourceMkdir?(uri: string): Promise<void>;
|
|
240
|
+
resourceMove?(from: string, to: string, options?: {
|
|
241
|
+
failIfExists?: boolean;
|
|
242
|
+
}): Promise<void>;
|
|
243
|
+
resourceCopy?(from: string, to: string, options?: {
|
|
244
|
+
failIfExists?: boolean;
|
|
245
|
+
}): Promise<void>;
|
|
246
|
+
/**
|
|
247
|
+
* Be told when something under a path changes, instead of asking again.
|
|
248
|
+
*
|
|
249
|
+
* `resource-watch-channel.md`: the receiver allocates the channel URI and it
|
|
250
|
+
* is opaque; there is no dispose command, and the receiver MUST release the
|
|
251
|
+
* watcher once every subscriber has unsubscribed - so letting go of the
|
|
252
|
+
* returned handle is the whole of closing one. Creating one goes through the
|
|
253
|
+
* same permission flow as the rest of the family, so a refusal is `-32009`
|
|
254
|
+
* naming the grant that would lift it.
|
|
255
|
+
*/
|
|
256
|
+
watchResource?(uri: string, observer: (changes: {
|
|
257
|
+
uri: string;
|
|
258
|
+
kind: string;
|
|
259
|
+
}[]) => void, options?: {
|
|
260
|
+
recursive?: boolean;
|
|
261
|
+
}): Promise<{
|
|
262
|
+
close(): void;
|
|
263
|
+
}>;
|
|
264
|
+
/**
|
|
265
|
+
* Push a bearer token for one of the host's protected resources.
|
|
266
|
+
*
|
|
267
|
+
* `authentication.md`: `resource` MUST match one the server advertised -
|
|
268
|
+
* statically in `AgentInfo.protectedResources` or through a live MCP
|
|
269
|
+
* challenge. `expiresIn` MUST be a positive integer and MUST have elapsed
|
|
270
|
+
* time subtracted from it if the token response was held onto; it is
|
|
271
|
+
* omitted where the expiry is unknown. An empty token revokes.
|
|
272
|
+
*/
|
|
273
|
+
authenticate?(resource: string, token: string, options?: {
|
|
274
|
+
scopes?: string[];
|
|
275
|
+
expiresIn?: number;
|
|
276
|
+
}): Promise<void>;
|
|
277
|
+
/**
|
|
278
|
+
* Ask the host for a config property's values.
|
|
279
|
+
*
|
|
280
|
+
* For a property whose schema says `enumDynamic`: the `enum` on it is not
|
|
281
|
+
* the answer and the host has to be queried. Optional, because a host that
|
|
282
|
+
* marks nothing dynamic never needs it.
|
|
283
|
+
*/
|
|
284
|
+
configCompletions?(request: {
|
|
285
|
+
property: string;
|
|
286
|
+
provider?: string;
|
|
287
|
+
workingDirectory?: string;
|
|
288
|
+
values?: Record<string, string>;
|
|
289
|
+
query?: string;
|
|
290
|
+
}): Promise<{
|
|
291
|
+
value: string;
|
|
292
|
+
label: string;
|
|
293
|
+
description?: string;
|
|
294
|
+
}[]>;
|
|
295
|
+
/**
|
|
296
|
+
* The protected resources this host says it has, from `AgentInfo`.
|
|
297
|
+
*
|
|
298
|
+
* A `resource` this client invents is one the host MUST refuse, so this is
|
|
299
|
+
* what any token push has to name.
|
|
300
|
+
*/
|
|
301
|
+
protectedResources?(): Promise<{
|
|
302
|
+
resource: string;
|
|
303
|
+
name?: string;
|
|
304
|
+
scopes?: string[];
|
|
305
|
+
}[]>;
|
|
306
|
+
/** Close one. The last chat in a session is the session; dispose that instead. */
|
|
307
|
+
disposeChat(chat: string): Promise<void>;
|
|
308
|
+
/**
|
|
309
|
+
* The catalogue moved: a session appeared, finished, or is now waiting.
|
|
310
|
+
*
|
|
311
|
+
* Separate from `subscribe`, which is one session's channel and says nothing
|
|
312
|
+
* about the ninety-nine a client is not watching. Without this the only way
|
|
313
|
+
* a list gets fresh is somebody navigating away and back, which is a reader
|
|
314
|
+
* doing by hand what the host already said.
|
|
315
|
+
*
|
|
316
|
+
* It carries no payload on purpose. The host owns the catalogue and
|
|
317
|
+
* `listSessions` is how you read it; an event that carried a row would be a
|
|
318
|
+
* second, staler source of the same answer.
|
|
319
|
+
*/
|
|
320
|
+
onSessions(observer: () => void): {
|
|
321
|
+
close(): void;
|
|
322
|
+
};
|
|
323
|
+
/** Begin a turn. Any turn - this is not only how the first one starts. */
|
|
324
|
+
/**
|
|
325
|
+
* Put the message being composed where other clients can see it.
|
|
326
|
+
*
|
|
327
|
+
* `chat-channel.md`: clients MAY sync their input into `ChatState.draft` so
|
|
328
|
+
* it survives a reload and is visible to other clients on the same chat,
|
|
329
|
+
* SHOULD debounce rather than sync eagerly, and the host clears it when the
|
|
330
|
+
* message is sent. An empty string is the clear.
|
|
331
|
+
*/
|
|
332
|
+
setDraft(uri: SessionUri, text: string): void;
|
|
333
|
+
say(uri: SessionUri, text: string, model?: ModelSelection): void;
|
|
334
|
+
stopTurn(uri: SessionUri): void;
|
|
335
|
+
/**
|
|
336
|
+
* Say it *after* the turn that is running.
|
|
337
|
+
*
|
|
338
|
+
* Not `say` with a wait in front of it. The queue is the host's - it starts
|
|
339
|
+
* the next turn from the head the moment it goes idle, and every client
|
|
340
|
+
* watching the chat sees the same list - so a client that held the message
|
|
341
|
+
* itself would be the only thing that could ever send it, and would not,
|
|
342
|
+
* because nothing in a client is watching for the turn to end.
|
|
343
|
+
*/
|
|
344
|
+
queue(uri: SessionUri, text: string, model?: ModelSelection): void;
|
|
345
|
+
/** Take one back, while it is still waiting. */
|
|
346
|
+
unqueue(uri: SessionUri, id: string): void;
|
|
347
|
+
/** Answer a tool confirmation. */
|
|
348
|
+
confirmToolCall(uri: SessionUri, toolCallId: string, approved: boolean, optionId?: string): void;
|
|
349
|
+
/** Answer a question. An accept with no answers resumes the agent on none. */
|
|
350
|
+
completeInput(uri: SessionUri, requestId: string, accepted: boolean, answers: Record<string, Answer>): void;
|
|
351
|
+
/**
|
|
352
|
+
* Which changesets this session offers.
|
|
353
|
+
*
|
|
354
|
+
* The protocol has a session advertise several - what the conversation
|
|
355
|
+
* changed, what one turn changed, what the working tree has - and a client
|
|
356
|
+
* that reads only the first shows one of them and hides the rest.
|
|
357
|
+
*/
|
|
358
|
+
changesets?(uri: SessionUri): Promise<ChangesetScope[]>;
|
|
359
|
+
/**
|
|
360
|
+
* One of them, by the URI its template became.
|
|
361
|
+
*
|
|
362
|
+
* Left out, the first that needs no filling in - which is what a screen
|
|
363
|
+
* showing a single changeset wants and what this answered before there was
|
|
364
|
+
* any way to ask for another.
|
|
365
|
+
*/
|
|
366
|
+
changes(uri: SessionUri, uri_?: string): Promise<Changeset>;
|
|
367
|
+
/**
|
|
368
|
+
* Mark files in a changeset reviewed, or clear them.
|
|
369
|
+
*
|
|
370
|
+
* Optional, and only where the changeset's catalogue entry says it is
|
|
371
|
+
* reviewable. Sent to the changeset's own channel, not the session's.
|
|
372
|
+
*/
|
|
373
|
+
review?(changeset: string, files: string[], reviewed: boolean): void;
|
|
374
|
+
/**
|
|
375
|
+
* Run one of the verbs a changeset advertised.
|
|
376
|
+
*
|
|
377
|
+
* Not fire-and-forget, unlike most of what a client sends: the host answers
|
|
378
|
+
* whether it accepted, and refuses out loud - so a button that failed can
|
|
379
|
+
* say why instead of looking like one that did nothing. What the operation
|
|
380
|
+
* *did* still arrives on the changeset's channel, because every other client
|
|
381
|
+
* has to see it too.
|
|
382
|
+
*
|
|
383
|
+
* `target` is omitted for a changeset-scoped operation and required for the
|
|
384
|
+
* others. A host refuses an `operationId` it did not advertise, which is why
|
|
385
|
+
* this takes one rather than an enum.
|
|
386
|
+
*/
|
|
387
|
+
invoke?(changeset: string, operationId: string, target?: ChangesetOperationTarget): Promise<{
|
|
388
|
+
message?: string;
|
|
389
|
+
}>;
|
|
390
|
+
/**
|
|
391
|
+
* Ask to be allowed to write something.
|
|
392
|
+
*
|
|
393
|
+
* The other half of `invoke`, and the reason a refused operation is not a
|
|
394
|
+
* dead end: a host that will not run a write refuses with a payload naming
|
|
395
|
+
* the request that would unlock it, and this is what sends that request. A
|
|
396
|
+
* client that only knew how to press the button would show one that fails
|
|
397
|
+
* and cannot explain itself.
|
|
398
|
+
*/
|
|
399
|
+
requestResource?(uri: string, access: {
|
|
400
|
+
read?: boolean;
|
|
401
|
+
write?: boolean;
|
|
402
|
+
}): Promise<void>;
|
|
403
|
+
/**
|
|
404
|
+
* One file out of a changeset, fetched.
|
|
405
|
+
*
|
|
406
|
+
* Separate from `changes` on purpose: a changeset is a list of rows and this
|
|
407
|
+
* is one file's worth of bytes, and a client that returned both together
|
|
408
|
+
* would download a session's entire diff to draw a list of names. Nothing
|
|
409
|
+
* calls this until somebody opens a row.
|
|
410
|
+
*/
|
|
411
|
+
content(ref: ContentRef): Promise<FileContent>;
|
|
412
|
+
/**
|
|
413
|
+
* What this session was given: plugins, directories, skills, MCP servers.
|
|
414
|
+
*
|
|
415
|
+
* Read from the session channel rather than the catalogue, because it is
|
|
416
|
+
* per-session - two sessions on the same host, in different directories,
|
|
417
|
+
* are handed different skills. Flattened on the way out; see
|
|
418
|
+
* `Customization`.
|
|
419
|
+
*/
|
|
420
|
+
customizations(uri: SessionUri): Promise<Customization[]>;
|
|
421
|
+
/**
|
|
422
|
+
* What a slash offers before any session exists.
|
|
423
|
+
*
|
|
424
|
+
* Separate from `customizations` because that one takes a session, and on
|
|
425
|
+
* the new-session screen there is not one yet - which is exactly when
|
|
426
|
+
* somebody wants to open with a skill. The host knows what its harness
|
|
427
|
+
* contributes without having been asked to run anything, so this is a
|
|
428
|
+
* question it can answer, and the only one that can: a client cannot derive
|
|
429
|
+
* it from a catalogue of sessions it is not opening.
|
|
430
|
+
*
|
|
431
|
+
* Answering with nothing is a real answer, and the one to give for a
|
|
432
|
+
* harness nobody has signed into.
|
|
433
|
+
*/
|
|
434
|
+
harnessCommands(): Promise<Customization[]>;
|
|
435
|
+
/**
|
|
436
|
+
* Turn one on or off, by id.
|
|
437
|
+
*
|
|
438
|
+
* Fire-and-forget like the rest of the dispatches: the host decides, tells
|
|
439
|
+
* every client watching, and what comes back is the customization list
|
|
440
|
+
* having changed - not a return value here.
|
|
441
|
+
*/
|
|
442
|
+
setCustomizationEnabled(uri: SessionUri, id: string, enabled: boolean): void;
|
|
443
|
+
/**
|
|
444
|
+
* The session channel's own state: its chat, its lifecycle, its settings.
|
|
445
|
+
*
|
|
446
|
+
* Separate from `listSessions` because a summary is what a *row* needs and
|
|
447
|
+
* this is what a reader needs - asking every session about itself to draw a
|
|
448
|
+
* catalogue is a round trip per row.
|
|
449
|
+
*/
|
|
450
|
+
detail(uri: SessionUri): Promise<SessionDetail>;
|
|
451
|
+
/** What this session can be told to do differently. */
|
|
452
|
+
config(uri: SessionUri): Promise<SessionConfig>;
|
|
453
|
+
/**
|
|
454
|
+
* Change one key.
|
|
455
|
+
*
|
|
456
|
+
* The action merges into `config.values`, so sending the whole object writes
|
|
457
|
+
* back everything this client happened to be holding - including a value
|
|
458
|
+
* another client changed while the page had it on screen.
|
|
459
|
+
*/
|
|
460
|
+
setConfig(uri: SessionUri, key: string, value: string): void;
|
|
461
|
+
/**
|
|
462
|
+
* Let go of whatever the connection is holding.
|
|
463
|
+
*
|
|
464
|
+
* Optional because not every host holds anything: the scripted one is a
|
|
465
|
+
* pile of objects and ends when the process does. A socket and a
|
|
466
|
+
* subprocess are the opposite - they keep the event loop alive on their
|
|
467
|
+
* own, so a program that has finished drawing and returned from `main`
|
|
468
|
+
* still does not exit. That is what this is for, and the reason it is on
|
|
469
|
+
* the seam rather than only on the implementations that need it: the caller
|
|
470
|
+
* cannot know which kind it was handed.
|
|
471
|
+
*/
|
|
472
|
+
/**
|
|
473
|
+
* Wait for what has been dispatched to have actually left.
|
|
474
|
+
*
|
|
475
|
+
* For a caller that sends one thing and exits. Everything here is
|
|
476
|
+
* fire-and-forget, and one of those is still asynchronous on the way out -
|
|
477
|
+
* closing the connection in the same breath closes it first, and the
|
|
478
|
+
* dispatch is never sent at all.
|
|
479
|
+
*/
|
|
480
|
+
/**
|
|
481
|
+
* Every automation this host holds.
|
|
482
|
+
*
|
|
483
|
+
* Optional, and the absence is the answer: a host that serves no automations
|
|
484
|
+
* channel answers `-32601`, and a client that drew an empty list for it
|
|
485
|
+
* would be claiming the host has none rather than that it has no such thing.
|
|
486
|
+
*/
|
|
487
|
+
automations?(): Promise<Automation[]>;
|
|
488
|
+
/**
|
|
489
|
+
* Told when one moves, so the screen is not polled.
|
|
490
|
+
*
|
|
491
|
+
* The interesting change is the one nobody made: an automation firing at
|
|
492
|
+
* nine in the morning arrives here and nowhere else.
|
|
493
|
+
*/
|
|
494
|
+
onAutomations?(observer: () => void): {
|
|
495
|
+
close(): void;
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* Write a new one, and answer with the URI it was given.
|
|
499
|
+
*
|
|
500
|
+
* The client picks the URI, as it does for a session and a chat, so the
|
|
501
|
+
* thing is addressable before the host has answered. `definition` is the
|
|
502
|
+
* protocol's own shape and is passed through rather than modelled here -
|
|
503
|
+
* what this client fills in is a subset, and a host may hold keys it never
|
|
504
|
+
* wrote.
|
|
505
|
+
*/
|
|
506
|
+
createAutomation?(definition: Record<string, unknown>): Promise<string>;
|
|
507
|
+
/** Start one now, whatever its schedule says. */
|
|
508
|
+
runAutomation?(uri: string): Promise<void>;
|
|
509
|
+
/** Switch one on or off, which is a patch of its definition. */
|
|
510
|
+
setAutomationEnabled?(uri: string, enabled: boolean): Promise<void>;
|
|
511
|
+
/** Forget one, and everything it has done. */
|
|
512
|
+
removeAutomation?(uri: string): Promise<void>;
|
|
513
|
+
/**
|
|
514
|
+
* Which triggers this host has, so one can be offered.
|
|
515
|
+
*
|
|
516
|
+
* Without asking, only a schedule can be authored: an event trigger is not
|
|
517
|
+
* offerable because nothing here knows what events exist. The kinds are the
|
|
518
|
+
* host's, not a list kept here.
|
|
519
|
+
*/
|
|
520
|
+
automationTriggers?(): Promise<{
|
|
521
|
+
kind: string;
|
|
522
|
+
title?: string;
|
|
523
|
+
description?: string;
|
|
524
|
+
}[]>;
|
|
525
|
+
/**
|
|
526
|
+
* Follow the host's own log.
|
|
527
|
+
*
|
|
528
|
+
* Absent where the host advertised no `telemetry.logs`, which the
|
|
529
|
+
* specification says is how a host that emits none says so. `level` expands
|
|
530
|
+
* the `{level}` template variable, the only one defined.
|
|
531
|
+
*/
|
|
532
|
+
watchLogs?(observer: (record: {
|
|
533
|
+
at?: string;
|
|
534
|
+
severity?: string;
|
|
535
|
+
body: string;
|
|
536
|
+
attributes: Record<string, string>;
|
|
537
|
+
}) => void, options?: {
|
|
538
|
+
level?: string;
|
|
539
|
+
}): Promise<{
|
|
540
|
+
close(): void;
|
|
541
|
+
}>;
|
|
542
|
+
/** A page of one automation's run history, oldest last, with the host's cursor. */
|
|
543
|
+
automationRuns?(uri: string, cursor?: string): Promise<{
|
|
544
|
+
runs: AutomationRun[];
|
|
545
|
+
nextCursor?: string;
|
|
546
|
+
}>;
|
|
547
|
+
flush?(): Promise<void>;
|
|
548
|
+
close?(): void | Promise<void>;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* What the host says happened.
|
|
552
|
+
*
|
|
553
|
+
* Named after the actions rather than after what a screen does with them: the
|
|
554
|
+
* host is describing its own state changing, and a client that renamed
|
|
555
|
+
* `chat/delta` to `appendToBubble` would have written the UI into the wire.
|
|
556
|
+
*/
|
|
557
|
+
export type HostEvent = {
|
|
558
|
+
type: 'snapshot';
|
|
559
|
+
turns: Turn[];
|
|
560
|
+
active?: Turn;
|
|
561
|
+
input?: PendingInput;
|
|
562
|
+
status: number;
|
|
563
|
+
queued: QueuedMessage[];
|
|
564
|
+
/**
|
|
565
|
+
* The message being composed, as the host is holding it.
|
|
566
|
+
*
|
|
567
|
+
* `ChatState.draft` is shared: another client typing into this chat is
|
|
568
|
+
* visible here, and a draft survives this client being restarted. Present
|
|
569
|
+
* on every snapshot so a screen can take it when it opens; empty is a real
|
|
570
|
+
* answer, meaning the host holds no draft.
|
|
571
|
+
*/
|
|
572
|
+
draft: string;
|
|
573
|
+
} | {
|
|
574
|
+
type: 'turnStarted';
|
|
575
|
+
turn: Turn;
|
|
576
|
+
} | {
|
|
577
|
+
type: 'delta';
|
|
578
|
+
partId: string;
|
|
579
|
+
kind: 'markdown' | 'reasoning';
|
|
580
|
+
text: string;
|
|
581
|
+
} | {
|
|
582
|
+
type: 'toolCall';
|
|
583
|
+
call: ToolCall;
|
|
584
|
+
} | {
|
|
585
|
+
type: 'inputNeeded';
|
|
586
|
+
input: PendingInput;
|
|
587
|
+
} | {
|
|
588
|
+
type: 'inputResolved';
|
|
589
|
+
} | {
|
|
590
|
+
type: 'turnComplete';
|
|
591
|
+
turn: Turn;
|
|
592
|
+
}
|
|
593
|
+
/** The whole queue, as the host now has it. */
|
|
594
|
+
| {
|
|
595
|
+
type: 'queued';
|
|
596
|
+
messages: QueuedMessage[];
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* The session's chats, as the host now has them.
|
|
600
|
+
*
|
|
601
|
+
* A session is a container and its chats come and go on their own - one
|
|
602
|
+
* opened from another client, one closed - so this is separate from the
|
|
603
|
+
* chat snapshot, which is about the conversation being watched.
|
|
604
|
+
*/
|
|
605
|
+
| {
|
|
606
|
+
type: 'chats';
|
|
607
|
+
items: {
|
|
608
|
+
resource: string;
|
|
609
|
+
title: string;
|
|
610
|
+
}[];
|
|
611
|
+
defaultChat: string;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* The session's skills, prompts and MCP servers, as the host now has them.
|
|
615
|
+
*
|
|
616
|
+
* Sent when the session channel says they changed - a server signing in, a
|
|
617
|
+
* switch answered, a plugin arriving late. Without it the panel shows what
|
|
618
|
+
* was true when it was opened, so a toggle that worked looked like one that
|
|
619
|
+
* did nothing.
|
|
620
|
+
*/
|
|
621
|
+
| {
|
|
622
|
+
type: 'customizations';
|
|
623
|
+
items: Customization[];
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Who else is in this session, as the host has it.
|
|
627
|
+
*
|
|
628
|
+
* `SessionState.activeClients` is host-kept membership: a client adds itself
|
|
629
|
+
* with `session/activeClientSet` and the host removes it when the last
|
|
630
|
+
* subscription goes. Two people on one session is the case this exists for.
|
|
631
|
+
*/
|
|
632
|
+
| {
|
|
633
|
+
type: 'present';
|
|
634
|
+
clients: {
|
|
635
|
+
clientId: string;
|
|
636
|
+
displayName?: string;
|
|
637
|
+
}[];
|
|
638
|
+
} | {
|
|
639
|
+
type: 'status';
|
|
640
|
+
status: number;
|
|
641
|
+
} | {
|
|
642
|
+
type: 'changes';
|
|
643
|
+
changes: Changeset;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The host answered, and the answer was no.
|
|
647
|
+
*
|
|
648
|
+
* Not the same as the connection dropping, and worth its own event for that
|
|
649
|
+
* reason: a session whose agent has gone is refused for ever while the host
|
|
650
|
+
* is perfectly well, and a client that reported that as "offline" would send
|
|
651
|
+
* somebody to check their network.
|
|
652
|
+
*/
|
|
653
|
+
| {
|
|
654
|
+
type: 'error';
|
|
655
|
+
message: string;
|
|
656
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|