ds-harness-remote 0.5.8 → 0.5.10
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/dist/acp/adapter.d.ts +1 -1
- package/dist/acp/adapter.d.ts.map +1 -1
- package/dist/acp/adapters/claude-history.d.ts +3 -0
- package/dist/acp/adapters/claude-history.d.ts.map +1 -0
- package/dist/acp/adapters/claude-history.js +118 -0
- package/dist/acp/adapters/claude-history.js.map +1 -0
- package/dist/acp/adapters/claude-process.d.ts +21 -0
- package/dist/acp/adapters/claude-process.d.ts.map +1 -0
- package/dist/acp/adapters/claude-process.js +147 -0
- package/dist/acp/adapters/claude-process.js.map +1 -0
- package/dist/acp/adapters/cursor-process.d.ts +9 -4
- package/dist/acp/adapters/cursor-process.d.ts.map +1 -1
- package/dist/acp/adapters/cursor-process.js +31 -3
- package/dist/acp/adapters/cursor-process.js.map +1 -1
- package/dist/acp/adapters/opencode-process.d.ts +67 -0
- package/dist/acp/adapters/opencode-process.d.ts.map +1 -0
- package/dist/acp/adapters/opencode-process.js +272 -0
- package/dist/acp/adapters/opencode-process.js.map +1 -0
- package/dist/acp/adapters/opencode.d.ts +8 -0
- package/dist/acp/adapters/opencode.d.ts.map +1 -0
- package/dist/acp/adapters/opencode.js +8 -0
- package/dist/acp/adapters/opencode.js.map +1 -0
- package/dist/acp/adapters/pi-process.d.ts +42 -0
- package/dist/acp/adapters/pi-process.d.ts.map +1 -0
- package/dist/acp/adapters/pi-process.js +410 -0
- package/dist/acp/adapters/pi-process.js.map +1 -0
- package/dist/acp/gateway.d.ts +8 -0
- package/dist/acp/gateway.d.ts.map +1 -1
- package/dist/acp/gateway.js +299 -67
- package/dist/acp/gateway.js.map +1 -1
- package/dist/acp/method-policy.d.ts +19 -19
- package/dist/acp/method-policy.js +5 -5
- package/dist/acp/method-policy.js.map +1 -1
- package/dist/acp/pi-tool-result.d.ts +9 -0
- package/dist/acp/pi-tool-result.d.ts.map +1 -0
- package/dist/acp/pi-tool-result.js +22 -0
- package/dist/acp/pi-tool-result.js.map +1 -0
- package/dist/acp/virtual-catalog.d.ts +50 -0
- package/dist/acp/virtual-catalog.d.ts.map +1 -0
- package/dist/acp/virtual-catalog.js +103 -0
- package/dist/acp/virtual-catalog.js.map +1 -0
- package/dist/acp/virtual-contract.d.ts +165 -0
- package/dist/acp/virtual-contract.d.ts.map +1 -0
- package/dist/acp/virtual-contract.js +216 -0
- package/dist/acp/virtual-contract.js.map +1 -0
- package/dist/acp/virtual-event-carrier.d.ts +50 -0
- package/dist/acp/virtual-event-carrier.d.ts.map +1 -0
- package/dist/acp/virtual-event-carrier.js +222 -0
- package/dist/acp/virtual-event-carrier.js.map +1 -0
- package/dist/acp/virtual-harness.d.ts +13 -112
- package/dist/acp/virtual-harness.d.ts.map +1 -1
- package/dist/acp/virtual-harness.js +129 -911
- package/dist/acp/virtual-harness.js.map +1 -1
- package/dist/acp/virtual-live.d.ts +78 -0
- package/dist/acp/virtual-live.d.ts.map +1 -0
- package/dist/acp/virtual-live.js +513 -0
- package/dist/acp/virtual-live.js.map +1 -0
- package/dist/acp/workspace-tools.js +1 -1
- package/dist/acp/workspace-tools.js.map +1 -1
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +1 -0
- package/dist/acp.js.map +1 -1
- package/dist/client/header.d.ts +10 -0
- package/dist/client/header.d.ts.map +1 -0
- package/dist/client/header.js +169 -0
- package/dist/client/header.js.map +1 -0
- package/dist/client/icons.d.ts +3 -0
- package/dist/client/icons.d.ts.map +1 -0
- package/dist/client/icons.js +3 -0
- package/dist/client/icons.js.map +1 -0
- package/dist/client/navigation.d.ts +5 -0
- package/dist/client/navigation.d.ts.map +1 -0
- package/dist/client/navigation.js +30 -0
- package/dist/client/navigation.js.map +1 -0
- package/dist/client/presentation.d.ts +17 -0
- package/dist/client/presentation.d.ts.map +1 -0
- package/dist/client/presentation.js +208 -0
- package/dist/client/presentation.js.map +1 -0
- package/dist/client/progress.d.ts +10 -0
- package/dist/client/progress.d.ts.map +1 -0
- package/dist/client/progress.js +82 -0
- package/dist/client/progress.js.map +1 -0
- package/dist/client/settings.d.ts +10 -0
- package/dist/client/settings.d.ts.map +1 -0
- package/dist/client/settings.js +335 -0
- package/dist/client/settings.js.map +1 -0
- package/dist/client/shell.d.ts +6 -0
- package/dist/client/shell.d.ts.map +1 -0
- package/dist/client/shell.js +128 -0
- package/dist/client/shell.js.map +1 -0
- package/dist/client/types.d.ts +264 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/workspaces.d.ts +11 -0
- package/dist/client/workspaces.d.ts.map +1 -0
- package/dist/client/workspaces.js +1179 -0
- package/dist/client/workspaces.js.map +1 -0
- package/dist/client-capabilities.d.ts +6 -0
- package/dist/client-capabilities.d.ts.map +1 -0
- package/dist/client-capabilities.js +88 -0
- package/dist/client-capabilities.js.map +1 -0
- package/dist/client-codex-streams.d.ts +23 -0
- package/dist/client-codex-streams.d.ts.map +1 -0
- package/dist/client-codex-streams.js +156 -0
- package/dist/client-codex-streams.js.map +1 -0
- package/dist/client-connection.d.ts +38 -0
- package/dist/client-connection.d.ts.map +1 -0
- package/dist/client-connection.js +210 -0
- package/dist/client-connection.js.map +1 -0
- package/dist/client-control.d.ts +48 -0
- package/dist/client-control.d.ts.map +1 -0
- package/dist/client-control.js +273 -0
- package/dist/client-control.js.map +1 -0
- package/dist/client-locale.d.ts +8 -0
- package/dist/client-locale.d.ts.map +1 -1
- package/dist/client-locale.js +16 -0
- package/dist/client-locale.js.map +1 -1
- package/dist/client-runtime-contract.d.ts +161 -0
- package/dist/client-runtime-contract.d.ts.map +1 -0
- package/dist/client-runtime-contract.js +127 -0
- package/dist/client-runtime-contract.js.map +1 -0
- package/dist/client-runtime.d.ts +24 -126
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +171 -867
- package/dist/client-runtime.js.map +1 -1
- package/dist/client-secure-transport.d.ts +1 -0
- package/dist/client-secure-transport.d.ts.map +1 -1
- package/dist/client-secure-transport.js +35 -16
- package/dist/client-secure-transport.js.map +1 -1
- package/dist/client-workspace-queries.d.ts +12 -0
- package/dist/client-workspace-queries.d.ts.map +1 -0
- package/dist/client-workspace-queries.js +66 -0
- package/dist/client-workspace-queries.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.github.js +2091 -1896
- package/dist/client.js +2091 -1896
- package/dist/client.js.map +1 -1
- package/dist/codex/domain.d.ts.map +1 -1
- package/dist/codex/domain.js +67 -46
- package/dist/codex/domain.js.map +1 -1
- package/dist/codex/method-policy.d.ts +9 -9
- package/dist/codex/virtual-catalog.d.ts +90 -0
- package/dist/codex/virtual-catalog.d.ts.map +1 -0
- package/dist/codex/virtual-catalog.js +317 -0
- package/dist/codex/virtual-catalog.js.map +1 -0
- package/dist/codex/virtual-contract.d.ts +96 -0
- package/dist/codex/virtual-contract.d.ts.map +1 -0
- package/dist/codex/virtual-contract.js +192 -0
- package/dist/codex/virtual-contract.js.map +1 -0
- package/dist/codex/virtual-event-carrier.d.ts +52 -0
- package/dist/codex/virtual-event-carrier.d.ts.map +1 -0
- package/dist/codex/virtual-event-carrier.js +212 -0
- package/dist/codex/virtual-event-carrier.js.map +1 -0
- package/dist/codex/virtual-harness.d.ts +16 -117
- package/dist/codex/virtual-harness.d.ts.map +1 -1
- package/dist/codex/virtual-harness.js +123 -1909
- package/dist/codex/virtual-harness.js.map +1 -1
- package/dist/codex/virtual-history.d.ts +74 -0
- package/dist/codex/virtual-history.d.ts.map +1 -0
- package/dist/codex/virtual-history.js +546 -0
- package/dist/codex/virtual-history.js.map +1 -0
- package/dist/codex/virtual-live.d.ts +75 -0
- package/dist/codex/virtual-live.d.ts.map +1 -0
- package/dist/codex/virtual-live.js +563 -0
- package/dist/codex/virtual-live.js.map +1 -0
- package/dist/codex/virtual-values.d.ts +29 -0
- package/dist/codex/virtual-values.d.ts.map +1 -0
- package/dist/codex/virtual-values.js +75 -0
- package/dist/codex/virtual-values.js.map +1 -0
- package/dist/config.js +4 -4
- package/dist/config.js.map +1 -1
- package/dist/control-runtime.js +2 -2
- package/dist/control-runtime.js.map +1 -1
- package/dist/harness-remote-bridge.d.ts +2 -1
- package/dist/harness-remote-bridge.d.ts.map +1 -1
- package/dist/harness-remote-bridge.js +11 -3
- package/dist/harness-remote-bridge.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16044 -13878
- package/dist/index.js.map +4 -4
- package/dist/plugin-route-dispatcher.d.ts +2 -2
- package/dist/plugin-route-dispatcher.d.ts.map +1 -1
- package/dist/plugin-route-dispatcher.js +2 -2
- package/dist/plugin-route-dispatcher.js.map +1 -1
- package/dist/remote-typert-gateway.js +7 -1
- package/dist/remote-typert-gateway.js.map +1 -1
- package/dist/server-connection.d.ts +1 -0
- package/dist/server-connection.d.ts.map +1 -1
- package/dist/server-connection.js +6 -1
- package/dist/server-connection.js.map +1 -1
- package/dist/service.d.ts +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +6 -5
- package/dist/service.js.map +1 -1
- package/dist/terminal-carrier-compat.d.ts +14 -0
- package/dist/terminal-carrier-compat.d.ts.map +1 -0
- package/dist/terminal-carrier-compat.js +259 -0
- package/dist/terminal-carrier-compat.js.map +1 -0
- package/dist/typert-gateway-contract.d.ts +6 -1
- package/dist/typert-gateway-contract.d.ts.map +1 -1
- package/dist/typert-gateway-switch.d.ts +2 -1
- package/dist/typert-gateway-switch.d.ts.map +1 -1
- package/dist/typert-gateway-switch.js +31 -15
- package/dist/typert-gateway-switch.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dsh-plugin.json +1 -1
- package/package.json +21 -19
|
@@ -1,53 +1,10 @@
|
|
|
1
|
-
import { ACP_TOOL_CALL_ENDPOINTS, ACP_TOOL_STREAM_ENDPOINTS } from '@dsh-remote/protocol';
|
|
2
|
-
import { parseAcpImage, acpImageContent, acpImageLimits } from './image-content.js';
|
|
3
1
|
import { AgentAcpClient } from '@dsh-remote/client-core';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/** Build a backend-scoped virtual Workspace id for an absolute Host cwd. */
|
|
11
|
-
export function acpCwdWorkspaceId(path, backend) {
|
|
12
|
-
if (backend !== 'cursor' && backend !== 'antigravity')
|
|
13
|
-
throw new Error('An explicit ACP workspace backend is required.');
|
|
14
|
-
return `${backend === 'antigravity' ? AGY_WORKSPACE_PREFIX : CURSOR_WORKSPACE_PREFIX}${encodeURIComponent(path)}`;
|
|
15
|
-
}
|
|
16
|
-
export function createAcpWorkspaceView(path, backend, title) {
|
|
17
|
-
const now = new Date().toISOString();
|
|
18
|
-
const label = title?.trim() || path.split(/[\\/]/).filter(Boolean).at(-1) || path;
|
|
19
|
-
return {
|
|
20
|
-
workspaceId: acpCwdWorkspaceId(path, backend),
|
|
21
|
-
path,
|
|
22
|
-
title: label,
|
|
23
|
-
sessionIds: [],
|
|
24
|
-
sessionCount: 0,
|
|
25
|
-
createdAt: now,
|
|
26
|
-
updatedAt: now,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/** Discover ACP virtual workspaces visible on the Host. */
|
|
30
|
-
export async function discoverAcpVirtualWorkspaces(client, backend, signal) {
|
|
31
|
-
if (client?.listWorkspaces !== undefined) {
|
|
32
|
-
try {
|
|
33
|
-
const items = await client.listWorkspaces(backend, signal);
|
|
34
|
-
if (Array.isArray(items) && items.length > 0) {
|
|
35
|
-
return items
|
|
36
|
-
.filter(item => typeof item?.path === 'string' && item.path.trim() !== '')
|
|
37
|
-
.map(item => {
|
|
38
|
-
const view = createAcpWorkspaceView(item.path, backend, item.title);
|
|
39
|
-
if (typeof item.sessionCount === 'number')
|
|
40
|
-
view.sessionCount = item.sessionCount;
|
|
41
|
-
return view;
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// Discovery is optional; an unavailable Host returns no workspaces.
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
2
|
+
import { ACP_TOOL_CALL_ENDPOINTS, ACP_TOOL_STREAM_ENDPOINTS } from '@dsh-remote/protocol';
|
|
3
|
+
import { acpImageContent, acpImageLimits, parseAcpImage } from './image-content.js';
|
|
4
|
+
import { createAcpWorkspaceView, modelCatalog, nativeWorkspace, recreateWorkspaceFromId } from './virtual-catalog.js';
|
|
5
|
+
import { array, AsyncValueQueue, business, carrierArgs, extractPromptText, extractSessionId, fail, failFrom, failure, isRecord, nativeAcpId, optionalInteger, optionalPositiveInteger, record, requestArg, requiredString, sessionPrefix, string, success, } from './virtual-contract.js';
|
|
6
|
+
import { AcpEventCarrier } from './virtual-event-carrier.js';
|
|
7
|
+
import { AcpLiveProjection } from './virtual-live.js';
|
|
51
8
|
/**
|
|
52
9
|
* Browser-safe, in-memory Harness projection for the independent Agent ACP domain. Projects
|
|
53
10
|
* cwd workspaces and ACP sessions onto the native DSH Workspace / Session /
|
|
@@ -58,6 +15,8 @@ export class AcpVirtualHarness {
|
|
|
58
15
|
host;
|
|
59
16
|
backend;
|
|
60
17
|
hostCarrier;
|
|
18
|
+
live;
|
|
19
|
+
events;
|
|
61
20
|
api;
|
|
62
21
|
workspaceById = new Map();
|
|
63
22
|
sessions = new Map();
|
|
@@ -68,12 +27,13 @@ export class AcpVirtualHarness {
|
|
|
68
27
|
rcHostStreams = new Set();
|
|
69
28
|
follows = new Set();
|
|
70
29
|
pendingApprovals = new Map();
|
|
30
|
+
pendingHistory = new Map();
|
|
71
31
|
selectedWorkspaceId;
|
|
72
32
|
selectedModels = new Map();
|
|
73
33
|
lastProjectionSeq = 0;
|
|
74
34
|
imageAttachments = new Map();
|
|
75
35
|
closed = false;
|
|
76
|
-
constructor(client, host, backend =
|
|
36
|
+
constructor(client, host, backend = "cursor",
|
|
77
37
|
/**
|
|
78
38
|
* Host gateway used for endpoints this virtual Harness does not project
|
|
79
39
|
* (settings, plugins, models, credentials, …). A real Harness workspace
|
|
@@ -85,16 +45,52 @@ export class AcpVirtualHarness {
|
|
|
85
45
|
this.host = host;
|
|
86
46
|
this.backend = backend;
|
|
87
47
|
this.hostCarrier = hostCarrier;
|
|
88
|
-
|
|
48
|
+
const owner = this;
|
|
49
|
+
this.live = new AcpLiveProjection({
|
|
50
|
+
ensureSessionRegistered: (...args) => owner.ensureSessionRegistered(...args),
|
|
51
|
+
get sessions() { return owner.sessions; },
|
|
52
|
+
hydrateSession: (...args) => owner.hydrateSession(...args),
|
|
53
|
+
historyPage: (...args) => owner.historyPage(...args),
|
|
54
|
+
get follows() { return owner.follows; },
|
|
55
|
+
get backend() { return owner.backend; },
|
|
56
|
+
modelSelectionProjection: (...args) => owner.modelSelectionProjection(...args),
|
|
57
|
+
get client() { return owner.client; },
|
|
58
|
+
emitRemoteEvent: (...args) => owner.events.emitRemoteEvent(...args),
|
|
59
|
+
get selectedModels() { return owner.selectedModels; },
|
|
60
|
+
publishProjection: (...args) => owner.events.publishProjection(...args),
|
|
61
|
+
nextProjectionSeq: (...args) => owner.nextProjectionSeq(...args),
|
|
62
|
+
refreshSessionTitle: (...args) => owner.refreshSessionTitle(...args),
|
|
63
|
+
get pendingApprovals() { return owner.pendingApprovals; },
|
|
64
|
+
emitApproval: (...args) => owner.events.emitApproval(...args),
|
|
65
|
+
modelSelection: (...args) => owner.modelSelection(...args),
|
|
66
|
+
broadcastRcMux: (...args) => owner.events.broadcastRcMux(...args),
|
|
67
|
+
});
|
|
68
|
+
this.events = new AcpEventCarrier({
|
|
69
|
+
get workspaceStreams() { return owner.workspaceStreams; },
|
|
70
|
+
visibleWorkspaces: (...args) => owner.visibleWorkspaces(...args),
|
|
71
|
+
get controlStreams() { return owner.controlStreams; },
|
|
72
|
+
get eventStreams() { return owner.eventStreams; },
|
|
73
|
+
get selectedWorkspaceId() { return owner.selectedWorkspaceId; },
|
|
74
|
+
get workspaceById() { return owner.workspaceById; },
|
|
75
|
+
backendLabel: (...args) => owner.backendLabel(...args),
|
|
76
|
+
get pendingApprovals() { return owner.pendingApprovals; },
|
|
77
|
+
get client() { return owner.client; },
|
|
78
|
+
dispatch: (...args) => owner.dispatch(...args),
|
|
79
|
+
get rcMuxStreams() { return owner.rcMuxStreams; },
|
|
80
|
+
get sessions() { return owner.sessions; },
|
|
81
|
+
get rcHostStreams() { return owner.rcHostStreams; },
|
|
82
|
+
nextProjectionSeq: (...args) => owner.nextProjectionSeq(...args),
|
|
83
|
+
});
|
|
84
|
+
this.api = this.events.createApiProxy();
|
|
89
85
|
}
|
|
90
|
-
static remote(core, host, backend =
|
|
86
|
+
static remote(core, host, backend = "cursor", capabilities = [], hostCarrier) {
|
|
91
87
|
return new AcpVirtualHarness(new AgentAcpClient(core, capabilities), host, backend, hostCarrier);
|
|
92
88
|
}
|
|
93
89
|
async workspaces() {
|
|
94
90
|
return [...this.workspaceById.values()];
|
|
95
91
|
}
|
|
96
92
|
backendLabel() {
|
|
97
|
-
return this.backend ===
|
|
93
|
+
return this.backend === "pi" ? "PI" : this.backend === "opencode" ? "OpenCode" : this.backend === "antigravity" ? "Antigravity" : "Cursor";
|
|
98
94
|
}
|
|
99
95
|
async selectWorkspace(workspaceId) {
|
|
100
96
|
const workspace = this.workspaceById.get(workspaceId) ?? recreateWorkspaceFromId(workspaceId, this.backend);
|
|
@@ -167,7 +163,7 @@ export class AcpVirtualHarness {
|
|
|
167
163
|
return business(await this.client.workspaceToolCall(nativeAcpId(sessionId, this.backend), this.backend, endpoint, args, signal));
|
|
168
164
|
}
|
|
169
165
|
switch (endpoint) {
|
|
170
|
-
case '$events/result': return business(await this.answerRemoteEvent(args, signal));
|
|
166
|
+
case '$events/result': return business(await this.events.answerRemoteEvent(args, signal));
|
|
171
167
|
case 'workspace/list': return business(success({
|
|
172
168
|
items: this.visibleWorkspaces().map(nativeWorkspace),
|
|
173
169
|
archivedSessionIds: [],
|
|
@@ -182,6 +178,7 @@ export class AcpVirtualHarness {
|
|
|
182
178
|
case 'workspace/insertSessionBefore': return business(await this.workspaceForSession(requestArg(args)));
|
|
183
179
|
case 'workspace/archiveSession': return business(await this.archiveSession(requestArg(args)));
|
|
184
180
|
case 'session/list': return business(success({ items: await this.handleSessionList() }));
|
|
181
|
+
case 'session/projections': return business(await this.sessionProjections(requestArg(args)));
|
|
185
182
|
case 'session/search': return business(success({ items: [], hasMore: false }));
|
|
186
183
|
case 'session/create': return business(await this.createSession(requestArg(args), signal));
|
|
187
184
|
case 'session/fork': return business(failure('bad-request', `${this.backendLabel()} Remote does not support session fork yet.`));
|
|
@@ -194,7 +191,7 @@ export class AcpVirtualHarness {
|
|
|
194
191
|
case 'session/attachment': return business(await this.attachment(requestArg(args)));
|
|
195
192
|
case 'session/modelCatalog': {
|
|
196
193
|
const sessionId = await this.preferredSessionId();
|
|
197
|
-
if (this.backend
|
|
194
|
+
if (this.backend === 'cursor' || !sessionId)
|
|
198
195
|
return business(success(modelCatalog(this.backend)));
|
|
199
196
|
const models = await this.hostSessionModels(sessionId);
|
|
200
197
|
return business(success({ default: models.current, routableProviders: models.routable ? [models.current.provider] : [], groups: models.groups, failures: models.failures }));
|
|
@@ -202,7 +199,7 @@ export class AcpVirtualHarness {
|
|
|
202
199
|
case 'session/models': {
|
|
203
200
|
const rawId = extractSessionId(requestArg(args));
|
|
204
201
|
nativeAcpId(rawId, this.backend);
|
|
205
|
-
if (this.backend
|
|
202
|
+
if (this.backend !== 'cursor')
|
|
206
203
|
return business(success(await this.hostSessionModels(rawId)));
|
|
207
204
|
const catalog = modelCatalog(this.backend);
|
|
208
205
|
return business(success({
|
|
@@ -243,15 +240,15 @@ export class AcpVirtualHarness {
|
|
|
243
240
|
return queue.iterate(() => { void stream.close().catch(() => undefined); });
|
|
244
241
|
}
|
|
245
242
|
if (endpoint === 'workspace/follow')
|
|
246
|
-
return this.workspaceFollow(signal);
|
|
243
|
+
return this.events.workspaceFollow(signal);
|
|
247
244
|
if (endpoint === 'session/control')
|
|
248
|
-
return this.sessionControl(signal);
|
|
245
|
+
return this.events.sessionControl(signal);
|
|
249
246
|
if (endpoint === 'session/follow')
|
|
250
|
-
return this.sessionFollow(requestArg(args), signal);
|
|
247
|
+
return this.live.sessionFollow(requestArg(args), signal);
|
|
251
248
|
if (endpoint === '$events')
|
|
252
|
-
return this.remoteEvents(signal);
|
|
249
|
+
return this.events.remoteEvents(signal);
|
|
253
250
|
if (endpoint === 'job/list' || endpoint === 'job/follow')
|
|
254
|
-
return this.emptyJobStream(endpoint, signal);
|
|
251
|
+
return this.events.emptyJobStream(endpoint, signal);
|
|
255
252
|
if (this.hostCarrier !== undefined)
|
|
256
253
|
return this.hostCarrier.open(endpoint, payload, signal);
|
|
257
254
|
throw Object.assign(new Error(`${this.backendLabel()} virtual Harness does not implement stream ${endpoint}.`), {
|
|
@@ -341,7 +338,7 @@ export class AcpVirtualHarness {
|
|
|
341
338
|
updatedAt: new Date().toISOString(),
|
|
342
339
|
});
|
|
343
340
|
}
|
|
344
|
-
this.emitRemoteEvent('api-session/removed', [sessionId]);
|
|
341
|
+
this.events.emitRemoteEvent('api-session/removed', [sessionId]);
|
|
345
342
|
this.publishWorkspaceBaseline();
|
|
346
343
|
return success({ archivedSessionIds: [sessionId] });
|
|
347
344
|
}
|
|
@@ -357,26 +354,26 @@ export class AcpVirtualHarness {
|
|
|
357
354
|
return success({ sessionId: reusable.sessionId });
|
|
358
355
|
}
|
|
359
356
|
const created = await this.client.createSession(cwd, 'agent', this.backend, signal);
|
|
360
|
-
if (this.backend
|
|
357
|
+
if (this.backend !== 'cursor' && this.sessions.has(`${sessionPrefix(this.backend)}${created.sessionId}`)) {
|
|
361
358
|
return failure('session-already-exists', 'The backend returned an existing conversation for a new Session.');
|
|
362
359
|
}
|
|
363
360
|
const session = this.registerSession(created.sessionId, cwd, workspace?.title);
|
|
364
361
|
this.attachSessionToWorkspace(cwd, session.sessionId);
|
|
365
362
|
this.publishWorkspaceBaseline();
|
|
366
363
|
const seq = this.nextProjectionSeq();
|
|
367
|
-
this.emitRemoteEvent('api-session/added', [this.sessionSummary(session, seq)]);
|
|
368
|
-
this.publishProjection(session.sessionId, 'title', session.title ?? this.backendLabel(), seq);
|
|
364
|
+
this.events.emitRemoteEvent('api-session/added', [this.sessionSummary(session, seq)]);
|
|
365
|
+
this.events.publishProjection(session.sessionId, 'title', session.title ?? this.backendLabel(), seq);
|
|
369
366
|
return success({ sessionId: session.sessionId });
|
|
370
367
|
}
|
|
371
368
|
ensureSessionRegistered(sessionId) {
|
|
372
369
|
let session = this.sessions.get(sessionId);
|
|
373
370
|
if (session !== undefined)
|
|
374
371
|
return session;
|
|
375
|
-
if (this.backend
|
|
376
|
-
const acpSessionId = sessionId.slice(
|
|
372
|
+
if (this.backend !== 'cursor' && sessionId.startsWith(sessionPrefix(this.backend))) {
|
|
373
|
+
const acpSessionId = sessionId.slice(sessionPrefix(this.backend).length);
|
|
377
374
|
const workspace = this.selectedWorkspaceId !== undefined ? this.workspaceById.get(this.selectedWorkspaceId) : undefined;
|
|
378
375
|
const cwd = workspace?.path ?? '/';
|
|
379
|
-
session = this.registerSession(acpSessionId, cwd,
|
|
376
|
+
session = this.registerSession(acpSessionId, cwd, this.backendLabel(), Date.now(), Date.now(), false);
|
|
380
377
|
if (workspace && !workspace.sessionIds.includes(sessionId)) {
|
|
381
378
|
workspace.sessionIds.unshift(sessionId);
|
|
382
379
|
workspace.sessionCount = workspace.sessionIds.length;
|
|
@@ -409,14 +406,14 @@ export class AcpVirtualHarness {
|
|
|
409
406
|
const requestId = typeof request.requestId === 'string' && request.requestId.length > 0
|
|
410
407
|
? request.requestId
|
|
411
408
|
: undefined;
|
|
412
|
-
await this.ensureFollow(session);
|
|
409
|
+
await this.live.ensureFollow(session);
|
|
413
410
|
const follow = [...this.follows].find(f => f.sessionId === sessionId);
|
|
414
411
|
if (follow !== undefined) {
|
|
415
412
|
follow.turn += 1;
|
|
416
413
|
follow.stepOpen = true;
|
|
417
|
-
this.pushEvent(follow, 'turn/start', { turn: follow.turn });
|
|
418
|
-
this.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
|
|
419
|
-
this.pushEvent(follow, 'user/message', {
|
|
414
|
+
this.live.pushEvent(follow, 'turn/start', { turn: follow.turn });
|
|
415
|
+
this.live.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
|
|
416
|
+
this.live.pushEvent(follow, 'user/message', {
|
|
420
417
|
id: `user:${Date.now()}`,
|
|
421
418
|
role: 'user',
|
|
422
419
|
content: [...(text ? [{ type: 'text', text }] : []), ...imageBlocks],
|
|
@@ -426,7 +423,7 @@ export class AcpVirtualHarness {
|
|
|
426
423
|
session.blank = false;
|
|
427
424
|
session.running = true;
|
|
428
425
|
session.updatedAt = Date.now();
|
|
429
|
-
this.emitRemoteEvent('api-session/status', [sessionId, true]);
|
|
426
|
+
this.events.emitRemoteEvent('api-session/status', [sessionId, true]);
|
|
430
427
|
await this.client.prompt(session.acpSessionId, text ?? '', signal, images, this.backend);
|
|
431
428
|
return success({ accepted: true });
|
|
432
429
|
}
|
|
@@ -439,10 +436,10 @@ export class AcpVirtualHarness {
|
|
|
439
436
|
session.running = false;
|
|
440
437
|
for (const follow of this.follows) {
|
|
441
438
|
if (follow.sessionId === sessionId) {
|
|
442
|
-
this.closeFollowAfterRemoteStreamClosed(follow);
|
|
439
|
+
this.live.closeFollowAfterRemoteStreamClosed(follow, { kind: 'aborted', reason: { kind: 'user' } });
|
|
443
440
|
}
|
|
444
441
|
}
|
|
445
|
-
this.emitRemoteEvent('api-session/status', [sessionId, false]);
|
|
442
|
+
this.events.emitRemoteEvent('api-session/status', [sessionId, false]);
|
|
446
443
|
return success({ accepted: true });
|
|
447
444
|
}
|
|
448
445
|
async renameSession(request) {
|
|
@@ -456,7 +453,7 @@ export class AcpVirtualHarness {
|
|
|
456
453
|
session.title = title;
|
|
457
454
|
session.titleOverridden = true;
|
|
458
455
|
session.updatedAt = Date.now();
|
|
459
|
-
this.publishProjection(sessionId, 'title', title);
|
|
456
|
+
this.events.publishProjection(sessionId, 'title', title);
|
|
460
457
|
return success({ sessionId });
|
|
461
458
|
}
|
|
462
459
|
cacheImageAttachments(sessionId, content) {
|
|
@@ -479,15 +476,35 @@ export class AcpVirtualHarness {
|
|
|
479
476
|
return cached?.sessionId === sessionId ? success({ attachment: cached.attachment, data: cached.data }) : failure('attachment-error', 'The AGY image is unavailable or expired.');
|
|
480
477
|
}
|
|
481
478
|
async hydrateSession(session) {
|
|
482
|
-
if (this.backend
|
|
479
|
+
if (this.backend === 'cursor' || session.blank || session.events.length > 0)
|
|
483
480
|
return;
|
|
481
|
+
if (this.backend !== 'pi')
|
|
482
|
+
return this.hydrateSessionOnce(session);
|
|
483
|
+
const pending = this.pendingHistory.get(session.sessionId);
|
|
484
|
+
if (pending)
|
|
485
|
+
return pending;
|
|
486
|
+
const read = this.hydrateSessionOnce(session);
|
|
487
|
+
this.pendingHistory.set(session.sessionId, read);
|
|
488
|
+
try {
|
|
489
|
+
await read;
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
if (this.pendingHistory.get(session.sessionId) === read)
|
|
493
|
+
this.pendingHistory.delete(session.sessionId);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async hydrateSessionOnce(session) {
|
|
484
497
|
let events = [];
|
|
485
498
|
if (this.client.loadSessionHistory !== undefined) {
|
|
486
499
|
try {
|
|
487
500
|
events = await this.client.loadSessionHistory(session.sessionId, this.backend);
|
|
488
501
|
}
|
|
489
|
-
catch {
|
|
490
|
-
//
|
|
502
|
+
catch (error) {
|
|
503
|
+
// A failed PI read is not a successful empty baseline; do not continue
|
|
504
|
+
// a restored conversation until its authoritative history is available.
|
|
505
|
+
if (this.backend === 'pi')
|
|
506
|
+
throw error;
|
|
507
|
+
// Preserve legacy AGY history availability behavior.
|
|
491
508
|
}
|
|
492
509
|
}
|
|
493
510
|
if (events.length > 0) {
|
|
@@ -514,7 +531,7 @@ export class AcpVirtualHarness {
|
|
|
514
531
|
projections: {
|
|
515
532
|
asOfSeq: page.cursor,
|
|
516
533
|
values: {
|
|
517
|
-
title: session.title ?? (this.backend ===
|
|
534
|
+
title: session.title ?? (this.backend === "pi" ? "PI" : this.backend === "opencode" ? "OpenCode" : this.backend === "antigravity" ? "Antigravity" : "Cursor"),
|
|
518
535
|
sessionListMetadata: { blank: session.blank && session.events.length === 0, lastPromptAt: null },
|
|
519
536
|
modelSelection: this.modelSelectionProjection(sessionId),
|
|
520
537
|
imageLimits: acpImageLimits(this.backend),
|
|
@@ -522,6 +539,14 @@ export class AcpVirtualHarness {
|
|
|
522
539
|
},
|
|
523
540
|
});
|
|
524
541
|
}
|
|
542
|
+
async sessionProjections(request) {
|
|
543
|
+
const sessionId = extractSessionId(request);
|
|
544
|
+
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
545
|
+
if (session === undefined)
|
|
546
|
+
return success(null);
|
|
547
|
+
await this.hydrateSession(session);
|
|
548
|
+
return success(this.sessionSummary(session, session.events.at(-1)?.event.seq ?? -1).projections);
|
|
549
|
+
}
|
|
525
550
|
async sessionPage(request) {
|
|
526
551
|
const sessionId = extractSessionId(request);
|
|
527
552
|
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
@@ -583,437 +608,9 @@ export class AcpVirtualHarness {
|
|
|
583
608
|
const path = string(request.path) ?? workspace.path;
|
|
584
609
|
return success(await this.client.listDirectory(path, signal));
|
|
585
610
|
}
|
|
586
|
-
async answerRemoteEvent(args, signal) {
|
|
587
|
-
const eventId = string(args.eventId);
|
|
588
|
-
const outcome = record(args.outcome);
|
|
589
|
-
if (eventId === undefined)
|
|
590
|
-
throw new Error(`The ${this.backendLabel()} approval result is missing its event id.`);
|
|
591
|
-
const pending = this.pendingApprovals.get(eventId);
|
|
592
|
-
if (pending === undefined)
|
|
593
|
-
return undefined;
|
|
594
|
-
this.pendingApprovals.delete(eventId);
|
|
595
|
-
const decision = outcome.kind === 'result' && outcome.value === 'allowed-once'
|
|
596
|
-
? 'allow-once'
|
|
597
|
-
: outcome.kind === 'result' && outcome.value === 'cancelled'
|
|
598
|
-
? 'cancel'
|
|
599
|
-
: 'reject-once';
|
|
600
|
-
await this.client.respond(pending.requestHandle, decision, undefined, signal);
|
|
601
|
-
return undefined;
|
|
602
|
-
}
|
|
603
|
-
async workspaceFollow(signal) {
|
|
604
|
-
const queue = new AsyncValueQueue(signal);
|
|
605
|
-
this.workspaceStreams.add(queue);
|
|
606
|
-
queue.push({
|
|
607
|
-
type: 'baseline',
|
|
608
|
-
value: {
|
|
609
|
-
items: this.visibleWorkspaces().map(nativeWorkspace),
|
|
610
|
-
archivedSessionIds: [],
|
|
611
|
-
pinnedSessionIds: [],
|
|
612
|
-
},
|
|
613
|
-
});
|
|
614
|
-
return queue.iterate(() => this.workspaceStreams.delete(queue));
|
|
615
|
-
}
|
|
616
|
-
async sessionControl(signal) {
|
|
617
|
-
const queue = new AsyncValueQueue(signal);
|
|
618
|
-
this.controlStreams.add(queue);
|
|
619
|
-
// Session Controller treats the first control frame as the opening
|
|
620
|
-
// snapshot. Without it the Client-side RemoteSnapshotStream never moves
|
|
621
|
-
// to `ready`, so a workspace selected through the Remote modal remains
|
|
622
|
-
// stuck on the empty shell after the page reload.
|
|
623
|
-
queue.push({ type: 'baseline', value: { projections: {} } });
|
|
624
|
-
return queue.iterate(() => this.controlStreams.delete(queue));
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Background jobs belong to the Host Harness session, not to a backend-scoped
|
|
628
|
-
* virtual Workspace. The native session header mounts a job roster stream as
|
|
629
|
-
* soon as a Session opens, so answer it with an authoritative empty roster
|
|
630
|
-
* instead of a terminal `method-not-found` failure that tears the page down.
|
|
631
|
-
*/
|
|
632
|
-
emptyJobStream(endpoint, signal) {
|
|
633
|
-
const queue = new AsyncValueQueue(signal);
|
|
634
|
-
if (endpoint === 'job/list')
|
|
635
|
-
queue.push({ type: 'rows', jobs: [] });
|
|
636
|
-
return queue.iterate(() => queue.close());
|
|
637
|
-
}
|
|
638
|
-
async remoteEvents(signal) {
|
|
639
|
-
const id = `cursor-events:${Date.now()}:${Math.random()}`;
|
|
640
|
-
const queue = new AsyncValueQueue(signal);
|
|
641
|
-
this.eventStreams.set(id, queue);
|
|
642
|
-
const workspace = this.selectedWorkspaceId === undefined
|
|
643
|
-
? undefined
|
|
644
|
-
: this.workspaceById.get(this.selectedWorkspaceId);
|
|
645
|
-
queue.push({
|
|
646
|
-
type: 'ready',
|
|
647
|
-
clientId: id,
|
|
648
|
-
host: { home: workspace?.path ?? '/' },
|
|
649
|
-
});
|
|
650
|
-
return queue.iterate(() => this.eventStreams.delete(id));
|
|
651
|
-
}
|
|
652
|
-
async sessionFollow(request, signal) {
|
|
653
|
-
const sessionId = extractSessionId(request);
|
|
654
|
-
const session = this.ensureSessionRegistered(sessionId) ?? this.sessions.get(sessionId);
|
|
655
|
-
if (session === undefined)
|
|
656
|
-
throw new Error('The Session was not found.');
|
|
657
|
-
await this.hydrateSession(session);
|
|
658
|
-
const history = this.historyPage(session, {
|
|
659
|
-
limit: optionalPositiveInteger(request.maxMessages) ?? 50,
|
|
660
|
-
});
|
|
661
|
-
const queue = new AsyncValueQueue(signal);
|
|
662
|
-
const follow = {
|
|
663
|
-
sessionId,
|
|
664
|
-
acpSessionId: session.acpSessionId,
|
|
665
|
-
queue,
|
|
666
|
-
nextSeq: session.events.length,
|
|
667
|
-
turn: Math.max(0, ...session.events.map(entry => (isRecord(entry.event.data) && typeof entry.event.data.turn === 'number' ? entry.event.data.turn : 0))),
|
|
668
|
-
stepOpen: session.running,
|
|
669
|
-
streamActive: false,
|
|
670
|
-
assistantStreamRevision: 0,
|
|
671
|
-
accumulatedText: '',
|
|
672
|
-
activeToolCalls: new Set(),
|
|
673
|
-
};
|
|
674
|
-
this.follows.add(follow);
|
|
675
|
-
queue.push({
|
|
676
|
-
type: 'snapshot',
|
|
677
|
-
header: history.header,
|
|
678
|
-
cursor: history.cursor,
|
|
679
|
-
records: history.records,
|
|
680
|
-
hasMore: history.hasMore,
|
|
681
|
-
projections: {
|
|
682
|
-
asOfSeq: history.cursor,
|
|
683
|
-
values: {
|
|
684
|
-
title: session.title ?? (this.backend === 'antigravity' ? 'Antigravity' : 'Cursor'),
|
|
685
|
-
sessionListMetadata: { blank: session.blank && session.events.length === 0, lastPromptAt: null },
|
|
686
|
-
modelSelection: this.modelSelectionProjection(sessionId),
|
|
687
|
-
imageLimits: acpImageLimits(this.backend),
|
|
688
|
-
},
|
|
689
|
-
},
|
|
690
|
-
assistantStream: { revision: 0 },
|
|
691
|
-
});
|
|
692
|
-
try {
|
|
693
|
-
const stream = await this.client.openStream(session.acpSessionId, frame => this.acceptAcpFrame(follow, frame), () => {
|
|
694
|
-
session.running = false;
|
|
695
|
-
this.emitRemoteEvent('api-session/status', [sessionId, false]);
|
|
696
|
-
this.closeFollowAfterRemoteStreamClosed(follow);
|
|
697
|
-
}, signal);
|
|
698
|
-
follow.close = () => stream.close();
|
|
699
|
-
}
|
|
700
|
-
catch (error) {
|
|
701
|
-
this.follows.delete(follow);
|
|
702
|
-
queue.close();
|
|
703
|
-
throw error;
|
|
704
|
-
}
|
|
705
|
-
return queue.iterate(() => {
|
|
706
|
-
this.follows.delete(follow);
|
|
707
|
-
void follow.close?.().catch(() => undefined);
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
async ensureFollow(session) {
|
|
711
|
-
if ([...this.follows].some(follow => follow.sessionId === session.sessionId))
|
|
712
|
-
return;
|
|
713
|
-
// Native UI opens session/follow; prompts before follow still work via Host stream open on demand.
|
|
714
|
-
const controller = new AbortController();
|
|
715
|
-
const queue = new AsyncValueQueue(controller.signal);
|
|
716
|
-
const follow = {
|
|
717
|
-
sessionId: session.sessionId,
|
|
718
|
-
acpSessionId: session.acpSessionId,
|
|
719
|
-
queue,
|
|
720
|
-
nextSeq: session.events.length === 0 ? 0 : Math.max(...session.events.map(entry => entry.event.seq)) + 1,
|
|
721
|
-
turn: Math.max(0, ...session.events.map(entry => (isRecord(entry.event.data) && typeof entry.event.data.turn === 'number' ? entry.event.data.turn : 0))),
|
|
722
|
-
stepOpen: false,
|
|
723
|
-
streamActive: false,
|
|
724
|
-
assistantStreamRevision: 0,
|
|
725
|
-
accumulatedText: '',
|
|
726
|
-
activeToolCalls: new Set(),
|
|
727
|
-
};
|
|
728
|
-
this.follows.add(follow);
|
|
729
|
-
const stream = await this.client.openStream(session.acpSessionId, frame => this.acceptAcpFrame(follow, frame), () => this.closeFollowAfterRemoteStreamClosed(follow));
|
|
730
|
-
follow.close = async () => {
|
|
731
|
-
controller.abort();
|
|
732
|
-
await stream.close();
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
acceptAcpFrame(follow, frame) {
|
|
736
|
-
const method = frame.frame.method;
|
|
737
|
-
const params = record(frame.frame.params);
|
|
738
|
-
if (method === 'session/update') {
|
|
739
|
-
this.acceptSessionUpdate(follow, params);
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
if (method === 'session/request_permission' || method === 'cursor/ask_question' || method === 'cursor/create_plan') {
|
|
743
|
-
this.acceptApproval(follow, params, method);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
acceptSessionUpdate(follow, params) {
|
|
747
|
-
const update = isRecord(params.update) ? params.update : params;
|
|
748
|
-
const kind = string(update.sessionUpdate) ?? string(update.type);
|
|
749
|
-
const session = this.sessions.get(follow.sessionId);
|
|
750
|
-
if (kind === 'model_selection' && isRecord(update.selected)
|
|
751
|
-
&& update.selected.provider === this.backend && typeof update.selected.model === 'string') {
|
|
752
|
-
this.selectedModels.set(follow.sessionId, update.selected);
|
|
753
|
-
this.publishProjection(follow.sessionId, 'modelSelection', this.modelSelectionProjection(follow.sessionId), this.nextProjectionSeq());
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if (kind === 'agent_message_chunk' || kind === 'agent_message') {
|
|
757
|
-
const text = extractText(update);
|
|
758
|
-
if (text === undefined || text.length === 0)
|
|
759
|
-
return;
|
|
760
|
-
this.appendAssistantDelta(follow, text);
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
764
|
-
const toolName = string(update.title) ?? string(update.toolName) ?? string(update.name) ?? 'tool';
|
|
765
|
-
const status = string(update.status);
|
|
766
|
-
const callId = string(update.toolCallId) ?? string(update.callId) ?? toolName;
|
|
767
|
-
const terminal = status === 'completed' || status === 'failed';
|
|
768
|
-
if (kind === 'tool_call_update' && !terminal)
|
|
769
|
-
return;
|
|
770
|
-
if (kind === 'tool_call' && follow.activeToolCalls.has(callId))
|
|
771
|
-
return;
|
|
772
|
-
this.pushEvent(follow, 'tool/call', {
|
|
773
|
-
turn: follow.turn,
|
|
774
|
-
step: 1,
|
|
775
|
-
callId,
|
|
776
|
-
name: toolName,
|
|
777
|
-
arguments: typeof update.parameters === 'object' && update.parameters !== null
|
|
778
|
-
? JSON.stringify(update.parameters)
|
|
779
|
-
: typeof update.rawInput === 'object' && update.rawInput !== null
|
|
780
|
-
? JSON.stringify(update.rawInput)
|
|
781
|
-
: '{}',
|
|
782
|
-
toolCallId: callId,
|
|
783
|
-
toolName,
|
|
784
|
-
status: status === 'completed' ? 'finished' : status === 'failed' ? 'failed' : 'running',
|
|
785
|
-
});
|
|
786
|
-
if (terminal)
|
|
787
|
-
follow.activeToolCalls.delete(callId);
|
|
788
|
-
else
|
|
789
|
-
follow.activeToolCalls.add(callId);
|
|
790
|
-
if (session !== undefined)
|
|
791
|
-
session.updatedAt = Date.now();
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
if (kind === 'agent_thought_chunk') {
|
|
795
|
-
const text = extractText(update);
|
|
796
|
-
if (text === undefined || text.length === 0)
|
|
797
|
-
return;
|
|
798
|
-
this.appendReasoningDelta(follow, text);
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
if (kind === 'prompt_completed' || kind === 'prompt_failed') {
|
|
802
|
-
const hasLiveContent = follow.stepOpen || follow.streamActive;
|
|
803
|
-
if (!hasLiveContent && Array.isArray(update.catchUp)) {
|
|
804
|
-
for (const item of update.catchUp) {
|
|
805
|
-
if (!isRecord(item) || typeof item.method !== 'string')
|
|
806
|
-
continue;
|
|
807
|
-
const params = isRecord(item.params) ? item.params : {};
|
|
808
|
-
if (item.method === 'session/update')
|
|
809
|
-
this.acceptSessionUpdate(follow, params);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
if (session !== undefined) {
|
|
813
|
-
session.running = false;
|
|
814
|
-
session.updatedAt = Date.now();
|
|
815
|
-
this.emitRemoteEvent('api-session/status', [follow.sessionId, false]);
|
|
816
|
-
if (kind === 'prompt_completed')
|
|
817
|
-
void this.refreshSessionTitle(session);
|
|
818
|
-
}
|
|
819
|
-
this.closeFollowAfterRemoteStreamClosed(follow);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
acceptApproval(follow, params, method) {
|
|
823
|
-
const requestHandle = string(params.requestHandle);
|
|
824
|
-
if (requestHandle === undefined)
|
|
825
|
-
return;
|
|
826
|
-
this.pendingApprovals.set(requestHandle, { requestHandle, sessionId: follow.sessionId });
|
|
827
|
-
const toolName = method === 'cursor/create_plan'
|
|
828
|
-
? (string(params.name) ?? 'plan')
|
|
829
|
-
: method === 'cursor/ask_question'
|
|
830
|
-
? (string(params.title) ?? 'question')
|
|
831
|
-
: (string(params.toolName) ?? 'permission');
|
|
832
|
-
const reason = method === 'cursor/create_plan'
|
|
833
|
-
? string(params.overview) ?? string(params.plan)
|
|
834
|
-
: method === 'cursor/ask_question'
|
|
835
|
-
? summarizeQuestions(params)
|
|
836
|
-
: string(params.reason);
|
|
837
|
-
this.emitApproval({
|
|
838
|
-
eventId: requestHandle,
|
|
839
|
-
agentId: follow.sessionId,
|
|
840
|
-
request: {
|
|
841
|
-
toolName,
|
|
842
|
-
...(reason === undefined ? {} : { reason }),
|
|
843
|
-
},
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
pushAssistantChunk(follow, chunk) {
|
|
847
|
-
let attempt = follow.assistantAttempt;
|
|
848
|
-
if (attempt === undefined) {
|
|
849
|
-
attempt = {
|
|
850
|
-
attemptId: `cursor-attempt:${follow.turn}:${follow.nextSeq}`,
|
|
851
|
-
startedAfterSeq: follow.nextSeq - 1,
|
|
852
|
-
nextIndex: 0,
|
|
853
|
-
stream: [],
|
|
854
|
-
};
|
|
855
|
-
follow.assistantAttempt = attempt;
|
|
856
|
-
follow.assistantStreamRevision += 1;
|
|
857
|
-
follow.queue.push({
|
|
858
|
-
type: 'assistant-stream',
|
|
859
|
-
frame: {
|
|
860
|
-
type: 'start',
|
|
861
|
-
attemptId: attempt.attemptId,
|
|
862
|
-
revision: follow.assistantStreamRevision,
|
|
863
|
-
startedAfterSeq: attempt.startedAfterSeq,
|
|
864
|
-
turn: follow.turn,
|
|
865
|
-
step: 1,
|
|
866
|
-
},
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
const time = Date.now();
|
|
870
|
-
const index = attempt.nextIndex++;
|
|
871
|
-
// Durable streams use AssistantStreamRecord, unlike the timed chunks in
|
|
872
|
-
// the live feed. Without this discriminator the native reader treats a
|
|
873
|
-
// raw chunk as a packed run and throws while settling the conversation.
|
|
874
|
-
attempt.stream.push({ type: 'chunk', time, chunk });
|
|
875
|
-
follow.assistantStreamRevision += 1;
|
|
876
|
-
follow.queue.push({
|
|
877
|
-
type: 'assistant-stream',
|
|
878
|
-
frame: {
|
|
879
|
-
type: 'chunk',
|
|
880
|
-
attemptId: attempt.attemptId,
|
|
881
|
-
revision: follow.assistantStreamRevision,
|
|
882
|
-
index,
|
|
883
|
-
time,
|
|
884
|
-
chunk,
|
|
885
|
-
},
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
endAssistantAttempt(follow, outcome) {
|
|
889
|
-
const attempt = follow.assistantAttempt;
|
|
890
|
-
if (attempt === undefined)
|
|
891
|
-
return;
|
|
892
|
-
follow.assistantAttempt = undefined;
|
|
893
|
-
follow.assistantStreamRevision += 1;
|
|
894
|
-
follow.queue.push({
|
|
895
|
-
type: 'assistant-stream',
|
|
896
|
-
frame: {
|
|
897
|
-
type: 'end',
|
|
898
|
-
attemptId: attempt.attemptId,
|
|
899
|
-
revision: follow.assistantStreamRevision,
|
|
900
|
-
index: attempt.nextIndex,
|
|
901
|
-
outcome,
|
|
902
|
-
},
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
appendAssistantDelta(follow, delta) {
|
|
906
|
-
if (!follow.stepOpen) {
|
|
907
|
-
follow.turn += 1;
|
|
908
|
-
follow.stepOpen = true;
|
|
909
|
-
this.pushEvent(follow, 'turn/start', { turn: follow.turn });
|
|
910
|
-
this.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
|
|
911
|
-
}
|
|
912
|
-
if (follow.blockIndex === undefined) {
|
|
913
|
-
follow.blockIndex = (follow.reasoningBlockIndex ?? -1) + 1;
|
|
914
|
-
follow.streamActive = true;
|
|
915
|
-
this.pushAssistantChunk(follow, { type: 'block-start', index: follow.blockIndex, blockType: 'text' });
|
|
916
|
-
}
|
|
917
|
-
follow.accumulatedText += delta;
|
|
918
|
-
this.pushAssistantChunk(follow, { type: 'text-delta', index: follow.blockIndex, text: delta });
|
|
919
|
-
}
|
|
920
|
-
appendReasoningDelta(follow, delta) {
|
|
921
|
-
if (!follow.stepOpen) {
|
|
922
|
-
follow.turn += 1;
|
|
923
|
-
follow.stepOpen = true;
|
|
924
|
-
this.pushEvent(follow, 'turn/start', { turn: follow.turn });
|
|
925
|
-
this.pushEvent(follow, 'step/start', { turn: follow.turn, step: 1 });
|
|
926
|
-
}
|
|
927
|
-
if (follow.reasoningBlockIndex === undefined) {
|
|
928
|
-
follow.reasoningBlockIndex = (follow.blockIndex ?? -1) + 1;
|
|
929
|
-
follow.streamActive = true;
|
|
930
|
-
this.pushAssistantChunk(follow, { type: 'block-start', index: follow.reasoningBlockIndex, blockType: 'reasoning' });
|
|
931
|
-
}
|
|
932
|
-
this.pushAssistantChunk(follow, { type: 'reasoning-delta', index: follow.reasoningBlockIndex, text: delta });
|
|
933
|
-
}
|
|
934
|
-
closeFollowAfterRemoteStreamClosed(follow) {
|
|
935
|
-
if (follow.streamActive) {
|
|
936
|
-
if (follow.reasoningBlockIndex !== undefined) {
|
|
937
|
-
this.pushAssistantChunk(follow, {
|
|
938
|
-
type: 'block-end',
|
|
939
|
-
index: follow.reasoningBlockIndex,
|
|
940
|
-
block: { type: 'reasoning', text: '' },
|
|
941
|
-
});
|
|
942
|
-
follow.reasoningBlockIndex = undefined;
|
|
943
|
-
}
|
|
944
|
-
if (follow.blockIndex !== undefined) {
|
|
945
|
-
const fullText = follow.accumulatedText;
|
|
946
|
-
this.pushAssistantChunk(follow, {
|
|
947
|
-
type: 'block-end',
|
|
948
|
-
index: follow.blockIndex,
|
|
949
|
-
block: { type: 'text', text: fullText },
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
this.pushAssistantChunk(follow, {
|
|
953
|
-
type: 'finish',
|
|
954
|
-
reason: { kind: 'stop' },
|
|
955
|
-
});
|
|
956
|
-
const fullText = follow.accumulatedText;
|
|
957
|
-
const selection = this.modelSelection(follow.sessionId);
|
|
958
|
-
const stream = follow.assistantAttempt?.stream ?? [];
|
|
959
|
-
const assistantMessageSeq = this.pushEvent(follow, 'assistant/message', {
|
|
960
|
-
turn: follow.turn,
|
|
961
|
-
step: 1,
|
|
962
|
-
message: {
|
|
963
|
-
id: `${follow.sessionId}:${follow.turn}`,
|
|
964
|
-
role: 'assistant',
|
|
965
|
-
content: [{ type: 'text', text: fullText }],
|
|
966
|
-
source: { kind: 'model', provider: selection.provider, model: selection.model },
|
|
967
|
-
},
|
|
968
|
-
stream,
|
|
969
|
-
});
|
|
970
|
-
this.endAssistantAttempt(follow, {
|
|
971
|
-
kind: 'committed',
|
|
972
|
-
eventType: 'assistant/message',
|
|
973
|
-
seq: assistantMessageSeq,
|
|
974
|
-
});
|
|
975
|
-
this.pushEvent(follow, 'step/end', { turn: follow.turn, step: 1 });
|
|
976
|
-
this.pushEvent(follow, 'turn/end', { turn: follow.turn, reason: { kind: 'completed' } });
|
|
977
|
-
}
|
|
978
|
-
else if (follow.stepOpen) {
|
|
979
|
-
this.pushEvent(follow, 'step/end', { turn: follow.turn, step: 1 });
|
|
980
|
-
this.pushEvent(follow, 'turn/end', { turn: follow.turn, reason: { kind: 'completed' } });
|
|
981
|
-
}
|
|
982
|
-
follow.streamActive = false;
|
|
983
|
-
follow.stepOpen = false;
|
|
984
|
-
follow.blockIndex = undefined;
|
|
985
|
-
follow.reasoningBlockIndex = undefined;
|
|
986
|
-
follow.accumulatedText = '';
|
|
987
|
-
}
|
|
988
|
-
pushEvent(follow, type, data) {
|
|
989
|
-
const session = this.sessions.get(follow.sessionId);
|
|
990
|
-
const seq = session !== undefined ? session.events.length : follow.nextSeq++;
|
|
991
|
-
const event = {
|
|
992
|
-
type,
|
|
993
|
-
seq,
|
|
994
|
-
time: Date.now(),
|
|
995
|
-
data,
|
|
996
|
-
...(isSurfaceEvent(type) ? { surfaceOp: 'append' } : {}),
|
|
997
|
-
};
|
|
998
|
-
if (session !== undefined) {
|
|
999
|
-
session.events.push({ type: 'event', event });
|
|
1000
|
-
}
|
|
1001
|
-
for (const f of this.follows) {
|
|
1002
|
-
if (f.sessionId === follow.sessionId) {
|
|
1003
|
-
f.queue.push({ type: 'event', event });
|
|
1004
|
-
f.nextSeq = seq + 1;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
this.broadcastRcMux({
|
|
1008
|
-
type: 'session/event',
|
|
1009
|
-
sessionId: follow.sessionId,
|
|
1010
|
-
event,
|
|
1011
|
-
});
|
|
1012
|
-
return seq;
|
|
1013
|
-
}
|
|
1014
611
|
registerSession(acpSessionId, cwd, title, createdAt = Date.now(), updatedAt = Date.now(), blank = true) {
|
|
1015
612
|
const session = {
|
|
1016
|
-
sessionId: `${this.backend
|
|
613
|
+
sessionId: `${sessionPrefix(this.backend)}${acpSessionId}`,
|
|
1017
614
|
acpSessionId,
|
|
1018
615
|
cwd,
|
|
1019
616
|
...(title === undefined ? {} : { title }),
|
|
@@ -1031,10 +628,10 @@ export class AcpVirtualHarness {
|
|
|
1031
628
|
if (this.closed || session.titleOverridden || !next || next === 'Untitled Session' || next === session.title)
|
|
1032
629
|
return;
|
|
1033
630
|
session.title = next;
|
|
1034
|
-
this.publishProjection(session.sessionId, 'title', next);
|
|
631
|
+
this.events.publishProjection(session.sessionId, 'title', next);
|
|
1035
632
|
}
|
|
1036
633
|
async refreshSessionTitle(session) {
|
|
1037
|
-
if (this.backend
|
|
634
|
+
if (this.backend === 'cursor' || this.closed || session.titleOverridden || this.client.listSessions === undefined)
|
|
1038
635
|
return;
|
|
1039
636
|
try {
|
|
1040
637
|
const items = await this.client.listSessions(session.cwd, this.backend, 30);
|
|
@@ -1047,7 +644,7 @@ export class AcpVirtualHarness {
|
|
|
1047
644
|
}
|
|
1048
645
|
}
|
|
1049
646
|
async discoverAndAttachSessions(workspace) {
|
|
1050
|
-
if (this.backend
|
|
647
|
+
if (this.backend === 'cursor')
|
|
1051
648
|
return;
|
|
1052
649
|
try {
|
|
1053
650
|
let discovered = [];
|
|
@@ -1055,12 +652,14 @@ export class AcpVirtualHarness {
|
|
|
1055
652
|
try {
|
|
1056
653
|
discovered = await this.client.listSessions(workspace.path, this.backend, 30);
|
|
1057
654
|
}
|
|
1058
|
-
catch {
|
|
1059
|
-
|
|
655
|
+
catch (error) {
|
|
656
|
+
if (this.backend === 'pi')
|
|
657
|
+
throw error;
|
|
658
|
+
// Preserve legacy AGY catalog availability behavior.
|
|
1060
659
|
}
|
|
1061
660
|
}
|
|
1062
661
|
for (const item of discovered) {
|
|
1063
|
-
const sessionId = `${
|
|
662
|
+
const sessionId = `${sessionPrefix(this.backend)}${item.conversationId}`;
|
|
1064
663
|
if (!this.sessions.has(sessionId)) {
|
|
1065
664
|
this.registerSession(item.conversationId, workspace.path, item.title, item.createdAt, item.updatedAt, false);
|
|
1066
665
|
}
|
|
@@ -1074,8 +673,10 @@ export class AcpVirtualHarness {
|
|
|
1074
673
|
workspace.sessionCount = workspace.sessionIds.length;
|
|
1075
674
|
workspace.updatedAt = new Date().toISOString();
|
|
1076
675
|
}
|
|
1077
|
-
catch {
|
|
1078
|
-
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (this.backend === 'pi')
|
|
678
|
+
throw error;
|
|
679
|
+
// Preserve legacy AGY catalog availability behavior.
|
|
1079
680
|
}
|
|
1080
681
|
}
|
|
1081
682
|
attachSessionToWorkspace(cwd, sessionId) {
|
|
@@ -1096,10 +697,10 @@ export class AcpVirtualHarness {
|
|
|
1096
697
|
const cwd = workspace.path;
|
|
1097
698
|
const created = await this.client.createSession(cwd, 'agent', this.backend);
|
|
1098
699
|
const sessionId = created.sessionId;
|
|
1099
|
-
const title = this.backend === 'antigravity' ? 'Antigravity' : workspace.title;
|
|
700
|
+
const title = this.backend === 'antigravity' ? 'Antigravity' : this.backend === 'opencode' ? 'OpenCode' : workspace.title;
|
|
1100
701
|
const session = this.registerSession(sessionId, cwd, title);
|
|
1101
702
|
this.attachSessionToWorkspace(cwd, session.sessionId);
|
|
1102
|
-
this.emitRemoteEvent('api-session/added', [this.sessionSummary(session, 0)]);
|
|
703
|
+
this.events.emitRemoteEvent('api-session/added', [this.sessionSummary(session, 0)]);
|
|
1103
704
|
this.publishWorkspaceBaseline();
|
|
1104
705
|
return session;
|
|
1105
706
|
}
|
|
@@ -1133,7 +734,7 @@ export class AcpVirtualHarness {
|
|
|
1133
734
|
kind: 'sequenced',
|
|
1134
735
|
asOfSeq,
|
|
1135
736
|
values: {
|
|
1136
|
-
title: session.title ?? (this.backend ===
|
|
737
|
+
title: session.title ?? (this.backend === "pi" ? "PI" : this.backend === "opencode" ? "OpenCode" : this.backend === "antigravity" ? "Antigravity" : "Cursor"),
|
|
1137
738
|
sessionListMetadata: { blank: session.blank, lastPromptAt: null },
|
|
1138
739
|
modelSelection: this.modelSelectionProjection(session.sessionId),
|
|
1139
740
|
imageLimits: acpImageLimits(this.backend),
|
|
@@ -1149,9 +750,9 @@ export class AcpVirtualHarness {
|
|
|
1149
750
|
return { lastUsed: selection, next: selection };
|
|
1150
751
|
}
|
|
1151
752
|
async hostSessionModels(sessionId) {
|
|
1152
|
-
if (this.backend
|
|
753
|
+
if (this.backend === 'cursor' || this.backend === 'opencode' || !this.client.sessionModels)
|
|
1153
754
|
return { current: { provider: this.backend, model: 'host-settings' }, routable: true, groups: [], failures: [] };
|
|
1154
|
-
const models = await this.client.sessionModels(nativeAcpId(sessionId, this.backend),
|
|
755
|
+
const models = await this.client.sessionModels(nativeAcpId(sessionId, this.backend), this.backend);
|
|
1155
756
|
this.selectedModels.set(sessionId, models.current);
|
|
1156
757
|
return models;
|
|
1157
758
|
}
|
|
@@ -1161,7 +762,7 @@ export class AcpVirtualHarness {
|
|
|
1161
762
|
const provider = requiredString(request.provider, 'provider');
|
|
1162
763
|
const model = requiredString(request.model, 'model');
|
|
1163
764
|
const reasoningEffort = string(request.reasoningEffort);
|
|
1164
|
-
const catalog = this.backend
|
|
765
|
+
const catalog = this.backend !== 'cursor' ? await this.hostSessionModels(sessionId) : modelCatalog(this.backend);
|
|
1165
766
|
const group = catalog.groups.find(g => g.id === provider);
|
|
1166
767
|
const targetModel = group?.models.find(m => m.id === model);
|
|
1167
768
|
if (group === undefined || targetModel === undefined) {
|
|
@@ -1176,16 +777,16 @@ export class AcpVirtualHarness {
|
|
|
1176
777
|
model,
|
|
1177
778
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
1178
779
|
};
|
|
1179
|
-
if (this.backend
|
|
780
|
+
if (this.backend !== 'cursor' && this.backend !== 'opencode') {
|
|
1180
781
|
if (!this.client.selectModel)
|
|
1181
782
|
return failure('model-unavailable', 'The Host does not support ACP model selection.');
|
|
1182
|
-
const confirmed = await this.client.selectModel(nativeAcpId(sessionId, this.backend),
|
|
783
|
+
const confirmed = await this.client.selectModel(nativeAcpId(sessionId, this.backend), this.backend, selected);
|
|
1183
784
|
this.selectedModels.set(sessionId, confirmed);
|
|
1184
785
|
}
|
|
1185
786
|
else
|
|
1186
|
-
return failure('model-unavailable',
|
|
787
|
+
return failure('model-unavailable', `${this.backendLabel()} model selection follows Host settings.`);
|
|
1187
788
|
const seq = this.nextProjectionSeq();
|
|
1188
|
-
this.publishProjection(sessionId, 'modelSelection', this.modelSelectionProjection(sessionId), seq);
|
|
789
|
+
this.events.publishProjection(sessionId, 'modelSelection', this.modelSelectionProjection(sessionId), seq);
|
|
1189
790
|
return success({ selected: this.modelSelection(sessionId) });
|
|
1190
791
|
}
|
|
1191
792
|
publishWorkspaceBaseline() {
|
|
@@ -1196,393 +797,10 @@ export class AcpVirtualHarness {
|
|
|
1196
797
|
queue.push({ type: 'archived', archivedSessionIds: [] });
|
|
1197
798
|
}
|
|
1198
799
|
}
|
|
1199
|
-
publishProjection(sessionId, key, value, seq = this.nextProjectionSeq()) {
|
|
1200
|
-
for (const queue of this.controlStreams) {
|
|
1201
|
-
queue.push({ type: 'projection', sessionId, key, value, asOfSeq: seq, seq });
|
|
1202
|
-
}
|
|
1203
|
-
this.broadcastRcMux({ type: 'session/projection', sessionId, key, value, seq });
|
|
1204
|
-
}
|
|
1205
|
-
emitRemoteEvent(event, args) {
|
|
1206
|
-
for (const queue of this.eventStreams.values())
|
|
1207
|
-
queue.push({ type: 'emit', event, args });
|
|
1208
|
-
const sessionId = typeof args[0] === 'string' ? args[0] : undefined;
|
|
1209
|
-
if (event === 'api-session/status' && sessionId !== undefined && typeof args[1] === 'boolean') {
|
|
1210
|
-
this.broadcastRcHost({ type: 'host/session-status', sessionId, running: args[1] });
|
|
1211
|
-
}
|
|
1212
|
-
else if (event === 'api-session/removed' && sessionId !== undefined) {
|
|
1213
|
-
this.broadcastRcHost({ type: 'host/session-removed', sessionId });
|
|
1214
|
-
}
|
|
1215
|
-
else if (event === 'api-session/added' && isRecord(args[0])) {
|
|
1216
|
-
const summary = args[0];
|
|
1217
|
-
this.broadcastRcHost({
|
|
1218
|
-
type: 'host/session-added',
|
|
1219
|
-
sessionId: summary.sessionId,
|
|
1220
|
-
blank: summary.blank === true,
|
|
1221
|
-
...(typeof summary.cwd === 'string' ? { cwd: summary.cwd } : {}),
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
emitApproval(input) {
|
|
1226
|
-
for (const queue of this.eventStreams.values())
|
|
1227
|
-
queue.push({
|
|
1228
|
-
type: 'waterfall',
|
|
1229
|
-
event: 'approval/request',
|
|
1230
|
-
eventId: input.eventId,
|
|
1231
|
-
agentId: input.agentId,
|
|
1232
|
-
request: input.request,
|
|
1233
|
-
});
|
|
1234
|
-
this.broadcastRcMux({
|
|
1235
|
-
type: 'approval/requested',
|
|
1236
|
-
sessionId: input.agentId,
|
|
1237
|
-
approvalId: input.eventId,
|
|
1238
|
-
toolName: string(input.request.toolName) ?? this.backendLabel(),
|
|
1239
|
-
...(typeof input.request.reason === 'string' ? { reason: input.request.reason } : {}),
|
|
1240
|
-
}, input.eventId);
|
|
1241
|
-
}
|
|
1242
800
|
nextProjectionSeq() {
|
|
1243
801
|
this.lastProjectionSeq += 1;
|
|
1244
802
|
return this.lastProjectionSeq;
|
|
1245
803
|
}
|
|
1246
|
-
createApiProxy() {
|
|
1247
|
-
const call = (endpoint) => async (request, signal) => {
|
|
1248
|
-
const payload = endpoint === 'session.prompt' && isRecord(request.payload)
|
|
1249
|
-
? { ...request.payload, requestId: String(request.rpcId) }
|
|
1250
|
-
: request.payload;
|
|
1251
|
-
const result = await this.dispatch(rcEndpoint(endpoint), { args: { request: payload } }, signal ?? new AbortController().signal);
|
|
1252
|
-
return {
|
|
1253
|
-
rpcId: request.rpcId,
|
|
1254
|
-
result: (result.ok
|
|
1255
|
-
? success(result.value)
|
|
1256
|
-
: failure(result.error.code, result.error.message, result.error.details)),
|
|
1257
|
-
};
|
|
1258
|
-
};
|
|
1259
|
-
return {
|
|
1260
|
-
sessions: {
|
|
1261
|
-
list: call('session.list'),
|
|
1262
|
-
search: call('session.search'),
|
|
1263
|
-
create: call('session.create'),
|
|
1264
|
-
history: call('session.history'),
|
|
1265
|
-
models: call('session.models'),
|
|
1266
|
-
selectModel: call('session.selectModel'),
|
|
1267
|
-
rename: call('session.rename'),
|
|
1268
|
-
fork: call('session.fork'),
|
|
1269
|
-
prompt: call('session.prompt'),
|
|
1270
|
-
attachment: call('session.attachment'),
|
|
1271
|
-
updateQueue: call('session.updateQueue'),
|
|
1272
|
-
cancel: call('session.cancel'),
|
|
1273
|
-
},
|
|
1274
|
-
workspace: {
|
|
1275
|
-
list: call('workspace.list'),
|
|
1276
|
-
create: call('workspace.create'),
|
|
1277
|
-
rename: call('workspace.rename'),
|
|
1278
|
-
delete: call('workspace.delete'),
|
|
1279
|
-
insertBefore: call('workspace.insertBefore'),
|
|
1280
|
-
insertSessionBefore: call('workspace.insertSessionBefore'),
|
|
1281
|
-
archiveSession: call('workspace.archiveSession'),
|
|
1282
|
-
},
|
|
1283
|
-
subagents: {},
|
|
1284
|
-
host: {
|
|
1285
|
-
describe: call('host.describe'),
|
|
1286
|
-
listDirectory: call('host.listDirectory'),
|
|
1287
|
-
},
|
|
1288
|
-
skills: { list: call('skills.list') },
|
|
1289
|
-
agentPresets: {},
|
|
1290
|
-
goals: {},
|
|
1291
|
-
settings: {},
|
|
1292
|
-
credentials: {},
|
|
1293
|
-
llm: {},
|
|
1294
|
-
events: {
|
|
1295
|
-
mux: ((request, signal) => this.rcMux(request, signal)),
|
|
1296
|
-
host: ((request, signal) => this.rcHost(request, signal)),
|
|
1297
|
-
},
|
|
1298
|
-
downloads: {},
|
|
1299
|
-
respond: async (message) => {
|
|
1300
|
-
const pending = this.pendingApprovals.get(String(message.rpcId))
|
|
1301
|
-
?? [...this.pendingApprovals.values()].find(item => item.requestHandle === String(message.rpcId));
|
|
1302
|
-
if (pending === undefined)
|
|
1303
|
-
return { accepted: false, reason: 'not-pending' };
|
|
1304
|
-
const result = message.result;
|
|
1305
|
-
const outcome = result.ok ? result.value : undefined;
|
|
1306
|
-
const decision = outcome === 'allowed-once' ? 'allow-once' : outcome === 'cancelled' ? 'cancel' : 'reject-once';
|
|
1307
|
-
await this.client.respond(pending.requestHandle, decision);
|
|
1308
|
-
this.pendingApprovals.delete(pending.requestHandle);
|
|
1309
|
-
return { accepted: true };
|
|
1310
|
-
},
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
async *rcMux(request, signal) {
|
|
1314
|
-
const queue = new AsyncValueQueue(signal);
|
|
1315
|
-
this.rcMuxStreams.add(queue);
|
|
1316
|
-
for (const session of this.sessions.values()) {
|
|
1317
|
-
queue.push({
|
|
1318
|
-
rpcId: `${String(request.rpcId)}:${session.sessionId}:subscribed`,
|
|
1319
|
-
payload: { type: 'session/subscribed', sessionId: session.sessionId, lastSeq: -1 },
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
try {
|
|
1323
|
-
yield* queue;
|
|
1324
|
-
}
|
|
1325
|
-
finally {
|
|
1326
|
-
this.rcMuxStreams.delete(queue);
|
|
1327
|
-
queue.close();
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
async *rcHost(_request, signal) {
|
|
1331
|
-
const queue = new AsyncValueQueue(signal);
|
|
1332
|
-
this.rcHostStreams.add(queue);
|
|
1333
|
-
try {
|
|
1334
|
-
yield* queue;
|
|
1335
|
-
}
|
|
1336
|
-
finally {
|
|
1337
|
-
this.rcHostStreams.delete(queue);
|
|
1338
|
-
queue.close();
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
broadcastRcMux(payload, rpcId = `cursor-mux:${Date.now()}:${Math.random()}`) {
|
|
1342
|
-
for (const queue of this.rcMuxStreams)
|
|
1343
|
-
queue.push({ rpcId, payload });
|
|
1344
|
-
}
|
|
1345
|
-
broadcastRcHost(payload) {
|
|
1346
|
-
const frame = { rpcId: `cursor-host:${Date.now()}:${Math.random()}`, payload };
|
|
1347
|
-
for (const queue of this.rcHostStreams)
|
|
1348
|
-
queue.push(frame);
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
function recreateWorkspaceFromId(workspaceId, backend) {
|
|
1352
|
-
const prefix = backend === 'antigravity' ? AGY_WORKSPACE_PREFIX : CURSOR_WORKSPACE_PREFIX;
|
|
1353
|
-
if (!workspaceId.startsWith(prefix))
|
|
1354
|
-
return undefined;
|
|
1355
|
-
try {
|
|
1356
|
-
const path = decodeURIComponent(workspaceId.slice(prefix.length));
|
|
1357
|
-
if (path.length > 0)
|
|
1358
|
-
return createAcpWorkspaceView(path, backend);
|
|
1359
|
-
}
|
|
1360
|
-
catch {
|
|
1361
|
-
return undefined;
|
|
1362
|
-
}
|
|
1363
|
-
return undefined;
|
|
1364
|
-
}
|
|
1365
|
-
function nativeWorkspace(view) {
|
|
1366
|
-
return {
|
|
1367
|
-
workspaceId: view.workspaceId,
|
|
1368
|
-
path: view.path,
|
|
1369
|
-
title: view.title,
|
|
1370
|
-
sessionIds: view.sessionIds,
|
|
1371
|
-
createdAt: view.createdAt,
|
|
1372
|
-
updatedAt: view.updatedAt,
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
const CURSOR_MODELS = [
|
|
1376
|
-
{ id: 'auto', name: 'Auto' },
|
|
1377
|
-
{ id: 'claude-3.7-sonnet', name: 'Claude 3.7 Sonnet' },
|
|
1378
|
-
{ id: 'claude-3.5-sonnet', name: 'Claude 3.5 Sonnet' },
|
|
1379
|
-
{ id: 'gpt-4o', name: 'GPT-4o' },
|
|
1380
|
-
{ id: 'o3-mini', name: 'o3-mini' },
|
|
1381
|
-
{ id: 'deepseek-r1', name: 'DeepSeek R1' },
|
|
1382
|
-
];
|
|
1383
|
-
export function modelCatalog(backend = 'cursor') {
|
|
1384
|
-
const provider = backend === 'antigravity' ? 'antigravity' : CURSOR_PROVIDER;
|
|
1385
|
-
const name = backend === 'antigravity' ? 'Antigravity' : 'Cursor';
|
|
1386
|
-
const models = backend === 'antigravity' ? [] : CURSOR_MODELS;
|
|
1387
|
-
const defaultModel = models[0]?.id ?? 'host-settings';
|
|
1388
|
-
return {
|
|
1389
|
-
default: { provider, model: defaultModel },
|
|
1390
|
-
routableProviders: [provider],
|
|
1391
|
-
groups: [{
|
|
1392
|
-
id: provider,
|
|
1393
|
-
name,
|
|
1394
|
-
models,
|
|
1395
|
-
}],
|
|
1396
|
-
failures: [],
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
function extractPromptText(content) {
|
|
1400
|
-
const parts = content.flatMap(part => {
|
|
1401
|
-
const item = record(part);
|
|
1402
|
-
if (item.type === 'text' && typeof item.text === 'string')
|
|
1403
|
-
return [item.text];
|
|
1404
|
-
return [];
|
|
1405
|
-
});
|
|
1406
|
-
const text = parts.join('');
|
|
1407
|
-
return text.length === 0 ? undefined : text;
|
|
1408
|
-
}
|
|
1409
|
-
function extractText(update) {
|
|
1410
|
-
const content = update.content;
|
|
1411
|
-
if (typeof content === 'string')
|
|
1412
|
-
return content;
|
|
1413
|
-
if (isRecord(content) && typeof content.text === 'string')
|
|
1414
|
-
return content.text;
|
|
1415
|
-
if (Array.isArray(content)) {
|
|
1416
|
-
const parts = content
|
|
1417
|
-
.map(part => (isRecord(part) && typeof part.text === 'string' ? part.text : undefined))
|
|
1418
|
-
.filter((part) => part !== undefined);
|
|
1419
|
-
return parts.length === 0 ? undefined : parts.join('');
|
|
1420
|
-
}
|
|
1421
|
-
return typeof update.text === 'string' ? update.text : undefined;
|
|
1422
|
-
}
|
|
1423
|
-
function summarizeQuestions(params) {
|
|
1424
|
-
if (!Array.isArray(params.questions))
|
|
1425
|
-
return undefined;
|
|
1426
|
-
const lines = params.questions
|
|
1427
|
-
.map(question => (isRecord(question) ? string(question.prompt) : undefined))
|
|
1428
|
-
.filter((line) => line !== undefined);
|
|
1429
|
-
return lines.length === 0 ? undefined : lines.join('\n');
|
|
1430
|
-
}
|
|
1431
|
-
function sessionIdFromAddress(address) {
|
|
1432
|
-
if (typeof address.sessionId === 'string' && address.sessionId.length > 0)
|
|
1433
|
-
return address.sessionId;
|
|
1434
|
-
if (typeof address.childSessionId === 'string' && address.childSessionId.length > 0)
|
|
1435
|
-
return address.childSessionId;
|
|
1436
|
-
if (address.kind === 'session' && typeof address.sessionId === 'string' && address.sessionId.length > 0) {
|
|
1437
|
-
return address.sessionId;
|
|
1438
|
-
}
|
|
1439
|
-
throw new Error('The sessionId is required.');
|
|
1440
|
-
}
|
|
1441
|
-
function extractSessionId(request) {
|
|
1442
|
-
if (typeof request.sessionId === 'string' && request.sessionId.length > 0)
|
|
1443
|
-
return request.sessionId;
|
|
1444
|
-
if (isRecord(request.address))
|
|
1445
|
-
return sessionIdFromAddress(request.address);
|
|
1446
|
-
throw new Error('The sessionId is required.');
|
|
1447
|
-
}
|
|
1448
|
-
function nativeAcpId(sessionId, backend) {
|
|
1449
|
-
const prefix = backend === 'antigravity' ? ACP_SESSION_PREFIX : CURSOR_SESSION_PREFIX;
|
|
1450
|
-
if (!sessionId.startsWith(prefix) || sessionId.length === prefix.length) {
|
|
1451
|
-
throw new Error('The selected Session does not belong to the virtual harness.');
|
|
1452
|
-
}
|
|
1453
|
-
return sessionId.slice(prefix.length);
|
|
1454
|
-
}
|
|
1455
|
-
function isSurfaceEvent(type) {
|
|
1456
|
-
return type === 'user/message' || type === 'assistant/message' || type === 'tool/result';
|
|
1457
|
-
}
|
|
1458
|
-
function carrierArgs(payload) {
|
|
1459
|
-
return record(record(payload).args);
|
|
1460
|
-
}
|
|
1461
|
-
function requestArg(args) {
|
|
1462
|
-
return record(args.request ?? args._request ?? args);
|
|
1463
|
-
}
|
|
1464
|
-
function rcEndpoint(endpoint) {
|
|
1465
|
-
if (endpoint === 'workspace.list')
|
|
1466
|
-
return 'workspace/list';
|
|
1467
|
-
return endpoint.replace('.', '/');
|
|
1468
|
-
}
|
|
1469
|
-
function success(value) {
|
|
1470
|
-
return { ok: true, value };
|
|
1471
|
-
}
|
|
1472
|
-
function failure(code, message, details = {}) {
|
|
1473
|
-
return { ok: false, error: { code, message, details } };
|
|
1474
|
-
}
|
|
1475
|
-
function business(value) {
|
|
1476
|
-
if (isRecord(value) && value.ok === false && isRecord(value.error)) {
|
|
1477
|
-
return { ok: false, error: {
|
|
1478
|
-
code: string(value.error.code) ?? 'internal',
|
|
1479
|
-
message: string(value.error.message) ?? 'The virtual Harness request failed.',
|
|
1480
|
-
details: isRecord(value.error.details) ? value.error.details : {},
|
|
1481
|
-
} };
|
|
1482
|
-
}
|
|
1483
|
-
if (isRecord(value) && value.ok === true)
|
|
1484
|
-
return { ok: true, value: value.value };
|
|
1485
|
-
return { ok: true, value };
|
|
1486
|
-
}
|
|
1487
|
-
function fail(code, message, details = {}) {
|
|
1488
|
-
return { ok: false, error: { code, message, details } };
|
|
1489
|
-
}
|
|
1490
|
-
function failFrom(error) {
|
|
1491
|
-
const source = error instanceof Error ? error : new Error(String(error));
|
|
1492
|
-
const code = 'code' in source && typeof source.code === 'string' ? source.code : 'internal';
|
|
1493
|
-
return fail(code, source.message);
|
|
1494
|
-
}
|
|
1495
|
-
function record(value) {
|
|
1496
|
-
return isRecord(value) ? value : {};
|
|
1497
|
-
}
|
|
1498
|
-
function isRecord(value) {
|
|
1499
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1500
|
-
}
|
|
1501
|
-
function array(value) {
|
|
1502
|
-
return Array.isArray(value) ? value : [];
|
|
1503
|
-
}
|
|
1504
|
-
function string(value) {
|
|
1505
|
-
return typeof value === 'string' ? value : undefined;
|
|
1506
|
-
}
|
|
1507
|
-
function integer(value) {
|
|
1508
|
-
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
1509
|
-
}
|
|
1510
|
-
function optionalInteger(value) {
|
|
1511
|
-
if (value === undefined)
|
|
1512
|
-
return undefined;
|
|
1513
|
-
const parsed = integer(value);
|
|
1514
|
-
if (parsed === undefined)
|
|
1515
|
-
throw new Error('The History cursor is invalid.');
|
|
1516
|
-
return parsed;
|
|
1517
|
-
}
|
|
1518
|
-
function optionalPositiveInteger(value) {
|
|
1519
|
-
const parsed = optionalInteger(value);
|
|
1520
|
-
if (parsed !== undefined && parsed <= 0)
|
|
1521
|
-
throw new Error('The History page size is invalid.');
|
|
1522
|
-
return parsed;
|
|
1523
|
-
}
|
|
1524
|
-
function requiredString(value, field) {
|
|
1525
|
-
if (typeof value !== 'string' || value.length === 0)
|
|
1526
|
-
throw new Error(`The ${field} is required.`);
|
|
1527
|
-
return value;
|
|
1528
|
-
}
|
|
1529
|
-
class AsyncValueQueue {
|
|
1530
|
-
signal;
|
|
1531
|
-
values = [];
|
|
1532
|
-
waiters = [];
|
|
1533
|
-
closed = false;
|
|
1534
|
-
onAbort;
|
|
1535
|
-
constructor(signal) {
|
|
1536
|
-
this.signal = signal;
|
|
1537
|
-
this.onAbort = () => this.close();
|
|
1538
|
-
signal.addEventListener('abort', this.onAbort, { once: true });
|
|
1539
|
-
if (signal.aborted)
|
|
1540
|
-
this.close();
|
|
1541
|
-
}
|
|
1542
|
-
push(value) {
|
|
1543
|
-
if (this.closed)
|
|
1544
|
-
return;
|
|
1545
|
-
const waiter = this.waiters.shift();
|
|
1546
|
-
if (waiter === undefined)
|
|
1547
|
-
this.values.push(value);
|
|
1548
|
-
else
|
|
1549
|
-
waiter({ done: false, value });
|
|
1550
|
-
}
|
|
1551
|
-
close() {
|
|
1552
|
-
if (this.closed)
|
|
1553
|
-
return;
|
|
1554
|
-
this.closed = true;
|
|
1555
|
-
this.signal.removeEventListener('abort', this.onAbort);
|
|
1556
|
-
for (const waiter of this.waiters.splice(0))
|
|
1557
|
-
waiter({ done: true, value: undefined });
|
|
1558
|
-
}
|
|
1559
|
-
iterate(dispose) {
|
|
1560
|
-
const queue = this;
|
|
1561
|
-
return {
|
|
1562
|
-
async *[Symbol.asyncIterator]() {
|
|
1563
|
-
try {
|
|
1564
|
-
yield* queue;
|
|
1565
|
-
}
|
|
1566
|
-
finally {
|
|
1567
|
-
dispose();
|
|
1568
|
-
queue.close();
|
|
1569
|
-
}
|
|
1570
|
-
},
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
async *[Symbol.asyncIterator]() {
|
|
1574
|
-
while (true) {
|
|
1575
|
-
if (this.values.length > 0) {
|
|
1576
|
-
yield this.values.shift();
|
|
1577
|
-
continue;
|
|
1578
|
-
}
|
|
1579
|
-
if (this.closed)
|
|
1580
|
-
return;
|
|
1581
|
-
const next = await new Promise(resolve => this.waiters.push(resolve));
|
|
1582
|
-
if (next.done)
|
|
1583
|
-
return;
|
|
1584
|
-
yield next.value;
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
804
|
}
|
|
805
|
+
export { acpCwdWorkspaceId, createAcpWorkspaceView, discoverAcpVirtualWorkspaces, modelCatalog } from './virtual-catalog.js';
|
|
1588
806
|
//# sourceMappingURL=virtual-harness.js.map
|