@simonepri/refined-antigravity-acp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.fallowrc.json +11 -0
- package/.github/workflows/ci.yml +24 -0
- package/.github/workflows/release.yml +41 -0
- package/.oxlintignore +2 -0
- package/.oxlintrc.json +152 -0
- package/.release-please-manifest.json +3 -0
- package/assets/icon.svg +3 -0
- package/assets/logo.svg +62 -0
- package/bin/cli.ts +58 -0
- package/index.server.ts +9 -0
- package/license +21 -0
- package/package.json +45 -0
- package/paseo-plugin.json +8 -0
- package/pnpm-workspace.yaml +44 -0
- package/readme.md +292 -0
- package/release-please-config.json +13 -0
- package/server/agy-patch.test.ts +445 -0
- package/server/agy-patch.ts +445 -0
- package/server/auth.test.ts +51 -0
- package/server/auth.ts +122 -0
- package/server/command.test.ts +18 -0
- package/server/command.ts +92 -0
- package/server/connector.test.ts +1136 -0
- package/server/connector.ts +1386 -0
- package/server/history.test.ts +139 -0
- package/server/history.ts +157 -0
- package/server/mcp-proxy.test.ts +368 -0
- package/server/mcp-proxy.ts +375 -0
- package/server/provider.test.ts +11 -0
- package/server/provider.ts +37 -0
- package/server/sanitize.test.ts +174 -0
- package/server/sanitize.ts +286 -0
- package/server/skills.test.ts +121 -0
- package/server/skills.ts +183 -0
- package/server/steering.test.ts +405 -0
- package/server/steering.ts +199 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("./history.js", () => ({
|
|
4
|
+
readConversationHistory: vi.fn((sessionId: string) => {
|
|
5
|
+
if (sessionId === "bb2bf99a-ec9e-4d4a-9924-6aec7b7291cf") {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
type: "timeline.item",
|
|
9
|
+
sessionId: "session-replay-1",
|
|
10
|
+
item: {
|
|
11
|
+
id: "item-1",
|
|
12
|
+
type: "user_message",
|
|
13
|
+
text: "test message",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { SteeringConnection } from "./steering.js";
|
|
26
|
+
import type {
|
|
27
|
+
ProviderConnectRequest,
|
|
28
|
+
ProviderConnection,
|
|
29
|
+
ProviderEvent,
|
|
30
|
+
ProviderInput,
|
|
31
|
+
} from "@getpaseo/plugin/server/provider";
|
|
32
|
+
|
|
33
|
+
describe("SteeringConnection", () => {
|
|
34
|
+
function createMockBaseConnection(): {
|
|
35
|
+
connection: ProviderConnection;
|
|
36
|
+
emitBaseEvent: (event: ProviderEvent) => void;
|
|
37
|
+
sentInputs: ProviderInput[];
|
|
38
|
+
closed: boolean;
|
|
39
|
+
} {
|
|
40
|
+
let eventListener: ((event: ProviderEvent) => void) | null = null;
|
|
41
|
+
const sentInputs: ProviderInput[] = [];
|
|
42
|
+
let closed = false;
|
|
43
|
+
|
|
44
|
+
const connection: ProviderConnection = {
|
|
45
|
+
version: 1,
|
|
46
|
+
capabilities: ["prompt.message", "session.configure"],
|
|
47
|
+
async send(input: ProviderInput): Promise<void> {
|
|
48
|
+
sentInputs.push(input);
|
|
49
|
+
},
|
|
50
|
+
onEvent(listener: (event: ProviderEvent) => void): () => void {
|
|
51
|
+
eventListener = listener;
|
|
52
|
+
return () => {
|
|
53
|
+
if (eventListener === listener) {
|
|
54
|
+
eventListener = null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
async close(): Promise<void> {
|
|
59
|
+
closed = true;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
connection,
|
|
65
|
+
emitBaseEvent: (event: ProviderEvent) => {
|
|
66
|
+
eventListener?.(event);
|
|
67
|
+
},
|
|
68
|
+
sentInputs,
|
|
69
|
+
get closed() {
|
|
70
|
+
return closed;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
it("advertises prompt.steer capability when requested", () => {
|
|
76
|
+
const { connection } = createMockBaseConnection();
|
|
77
|
+
const request: ProviderConnectRequest = {
|
|
78
|
+
versions: [1],
|
|
79
|
+
capabilities: ["prompt.message", "prompt.steer"],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
83
|
+
expect(wrapped.capabilities).toContain("prompt.steer");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not advertise prompt.steer capability when not requested", () => {
|
|
87
|
+
const { connection } = createMockBaseConnection();
|
|
88
|
+
const request: ProviderConnectRequest = {
|
|
89
|
+
versions: [1],
|
|
90
|
+
capabilities: ["prompt.message"],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
94
|
+
expect(wrapped.capabilities).not.toContain("prompt.steer");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("augments session.opened event with prompt.steer capability when requested", () => {
|
|
98
|
+
const { connection, emitBaseEvent } = createMockBaseConnection();
|
|
99
|
+
const request: ProviderConnectRequest = {
|
|
100
|
+
versions: [1],
|
|
101
|
+
capabilities: ["prompt.message", "prompt.steer"],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
105
|
+
const receivedEvents: ProviderEvent[] = [];
|
|
106
|
+
wrapped.onEvent((event) => receivedEvents.push(event));
|
|
107
|
+
|
|
108
|
+
emitBaseEvent({
|
|
109
|
+
type: "session.opened",
|
|
110
|
+
sessionId: "session-1",
|
|
111
|
+
capabilities: ["prompt.message"],
|
|
112
|
+
restoration: "core",
|
|
113
|
+
cwd: "/test",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const opened = receivedEvents.find((e) => e.type === "session.opened");
|
|
117
|
+
expect(opened).toBeDefined();
|
|
118
|
+
if (opened && opened.type === "session.opened") {
|
|
119
|
+
expect(opened.capabilities).toContain("prompt.steer");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("handles mid-turn steer prompt by emitting completed session.prompt_result to trigger Paseo replacement", async () => {
|
|
124
|
+
const { connection, emitBaseEvent, sentInputs } = createMockBaseConnection();
|
|
125
|
+
const request: ProviderConnectRequest = {
|
|
126
|
+
versions: [1],
|
|
127
|
+
capabilities: ["prompt.message", "prompt.steer"],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
131
|
+
const receivedEvents: ProviderEvent[] = [];
|
|
132
|
+
wrapped.onEvent((event) => receivedEvents.push(event));
|
|
133
|
+
|
|
134
|
+
emitBaseEvent({
|
|
135
|
+
type: "session.turn",
|
|
136
|
+
sessionId: "session-1",
|
|
137
|
+
turnId: "turn-42",
|
|
138
|
+
state: "started",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await wrapped.send({
|
|
142
|
+
type: "session.prompt",
|
|
143
|
+
sessionId: "session-1",
|
|
144
|
+
prompt: {
|
|
145
|
+
clientMessageId: "msg-steer-1",
|
|
146
|
+
delivery: "steer",
|
|
147
|
+
input: {
|
|
148
|
+
type: "message",
|
|
149
|
+
content: [{ type: "text", text: "Focus on unit tests" }],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(sentInputs).toHaveLength(0);
|
|
155
|
+
const promptResults = receivedEvents.filter((e) => e.type === "session.prompt_result");
|
|
156
|
+
expect(promptResults).toHaveLength(1);
|
|
157
|
+
expect(promptResults[0]).toEqual({
|
|
158
|
+
type: "session.prompt_result",
|
|
159
|
+
sessionId: "session-1",
|
|
160
|
+
clientMessageId: "msg-steer-1",
|
|
161
|
+
result: {
|
|
162
|
+
type: "completed",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Paseo follows up by cancelling the turn and re-dispatching the text. That
|
|
167
|
+
// replacement must reach the agent marked as a mid-turn interruption.
|
|
168
|
+
await wrapped.send({
|
|
169
|
+
type: "session.prompt",
|
|
170
|
+
sessionId: "session-1",
|
|
171
|
+
prompt: {
|
|
172
|
+
clientMessageId: "msg-replaced-1",
|
|
173
|
+
delivery: "auto",
|
|
174
|
+
input: {
|
|
175
|
+
type: "message",
|
|
176
|
+
content: [{ type: "text", text: "Focus on unit tests" }],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(sentInputs).toHaveLength(1);
|
|
182
|
+
expect(sentInputs[0]).toEqual({
|
|
183
|
+
type: "session.prompt",
|
|
184
|
+
sessionId: "session-1",
|
|
185
|
+
prompt: {
|
|
186
|
+
clientMessageId: "msg-replaced-1",
|
|
187
|
+
delivery: "auto",
|
|
188
|
+
input: {
|
|
189
|
+
type: "message",
|
|
190
|
+
content: [{ type: "text", text: "[Mid-turn update]: Focus on unit tests" }],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("does not mark an ordinary prompt that did not follow a steer", async () => {
|
|
197
|
+
const { connection, sentInputs } = createMockBaseConnection();
|
|
198
|
+
const wrapped = new SteeringConnection(connection, {
|
|
199
|
+
versions: [1],
|
|
200
|
+
capabilities: ["prompt.message", "prompt.steer"],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await wrapped.send({
|
|
204
|
+
type: "session.prompt",
|
|
205
|
+
sessionId: "session-1",
|
|
206
|
+
prompt: {
|
|
207
|
+
clientMessageId: "msg-plain-1",
|
|
208
|
+
delivery: "auto",
|
|
209
|
+
input: { type: "message", content: [{ type: "text", text: "Focus on unit tests" }] },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const sent = sentInputs[0] as { prompt: { input: { content: Array<{ text: string }> } } };
|
|
214
|
+
expect(sent.prompt.input.content[0].text).toBe("Focus on unit tests");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("expands slash skills in prompt input", async () => {
|
|
218
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "steering-skills-test-"));
|
|
219
|
+
const skillDir = path.join(tmpDir, ".agents", "skills", "test-steer");
|
|
220
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
221
|
+
fs.writeFileSync(
|
|
222
|
+
path.join(skillDir, "SKILL.md"),
|
|
223
|
+
`---
|
|
224
|
+
name: test-steer
|
|
225
|
+
description: Steer test
|
|
226
|
+
---
|
|
227
|
+
Running with $ARGUMENTS`,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const { connection, emitBaseEvent, sentInputs } = createMockBaseConnection();
|
|
231
|
+
const request: ProviderConnectRequest = {
|
|
232
|
+
versions: [1],
|
|
233
|
+
capabilities: ["prompt.message"],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
237
|
+
|
|
238
|
+
emitBaseEvent({
|
|
239
|
+
type: "session.opened",
|
|
240
|
+
sessionId: "sess_steer_1",
|
|
241
|
+
capabilities: ["prompt.message"],
|
|
242
|
+
restoration: "core",
|
|
243
|
+
cwd: tmpDir,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await wrapped.send({
|
|
247
|
+
type: "session.prompt",
|
|
248
|
+
sessionId: "sess_steer_1",
|
|
249
|
+
prompt: {
|
|
250
|
+
clientMessageId: "msg_prompt_1",
|
|
251
|
+
delivery: "auto",
|
|
252
|
+
input: {
|
|
253
|
+
type: "message",
|
|
254
|
+
content: [{ type: "text", text: "/test-steer arg1" }],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(sentInputs).toHaveLength(1);
|
|
260
|
+
const sent = sentInputs[0] as {
|
|
261
|
+
prompt: { input: { content: Array<{ type: string; text: string }> } };
|
|
262
|
+
};
|
|
263
|
+
expect(sent.prompt.input.content[0].text).toContain("[Skill: test-steer]");
|
|
264
|
+
expect(sent.prompt.input.content[0].text).toContain("Running with arg1");
|
|
265
|
+
|
|
266
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("forwards non-steer inputs directly to the underlying connection", async () => {
|
|
270
|
+
const { connection, sentInputs } = createMockBaseConnection();
|
|
271
|
+
const request: ProviderConnectRequest = {
|
|
272
|
+
versions: [1],
|
|
273
|
+
capabilities: ["prompt.message", "prompt.steer"],
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
277
|
+
|
|
278
|
+
const normalInput: ProviderInput = {
|
|
279
|
+
type: "catalog",
|
|
280
|
+
requestId: "req-catalog-1",
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
await wrapped.send(normalInput);
|
|
284
|
+
expect(sentInputs).toEqual([normalInput]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("records replay request on session.open and replays history on session.opened", async () => {
|
|
288
|
+
const { connection, emitBaseEvent } = createMockBaseConnection();
|
|
289
|
+
const request: ProviderConnectRequest = {
|
|
290
|
+
versions: [1],
|
|
291
|
+
capabilities: ["prompt.message"],
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
295
|
+
const receivedEvents: ProviderEvent[] = [];
|
|
296
|
+
wrapped.onEvent((event) => receivedEvents.push(event));
|
|
297
|
+
|
|
298
|
+
await wrapped.send({
|
|
299
|
+
type: "session.open",
|
|
300
|
+
requestId: "req-1",
|
|
301
|
+
sessionId: "session-replay-1",
|
|
302
|
+
history: "replay",
|
|
303
|
+
config: {
|
|
304
|
+
cwd: "/test",
|
|
305
|
+
env: {},
|
|
306
|
+
mcpServers: {},
|
|
307
|
+
settings: {},
|
|
308
|
+
persist: true,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
emitBaseEvent({
|
|
313
|
+
type: "session.opened",
|
|
314
|
+
sessionId: "session-replay-1",
|
|
315
|
+
capabilities: ["prompt.message"],
|
|
316
|
+
restoration: "core",
|
|
317
|
+
cwd: "/test",
|
|
318
|
+
persistence: {
|
|
319
|
+
version: 1,
|
|
320
|
+
data: {
|
|
321
|
+
sessionId: "bb2bf99a-ec9e-4d4a-9924-6aec7b7291cf",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const timelineItems = receivedEvents.filter((e) => e.type === "timeline.item");
|
|
327
|
+
expect(timelineItems.length).toBeGreaterThan(0);
|
|
328
|
+
expect(timelineItems[0].item.type).toBe("user_message");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("forwards close call to underlying connection", async () => {
|
|
332
|
+
const mock = createMockBaseConnection();
|
|
333
|
+
const request: ProviderConnectRequest = {
|
|
334
|
+
versions: [1],
|
|
335
|
+
capabilities: ["prompt.message"],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const wrapped = new SteeringConnection(mock.connection, request);
|
|
339
|
+
await wrapped.close();
|
|
340
|
+
expect(mock.closed).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
it("augments session.commands from base connection with discovered workspace skills", () => {
|
|
343
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "steering-commands-test-"));
|
|
344
|
+
const skillDir = path.join(tmpDir, ".agents", "skills", "test-skill");
|
|
345
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
346
|
+
fs.writeFileSync(
|
|
347
|
+
path.join(skillDir, "SKILL.md"),
|
|
348
|
+
`---
|
|
349
|
+
name: test-skill
|
|
350
|
+
description: Workspace test skill
|
|
351
|
+
---
|
|
352
|
+
Skill instructions`,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const { connection, emitBaseEvent } = createMockBaseConnection();
|
|
356
|
+
const request: ProviderConnectRequest = {
|
|
357
|
+
versions: [1],
|
|
358
|
+
capabilities: ["prompt.message"],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const wrapped = new SteeringConnection(connection, request);
|
|
362
|
+
const receivedEvents: ProviderEvent[] = [];
|
|
363
|
+
wrapped.onEvent((event) => receivedEvents.push(event));
|
|
364
|
+
|
|
365
|
+
emitBaseEvent({
|
|
366
|
+
type: "session.opened",
|
|
367
|
+
sessionId: "sess-commands-1",
|
|
368
|
+
capabilities: ["prompt.message"],
|
|
369
|
+
restoration: "core",
|
|
370
|
+
cwd: tmpDir,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
emitBaseEvent({
|
|
374
|
+
type: "session.commands",
|
|
375
|
+
sessionId: "sess-commands-1",
|
|
376
|
+
commands: [
|
|
377
|
+
{ name: "plan", description: "Plan mode" },
|
|
378
|
+
{ name: "logout", description: "Log out" },
|
|
379
|
+
],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const commandEvents = receivedEvents.filter(
|
|
383
|
+
(e): e is Extract<ProviderEvent, { type: "session.commands" }> =>
|
|
384
|
+
e.type === "session.commands",
|
|
385
|
+
);
|
|
386
|
+
expect(commandEvents).toHaveLength(2);
|
|
387
|
+
|
|
388
|
+
// Initial commands emitted on session.opened include the workspace skill
|
|
389
|
+
expect(commandEvents[0].commands).toContainEqual({
|
|
390
|
+
name: "test-skill",
|
|
391
|
+
description: "Workspace test skill",
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Subsequent session.commands from base connection is augmented with the workspace skill
|
|
395
|
+
const augmentedCommands = commandEvents[1].commands;
|
|
396
|
+
expect(augmentedCommands).toContainEqual({ name: "plan", description: "Plan mode" });
|
|
397
|
+
expect(augmentedCommands).toContainEqual({ name: "logout", description: "Log out" });
|
|
398
|
+
expect(augmentedCommands).toContainEqual({
|
|
399
|
+
name: "test-skill",
|
|
400
|
+
description: "Workspace test skill",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
404
|
+
});
|
|
405
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProviderConnectRequest,
|
|
3
|
+
ProviderConnection,
|
|
4
|
+
ProviderEvent,
|
|
5
|
+
ProviderInput,
|
|
6
|
+
} from "@getpaseo/plugin/server/provider";
|
|
7
|
+
import { readConversationHistory } from "./history.js";
|
|
8
|
+
import { formatSteeringPrompt } from "./sanitize.js";
|
|
9
|
+
import { augmentCommands, expandSkillInvocation } from "./skills.js";
|
|
10
|
+
|
|
11
|
+
function transformPromptInput(
|
|
12
|
+
input: ProviderInput,
|
|
13
|
+
isSteerReplacement: boolean,
|
|
14
|
+
cwd?: string,
|
|
15
|
+
): ProviderInput {
|
|
16
|
+
if (input.type !== "session.prompt" || input.prompt.input.type !== "message") {
|
|
17
|
+
return input;
|
|
18
|
+
}
|
|
19
|
+
// Only the first text part carries the steering marker; prefixing every part would
|
|
20
|
+
// repeat it throughout a multi-part prompt.
|
|
21
|
+
let markerPending = isSteerReplacement;
|
|
22
|
+
const content = input.prompt.input.content.map((part) => {
|
|
23
|
+
if (part.type !== "text") return part;
|
|
24
|
+
let text = expandSkillInvocation(part.text, cwd);
|
|
25
|
+
if (markerPending) {
|
|
26
|
+
text = formatSteeringPrompt(text);
|
|
27
|
+
markerPending = false;
|
|
28
|
+
}
|
|
29
|
+
return { ...part, text };
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
...input,
|
|
33
|
+
prompt: {
|
|
34
|
+
...input.prompt,
|
|
35
|
+
input: { ...input.prompt.input, content },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readNativeSessionId(persistence: unknown): string | null {
|
|
41
|
+
if (!persistence || typeof persistence !== "object" || Array.isArray(persistence)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const data = (persistence as { data?: unknown }).data;
|
|
45
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
46
|
+
const sessionId = (data as { sessionId?: unknown }).sessionId;
|
|
47
|
+
return typeof sessionId === "string" ? sessionId : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Connection wrapper that decorates a base ProviderConnection with:
|
|
52
|
+
* 1. Mid-turn steering (`prompt.steer`)
|
|
53
|
+
* 2. Slash skill command advertising (`session.commands`)
|
|
54
|
+
* 3. Slash skill expansion on prompt submission
|
|
55
|
+
* 4. Automatic conversation history replay from Antigravity SQLite on session reload
|
|
56
|
+
*/
|
|
57
|
+
export class SteeringConnection implements ProviderConnection {
|
|
58
|
+
readonly version: number;
|
|
59
|
+
readonly capabilities: readonly string[];
|
|
60
|
+
|
|
61
|
+
private readonly sessionCwds = new Map<string, string>();
|
|
62
|
+
private readonly pendingSteers = new Set<string>();
|
|
63
|
+
private readonly replaySessions = new Set<string>();
|
|
64
|
+
private readonly listeners = new Set<(event: ProviderEvent) => void>();
|
|
65
|
+
private readonly unbindBase: () => void;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly base: ProviderConnection,
|
|
69
|
+
private readonly request: ProviderConnectRequest,
|
|
70
|
+
) {
|
|
71
|
+
this.version = base.version;
|
|
72
|
+
this.capabilities = request.capabilities.includes("prompt.steer")
|
|
73
|
+
? Array.from(new Set([...base.capabilities, "prompt.steer"]))
|
|
74
|
+
: base.capabilities;
|
|
75
|
+
|
|
76
|
+
this.unbindBase = this.base.onEvent((event) => this.handleBaseEvent(event));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private emitSessionSkills(sessionId: string, cwd: string): void {
|
|
80
|
+
try {
|
|
81
|
+
const commands = augmentCommands(undefined, cwd);
|
|
82
|
+
if (commands.length > 0) {
|
|
83
|
+
this.emit({ type: "session.commands", sessionId, commands });
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error("[paseo-antigravity] Failed to emit session.commands on session.opened:", err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private replaySessionHistory(sessionId: string, nativeSessionId: string): void {
|
|
91
|
+
try {
|
|
92
|
+
const history = readConversationHistory(nativeSessionId);
|
|
93
|
+
for (const entry of history) {
|
|
94
|
+
this.emit({
|
|
95
|
+
type: "timeline.item",
|
|
96
|
+
sessionId,
|
|
97
|
+
item: entry.item,
|
|
98
|
+
timestamp: entry.timestamp,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`[paseo-antigravity] Failed to replay history for ${sessionId}:`, err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private handleSessionOpened(event: Extract<ProviderEvent, { type: "session.opened" }>): void {
|
|
107
|
+
const cwd = (event as { cwd?: string }).cwd ?? process.cwd();
|
|
108
|
+
this.sessionCwds.set(event.sessionId, cwd);
|
|
109
|
+
|
|
110
|
+
const sessionCaps = this.request.capabilities.includes("prompt.steer")
|
|
111
|
+
? Array.from(new Set([...event.capabilities, "prompt.steer"]))
|
|
112
|
+
: event.capabilities;
|
|
113
|
+
this.emit({ ...event, capabilities: sessionCaps });
|
|
114
|
+
|
|
115
|
+
if (this.replaySessions.delete(event.sessionId)) {
|
|
116
|
+
const nativeId = readNativeSessionId(event.persistence);
|
|
117
|
+
if (nativeId) {
|
|
118
|
+
this.replaySessionHistory(event.sessionId, nativeId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.emitSessionSkills(event.sessionId, cwd);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private handleBaseEvent(event: ProviderEvent): void {
|
|
126
|
+
if (event.type === "session.opened") {
|
|
127
|
+
this.handleSessionOpened(event);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (event.type === "session.commands") {
|
|
131
|
+
const cwd = this.sessionCwds.get(event.sessionId);
|
|
132
|
+
const commands = augmentCommands(event.commands, cwd);
|
|
133
|
+
this.emit({ ...event, commands });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.emit(event);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private emit(event: ProviderEvent): void {
|
|
140
|
+
for (const listener of this.listeners) {
|
|
141
|
+
listener(event);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private handleSteerPrompt(sessionId: string, clientMessageId: string): void {
|
|
146
|
+
this.pendingSteers.add(sessionId);
|
|
147
|
+
this.emit({
|
|
148
|
+
type: "session.prompt_result",
|
|
149
|
+
sessionId,
|
|
150
|
+
clientMessageId,
|
|
151
|
+
result: { type: "completed" },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async send(input: ProviderInput): Promise<void> {
|
|
156
|
+
if (input.type === "session.open") {
|
|
157
|
+
if (input.history === "replay") {
|
|
158
|
+
this.replaySessions.add(input.sessionId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (input.type === "session.prompt") {
|
|
163
|
+
if (input.prompt.delivery === "steer") {
|
|
164
|
+
this.handleSteerPrompt(input.sessionId, input.prompt.clientMessageId);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Paseo answers a steer by cancelling the turn and re-dispatching the text as a
|
|
168
|
+
// normal prompt. `pendingSteers` is how we recognise that replacement so it can be
|
|
169
|
+
// marked as a mid-turn interruption rather than a fresh instruction.
|
|
170
|
+
const isReplacement = this.pendingSteers.delete(input.sessionId);
|
|
171
|
+
const cwd = this.sessionCwds.get(input.sessionId);
|
|
172
|
+
input = transformPromptInput(input, isReplacement, cwd);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (input.type === "session.close") {
|
|
176
|
+
this.pendingSteers.delete(input.sessionId);
|
|
177
|
+
this.replaySessions.delete(input.sessionId);
|
|
178
|
+
this.sessionCwds.delete(input.sessionId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.base.send(input);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
onEvent(listener: (event: ProviderEvent) => void): () => void {
|
|
185
|
+
this.listeners.add(listener);
|
|
186
|
+
return () => {
|
|
187
|
+
this.listeners.delete(listener);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async close(): Promise<void> {
|
|
192
|
+
this.pendingSteers.clear();
|
|
193
|
+
this.replaySessions.clear();
|
|
194
|
+
this.sessionCwds.clear();
|
|
195
|
+
this.listeners.clear();
|
|
196
|
+
this.unbindBase();
|
|
197
|
+
return this.base.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"isolatedModules": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["index.server.ts", "server/**/*.ts"]
|
|
14
|
+
}
|