@volter/supercode-frontend-vscode 0.1.58 → 0.1.61
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/NOTICE.md +1 -1
- package/README.md +112 -10
- package/dist/chat-setup.js +3 -2
- package/dist/extension.js +248 -44
- package/dist/host-controls.d.ts +15 -0
- package/dist/host-controls.js +12 -1
- package/dist/native-session.d.ts +2 -2
- package/dist/native-session.js +10 -1
- package/dist/session.d.ts +9 -0
- package/dist/session.js +134 -22
- package/dist/setup-flow.js +10 -2
- package/dist/view.d.ts +4 -2
- package/dist/view.js +34 -10
- package/dist/workbench-tools.d.ts +13 -0
- package/dist/workbench-tools.js +81 -0
- package/media/anthropic.svg +1 -1
- package/media/google.svg +1 -1
- package/media/openai.svg +1 -1
- package/media/xai.svg +1 -0
- package/package.json +22 -3
- package/vendor/supercode-frontend/README.md +27 -0
- package/vendor/supercode-frontend/client.d.mts +7 -1
- package/vendor/supercode-frontend/client.mjs +85 -11
- package/vendor/supercode-frontend/contract.json +7 -2
- package/vendor/supercode-frontend/generated-client.d.mts +1 -1
- package/vendor/supercode-frontend/generated-client.mjs +1 -1
- package/vendor/supercode-frontend/generated.d.mts +7 -0
- package/vendor/supercode-frontend/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workbench's tools, for the harness agents in its Chat (ADR 0255).
|
|
3
|
+
*
|
|
4
|
+
* The host names the tools and where they run (`HostState.workbenchTools`: a tag, a socket and a token), and lists
|
|
5
|
+
* them to its runtimes itself, through `supercode mcp workbench`. That relay calls them here: this serves, in the
|
|
6
|
+
* extension host where `vscode.lm` exists, one call of a tool `vscode.lm.tools` lists with the tag, through
|
|
7
|
+
* Code-OSS's own tool service. With no endpoint it serves nothing.
|
|
8
|
+
*
|
|
9
|
+
* The wire is one JSON line each way per connection: `{token, name, input}` → `{text, isError}`.
|
|
10
|
+
*/
|
|
11
|
+
import { rmSync } from 'node:fs';
|
|
12
|
+
import { createServer } from 'node:net';
|
|
13
|
+
import * as vscode from 'vscode';
|
|
14
|
+
export class WorkbenchTools {
|
|
15
|
+
#served;
|
|
16
|
+
#applying = Promise.resolve();
|
|
17
|
+
/** Serve on the endpoint the host names, or nothing when it names none. A changed endpoint replaces the server.
|
|
18
|
+
* Calls apply in order, so two quick state replies cannot race a close against a listen. */
|
|
19
|
+
serve(endpoint) {
|
|
20
|
+
const applying = this.#applying.then(() => this.apply(endpoint));
|
|
21
|
+
this.#applying = applying.catch(() => undefined);
|
|
22
|
+
return applying;
|
|
23
|
+
}
|
|
24
|
+
async apply(endpoint) {
|
|
25
|
+
const current = this.#served?.endpoint;
|
|
26
|
+
if (current?.tag === endpoint?.tag && current?.socket === endpoint?.socket && current?.token === endpoint?.token)
|
|
27
|
+
return;
|
|
28
|
+
this.close();
|
|
29
|
+
if (!endpoint)
|
|
30
|
+
return;
|
|
31
|
+
// A socket left by an earlier extension host of this window is this window's own.
|
|
32
|
+
if (process.platform !== 'win32')
|
|
33
|
+
rmSync(endpoint.socket, { force: true });
|
|
34
|
+
const server = createServer(socket => this.answer(socket, endpoint));
|
|
35
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(endpoint.socket, resolve); });
|
|
36
|
+
this.#served = { endpoint, server };
|
|
37
|
+
}
|
|
38
|
+
answer(socket, endpoint) {
|
|
39
|
+
let buffer = '';
|
|
40
|
+
socket.setEncoding('utf8');
|
|
41
|
+
socket.on('data', chunk => {
|
|
42
|
+
buffer += chunk;
|
|
43
|
+
const end = buffer.indexOf('\n');
|
|
44
|
+
if (end < 0)
|
|
45
|
+
return;
|
|
46
|
+
const line = buffer.slice(0, end);
|
|
47
|
+
buffer = '';
|
|
48
|
+
void this.handle(line, endpoint).then(reply => socket.end(`${JSON.stringify(reply)}\n`));
|
|
49
|
+
});
|
|
50
|
+
socket.on('error', () => socket.destroy());
|
|
51
|
+
}
|
|
52
|
+
async handle(line, { tag, token }) {
|
|
53
|
+
let request;
|
|
54
|
+
try {
|
|
55
|
+
request = JSON.parse(line);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { text: 'The call was not JSON.', isError: true };
|
|
59
|
+
}
|
|
60
|
+
if (request.token !== token)
|
|
61
|
+
return { text: 'The call was refused.', isError: true };
|
|
62
|
+
const name = request.name;
|
|
63
|
+
if (typeof name !== 'string' || !vscode.lm.tools.some(tool => tool.name === name && tool.tags.includes(tag))) {
|
|
64
|
+
return { text: `The workbench has no tool ${String(name)}.`, isError: true };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const input = request.input && typeof request.input === 'object' ? request.input : {};
|
|
68
|
+
const result = await vscode.lm.invokeTool(name, { input, toolInvocationToken: undefined });
|
|
69
|
+
return { text: result.content.map(part => part instanceof vscode.LanguageModelTextPart ? part.value : '').join(''), isError: false };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return { text: error instanceof Error ? error.message : String(error), isError: true };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
close() {
|
|
76
|
+
const served = this.#served;
|
|
77
|
+
this.#served = undefined;
|
|
78
|
+
served?.server.close();
|
|
79
|
+
}
|
|
80
|
+
dispose() { this.close(); }
|
|
81
|
+
}
|
package/media/anthropic.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 3.5 26 18" fill="#d97757"><path fill-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"/></svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 3.5 26 18" fill="#d97757"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"/></svg>
|
package/media/google.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#8b8df8"><path
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#8b8df8"><path d="M12 24C12 17.373 6.627 12 0 12 6.627 12 12 6.627 12 0c0 6.627 5.373 12 12 12-6.627 0-12 5.373-12 12z"/></svg>
|
package/media/openai.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 26 26" fill="#aaaaaa"><path fill-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 26 26" fill="#aaaaaa"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
|
package/media/xai.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 26 26" fill="#aaaaaa"><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"/></svg>
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-frontend-vscode",
|
|
3
3
|
"displayName": "Volter Harness",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.61",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/volter-ai/supercode.git",
|
|
10
|
+
"directory": "sdk/frontend-vscode"
|
|
11
|
+
},
|
|
7
12
|
"publisher": "volter-ai-dev",
|
|
8
13
|
"icon": "media/icon.png",
|
|
9
14
|
"main": "./dist/extension.js",
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
"media"
|
|
24
29
|
],
|
|
25
30
|
"scripts": {
|
|
26
|
-
"build": "node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && tsc -p tsconfig.build.json",
|
|
31
|
+
"build": "node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && node scripts/sync-marks.mjs && tsc -p tsconfig.build.json",
|
|
27
32
|
"extension": "npm run build && node scripts/pack-extension.mjs",
|
|
28
33
|
"prepack": "npm run build"
|
|
29
34
|
},
|
|
@@ -50,6 +55,20 @@
|
|
|
50
55
|
"chatProvider"
|
|
51
56
|
],
|
|
52
57
|
"contributes": {
|
|
58
|
+
"taskDefinitions": [
|
|
59
|
+
{
|
|
60
|
+
"type": "supercode-setup",
|
|
61
|
+
"required": [
|
|
62
|
+
"operation"
|
|
63
|
+
],
|
|
64
|
+
"properties": {
|
|
65
|
+
"operation": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "The identity of this coding-agent installation."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
],
|
|
53
72
|
"chatParticipants": [
|
|
54
73
|
{
|
|
55
74
|
"id": "supercode.chat",
|
|
@@ -190,7 +209,7 @@
|
|
|
190
209
|
},
|
|
191
210
|
"supercodeFrontendContract": {
|
|
192
211
|
"name": "@volter/supercode-frontend",
|
|
193
|
-
"version": "0.2.
|
|
212
|
+
"version": "0.2.12",
|
|
194
213
|
"mode": "generated-artifact"
|
|
195
214
|
},
|
|
196
215
|
"license": "MIT OR Apache-2.0"
|
|
@@ -16,6 +16,33 @@ race; HTTP uses bearer authentication plus an opaque client id and permission
|
|
|
16
16
|
narrowing. Dropping or detaching this client never closes the runtime;
|
|
17
17
|
`close()` is a separate terminate-authorized action.
|
|
18
18
|
|
|
19
|
+
Attachment snapshots may include `pending_requests`, the complete open-request
|
|
20
|
+
set at the snapshot boundary. An empty array means no request remains open;
|
|
21
|
+
absence denotes an older runtime with replay-derived state. IDs and payloads
|
|
22
|
+
come from the runtime; responses still use its existing exactly-once door.
|
|
23
|
+
|
|
24
|
+
`snapshot_cursor` is the full capture boundary of the descriptor and request
|
|
25
|
+
set, taken under the native projection lock. It differs from `history_cursor`,
|
|
26
|
+
which covers only canonical and retired display history. Transports return
|
|
27
|
+
unfiltered snapshot replay even when an existing stream has advanced.
|
|
28
|
+
`mergeFrontendSnapshot` joins later retained observations in sequence order;
|
|
29
|
+
an evicted suffix refuses attachment with `event_gap`. The view installs the
|
|
30
|
+
authoritative request set at capture before applying later requests, resolutions
|
|
31
|
+
and native turn boundaries. Legacy snapshots without the added cursor retain
|
|
32
|
+
their full replay boundary, and absence of the request set retains legacy
|
|
33
|
+
replay-derived behavior. These display fields grant no response authority.
|
|
34
|
+
|
|
35
|
+
`history_events` contains original sequenced events retired from replay since
|
|
36
|
+
canonical history was last captured. `history_cursor` covers canonical history
|
|
37
|
+
and these historical events; live/replay delivery begins after that boundary.
|
|
38
|
+
Historical events are display data, not events to send back to a harness. Both
|
|
39
|
+
native projection paths retain up to 4096 retired events plus 4096 replay events.
|
|
40
|
+
`history_events_dropped` counts older events outside that display window. A
|
|
41
|
+
frontend must show that omission rather than claim a complete transcript.
|
|
42
|
+
Canonical message history remains bounded separately. Open requests survive
|
|
43
|
+
independently of both event windows. Older clients need updating to render these
|
|
44
|
+
additive snapshot fields. See the [source and compilation record](../../docs/evidence/frontend-pending-snapshot-2026-10-10.md).
|
|
45
|
+
|
|
19
46
|
`FrontendWebSocketClient.connect()` requires a header-capable socket factory
|
|
20
47
|
and passes the bearer token only in `Authorization`. It deliberately has no
|
|
21
48
|
query-token fallback: browser clients that cannot set upgrade headers use the
|
|
@@ -33,6 +33,12 @@ export class FrontendClientError extends Error {
|
|
|
33
33
|
extensions: Record<string, unknown>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Native snapshot plus contiguous later retained events; refuses an evicted suffix. */
|
|
37
|
+
export function mergeFrontendSnapshot(snapshot: FrontendAttachSnapshot, observed?: readonly FrontendEvent[], cursor?: number): {
|
|
38
|
+
captureCursor: number; cursor: number; replay: FrontendEvent[]; later: FrontendEvent[];
|
|
39
|
+
};
|
|
40
|
+
export function projectFrontendDescriptor(descriptor: FrontendRuntimeDescriptor | null, event: FrontendEvent): FrontendRuntimeDescriptor | null;
|
|
41
|
+
|
|
36
42
|
export class FrontendClient extends GeneratedFrontendClient<RequestOptions> {
|
|
37
43
|
constructor(options: FrontendClientOptions);
|
|
38
44
|
readonly clientId: string;
|
|
@@ -43,7 +49,7 @@ export class FrontendClient extends GeneratedFrontendClient<RequestOptions> {
|
|
|
43
49
|
attach(limit?: number, options?: RequestOptions & {afterSequence?: number}): Promise<FrontendAttachSnapshot>;
|
|
44
50
|
sendInput(prompt: string, options?: RequestOptions): Promise<{accepted:boolean}>;
|
|
45
51
|
submit(prompt: string, options?: RequestOptions): Promise<{reply:string}>;
|
|
46
|
-
interrupt(options?: RequestOptions): Promise<{interrupted:boolean}>;
|
|
52
|
+
interrupt(options?: RequestOptions & {expectedTurn?: string}): Promise<{interrupted:boolean}>;
|
|
47
53
|
steer(prompt: string, options?: RequestOptions): Promise<{queued:boolean}>;
|
|
48
54
|
respond(response: FrontendResponse, options?: RequestOptions): Promise<{accepted:boolean}>;
|
|
49
55
|
invoke(operation: FrontendOperationInvocation, options?: RequestOptions): Promise<FrontendOperationResult>;
|
|
@@ -69,6 +69,79 @@ function validateDescriptor(descriptor) {
|
|
|
69
69
|
return descriptor;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function validateSnapshot(snapshot) {
|
|
73
|
+
validateDescriptor(snapshot?.descriptor);
|
|
74
|
+
if (!Array.isArray(snapshot.history) || !Array.isArray(snapshot.replay)
|
|
75
|
+
|| !Number.isSafeInteger(snapshot.history_cursor) || snapshot.history_cursor < 0
|
|
76
|
+
|| (snapshot.history_events !== undefined && !Array.isArray(snapshot.history_events))
|
|
77
|
+
|| (snapshot.history_events_dropped !== undefined
|
|
78
|
+
&& (!Number.isSafeInteger(snapshot.history_events_dropped) || snapshot.history_events_dropped < 0))) {
|
|
79
|
+
throw new FrontendClientError({name:'transport', message:'invalid frontend attachment snapshot'});
|
|
80
|
+
}
|
|
81
|
+
let previous = 0;
|
|
82
|
+
for (const event of snapshot.history_events ?? []) {
|
|
83
|
+
if (!Number.isSafeInteger(event?.sequence) || event.sequence <= previous
|
|
84
|
+
|| (previous > 0 && event.sequence !== previous + 1)
|
|
85
|
+
|| event.sequence > snapshot.history_cursor || typeof event.kind !== 'string') {
|
|
86
|
+
throw new FrontendClientError({name:'transport', message:'invalid frontend historical event boundary'});
|
|
87
|
+
}
|
|
88
|
+
previous = event.sequence;
|
|
89
|
+
}
|
|
90
|
+
if (previous > 0 && previous !== snapshot.history_cursor) {
|
|
91
|
+
throw new FrontendClientError({name:'transport', message:'incomplete frontend historical event boundary'});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Join a captured snapshot to retained later observations without changing
|
|
96
|
+
* the native capture point. A missing bounded suffix refuses attachment. */
|
|
97
|
+
export function mergeFrontendSnapshot(snapshot, observed = [], cursor = 0) {
|
|
98
|
+
validateSnapshot(snapshot);
|
|
99
|
+
const captureCursor = snapshot.snapshot_cursor ?? Math.max(snapshot.history_cursor,
|
|
100
|
+
...snapshot.replay.map(event => event.sequence));
|
|
101
|
+
if (!Number.isSafeInteger(captureCursor) || captureCursor < snapshot.history_cursor
|
|
102
|
+
|| !Number.isSafeInteger(cursor) || cursor < 0) {
|
|
103
|
+
throw new FrontendClientError({name:'transport', message:'invalid frontend snapshot capture boundary'});
|
|
104
|
+
}
|
|
105
|
+
const events = new Map();
|
|
106
|
+
for (const event of snapshot.replay) {
|
|
107
|
+
if (!Number.isSafeInteger(event?.sequence) || event.sequence <= snapshot.history_cursor
|
|
108
|
+
|| event.sequence > captureCursor || typeof event.kind !== 'string') {
|
|
109
|
+
throw new FrontendClientError({name:'transport', message:'invalid frontend snapshot replay boundary'});
|
|
110
|
+
}
|
|
111
|
+
events.set(event.sequence, event);
|
|
112
|
+
}
|
|
113
|
+
for (const event of observed) {
|
|
114
|
+
if (event.sequence > captureCursor) events.set(event.sequence, event);
|
|
115
|
+
}
|
|
116
|
+
const replay = [...events.values()].sort((a, b) => a.sequence - b.sequence);
|
|
117
|
+
const later = replay.filter(event => event.sequence > captureCursor);
|
|
118
|
+
let projected = captureCursor;
|
|
119
|
+
for (const event of later) {
|
|
120
|
+
if (!Number.isSafeInteger(event.sequence) || event.sequence !== projected + 1) {
|
|
121
|
+
throw new FrontendClientError({name:'event_gap', message:`frontend snapshot needs event ${projected + 1}, received ${event.sequence}`});
|
|
122
|
+
}
|
|
123
|
+
projected = event.sequence;
|
|
124
|
+
}
|
|
125
|
+
if (projected < cursor) {
|
|
126
|
+
throw new FrontendClientError({name:'event_gap', message:`frontend snapshot lost retained events ${projected + 1} through ${cursor}`});
|
|
127
|
+
}
|
|
128
|
+
return {captureCursor, cursor:projected, replay, later};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Project only native lifecycle events; a model round-trip does not idle a turn. */
|
|
132
|
+
export function projectFrontendDescriptor(descriptor, event) {
|
|
133
|
+
if (!descriptor) return descriptor;
|
|
134
|
+
if (event.kind === 'runtime_disconnected') {
|
|
135
|
+
return {...descriptor, connection_state:'shutting_down', turn_state:'idle',
|
|
136
|
+
actions:{...descriptor.actions, submit:false, steer:false, interrupt:false, respond:false}};
|
|
137
|
+
}
|
|
138
|
+
if (['turn_succeeded', 'turn_failed', 'turn_interrupted'].includes(event.kind)) {
|
|
139
|
+
return {...descriptor, turn_state:'idle'};
|
|
140
|
+
}
|
|
141
|
+
if (['turn_started', 'user_message'].includes(event.kind)) return {...descriptor, turn_state:'busy'};
|
|
142
|
+
return descriptor;
|
|
143
|
+
}
|
|
144
|
+
|
|
72
145
|
export class FrontendClient extends FrontendEmitter {
|
|
73
146
|
#baseUrl;
|
|
74
147
|
#token;
|
|
@@ -132,13 +205,12 @@ export class FrontendClient extends FrontendEmitter {
|
|
|
132
205
|
this.stopEvents();
|
|
133
206
|
throw error;
|
|
134
207
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (const event of snapshot.replay) this.#lastSequence = Math.max(this.#lastSequence, event.sequence);
|
|
208
|
+
validateSnapshot(snapshot);
|
|
209
|
+
// Replay remains the native snapshot's, even if a running stream already
|
|
210
|
+
// delivered some of it. View adapters need that capture boundary to merge
|
|
211
|
+
// their later observations; the stream's delivery cursor cannot erase it.
|
|
212
|
+
const {captureCursor} = mergeFrontendSnapshot(snapshot);
|
|
213
|
+
this.#lastSequence = Math.max(this.#lastSequence, afterSequence, captureCursor);
|
|
142
214
|
return snapshot;
|
|
143
215
|
}
|
|
144
216
|
async #openEventStream(afterSequence, signal) {
|
|
@@ -288,10 +360,12 @@ export class FrontendWebSocketClient extends FrontendEmitter {
|
|
|
288
360
|
async describe() { return validateDescriptor(await this.rpc(FRONTEND_METHODS.describe)); }
|
|
289
361
|
async attach(limit = 1000, {afterSequence = 0} = {}) {
|
|
290
362
|
const snapshot = await this.rpc(FRONTEND_METHODS.attach, {limit, after_sequence:afterSequence});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
363
|
+
validateSnapshot(snapshot);
|
|
364
|
+
// Replay remains the native snapshot's, even if a running stream already
|
|
365
|
+
// delivered some of it. View adapters need that capture boundary to merge
|
|
366
|
+
// their later observations; the stream's delivery cursor cannot erase it.
|
|
367
|
+
const {captureCursor} = mergeFrontendSnapshot(snapshot);
|
|
368
|
+
this.#lastSequence = Math.max(this.#lastSequence, afterSequence, captureCursor);
|
|
295
369
|
return snapshot;
|
|
296
370
|
}
|
|
297
371
|
async detach() { const result = await this.rpc(FRONTEND_METHODS.detach); this.closeTransport(); return result; }
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"types": {
|
|
29
29
|
"JsonValue": {"kind": "json"},
|
|
30
30
|
"Empty": {"kind": "object", "fields": {}},
|
|
31
|
+
"InterruptParams": {"kind": "object", "fields": {"expected_turn": {"type": "string", "optional": true}}},
|
|
31
32
|
"ChatRole": {"kind": "enum", "values": ["system", "user", "assistant", "tool"]},
|
|
32
33
|
"FunctionCall": {
|
|
33
34
|
"kind": "object",
|
|
@@ -143,7 +144,11 @@
|
|
|
143
144
|
"descriptor": {"type": "FrontendRuntimeDescriptor"},
|
|
144
145
|
"history": {"type": {"kind": "array", "items": "ChatMessage"}},
|
|
145
146
|
"history_cursor": {"type": "u64"},
|
|
146
|
-
"
|
|
147
|
+
"snapshot_cursor": {"type": "u64", "optional": true, "nullable": true},
|
|
148
|
+
"history_events": {"type": {"kind": "array", "items": "FrontendEvent"}, "optional": true},
|
|
149
|
+
"history_events_dropped": {"type": "u64", "optional": true},
|
|
150
|
+
"replay": {"type": {"kind": "array", "items": "FrontendEvent"}},
|
|
151
|
+
"pending_requests": {"type": {"kind": "array", "items": "FrontendRequest"}, "optional": true, "nullable": true}
|
|
147
152
|
}
|
|
148
153
|
},
|
|
149
154
|
"FrontendApprovalDecision": {"kind": "enum", "values": ["deny", "allow", "allow_for_session"]},
|
|
@@ -260,7 +265,7 @@
|
|
|
260
265
|
{"id": "attach", "wire": "frontend.v2.attach", "sdk_action": "events", "params": "AttachParams", "result": "FrontendAttachSnapshot", "transports": ["local", "http", "websocket", "acp"]},
|
|
261
266
|
{"id": "send_input", "wire": "frontend.v2.send_input", "sdk_action": "input", "params": "MultimodalPromptParams", "result": "Accepted", "transports": ["local", "http", "websocket", "acp"]},
|
|
262
267
|
{"id": "submit", "wire": "frontend.v2.submit", "sdk_action": "input", "params": "MultimodalPromptParams", "result": "Reply", "compatibility": "completion_waiting_alias", "transports": ["local", "http", "websocket", "acp"]},
|
|
263
|
-
{"id": "interrupt", "wire": "frontend.v2.interrupt", "sdk_action": "interrupt", "params": "
|
|
268
|
+
{"id": "interrupt", "wire": "frontend.v2.interrupt", "sdk_action": "interrupt", "params": "InterruptParams", "result": "Interrupted", "transports": ["local", "http", "websocket", "acp"]},
|
|
264
269
|
{"id": "steer", "wire": "frontend.v2.steer", "sdk_action": "steer", "params": "PromptParams", "result": "Queued", "transports": ["local", "http", "websocket", "acp"]},
|
|
265
270
|
{"id": "stop_task", "wire": "frontend.v2.stop_task", "sdk_action": "stop_task", "params": "TaskParams", "result": "Stopped", "transports": ["local", "http", "websocket", "acp"]},
|
|
266
271
|
{"id": "respond", "wire": "frontend.v2.respond", "sdk_action": "respond", "params": "RespondParams", "result": "Accepted", "transports": ["local", "http", "websocket", "acp"]},
|
|
@@ -6,7 +6,7 @@ export abstract class GeneratedFrontendClient<Options = unknown> {
|
|
|
6
6
|
attach(limit?: number, options?: Options & {afterSequence?: number}): Promise<FrontendAttachSnapshot>;
|
|
7
7
|
sendInput(prompt: string, options?: Options & {imageUrls?: string[]}): Promise<Accepted>;
|
|
8
8
|
submit(prompt: string, options?: Options & {imageUrls?: string[]}): Promise<Reply>;
|
|
9
|
-
interrupt(options?: Options): Promise<Interrupted>;
|
|
9
|
+
interrupt(options?: Options & {expectedTurn?: string}): Promise<Interrupted>;
|
|
10
10
|
steer(prompt: string, options?: Options): Promise<Queued>;
|
|
11
11
|
stopTask(taskId: string, options?: Options): Promise<Stopped>;
|
|
12
12
|
respond(response: FrontendResponse, options?: Options): Promise<Accepted>;
|
|
@@ -7,7 +7,7 @@ export class GeneratedFrontendClient {
|
|
|
7
7
|
attach(limit = 1000, { afterSequence, ...options } = {}) { return this.rpc(FRONTEND_METHODS.attach, { limit, ...(afterSequence === undefined ? {} : {after_sequence:afterSequence}) }, options); }
|
|
8
8
|
sendInput(prompt, { imageUrls, ...options } = {}) { return this.rpc(FRONTEND_METHODS.send_input, { prompt, ...(imageUrls?.length ? {image_urls:imageUrls} : {}) }, options); }
|
|
9
9
|
submit(prompt, { imageUrls, ...options } = {}) { return this.rpc(FRONTEND_METHODS.submit, { prompt, ...(imageUrls?.length ? {image_urls:imageUrls} : {}) }, options); }
|
|
10
|
-
interrupt(options) { return this.rpc(FRONTEND_METHODS.interrupt, {}, options); }
|
|
10
|
+
interrupt({ expectedTurn, ...options } = {}) { return this.rpc(FRONTEND_METHODS.interrupt, { ...(expectedTurn === undefined ? {} : { expected_turn: expectedTurn }) }, options); }
|
|
11
11
|
steer(prompt, options) { return this.rpc(FRONTEND_METHODS.steer, { prompt }, options); }
|
|
12
12
|
stopTask(taskId, options) { return this.rpc(FRONTEND_METHODS.stop_task, { task_id: taskId }, options); }
|
|
13
13
|
respond(response, options) { return this.rpc(FRONTEND_METHODS.respond, { response }, options); }
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
export type JsonPrimitive = string | number | boolean | null;
|
|
3
3
|
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
4
4
|
export type Empty = Record<string, never>;
|
|
5
|
+
export interface InterruptParams {
|
|
6
|
+
expected_turn?: string;
|
|
7
|
+
}
|
|
5
8
|
export type ChatRole = "system" | "user" | "assistant" | "tool";
|
|
6
9
|
export interface FunctionCall {
|
|
7
10
|
name: string;
|
|
@@ -79,7 +82,11 @@ export interface FrontendAttachSnapshot {
|
|
|
79
82
|
descriptor: FrontendRuntimeDescriptor;
|
|
80
83
|
history: ChatMessage[];
|
|
81
84
|
history_cursor: number;
|
|
85
|
+
snapshot_cursor?: number | null;
|
|
86
|
+
history_events?: FrontendEvent[];
|
|
87
|
+
history_events_dropped?: number;
|
|
82
88
|
replay: FrontendEvent[];
|
|
89
|
+
pending_requests?: FrontendRequest[] | null;
|
|
83
90
|
}
|
|
84
91
|
export type FrontendApprovalDecision = "deny" | "allow" | "allow_for_session";
|
|
85
92
|
export type FrontendElicitationAction = "accept" | "decline" | "cancel";
|