@cuylabs/agent-server 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +262 -0
- package/dist/index.d.ts +893 -0
- package/dist/index.js +2288 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +61 -0
- package/docs/clients-and-transports.md +92 -0
- package/docs/plugins-and-boundaries.md +41 -0
- package/docs/protocol.md +111 -0
- package/docs/runtime-and-dapr.md +41 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2288 @@
|
|
|
1
|
+
// src/protocol/capabilities.ts
|
|
2
|
+
function createDefaultAgentServerCapabilities() {
|
|
3
|
+
return {
|
|
4
|
+
protocol: {
|
|
5
|
+
version: 1,
|
|
6
|
+
transport: "in-process",
|
|
7
|
+
reconnectable: false,
|
|
8
|
+
multiClient: true
|
|
9
|
+
},
|
|
10
|
+
sessions: {
|
|
11
|
+
persistent: true,
|
|
12
|
+
list: true,
|
|
13
|
+
create: true,
|
|
14
|
+
read: true,
|
|
15
|
+
delete: true,
|
|
16
|
+
branch: true
|
|
17
|
+
},
|
|
18
|
+
turns: {
|
|
19
|
+
start: true,
|
|
20
|
+
wait: true,
|
|
21
|
+
interrupt: true,
|
|
22
|
+
steer: true,
|
|
23
|
+
followUp: true,
|
|
24
|
+
followUpManagement: true,
|
|
25
|
+
eventStreaming: true,
|
|
26
|
+
concurrentTurnsPerSession: false
|
|
27
|
+
},
|
|
28
|
+
interactive: {
|
|
29
|
+
approvalRequests: true,
|
|
30
|
+
humanRequests: true
|
|
31
|
+
},
|
|
32
|
+
runtime: {
|
|
33
|
+
execution: "local",
|
|
34
|
+
orchestration: "direct",
|
|
35
|
+
durability: "process",
|
|
36
|
+
dapr: {
|
|
37
|
+
available: false,
|
|
38
|
+
delegatedTurns: false,
|
|
39
|
+
workflowBacked: false
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
agent: {
|
|
43
|
+
workspaceSummary: true,
|
|
44
|
+
toolInventory: false,
|
|
45
|
+
skillInspection: false,
|
|
46
|
+
subAgentInspection: false,
|
|
47
|
+
modelInspection: false,
|
|
48
|
+
modelSwitch: false,
|
|
49
|
+
runtimeStatus: false,
|
|
50
|
+
contextCompaction: false,
|
|
51
|
+
turnUndo: false,
|
|
52
|
+
turnDiff: false
|
|
53
|
+
},
|
|
54
|
+
plugins: {
|
|
55
|
+
headlessOnly: true,
|
|
56
|
+
commandMetadata: true,
|
|
57
|
+
commandDiscovery: false,
|
|
58
|
+
commandExecution: false,
|
|
59
|
+
clientExtensions: false
|
|
60
|
+
},
|
|
61
|
+
events: {
|
|
62
|
+
eventBus: false,
|
|
63
|
+
pubSubBridge: false,
|
|
64
|
+
sseStreaming: false,
|
|
65
|
+
eventReplay: false
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function mergeAgentServerCapabilities(base, override) {
|
|
70
|
+
if (!override) {
|
|
71
|
+
return base;
|
|
72
|
+
}
|
|
73
|
+
const runtime = override.runtime ? {
|
|
74
|
+
...base.runtime,
|
|
75
|
+
...override.runtime,
|
|
76
|
+
dapr: {
|
|
77
|
+
...base.runtime.dapr,
|
|
78
|
+
...override.runtime.dapr ?? {}
|
|
79
|
+
}
|
|
80
|
+
} : base.runtime;
|
|
81
|
+
return {
|
|
82
|
+
...base,
|
|
83
|
+
...override.protocol ? { protocol: { ...base.protocol, ...override.protocol } } : {},
|
|
84
|
+
...override.sessions ? { sessions: { ...base.sessions, ...override.sessions } } : {},
|
|
85
|
+
...override.turns ? { turns: { ...base.turns, ...override.turns } } : {},
|
|
86
|
+
...override.interactive ? { interactive: { ...base.interactive, ...override.interactive } } : {},
|
|
87
|
+
runtime,
|
|
88
|
+
...override.agent ? { agent: { ...base.agent, ...override.agent } } : {},
|
|
89
|
+
...override.plugins ? { plugins: { ...base.plugins, ...override.plugins } } : {},
|
|
90
|
+
...override.events ? { events: { ...base.events, ...override.events } } : {}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/protocol/notifications.ts
|
|
95
|
+
function matchesNotificationSubscription(notification, options) {
|
|
96
|
+
if (!options) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (options.turnId) {
|
|
100
|
+
const notificationTurnId = notification.type === "turn/started" || notification.type === "turn/completed" ? notification.turn.id : notification.type === "turn/event" ? notification.turnId : notification.type === "input/request" ? notification.request.turnId : notification.type === "input/resolved" ? notification.turnId : void 0;
|
|
101
|
+
if (notificationTurnId !== options.turnId) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (options.teamId) {
|
|
106
|
+
const notificationTeamId = notification.type === "team/notification" ? notification.teamId : void 0;
|
|
107
|
+
if (notificationTeamId !== options.teamId) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (options.sessionId) {
|
|
112
|
+
const notificationSessionId = notification.type === "turn/started" || notification.type === "turn/completed" ? notification.turn.sessionId : notification.type === "turn/event" ? notification.sessionId : notification.type === "input/request" ? notification.request.sessionId : notification.type === "input/resolved" ? notification.sessionId : notification.type === "team/notification" ? void 0 : notification.type === "session/deleted" ? notification.sessionId : notification.type === "session/created" ? notification.session.id : notification.sessionId;
|
|
113
|
+
if (notificationSessionId !== options.sessionId) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/protocol/adapter.ts
|
|
121
|
+
import { extractModelId, extractProvider, ensureSessionLoaded } from "@cuylabs/agent-core";
|
|
122
|
+
function createAgentServerAdapter(agent, options = {}) {
|
|
123
|
+
const interactiveAgent = agent;
|
|
124
|
+
const pluginCommands = options.pluginCommands ?? [];
|
|
125
|
+
const pluginCommandInfos = pluginCommands.map((command) => ({
|
|
126
|
+
pluginId: command.pluginId,
|
|
127
|
+
name: command.name,
|
|
128
|
+
alias: [...command.alias],
|
|
129
|
+
summary: command.summary,
|
|
130
|
+
...command.metadata ? { metadata: command.metadata } : {}
|
|
131
|
+
}));
|
|
132
|
+
const pluginCommandTokens = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const command of pluginCommands) {
|
|
134
|
+
pluginCommandTokens.set(normalizePluginCommandToken(command.name), command);
|
|
135
|
+
for (const alias of command.alias) {
|
|
136
|
+
pluginCommandTokens.set(normalizePluginCommandToken(alias), command);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let currentModelLabel = options.currentModelLabel?.trim() || null;
|
|
140
|
+
let lastTrackedTurnSessionId = null;
|
|
141
|
+
const humanInputController = interactiveAgent.getHumanInputController();
|
|
142
|
+
const hasHumanInput = Boolean(
|
|
143
|
+
humanInputController && interactiveAgent.hasHumanInputTools()
|
|
144
|
+
);
|
|
145
|
+
const getModelState = currentModelLabel !== null ? async () => ({
|
|
146
|
+
label: currentModelLabel ?? "server-managed",
|
|
147
|
+
switchable: Boolean(options.switchModel)
|
|
148
|
+
}) : void 0;
|
|
149
|
+
const switchModel = options.switchModel ? async (spec) => {
|
|
150
|
+
const result = await options.switchModel(spec);
|
|
151
|
+
if (result.ok) {
|
|
152
|
+
currentModelLabel = result.label;
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
} : void 0;
|
|
156
|
+
const listTools = async () => agent.getToolIds().map((id) => ({ id })).sort((left, right) => left.id.localeCompare(right.id));
|
|
157
|
+
const listSkills = options.listSkills ? async () => [...await options.listSkills()].sort(
|
|
158
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
159
|
+
) : void 0;
|
|
160
|
+
const listSubAgents = options.listSubAgents ? async () => [...await options.listSubAgents()].sort(
|
|
161
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
162
|
+
) : void 0;
|
|
163
|
+
async function ensureActionSessionLoaded(sessionId) {
|
|
164
|
+
await ensureSessionLoaded({
|
|
165
|
+
sessionId,
|
|
166
|
+
sessions: agent.getSessionManager(),
|
|
167
|
+
cwd: agent.cwd
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const compactSessionContext = async (sessionId) => {
|
|
171
|
+
await ensureActionSessionLoaded(sessionId);
|
|
172
|
+
return await agent.compactContext();
|
|
173
|
+
};
|
|
174
|
+
const undoSessionTurn = async (sessionId) => {
|
|
175
|
+
if (lastTrackedTurnSessionId !== sessionId) {
|
|
176
|
+
return { restored: [], failed: [] };
|
|
177
|
+
}
|
|
178
|
+
return await agent.undoTurn();
|
|
179
|
+
};
|
|
180
|
+
const getSessionTurnDiff = async (sessionId) => {
|
|
181
|
+
if (lastTrackedTurnSessionId !== sessionId) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return await agent.getTurnDiff();
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
cwd: agent.cwd,
|
|
188
|
+
getCapabilities: () => mergeAgentServerCapabilities(
|
|
189
|
+
mergeAgentServerCapabilities(createDefaultAgentServerCapabilities(), {
|
|
190
|
+
protocol: { transport: "in-process", reconnectable: false, multiClient: true },
|
|
191
|
+
turns: { steer: true, followUp: true, followUpManagement: true },
|
|
192
|
+
interactive: { approvalRequests: true, humanRequests: hasHumanInput },
|
|
193
|
+
runtime: {
|
|
194
|
+
execution: "local",
|
|
195
|
+
orchestration: "direct",
|
|
196
|
+
durability: "process",
|
|
197
|
+
dapr: {
|
|
198
|
+
available: false,
|
|
199
|
+
delegatedTurns: false,
|
|
200
|
+
workflowBacked: false
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
agent: {
|
|
204
|
+
workspaceSummary: true,
|
|
205
|
+
toolInventory: true,
|
|
206
|
+
skillInspection: Boolean(listSkills),
|
|
207
|
+
subAgentInspection: Boolean(listSubAgents),
|
|
208
|
+
modelInspection: Boolean(getModelState),
|
|
209
|
+
modelSwitch: Boolean(switchModel),
|
|
210
|
+
runtimeStatus: true,
|
|
211
|
+
contextCompaction: true,
|
|
212
|
+
turnUndo: true,
|
|
213
|
+
turnDiff: true
|
|
214
|
+
},
|
|
215
|
+
plugins: {
|
|
216
|
+
headlessOnly: true,
|
|
217
|
+
commandMetadata: true,
|
|
218
|
+
commandDiscovery: true,
|
|
219
|
+
commandExecution: true,
|
|
220
|
+
clientExtensions: false
|
|
221
|
+
}
|
|
222
|
+
}),
|
|
223
|
+
options.capabilities
|
|
224
|
+
),
|
|
225
|
+
getModelState,
|
|
226
|
+
switchModel,
|
|
227
|
+
async getSessionRuntimeStatus(sessionId) {
|
|
228
|
+
await ensureActionSessionLoaded(sessionId);
|
|
229
|
+
const manager = agent.getSessionManager();
|
|
230
|
+
const loadedId = manager.getSessionId() ?? void 0;
|
|
231
|
+
const leafId = manager.getLeafId() ?? void 0;
|
|
232
|
+
const header = manager.getHeader();
|
|
233
|
+
const modelId = extractModelId(agent.model);
|
|
234
|
+
const provider = extractProvider(agent.model);
|
|
235
|
+
return {
|
|
236
|
+
session: {
|
|
237
|
+
selectedId: sessionId,
|
|
238
|
+
...loadedId ? { loadedId } : {},
|
|
239
|
+
messageCount: agent.getMessages().length,
|
|
240
|
+
...leafId ? { leafId } : {},
|
|
241
|
+
...header?.cwd ? { cwd: header.cwd } : {},
|
|
242
|
+
...header?.title ? { title: header.title } : {}
|
|
243
|
+
},
|
|
244
|
+
model: {
|
|
245
|
+
id: modelId,
|
|
246
|
+
...provider ? { provider } : {}
|
|
247
|
+
},
|
|
248
|
+
context: agent.getContextStats()
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
listTools,
|
|
252
|
+
listSkills,
|
|
253
|
+
listSubAgents,
|
|
254
|
+
...humanInputController ? {
|
|
255
|
+
async respondToInputRequest(requestId, payload) {
|
|
256
|
+
if (payload.kind !== "human") {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
await humanInputController.respondToRequest(
|
|
260
|
+
requestId,
|
|
261
|
+
payload.response
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
} : {},
|
|
265
|
+
...options.listTeamNotifications ? {
|
|
266
|
+
listTeamNotifications(optionsArg) {
|
|
267
|
+
return options.listTeamNotifications(optionsArg);
|
|
268
|
+
}
|
|
269
|
+
} : {},
|
|
270
|
+
...options.subscribeTeamNotifications ? {
|
|
271
|
+
subscribeTeamNotifications(listener) {
|
|
272
|
+
return options.subscribeTeamNotifications(listener);
|
|
273
|
+
}
|
|
274
|
+
} : {},
|
|
275
|
+
compactSessionContext,
|
|
276
|
+
undoSessionTurn,
|
|
277
|
+
getSessionTurnDiff,
|
|
278
|
+
async *chat(sessionId, message, chatOptions) {
|
|
279
|
+
lastTrackedTurnSessionId = sessionId;
|
|
280
|
+
for await (const event of agent.chat(sessionId, message, {
|
|
281
|
+
...chatOptions?.abort ? { abort: chatOptions.abort } : {},
|
|
282
|
+
...chatOptions?.system ? { system: chatOptions.system } : {}
|
|
283
|
+
})) {
|
|
284
|
+
yield event;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
steer: ({ message }) => {
|
|
288
|
+
const id = agent.intervene(message);
|
|
289
|
+
return { id, accepted: true };
|
|
290
|
+
},
|
|
291
|
+
followUp: ({ message }) => {
|
|
292
|
+
const id = agent.queueNext(message);
|
|
293
|
+
return { id, accepted: true };
|
|
294
|
+
},
|
|
295
|
+
drainQueuedFollowUps: () => agent.drainQueuedNext(),
|
|
296
|
+
listSessions: () => agent.listSessions(),
|
|
297
|
+
deleteSession: (sessionId) => agent.deleteSession(sessionId),
|
|
298
|
+
getSessionStorage: () => agent.getSessionManager().getStorage(),
|
|
299
|
+
listPluginCommands: () => pluginCommandInfos,
|
|
300
|
+
executePluginCommand: async (name, args) => {
|
|
301
|
+
const command = pluginCommandTokens.get(normalizePluginCommandToken(name));
|
|
302
|
+
if (!command) {
|
|
303
|
+
throw new Error(`Plugin command not found: ${name}`);
|
|
304
|
+
}
|
|
305
|
+
return await command.execute(args);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function normalizePluginCommandToken(token) {
|
|
310
|
+
return token.trim().replace(/^\//, "").toLowerCase();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/protocol/wire.ts
|
|
314
|
+
function isAgentServerWireEnvelope(value) {
|
|
315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
const record = value;
|
|
319
|
+
return record.kind === "request" || record.kind === "response" || record.kind === "notification";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/runtime/in-process-server.ts
|
|
323
|
+
import {
|
|
324
|
+
SessionManager,
|
|
325
|
+
canSeedQueuedFollowUp,
|
|
326
|
+
createQueuedFollowUpRecord,
|
|
327
|
+
markQueuedFollowUpApplied,
|
|
328
|
+
resolveQueuedFollowUp,
|
|
329
|
+
shouldCascadeApprovalDecision
|
|
330
|
+
} from "@cuylabs/agent-core";
|
|
331
|
+
|
|
332
|
+
// src/runtime/helpers.ts
|
|
333
|
+
function nowIso() {
|
|
334
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
335
|
+
}
|
|
336
|
+
function isTerminalTurnStatus(status) {
|
|
337
|
+
return status === "completed" || status === "failed" || status === "interrupted";
|
|
338
|
+
}
|
|
339
|
+
function isAbortError(error, signal) {
|
|
340
|
+
if (signal.aborted) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
if (error instanceof DOMException) {
|
|
344
|
+
return error.name === "AbortError";
|
|
345
|
+
}
|
|
346
|
+
if (error instanceof Error) {
|
|
347
|
+
return error.name === "AbortError" || /aborted/i.test(error.message);
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
function getDerivedParentSessionId(sessionId) {
|
|
352
|
+
const marker = ":sub:";
|
|
353
|
+
const index = sessionId.lastIndexOf(marker);
|
|
354
|
+
if (index <= 0) {
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
return sessionId.slice(0, index);
|
|
358
|
+
}
|
|
359
|
+
function toSessionSummary(info, runtime) {
|
|
360
|
+
return {
|
|
361
|
+
id: info.id,
|
|
362
|
+
cwd: info.cwd,
|
|
363
|
+
createdAt: info.createdAt.toISOString(),
|
|
364
|
+
updatedAt: info.updatedAt.toISOString(),
|
|
365
|
+
messageCount: info.messageCount,
|
|
366
|
+
...info.title ? { title: info.title } : {},
|
|
367
|
+
...info.name ? { name: info.name } : {},
|
|
368
|
+
...info.parentSessionId ? { parentSessionId: info.parentSessionId } : {},
|
|
369
|
+
...info.firstMessage ? { preview: info.firstMessage } : {},
|
|
370
|
+
runtime
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function toSessionSummaryFromDetail(detail) {
|
|
374
|
+
return {
|
|
375
|
+
id: detail.id,
|
|
376
|
+
cwd: detail.cwd,
|
|
377
|
+
createdAt: detail.createdAt,
|
|
378
|
+
updatedAt: detail.updatedAt,
|
|
379
|
+
messageCount: detail.messageCount,
|
|
380
|
+
...detail.title ? { title: detail.title } : {},
|
|
381
|
+
...detail.name ? { name: detail.name } : {},
|
|
382
|
+
...detail.parentSessionId ? { parentSessionId: detail.parentSessionId } : {},
|
|
383
|
+
...detail.preview ? { preview: detail.preview } : {},
|
|
384
|
+
runtime: detail.runtime
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function getNotificationChannel(notification) {
|
|
388
|
+
switch (notification.type) {
|
|
389
|
+
case "session/created":
|
|
390
|
+
return `session:${notification.session.id}`;
|
|
391
|
+
case "session/deleted":
|
|
392
|
+
return `session:${notification.sessionId}`;
|
|
393
|
+
case "session/branched":
|
|
394
|
+
case "session/runtime":
|
|
395
|
+
return `session:${notification.sessionId}`;
|
|
396
|
+
case "turn/started":
|
|
397
|
+
case "turn/completed":
|
|
398
|
+
return `session:${notification.turn.sessionId}`;
|
|
399
|
+
case "turn/event":
|
|
400
|
+
return `session:${notification.sessionId}`;
|
|
401
|
+
case "input/request":
|
|
402
|
+
return `session:${notification.request.sessionId}`;
|
|
403
|
+
case "input/resolved":
|
|
404
|
+
return `session:${notification.sessionId}`;
|
|
405
|
+
case "team/notification":
|
|
406
|
+
return `team:${notification.teamId}`;
|
|
407
|
+
default:
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/runtime/in-process-server.ts
|
|
413
|
+
var InProcessAgentServer = class {
|
|
414
|
+
adapter;
|
|
415
|
+
capabilities;
|
|
416
|
+
followUpPolicy;
|
|
417
|
+
eventBus;
|
|
418
|
+
turns = /* @__PURE__ */ new Map();
|
|
419
|
+
followUps = /* @__PURE__ */ new Map();
|
|
420
|
+
sessionRuntime = /* @__PURE__ */ new Map();
|
|
421
|
+
listeners = /* @__PURE__ */ new Set();
|
|
422
|
+
pendingInputs = /* @__PURE__ */ new Map();
|
|
423
|
+
teamNotificationUnsubscribe = null;
|
|
424
|
+
constructor(adapter, options = {}) {
|
|
425
|
+
this.adapter = adapter;
|
|
426
|
+
this.followUpPolicy = options.followUpPolicy ?? adapter.followUpPolicy ?? {};
|
|
427
|
+
this.eventBus = options.eventBus ?? null;
|
|
428
|
+
let capabilities = mergeAgentServerCapabilities(
|
|
429
|
+
createDefaultAgentServerCapabilities(),
|
|
430
|
+
adapter.getCapabilities?.()
|
|
431
|
+
);
|
|
432
|
+
if (this.eventBus) {
|
|
433
|
+
capabilities = mergeAgentServerCapabilities(capabilities, {
|
|
434
|
+
events: { eventBus: true, eventReplay: true }
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
this.capabilities = capabilities;
|
|
438
|
+
if (this.adapter.subscribeTeamNotifications) {
|
|
439
|
+
this.teamNotificationUnsubscribe = this.adapter.subscribeTeamNotifications(
|
|
440
|
+
(notification) => {
|
|
441
|
+
this.emit({
|
|
442
|
+
type: "team/notification",
|
|
443
|
+
teamId: notification.teamId,
|
|
444
|
+
notification
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
getCapabilities() {
|
|
451
|
+
return this.capabilities;
|
|
452
|
+
}
|
|
453
|
+
async getWorkspaceSummary() {
|
|
454
|
+
const [sessions, modelState, tools, skills, subAgents] = await Promise.all([
|
|
455
|
+
this.listSessions(),
|
|
456
|
+
this.getModelState().catch(() => null),
|
|
457
|
+
this.listTools(),
|
|
458
|
+
this.listSkills(),
|
|
459
|
+
this.listSubAgents()
|
|
460
|
+
]);
|
|
461
|
+
return {
|
|
462
|
+
workspace: {
|
|
463
|
+
cwd: this.adapter.cwd,
|
|
464
|
+
sessionCount: sessions.length
|
|
465
|
+
},
|
|
466
|
+
agent: {
|
|
467
|
+
modelLabel: modelState?.label ?? "server-managed",
|
|
468
|
+
switchable: modelState?.switchable ?? false,
|
|
469
|
+
toolCount: tools.length,
|
|
470
|
+
skillCount: skills.length,
|
|
471
|
+
subAgentCount: subAgents.length
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async getSessionRuntimeStatus(sessionId) {
|
|
476
|
+
if (!this.adapter.getSessionRuntimeStatus) {
|
|
477
|
+
throw new Error("Runtime status is not available on this agent-server.");
|
|
478
|
+
}
|
|
479
|
+
return await this.adapter.getSessionRuntimeStatus(sessionId);
|
|
480
|
+
}
|
|
481
|
+
async getModelState() {
|
|
482
|
+
if (!this.adapter.getModelState) {
|
|
483
|
+
throw new Error("Model inspection is not available on this agent-server.");
|
|
484
|
+
}
|
|
485
|
+
return await this.adapter.getModelState();
|
|
486
|
+
}
|
|
487
|
+
async switchModel(spec) {
|
|
488
|
+
if (!this.adapter.switchModel) {
|
|
489
|
+
throw new Error("Model switching is not available on this agent-server.");
|
|
490
|
+
}
|
|
491
|
+
return await this.adapter.switchModel(spec);
|
|
492
|
+
}
|
|
493
|
+
async listTools() {
|
|
494
|
+
return [...await this.adapter.listTools?.() ?? []].sort(
|
|
495
|
+
(left, right) => left.id.localeCompare(right.id)
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
async listSkills() {
|
|
499
|
+
return [...await this.adapter.listSkills?.() ?? []].sort(
|
|
500
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
async listSubAgents() {
|
|
504
|
+
return [...await this.adapter.listSubAgents?.() ?? []].sort(
|
|
505
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
async listPluginCommands() {
|
|
509
|
+
return [...await this.adapter.listPluginCommands?.() ?? []].sort(
|
|
510
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
async executePluginCommand(name, args) {
|
|
514
|
+
if (!this.adapter.executePluginCommand) {
|
|
515
|
+
throw new Error("Plugin command execution is not available on this agent-server.");
|
|
516
|
+
}
|
|
517
|
+
return await this.adapter.executePluginCommand(name, args);
|
|
518
|
+
}
|
|
519
|
+
async compactSessionContext(sessionId) {
|
|
520
|
+
if (!this.adapter.compactSessionContext) {
|
|
521
|
+
throw new Error("Context compaction is not available on this agent-server.");
|
|
522
|
+
}
|
|
523
|
+
return await this.adapter.compactSessionContext(sessionId);
|
|
524
|
+
}
|
|
525
|
+
async undoSessionTurn(sessionId) {
|
|
526
|
+
if (!this.adapter.undoSessionTurn) {
|
|
527
|
+
throw new Error("Turn undo is not available on this agent-server.");
|
|
528
|
+
}
|
|
529
|
+
return await this.adapter.undoSessionTurn(sessionId);
|
|
530
|
+
}
|
|
531
|
+
async getSessionTurnDiff(sessionId) {
|
|
532
|
+
if (!this.adapter.getSessionTurnDiff) {
|
|
533
|
+
throw new Error("Turn diff is not available on this agent-server.");
|
|
534
|
+
}
|
|
535
|
+
return await this.adapter.getSessionTurnDiff(sessionId);
|
|
536
|
+
}
|
|
537
|
+
async listSessions() {
|
|
538
|
+
const infos = await this.adapter.listSessions();
|
|
539
|
+
return infos.map((info) => toSessionSummary(info, this.getRuntimeSnapshot(info.id))).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
540
|
+
}
|
|
541
|
+
async createSession(options = {}) {
|
|
542
|
+
const manager = new SessionManager(this.adapter.getSessionStorage());
|
|
543
|
+
const id = await manager.create({
|
|
544
|
+
...options.id ? { id: options.id } : {},
|
|
545
|
+
cwd: options.cwd ?? this.adapter.cwd,
|
|
546
|
+
...options.title ? { title: options.title } : {}
|
|
547
|
+
});
|
|
548
|
+
const detail = await this.readSessionOrThrow(id);
|
|
549
|
+
this.emit({ type: "session/created", session: toSessionSummaryFromDetail(detail) });
|
|
550
|
+
return detail;
|
|
551
|
+
}
|
|
552
|
+
async readSession(sessionId) {
|
|
553
|
+
const manager = new SessionManager(this.adapter.getSessionStorage());
|
|
554
|
+
if (!await manager.sessionExists(sessionId)) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
await manager.load(sessionId);
|
|
558
|
+
const info = await this.findSessionInfo(sessionId);
|
|
559
|
+
const context = manager.getContext();
|
|
560
|
+
return {
|
|
561
|
+
id: sessionId,
|
|
562
|
+
cwd: context.metadata.cwd,
|
|
563
|
+
createdAt: info?.createdAt.toISOString() ?? nowIso(),
|
|
564
|
+
updatedAt: info?.updatedAt.toISOString() ?? nowIso(),
|
|
565
|
+
messageCount: info?.messageCount ?? context.messages.length,
|
|
566
|
+
...context.metadata.title ? { title: context.metadata.title } : {},
|
|
567
|
+
...context.metadata.name ? { name: context.metadata.name } : {},
|
|
568
|
+
...info?.parentSessionId ? { parentSessionId: info.parentSessionId } : {},
|
|
569
|
+
...info?.firstMessage ? { preview: info.firstMessage } : {},
|
|
570
|
+
...context.leafId ? { leafId: context.leafId } : {},
|
|
571
|
+
entries: manager.getEntries(),
|
|
572
|
+
messages: context.messages,
|
|
573
|
+
runtime: this.getRuntimeSnapshot(sessionId)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
async deleteSession(sessionId) {
|
|
577
|
+
const runtime = this.sessionRuntime.get(sessionId);
|
|
578
|
+
if (runtime && runtime.activeTurnIds.size > 0) {
|
|
579
|
+
throw new Error(`Cannot delete session "${sessionId}" while turns are running.`);
|
|
580
|
+
}
|
|
581
|
+
const deleted = await this.adapter.deleteSession(sessionId);
|
|
582
|
+
if (deleted) {
|
|
583
|
+
this.sessionRuntime.delete(sessionId);
|
|
584
|
+
this.emit({ type: "session/deleted", sessionId });
|
|
585
|
+
}
|
|
586
|
+
return deleted;
|
|
587
|
+
}
|
|
588
|
+
async branchSession(sessionId, options = {}) {
|
|
589
|
+
const manager = new SessionManager(this.adapter.getSessionStorage());
|
|
590
|
+
if (!await manager.sessionExists(sessionId)) {
|
|
591
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
592
|
+
}
|
|
593
|
+
await manager.load(sessionId);
|
|
594
|
+
if (options.leafId) {
|
|
595
|
+
manager.switchToLeaf(options.leafId);
|
|
596
|
+
}
|
|
597
|
+
const branchFrom = manager.getLeafId();
|
|
598
|
+
if (!branchFrom) {
|
|
599
|
+
throw new Error(`Session "${sessionId}" has no leaf to branch from.`);
|
|
600
|
+
}
|
|
601
|
+
const leafId = await manager.branch(branchFrom, options.summary);
|
|
602
|
+
this.emit({
|
|
603
|
+
type: "session/branched",
|
|
604
|
+
sessionId,
|
|
605
|
+
leafId,
|
|
606
|
+
...options.summary ? { summary: options.summary } : {}
|
|
607
|
+
});
|
|
608
|
+
return { sessionId, leafId };
|
|
609
|
+
}
|
|
610
|
+
async startTurn(sessionId, message, options = {}) {
|
|
611
|
+
if (this.hasRunningTurnForSession(sessionId)) {
|
|
612
|
+
throw new Error(`Session "${sessionId}" already has a running turn.`);
|
|
613
|
+
}
|
|
614
|
+
await this.ensureSessionExists(sessionId);
|
|
615
|
+
const turn = {
|
|
616
|
+
id: globalThis.crypto.randomUUID(),
|
|
617
|
+
sessionId,
|
|
618
|
+
message,
|
|
619
|
+
startedAt: nowIso(),
|
|
620
|
+
status: "running",
|
|
621
|
+
events: [],
|
|
622
|
+
queuedFollowUpIds: [],
|
|
623
|
+
abortController: new AbortController()
|
|
624
|
+
};
|
|
625
|
+
await this.seedQueuedFollowUps(sessionId, turn.id);
|
|
626
|
+
this.turns.set(turn.id, turn);
|
|
627
|
+
const runtime = this.getOrCreateRuntimeRecord(sessionId);
|
|
628
|
+
runtime.activeTurnIds.add(turn.id);
|
|
629
|
+
runtime.lastTurnId = turn.id;
|
|
630
|
+
this.emit({
|
|
631
|
+
type: "session/runtime",
|
|
632
|
+
sessionId,
|
|
633
|
+
runtime: this.getRuntimeSnapshot(sessionId)
|
|
634
|
+
});
|
|
635
|
+
this.emit({ type: "turn/started", turn: this.toTurnSnapshot(turn) });
|
|
636
|
+
void this.runTurn(turn, options);
|
|
637
|
+
return this.toTurnSnapshot(turn);
|
|
638
|
+
}
|
|
639
|
+
getTurn(turnId) {
|
|
640
|
+
const turn = this.turns.get(turnId);
|
|
641
|
+
return turn ? this.toTurnDetail(turn) : null;
|
|
642
|
+
}
|
|
643
|
+
async waitForTurn(turnId, options = {}) {
|
|
644
|
+
const turn = this.turns.get(turnId);
|
|
645
|
+
if (!turn) {
|
|
646
|
+
throw new Error(`Turn not found: ${turnId}`);
|
|
647
|
+
}
|
|
648
|
+
if (isTerminalTurnStatus(turn.status)) {
|
|
649
|
+
return this.toTurnSnapshot(turn);
|
|
650
|
+
}
|
|
651
|
+
return await new Promise((resolve, reject) => {
|
|
652
|
+
const off = this.subscribe(
|
|
653
|
+
(notification) => {
|
|
654
|
+
if (notification.type === "turn/completed" && notification.turn.id === turnId) {
|
|
655
|
+
cleanup();
|
|
656
|
+
resolve(notification.turn);
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
{ turnId }
|
|
660
|
+
);
|
|
661
|
+
const timeoutMs = options.timeoutMs ?? 0;
|
|
662
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
663
|
+
cleanup();
|
|
664
|
+
reject(new Error(`Timed out waiting for turn "${turnId}"`));
|
|
665
|
+
}, timeoutMs) : null;
|
|
666
|
+
const cleanup = () => {
|
|
667
|
+
off();
|
|
668
|
+
if (timer) {
|
|
669
|
+
clearTimeout(timer);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
steerTurn(turnId, message) {
|
|
675
|
+
const turn = this.turns.get(turnId);
|
|
676
|
+
if (!turn || isTerminalTurnStatus(turn.status) || !this.adapter.steer) {
|
|
677
|
+
return { id: "", accepted: false };
|
|
678
|
+
}
|
|
679
|
+
return this.adapter.steer({
|
|
680
|
+
turnId,
|
|
681
|
+
sessionId: turn.sessionId,
|
|
682
|
+
message
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
followUpTurn(turnId, message) {
|
|
686
|
+
const turn = this.turns.get(turnId);
|
|
687
|
+
if (!turn || isTerminalTurnStatus(turn.status) || !this.adapter.followUp) {
|
|
688
|
+
return { id: "", accepted: false };
|
|
689
|
+
}
|
|
690
|
+
const response = this.adapter.followUp({
|
|
691
|
+
turnId,
|
|
692
|
+
sessionId: turn.sessionId,
|
|
693
|
+
message
|
|
694
|
+
});
|
|
695
|
+
if (!response.accepted) {
|
|
696
|
+
return response;
|
|
697
|
+
}
|
|
698
|
+
const record = createQueuedFollowUpRecord(
|
|
699
|
+
{
|
|
700
|
+
id: response.id,
|
|
701
|
+
sessionId: turn.sessionId,
|
|
702
|
+
message,
|
|
703
|
+
timestamp: Date.now()
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
mode: this.followUpPolicy.mode,
|
|
707
|
+
sourceTurnId: turn.id,
|
|
708
|
+
createdAt: nowIso(),
|
|
709
|
+
updatedAt: nowIso()
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
this.followUps.set(record.id, record);
|
|
713
|
+
if (!turn.queuedFollowUpIds.includes(record.id)) {
|
|
714
|
+
turn.queuedFollowUpIds.push(record.id);
|
|
715
|
+
}
|
|
716
|
+
return response;
|
|
717
|
+
}
|
|
718
|
+
async listFollowUps(options = {}) {
|
|
719
|
+
if (this.adapter.listFollowUps) {
|
|
720
|
+
return await this.adapter.listFollowUps(options);
|
|
721
|
+
}
|
|
722
|
+
const statuses = options.status ? new Set(Array.isArray(options.status) ? options.status : [options.status]) : void 0;
|
|
723
|
+
return [...this.followUps.values()].filter((record) => {
|
|
724
|
+
if (options.sessionId && record.sessionId !== options.sessionId) {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
if (options.sourceTurnId && record.sourceTurnId !== options.sourceTurnId) {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
if (statuses && !statuses.has(record.status)) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
return true;
|
|
734
|
+
}).sort((left, right) => left.createdAt.localeCompare(right.createdAt)).map((record) => structuredClone(record));
|
|
735
|
+
}
|
|
736
|
+
async getFollowUp(followUpId) {
|
|
737
|
+
if (this.adapter.getFollowUp) {
|
|
738
|
+
return await this.adapter.getFollowUp(followUpId);
|
|
739
|
+
}
|
|
740
|
+
const record = this.followUps.get(followUpId);
|
|
741
|
+
return record ? structuredClone(record) : null;
|
|
742
|
+
}
|
|
743
|
+
async resolveFollowUp(followUpId, action) {
|
|
744
|
+
if (this.adapter.resolveFollowUp) {
|
|
745
|
+
const resolved = await this.adapter.resolveFollowUp(followUpId, action);
|
|
746
|
+
if (resolved) {
|
|
747
|
+
this.followUps.set(followUpId, structuredClone(resolved));
|
|
748
|
+
}
|
|
749
|
+
return resolved;
|
|
750
|
+
}
|
|
751
|
+
const existing = this.followUps.get(followUpId);
|
|
752
|
+
if (!existing || existing.status === "discarded" || existing.status === "applied") {
|
|
753
|
+
return existing ? structuredClone(existing) : null;
|
|
754
|
+
}
|
|
755
|
+
const updated = resolveQueuedFollowUp(existing, action, nowIso());
|
|
756
|
+
this.followUps.set(followUpId, updated);
|
|
757
|
+
return structuredClone(updated);
|
|
758
|
+
}
|
|
759
|
+
async listTeamNotifications(options = {}) {
|
|
760
|
+
if (!this.adapter.listTeamNotifications) {
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
const kinds = options.kind ? new Set(Array.isArray(options.kind) ? options.kind : [options.kind]) : void 0;
|
|
764
|
+
return [...await this.adapter.listTeamNotifications(options)].filter((notification) => {
|
|
765
|
+
if (options.teamId && notification.teamId !== options.teamId) {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
if (options.memberId && notification.memberId !== options.memberId) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
if (kinds && !kinds.has(notification.kind)) {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
return true;
|
|
775
|
+
}).sort((left, right) => left.createdAt.localeCompare(right.createdAt)).map((notification) => structuredClone(notification));
|
|
776
|
+
}
|
|
777
|
+
interruptTurn(turnId) {
|
|
778
|
+
const turn = this.turns.get(turnId);
|
|
779
|
+
if (!turn || isTerminalTurnStatus(turn.status)) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
this.resolvePendingInputsForTurn(turnId, "deny");
|
|
783
|
+
void this.adapter.interruptTurn?.({
|
|
784
|
+
turnId,
|
|
785
|
+
sessionId: turn.sessionId
|
|
786
|
+
});
|
|
787
|
+
turn.abortController.abort();
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
respondToInputRequest(requestId, payload) {
|
|
791
|
+
const pending = this.pendingInputs.get(requestId);
|
|
792
|
+
if (!pending || pending.resolved) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
if (pending.resolve) {
|
|
796
|
+
this.resolvePendingInput(requestId, pending, payload);
|
|
797
|
+
} else {
|
|
798
|
+
pending.resolved = true;
|
|
799
|
+
this.pendingInputs.delete(requestId);
|
|
800
|
+
void this.adapter.respondToInputRequest?.(requestId, payload);
|
|
801
|
+
this.emit({
|
|
802
|
+
type: "input/resolved",
|
|
803
|
+
requestId,
|
|
804
|
+
sessionId: pending.request.sessionId,
|
|
805
|
+
...pending.request.turnId ? { turnId: pending.request.turnId } : {}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
if (payload.kind === "approval" && pending.request.kind === "approval") {
|
|
809
|
+
this.resolveMatchingApprovalRequests(
|
|
810
|
+
pending.request.request,
|
|
811
|
+
{
|
|
812
|
+
action: payload.action,
|
|
813
|
+
...payload.feedback ? { feedback: payload.feedback } : {},
|
|
814
|
+
...payload.rememberScope ? { rememberScope: payload.rememberScope } : {}
|
|
815
|
+
}
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
subscribe(listener, options) {
|
|
821
|
+
const record = { listener, options };
|
|
822
|
+
this.listeners.add(record);
|
|
823
|
+
return () => {
|
|
824
|
+
this.listeners.delete(record);
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async close() {
|
|
828
|
+
this.teamNotificationUnsubscribe?.();
|
|
829
|
+
this.resolveAllPendingInputs("deny");
|
|
830
|
+
const running = [...this.turns.values()].filter(
|
|
831
|
+
(turn) => !isTerminalTurnStatus(turn.status)
|
|
832
|
+
);
|
|
833
|
+
for (const turn of running) {
|
|
834
|
+
turn.abortController.abort();
|
|
835
|
+
}
|
|
836
|
+
await Promise.allSettled(
|
|
837
|
+
running.map((turn) => this.waitForTurn(turn.id, { timeoutMs: 2e3 }))
|
|
838
|
+
);
|
|
839
|
+
this.listeners.clear();
|
|
840
|
+
}
|
|
841
|
+
requestApproval(request) {
|
|
842
|
+
const route = this.resolveInteractiveRequestRoute(request.sessionId);
|
|
843
|
+
const inputRequest = {
|
|
844
|
+
id: request.id,
|
|
845
|
+
kind: "approval",
|
|
846
|
+
sessionId: route.sessionId,
|
|
847
|
+
...route.turnId ? { turnId: route.turnId } : {},
|
|
848
|
+
createdAt: nowIso(),
|
|
849
|
+
request
|
|
850
|
+
};
|
|
851
|
+
return new Promise((resolve) => {
|
|
852
|
+
this.pendingInputs.set(request.id, {
|
|
853
|
+
request: inputRequest,
|
|
854
|
+
resolve,
|
|
855
|
+
resolved: false
|
|
856
|
+
});
|
|
857
|
+
this.emit({ type: "input/request", request: inputRequest });
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
requestHumanInput(request) {
|
|
861
|
+
const route = this.resolveInteractiveRequestRoute(request.sessionId);
|
|
862
|
+
const inputRequest = {
|
|
863
|
+
id: request.id,
|
|
864
|
+
kind: "human",
|
|
865
|
+
sessionId: route.sessionId,
|
|
866
|
+
...route.turnId ? { turnId: route.turnId } : {},
|
|
867
|
+
createdAt: nowIso(),
|
|
868
|
+
request
|
|
869
|
+
};
|
|
870
|
+
return new Promise((resolve) => {
|
|
871
|
+
this.pendingInputs.set(request.id, {
|
|
872
|
+
request: inputRequest,
|
|
873
|
+
resolve,
|
|
874
|
+
resolved: false
|
|
875
|
+
});
|
|
876
|
+
this.emit({ type: "input/request", request: inputRequest });
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
async runTurn(turn, options) {
|
|
880
|
+
try {
|
|
881
|
+
let sequence = 0;
|
|
882
|
+
for await (const event of this.adapter.chat(turn.sessionId, turn.message, {
|
|
883
|
+
abort: turn.abortController.signal,
|
|
884
|
+
...options.system ? { system: options.system } : {}
|
|
885
|
+
})) {
|
|
886
|
+
const record = {
|
|
887
|
+
sequence: ++sequence,
|
|
888
|
+
timestamp: nowIso(),
|
|
889
|
+
event
|
|
890
|
+
};
|
|
891
|
+
turn.events.push(record);
|
|
892
|
+
switch (event.type) {
|
|
893
|
+
case "approval-request":
|
|
894
|
+
this.registerEventInputRequest(turn, event);
|
|
895
|
+
break;
|
|
896
|
+
case "human-input-request":
|
|
897
|
+
this.registerEventInputRequest(turn, event);
|
|
898
|
+
break;
|
|
899
|
+
case "approval-resolved":
|
|
900
|
+
this.resolveEventInputRequest(turn, event.id);
|
|
901
|
+
break;
|
|
902
|
+
case "human-input-resolved":
|
|
903
|
+
this.resolveEventInputRequest(turn, event.id);
|
|
904
|
+
break;
|
|
905
|
+
case "complete":
|
|
906
|
+
turn.output = event.output;
|
|
907
|
+
turn.usage = event.usage;
|
|
908
|
+
break;
|
|
909
|
+
case "error":
|
|
910
|
+
turn.error = event.error.message;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
this.emit({
|
|
914
|
+
type: "turn/event",
|
|
915
|
+
sessionId: turn.sessionId,
|
|
916
|
+
turnId: turn.id,
|
|
917
|
+
sequence: record.sequence,
|
|
918
|
+
timestamp: record.timestamp,
|
|
919
|
+
event
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
if (turn.status === "running") {
|
|
923
|
+
turn.status = turn.abortController.signal.aborted ? "interrupted" : turn.error ? "failed" : "completed";
|
|
924
|
+
}
|
|
925
|
+
} catch (error) {
|
|
926
|
+
if (isAbortError(error, turn.abortController.signal)) {
|
|
927
|
+
turn.status = "interrupted";
|
|
928
|
+
} else {
|
|
929
|
+
turn.status = "failed";
|
|
930
|
+
turn.error = error instanceof Error ? error.message : String(error);
|
|
931
|
+
}
|
|
932
|
+
} finally {
|
|
933
|
+
this.captureDeferredFollowUps(turn);
|
|
934
|
+
this.resolvePendingInputsForTurn(turn.id, "deny");
|
|
935
|
+
turn.completedAt = nowIso();
|
|
936
|
+
const runtime = this.getOrCreateRuntimeRecord(turn.sessionId);
|
|
937
|
+
runtime.activeTurnIds.delete(turn.id);
|
|
938
|
+
runtime.lastTurnId = turn.id;
|
|
939
|
+
this.emit({
|
|
940
|
+
type: "session/runtime",
|
|
941
|
+
sessionId: turn.sessionId,
|
|
942
|
+
runtime: this.getRuntimeSnapshot(turn.sessionId)
|
|
943
|
+
});
|
|
944
|
+
this.emit({ type: "turn/completed", turn: this.toTurnSnapshot(turn) });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async ensureSessionExists(sessionId) {
|
|
948
|
+
const manager = new SessionManager(this.adapter.getSessionStorage());
|
|
949
|
+
if (await manager.sessionExists(sessionId)) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
await manager.create({ id: sessionId, cwd: this.adapter.cwd });
|
|
953
|
+
const detail = await this.readSessionOrThrow(sessionId);
|
|
954
|
+
this.emit({ type: "session/created", session: toSessionSummaryFromDetail(detail) });
|
|
955
|
+
}
|
|
956
|
+
getOrCreateRuntimeRecord(sessionId) {
|
|
957
|
+
let record = this.sessionRuntime.get(sessionId);
|
|
958
|
+
if (!record) {
|
|
959
|
+
record = { activeTurnIds: /* @__PURE__ */ new Set() };
|
|
960
|
+
this.sessionRuntime.set(sessionId, record);
|
|
961
|
+
}
|
|
962
|
+
return record;
|
|
963
|
+
}
|
|
964
|
+
getRuntimeSnapshot(sessionId) {
|
|
965
|
+
const runtime = this.sessionRuntime.get(sessionId);
|
|
966
|
+
const activeTurnIds = runtime ? [...runtime.activeTurnIds] : [];
|
|
967
|
+
return {
|
|
968
|
+
status: activeTurnIds.length > 0 ? "running" : "idle",
|
|
969
|
+
activeTurnCount: activeTurnIds.length,
|
|
970
|
+
activeTurnIds,
|
|
971
|
+
...runtime?.lastTurnId ? { lastTurnId: runtime.lastTurnId } : {}
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
hasRunningTurnForSession(sessionId) {
|
|
975
|
+
const runtime = this.sessionRuntime.get(sessionId);
|
|
976
|
+
return Boolean(runtime && runtime.activeTurnIds.size > 0);
|
|
977
|
+
}
|
|
978
|
+
findRunningTurnIdForSession(sessionId) {
|
|
979
|
+
const runtime = this.sessionRuntime.get(sessionId);
|
|
980
|
+
if (!runtime) {
|
|
981
|
+
return void 0;
|
|
982
|
+
}
|
|
983
|
+
const [turnId] = runtime.activeTurnIds;
|
|
984
|
+
return turnId;
|
|
985
|
+
}
|
|
986
|
+
resolveInteractiveRequestRoute(sessionId) {
|
|
987
|
+
let currentSessionId = sessionId;
|
|
988
|
+
const visited = /* @__PURE__ */ new Set();
|
|
989
|
+
while (!visited.has(currentSessionId)) {
|
|
990
|
+
visited.add(currentSessionId);
|
|
991
|
+
const turnId = this.findRunningTurnIdForSession(currentSessionId);
|
|
992
|
+
if (turnId) {
|
|
993
|
+
return { sessionId: currentSessionId, turnId };
|
|
994
|
+
}
|
|
995
|
+
const parentSessionId = getDerivedParentSessionId(currentSessionId);
|
|
996
|
+
if (!parentSessionId) {
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
currentSessionId = parentSessionId;
|
|
1000
|
+
}
|
|
1001
|
+
return { sessionId };
|
|
1002
|
+
}
|
|
1003
|
+
async findSessionInfo(sessionId) {
|
|
1004
|
+
const sessions = await this.adapter.listSessions();
|
|
1005
|
+
return sessions.find((session) => session.id === sessionId) ?? null;
|
|
1006
|
+
}
|
|
1007
|
+
async readSessionOrThrow(sessionId) {
|
|
1008
|
+
const detail = await this.readSession(sessionId);
|
|
1009
|
+
if (!detail) {
|
|
1010
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1011
|
+
}
|
|
1012
|
+
return detail;
|
|
1013
|
+
}
|
|
1014
|
+
emit(notification) {
|
|
1015
|
+
for (const record of this.listeners) {
|
|
1016
|
+
if (!matchesNotificationSubscription(notification, record.options)) {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
record.listener(notification);
|
|
1020
|
+
}
|
|
1021
|
+
if (this.eventBus) {
|
|
1022
|
+
const channel = getNotificationChannel(notification);
|
|
1023
|
+
if (channel) {
|
|
1024
|
+
this.eventBus.publish(channel, notification.type, notification);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
captureDeferredFollowUps(turn) {
|
|
1029
|
+
if (this.adapter.listFollowUps) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const queued = this.adapter.drainQueuedFollowUps?.() ?? [];
|
|
1033
|
+
if (queued.length === 0) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
for (const item of queued) {
|
|
1037
|
+
if (!this.followUps.has(item.id)) {
|
|
1038
|
+
this.followUps.set(
|
|
1039
|
+
item.id,
|
|
1040
|
+
createQueuedFollowUpRecord(
|
|
1041
|
+
{
|
|
1042
|
+
id: item.id,
|
|
1043
|
+
sessionId: turn.sessionId,
|
|
1044
|
+
message: item.message,
|
|
1045
|
+
timestamp: item.createdAt.getTime()
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
mode: this.followUpPolicy.mode,
|
|
1049
|
+
sourceTurnId: turn.id,
|
|
1050
|
+
createdAt: item.createdAt.toISOString(),
|
|
1051
|
+
updatedAt: nowIso()
|
|
1052
|
+
}
|
|
1053
|
+
)
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
if (!turn.queuedFollowUpIds.includes(item.id)) {
|
|
1057
|
+
turn.queuedFollowUpIds.push(item.id);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async seedQueuedFollowUps(sessionId, seededTurnId) {
|
|
1062
|
+
if (this.adapter.listFollowUps) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const ready = [...this.followUps.values()].filter(
|
|
1066
|
+
(record) => record.sessionId === sessionId && canSeedQueuedFollowUp(record)
|
|
1067
|
+
);
|
|
1068
|
+
if (ready.length === 0) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const manager = new SessionManager(this.adapter.getSessionStorage());
|
|
1072
|
+
if (!await manager.sessionExists(sessionId)) {
|
|
1073
|
+
await manager.create({ id: sessionId, cwd: this.adapter.cwd });
|
|
1074
|
+
} else {
|
|
1075
|
+
await manager.load(sessionId);
|
|
1076
|
+
}
|
|
1077
|
+
for (const record of ready) {
|
|
1078
|
+
await manager.addMessage({
|
|
1079
|
+
id: globalThis.crypto.randomUUID(),
|
|
1080
|
+
role: "user",
|
|
1081
|
+
content: record.message,
|
|
1082
|
+
createdAt: new Date(record.createdAt)
|
|
1083
|
+
});
|
|
1084
|
+
this.followUps.set(
|
|
1085
|
+
record.id,
|
|
1086
|
+
markQueuedFollowUpApplied(record, nowIso(), seededTurnId)
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
toTurnSnapshot(turn) {
|
|
1091
|
+
return {
|
|
1092
|
+
id: turn.id,
|
|
1093
|
+
sessionId: turn.sessionId,
|
|
1094
|
+
message: turn.message,
|
|
1095
|
+
startedAt: turn.startedAt,
|
|
1096
|
+
status: turn.status,
|
|
1097
|
+
...turn.completedAt ? { completedAt: turn.completedAt } : {},
|
|
1098
|
+
...turn.output !== void 0 ? { output: turn.output } : {},
|
|
1099
|
+
...turn.usage ? { usage: turn.usage } : {},
|
|
1100
|
+
...turn.error ? { error: turn.error } : {},
|
|
1101
|
+
...turn.queuedFollowUpIds.length > 0 ? {
|
|
1102
|
+
queuedFollowUps: turn.queuedFollowUpIds.map((id) => this.followUps.get(id)).filter((record) => Boolean(record)).map((record) => structuredClone(record))
|
|
1103
|
+
} : {}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
toTurnDetail(turn) {
|
|
1107
|
+
return {
|
|
1108
|
+
...this.toTurnSnapshot(turn),
|
|
1109
|
+
events: [...turn.events]
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
resolvePendingInputsForTurn(turnId, action) {
|
|
1113
|
+
for (const [requestId, pending] of this.pendingInputs) {
|
|
1114
|
+
if (pending.resolved || pending.request.turnId !== turnId) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (pending.request.kind === "approval") {
|
|
1118
|
+
this.respondToInputRequest(requestId, {
|
|
1119
|
+
kind: "approval",
|
|
1120
|
+
action
|
|
1121
|
+
});
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
this.respondToInputRequest(requestId, {
|
|
1125
|
+
kind: "human",
|
|
1126
|
+
response: {
|
|
1127
|
+
kind: "text",
|
|
1128
|
+
text: "Human input request cancelled."
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
resolveAllPendingInputs(action) {
|
|
1134
|
+
for (const requestId of [...this.pendingInputs.keys()]) {
|
|
1135
|
+
const pending = this.pendingInputs.get(requestId);
|
|
1136
|
+
if (!pending) continue;
|
|
1137
|
+
if (pending.request.kind === "approval") {
|
|
1138
|
+
this.respondToInputRequest(requestId, {
|
|
1139
|
+
kind: "approval",
|
|
1140
|
+
action
|
|
1141
|
+
});
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
this.respondToInputRequest(requestId, {
|
|
1145
|
+
kind: "human",
|
|
1146
|
+
response: {
|
|
1147
|
+
kind: "text",
|
|
1148
|
+
text: "Human input request cancelled."
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
resolveMatchingApprovalRequests(resolvedRequest, decision) {
|
|
1154
|
+
const payload = {
|
|
1155
|
+
kind: "approval",
|
|
1156
|
+
action: decision.action,
|
|
1157
|
+
...decision.feedback ? { feedback: decision.feedback } : {},
|
|
1158
|
+
...decision.rememberScope ? { rememberScope: decision.rememberScope } : {}
|
|
1159
|
+
};
|
|
1160
|
+
for (const [requestId, pending] of [...this.pendingInputs.entries()]) {
|
|
1161
|
+
if (pending.resolved || pending.request.kind !== "approval") {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
if (!shouldCascadeApprovalDecision(
|
|
1165
|
+
resolvedRequest,
|
|
1166
|
+
pending.request.request,
|
|
1167
|
+
decision
|
|
1168
|
+
)) {
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
this.resolvePendingInput(requestId, pending, payload);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
resolvePendingInput(requestId, pending, payload) {
|
|
1175
|
+
pending.resolved = true;
|
|
1176
|
+
this.pendingInputs.delete(requestId);
|
|
1177
|
+
switch (payload.kind) {
|
|
1178
|
+
case "approval":
|
|
1179
|
+
pending.resolve?.({
|
|
1180
|
+
action: payload.action,
|
|
1181
|
+
...payload.feedback ? { feedback: payload.feedback } : {},
|
|
1182
|
+
...payload.rememberScope ? { rememberScope: payload.rememberScope } : {}
|
|
1183
|
+
});
|
|
1184
|
+
break;
|
|
1185
|
+
case "human":
|
|
1186
|
+
pending.resolve?.(payload.response);
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
this.emit({
|
|
1190
|
+
type: "input/resolved",
|
|
1191
|
+
requestId,
|
|
1192
|
+
sessionId: pending.request.sessionId,
|
|
1193
|
+
...pending.request.turnId ? { turnId: pending.request.turnId } : {}
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
registerEventInputRequest(turn, event) {
|
|
1197
|
+
const request = event.type === "approval-request" ? {
|
|
1198
|
+
id: event.request.id,
|
|
1199
|
+
kind: "approval",
|
|
1200
|
+
sessionId: turn.sessionId,
|
|
1201
|
+
turnId: turn.id,
|
|
1202
|
+
createdAt: nowIso(),
|
|
1203
|
+
request: {
|
|
1204
|
+
id: event.request.id,
|
|
1205
|
+
sessionId: turn.sessionId,
|
|
1206
|
+
tool: event.request.tool,
|
|
1207
|
+
args: event.request.args,
|
|
1208
|
+
description: event.request.description,
|
|
1209
|
+
risk: event.request.risk,
|
|
1210
|
+
patterns: [],
|
|
1211
|
+
timestamp: Date.now(),
|
|
1212
|
+
...event.request.rememberScopes ? { rememberScopes: event.request.rememberScopes } : {},
|
|
1213
|
+
...event.request.defaultRememberScope ? { defaultRememberScope: event.request.defaultRememberScope } : {}
|
|
1214
|
+
}
|
|
1215
|
+
} : {
|
|
1216
|
+
id: event.request.id,
|
|
1217
|
+
kind: "human",
|
|
1218
|
+
sessionId: turn.sessionId,
|
|
1219
|
+
turnId: turn.id,
|
|
1220
|
+
createdAt: nowIso(),
|
|
1221
|
+
request: {
|
|
1222
|
+
id: event.request.id,
|
|
1223
|
+
sessionId: turn.sessionId,
|
|
1224
|
+
kind: event.request.kind,
|
|
1225
|
+
title: event.request.title,
|
|
1226
|
+
question: event.request.question,
|
|
1227
|
+
timestamp: Date.now(),
|
|
1228
|
+
...event.request.options ? { options: event.request.options } : {},
|
|
1229
|
+
...event.request.allowMultiple !== void 0 ? { allowMultiple: event.request.allowMultiple } : {},
|
|
1230
|
+
...event.request.placeholder ? { placeholder: event.request.placeholder } : {},
|
|
1231
|
+
...event.request.confirmLabel ? { confirmLabel: event.request.confirmLabel } : {},
|
|
1232
|
+
...event.request.denyLabel ? { denyLabel: event.request.denyLabel } : {}
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
const existing = this.pendingInputs.get(request.id);
|
|
1236
|
+
if (existing && !existing.resolved) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
this.pendingInputs.set(request.id, {
|
|
1240
|
+
request,
|
|
1241
|
+
resolved: false
|
|
1242
|
+
});
|
|
1243
|
+
this.emit({ type: "input/request", request });
|
|
1244
|
+
}
|
|
1245
|
+
resolveEventInputRequest(turn, requestId) {
|
|
1246
|
+
const pending = this.pendingInputs.get(requestId);
|
|
1247
|
+
if (pending && !pending.resolved) {
|
|
1248
|
+
pending.resolved = true;
|
|
1249
|
+
this.pendingInputs.delete(requestId);
|
|
1250
|
+
this.emit({
|
|
1251
|
+
type: "input/resolved",
|
|
1252
|
+
requestId,
|
|
1253
|
+
sessionId: turn.sessionId,
|
|
1254
|
+
turnId: turn.id
|
|
1255
|
+
});
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
this.emit({
|
|
1259
|
+
type: "input/resolved",
|
|
1260
|
+
requestId,
|
|
1261
|
+
sessionId: turn.sessionId,
|
|
1262
|
+
turnId: turn.id
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// src/runtime/interactive-handlers.ts
|
|
1268
|
+
function createServerApprovalRequestHandler(bridge) {
|
|
1269
|
+
if (!bridge.requestApproval) {
|
|
1270
|
+
throw new Error("Interactive request bridge does not support approvals.");
|
|
1271
|
+
}
|
|
1272
|
+
return async (request) => await bridge.requestApproval(request);
|
|
1273
|
+
}
|
|
1274
|
+
function createServerHumanInputRequestHandler(bridge) {
|
|
1275
|
+
if (!bridge.requestHumanInput) {
|
|
1276
|
+
throw new Error("Interactive request bridge does not support human input.");
|
|
1277
|
+
}
|
|
1278
|
+
return async (request) => await bridge.requestHumanInput(request);
|
|
1279
|
+
}
|
|
1280
|
+
function createServerInteractiveHandlers(bridge) {
|
|
1281
|
+
return {
|
|
1282
|
+
approval: createServerApprovalRequestHandler(bridge),
|
|
1283
|
+
humanInput: createServerHumanInputRequestHandler(bridge)
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/transport/rpc-connection.ts
|
|
1288
|
+
var AgentServerRpcConnection = class {
|
|
1289
|
+
server;
|
|
1290
|
+
capabilities;
|
|
1291
|
+
sendEnvelope;
|
|
1292
|
+
unsubscribe = null;
|
|
1293
|
+
constructor(options) {
|
|
1294
|
+
this.server = options.server;
|
|
1295
|
+
this.capabilities = mergeAgentServerCapabilities(
|
|
1296
|
+
this.server.getCapabilities(),
|
|
1297
|
+
options.capabilityOverride
|
|
1298
|
+
);
|
|
1299
|
+
this.sendEnvelope = options.send;
|
|
1300
|
+
this.unsubscribe = this.server.subscribe((notification) => {
|
|
1301
|
+
void this.sendEnvelope({
|
|
1302
|
+
kind: "notification",
|
|
1303
|
+
notification
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
async close() {
|
|
1308
|
+
this.unsubscribe?.();
|
|
1309
|
+
this.unsubscribe = null;
|
|
1310
|
+
}
|
|
1311
|
+
async handleRequest(envelope) {
|
|
1312
|
+
try {
|
|
1313
|
+
const result = await this.dispatchRequest({
|
|
1314
|
+
method: envelope.method,
|
|
1315
|
+
params: envelope.params
|
|
1316
|
+
});
|
|
1317
|
+
await this.sendEnvelope({
|
|
1318
|
+
kind: "response",
|
|
1319
|
+
id: envelope.id,
|
|
1320
|
+
ok: true,
|
|
1321
|
+
result
|
|
1322
|
+
});
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
await this.sendEnvelope(toErrorResponse(envelope.id, error));
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async dispatchRequest(request) {
|
|
1328
|
+
switch (request.method) {
|
|
1329
|
+
case "initialize":
|
|
1330
|
+
return {
|
|
1331
|
+
protocolVersion: 1,
|
|
1332
|
+
capabilities: this.capabilities
|
|
1333
|
+
};
|
|
1334
|
+
case "capabilities/get":
|
|
1335
|
+
return this.capabilities;
|
|
1336
|
+
case "workspace/summary":
|
|
1337
|
+
return await this.server.getWorkspaceSummary();
|
|
1338
|
+
case "agent/model/get":
|
|
1339
|
+
return await this.server.getModelState();
|
|
1340
|
+
case "agent/model/switch":
|
|
1341
|
+
return await this.server.switchModel(request.params.spec);
|
|
1342
|
+
case "session/status":
|
|
1343
|
+
return await this.server.getSessionRuntimeStatus(request.params.sessionId);
|
|
1344
|
+
case "agent/tools":
|
|
1345
|
+
return await this.server.listTools();
|
|
1346
|
+
case "agent/skills":
|
|
1347
|
+
return await this.server.listSkills();
|
|
1348
|
+
case "agent/sub-agents":
|
|
1349
|
+
return await this.server.listSubAgents();
|
|
1350
|
+
case "plugin-command/list":
|
|
1351
|
+
return await this.server.listPluginCommands();
|
|
1352
|
+
case "plugin-command/execute":
|
|
1353
|
+
return await this.server.executePluginCommand(
|
|
1354
|
+
request.params.name,
|
|
1355
|
+
request.params.args ?? ""
|
|
1356
|
+
);
|
|
1357
|
+
case "session/compact":
|
|
1358
|
+
return await this.server.compactSessionContext(request.params.sessionId);
|
|
1359
|
+
case "session/undo":
|
|
1360
|
+
return await this.server.undoSessionTurn(request.params.sessionId);
|
|
1361
|
+
case "session/diff":
|
|
1362
|
+
return await this.server.getSessionTurnDiff(request.params.sessionId);
|
|
1363
|
+
case "session/list":
|
|
1364
|
+
return await this.server.listSessions();
|
|
1365
|
+
case "session/create":
|
|
1366
|
+
return await this.server.createSession(request.params);
|
|
1367
|
+
case "session/read":
|
|
1368
|
+
return await this.server.readSession(request.params.sessionId);
|
|
1369
|
+
case "session/delete":
|
|
1370
|
+
return await this.server.deleteSession(request.params.sessionId);
|
|
1371
|
+
case "session/branch":
|
|
1372
|
+
return await this.server.branchSession(
|
|
1373
|
+
request.params.sessionId,
|
|
1374
|
+
request.params.options
|
|
1375
|
+
);
|
|
1376
|
+
case "turn/start":
|
|
1377
|
+
return await this.server.startTurn(
|
|
1378
|
+
request.params.sessionId,
|
|
1379
|
+
request.params.message,
|
|
1380
|
+
request.params.options
|
|
1381
|
+
);
|
|
1382
|
+
case "turn/get":
|
|
1383
|
+
return this.server.getTurn(request.params.turnId);
|
|
1384
|
+
case "turn/wait":
|
|
1385
|
+
return await this.server.waitForTurn(
|
|
1386
|
+
request.params.turnId,
|
|
1387
|
+
request.params.options
|
|
1388
|
+
);
|
|
1389
|
+
case "turn/interrupt":
|
|
1390
|
+
return this.server.interruptTurn(request.params.turnId);
|
|
1391
|
+
case "turn/steer":
|
|
1392
|
+
return this.server.steerTurn(request.params.turnId, request.params.message);
|
|
1393
|
+
case "turn/follow-up":
|
|
1394
|
+
return this.server.followUpTurn(
|
|
1395
|
+
request.params.turnId,
|
|
1396
|
+
request.params.message
|
|
1397
|
+
);
|
|
1398
|
+
case "follow-up/list":
|
|
1399
|
+
return await this.server.listFollowUps(request.params?.options);
|
|
1400
|
+
case "follow-up/get":
|
|
1401
|
+
return await this.server.getFollowUp(request.params.followUpId);
|
|
1402
|
+
case "follow-up/resolve":
|
|
1403
|
+
return await this.server.resolveFollowUp(
|
|
1404
|
+
request.params.followUpId,
|
|
1405
|
+
request.params.action
|
|
1406
|
+
);
|
|
1407
|
+
case "team/notification/list":
|
|
1408
|
+
return await this.server.listTeamNotifications(request.params?.options);
|
|
1409
|
+
case "input/respond":
|
|
1410
|
+
return this.server.respondToInputRequest(
|
|
1411
|
+
request.params.requestId,
|
|
1412
|
+
request.params.payload
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
function toErrorResponse(id, error) {
|
|
1418
|
+
return {
|
|
1419
|
+
kind: "response",
|
|
1420
|
+
id,
|
|
1421
|
+
ok: false,
|
|
1422
|
+
error: {
|
|
1423
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/transport/stdio.ts
|
|
1429
|
+
import { spawn } from "child_process";
|
|
1430
|
+
import readline from "readline";
|
|
1431
|
+
|
|
1432
|
+
// src/client/remote-client.ts
|
|
1433
|
+
var RemoteAgentServerClient = class {
|
|
1434
|
+
transport;
|
|
1435
|
+
requestTimeoutMs;
|
|
1436
|
+
pending = /* @__PURE__ */ new Map();
|
|
1437
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1438
|
+
turns = /* @__PURE__ */ new Map();
|
|
1439
|
+
capabilities = null;
|
|
1440
|
+
counter = 0;
|
|
1441
|
+
connected = false;
|
|
1442
|
+
constructor(transport, options = {}) {
|
|
1443
|
+
this.transport = transport;
|
|
1444
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
1445
|
+
this.transport.setMessageHandler((envelope) => {
|
|
1446
|
+
void this.handleEnvelope(envelope);
|
|
1447
|
+
});
|
|
1448
|
+
this.transport.setCloseHandler((error) => {
|
|
1449
|
+
this.failPending(error ?? new Error("agent-server transport closed"));
|
|
1450
|
+
this.connected = false;
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
async connect() {
|
|
1454
|
+
if (this.connected) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
await this.transport.connect();
|
|
1458
|
+
const result = await this.request(
|
|
1459
|
+
"initialize",
|
|
1460
|
+
{}
|
|
1461
|
+
);
|
|
1462
|
+
this.capabilities = result.capabilities;
|
|
1463
|
+
this.connected = true;
|
|
1464
|
+
}
|
|
1465
|
+
getCapabilities() {
|
|
1466
|
+
if (!this.capabilities) {
|
|
1467
|
+
throw new Error("Agent server client is not connected.");
|
|
1468
|
+
}
|
|
1469
|
+
return this.capabilities;
|
|
1470
|
+
}
|
|
1471
|
+
async getWorkspaceSummary() {
|
|
1472
|
+
return await this.request("workspace/summary", {});
|
|
1473
|
+
}
|
|
1474
|
+
async getSessionRuntimeStatus(sessionId) {
|
|
1475
|
+
return await this.request("session/status", {
|
|
1476
|
+
sessionId
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
async getModelState() {
|
|
1480
|
+
return await this.request("agent/model/get", {});
|
|
1481
|
+
}
|
|
1482
|
+
async switchModel(spec) {
|
|
1483
|
+
return await this.request("agent/model/switch", { spec });
|
|
1484
|
+
}
|
|
1485
|
+
async listTools() {
|
|
1486
|
+
return await this.request("agent/tools", {});
|
|
1487
|
+
}
|
|
1488
|
+
async listSkills() {
|
|
1489
|
+
return await this.request("agent/skills", {});
|
|
1490
|
+
}
|
|
1491
|
+
async listSubAgents() {
|
|
1492
|
+
return await this.request("agent/sub-agents", {});
|
|
1493
|
+
}
|
|
1494
|
+
async listPluginCommands() {
|
|
1495
|
+
return await this.request("plugin-command/list", {});
|
|
1496
|
+
}
|
|
1497
|
+
async executePluginCommand(name, args) {
|
|
1498
|
+
return await this.request("plugin-command/execute", { name, args });
|
|
1499
|
+
}
|
|
1500
|
+
async compactSessionContext(sessionId) {
|
|
1501
|
+
return await this.request("session/compact", {
|
|
1502
|
+
sessionId
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
async undoSessionTurn(sessionId) {
|
|
1506
|
+
return await this.request("session/undo", { sessionId });
|
|
1507
|
+
}
|
|
1508
|
+
async getSessionTurnDiff(sessionId) {
|
|
1509
|
+
return await this.request("session/diff", { sessionId });
|
|
1510
|
+
}
|
|
1511
|
+
async listSessions() {
|
|
1512
|
+
return await this.request("session/list", {});
|
|
1513
|
+
}
|
|
1514
|
+
async createSession(options) {
|
|
1515
|
+
return await this.request("session/create", options ?? {});
|
|
1516
|
+
}
|
|
1517
|
+
async readSession(sessionId) {
|
|
1518
|
+
return await this.request("session/read", { sessionId });
|
|
1519
|
+
}
|
|
1520
|
+
async deleteSession(sessionId) {
|
|
1521
|
+
return await this.request("session/delete", { sessionId });
|
|
1522
|
+
}
|
|
1523
|
+
async branchSession(sessionId, options) {
|
|
1524
|
+
return await this.request("session/branch", {
|
|
1525
|
+
sessionId,
|
|
1526
|
+
...options ? { options } : {}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
async startTurn(sessionId, message, options) {
|
|
1530
|
+
return await this.request("turn/start", {
|
|
1531
|
+
sessionId,
|
|
1532
|
+
message,
|
|
1533
|
+
...options ? { options } : {}
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
steerTurn(turnId, message) {
|
|
1537
|
+
void this.request("turn/steer", { turnId, message }).catch((error) => {
|
|
1538
|
+
logFireAndForgetError("turn/steer", error);
|
|
1539
|
+
});
|
|
1540
|
+
return { id: "", accepted: true };
|
|
1541
|
+
}
|
|
1542
|
+
followUpTurn(turnId, message) {
|
|
1543
|
+
void this.request("turn/follow-up", { turnId, message }).catch((error) => {
|
|
1544
|
+
logFireAndForgetError("turn/follow-up", error);
|
|
1545
|
+
});
|
|
1546
|
+
return { id: "", accepted: true };
|
|
1547
|
+
}
|
|
1548
|
+
async listFollowUps(options) {
|
|
1549
|
+
return await this.request("follow-up/list", {
|
|
1550
|
+
...options ? { options } : {}
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
async getFollowUp(followUpId) {
|
|
1554
|
+
return await this.request("follow-up/get", {
|
|
1555
|
+
followUpId
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
async resolveFollowUp(followUpId, action) {
|
|
1559
|
+
return await this.request("follow-up/resolve", {
|
|
1560
|
+
followUpId,
|
|
1561
|
+
action
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
async listTeamNotifications(options) {
|
|
1565
|
+
return await this.request("team/notification/list", {
|
|
1566
|
+
...options ? { options } : {}
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
getTurn(turnId) {
|
|
1570
|
+
return this.turns.get(turnId) ?? null;
|
|
1571
|
+
}
|
|
1572
|
+
async waitForTurn(turnId, options) {
|
|
1573
|
+
return await this.request("turn/wait", {
|
|
1574
|
+
turnId,
|
|
1575
|
+
...options ? { options } : {}
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
interruptTurn(turnId) {
|
|
1579
|
+
void this.request("turn/interrupt", { turnId }).catch((error) => {
|
|
1580
|
+
logFireAndForgetError("turn/interrupt", error);
|
|
1581
|
+
});
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
respondToInputRequest(requestId, payload) {
|
|
1585
|
+
void this.request("input/respond", { requestId, payload }).catch((error) => {
|
|
1586
|
+
logFireAndForgetError("input/respond", error);
|
|
1587
|
+
});
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
subscribe(listener, options) {
|
|
1591
|
+
const record = { listener, options };
|
|
1592
|
+
this.listeners.add(record);
|
|
1593
|
+
return () => {
|
|
1594
|
+
this.listeners.delete(record);
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
async close() {
|
|
1598
|
+
this.failPending(new Error("agent-server client closed"));
|
|
1599
|
+
await this.transport.close();
|
|
1600
|
+
this.connected = false;
|
|
1601
|
+
}
|
|
1602
|
+
async disconnect() {
|
|
1603
|
+
await this.close();
|
|
1604
|
+
}
|
|
1605
|
+
async request(method, params) {
|
|
1606
|
+
if (!this.connected && method !== "initialize") {
|
|
1607
|
+
await this.connect();
|
|
1608
|
+
}
|
|
1609
|
+
const id = `req-${++this.counter}`;
|
|
1610
|
+
const result = new Promise((resolve, reject) => {
|
|
1611
|
+
const timer = setTimeout(() => {
|
|
1612
|
+
this.pending.delete(id);
|
|
1613
|
+
reject(new Error(`agent-server timeout: ${method}`));
|
|
1614
|
+
}, this.requestTimeoutMs);
|
|
1615
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
1616
|
+
});
|
|
1617
|
+
await this.transport.send({
|
|
1618
|
+
kind: "request",
|
|
1619
|
+
id,
|
|
1620
|
+
method,
|
|
1621
|
+
params
|
|
1622
|
+
});
|
|
1623
|
+
return await result;
|
|
1624
|
+
}
|
|
1625
|
+
async handleEnvelope(envelope) {
|
|
1626
|
+
if (envelope.kind === "notification") {
|
|
1627
|
+
this.trackNotification(envelope.notification);
|
|
1628
|
+
for (const record of this.listeners) {
|
|
1629
|
+
if (!matchesNotificationSubscription(envelope.notification, record.options)) {
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
record.listener(envelope.notification);
|
|
1633
|
+
}
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (envelope.kind !== "response") {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const pending = this.pending.get(envelope.id);
|
|
1640
|
+
if (!pending) {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
clearTimeout(pending.timer);
|
|
1644
|
+
this.pending.delete(envelope.id);
|
|
1645
|
+
if (envelope.ok) {
|
|
1646
|
+
pending.resolve(envelope.result);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
pending.reject(new Error(envelope.error?.message ?? "agent-server request failed"));
|
|
1650
|
+
}
|
|
1651
|
+
failPending(error) {
|
|
1652
|
+
for (const [id, pending] of this.pending) {
|
|
1653
|
+
clearTimeout(pending.timer);
|
|
1654
|
+
pending.reject(error);
|
|
1655
|
+
this.pending.delete(id);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
trackNotification(notification) {
|
|
1659
|
+
switch (notification.type) {
|
|
1660
|
+
case "turn/started":
|
|
1661
|
+
case "turn/completed": {
|
|
1662
|
+
const existing = this.turns.get(notification.turn.id);
|
|
1663
|
+
this.turns.set(notification.turn.id, {
|
|
1664
|
+
...notification.turn,
|
|
1665
|
+
events: existing?.events ?? []
|
|
1666
|
+
});
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
case "turn/event": {
|
|
1670
|
+
const existing = this.turns.get(notification.turnId);
|
|
1671
|
+
if (!existing) {
|
|
1672
|
+
this.turns.set(notification.turnId, {
|
|
1673
|
+
id: notification.turnId,
|
|
1674
|
+
sessionId: notification.sessionId,
|
|
1675
|
+
message: "",
|
|
1676
|
+
startedAt: notification.timestamp,
|
|
1677
|
+
status: "running",
|
|
1678
|
+
events: [
|
|
1679
|
+
{
|
|
1680
|
+
sequence: notification.sequence,
|
|
1681
|
+
timestamp: notification.timestamp,
|
|
1682
|
+
event: notification.event
|
|
1683
|
+
}
|
|
1684
|
+
]
|
|
1685
|
+
});
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
existing.events = [
|
|
1689
|
+
...existing.events,
|
|
1690
|
+
{
|
|
1691
|
+
sequence: notification.sequence,
|
|
1692
|
+
timestamp: notification.timestamp,
|
|
1693
|
+
event: notification.event
|
|
1694
|
+
}
|
|
1695
|
+
];
|
|
1696
|
+
this.turns.set(notification.turnId, existing);
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
function logFireAndForgetError(method, error) {
|
|
1703
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1704
|
+
console.warn(`[agent-server] ${method} fire-and-forget error: ${message}`);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/transport/stdio.ts
|
|
1708
|
+
function attachAgentServerStdio(server, options = {}) {
|
|
1709
|
+
const input = options.input ?? process.stdin;
|
|
1710
|
+
const output = options.output ?? process.stdout;
|
|
1711
|
+
const onError = options.onError ?? defaultTransportErrorHandler;
|
|
1712
|
+
const connection = new AgentServerRpcConnection({
|
|
1713
|
+
server,
|
|
1714
|
+
capabilityOverride: {
|
|
1715
|
+
protocol: {
|
|
1716
|
+
transport: "stdio",
|
|
1717
|
+
reconnectable: false,
|
|
1718
|
+
multiClient: false
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
send: async (envelope) => {
|
|
1722
|
+
output.write(`${JSON.stringify(envelope)}
|
|
1723
|
+
`);
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
const reader = readline.createInterface({ input });
|
|
1727
|
+
reader.on("line", (line) => {
|
|
1728
|
+
void handleStdioLine(line, connection, onError);
|
|
1729
|
+
});
|
|
1730
|
+
reader.on("close", () => {
|
|
1731
|
+
void connection.close();
|
|
1732
|
+
});
|
|
1733
|
+
return {
|
|
1734
|
+
close: async () => {
|
|
1735
|
+
reader.close();
|
|
1736
|
+
await connection.close();
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
async function connectStdioAgentServerClient(options) {
|
|
1741
|
+
const child = spawn(options.command, options.args ?? [], {
|
|
1742
|
+
cwd: options.cwd,
|
|
1743
|
+
env: options.env,
|
|
1744
|
+
stdio: ["pipe", "pipe", "inherit"]
|
|
1745
|
+
});
|
|
1746
|
+
if (!child.stdout || !child.stdin) {
|
|
1747
|
+
throw new Error("stdio agent-server child process did not expose pipes");
|
|
1748
|
+
}
|
|
1749
|
+
const transport = new StdioAgentServerTransport(child);
|
|
1750
|
+
const client = new RemoteAgentServerClient(transport, {
|
|
1751
|
+
requestTimeoutMs: options.requestTimeoutMs
|
|
1752
|
+
});
|
|
1753
|
+
await client.connect();
|
|
1754
|
+
return client;
|
|
1755
|
+
}
|
|
1756
|
+
var StdioAgentServerTransport = class {
|
|
1757
|
+
child;
|
|
1758
|
+
reader;
|
|
1759
|
+
onMessage = () => {
|
|
1760
|
+
};
|
|
1761
|
+
onClose = () => {
|
|
1762
|
+
};
|
|
1763
|
+
connected = false;
|
|
1764
|
+
constructor(child) {
|
|
1765
|
+
this.child = child;
|
|
1766
|
+
this.reader = readline.createInterface({ input: child.stdout });
|
|
1767
|
+
}
|
|
1768
|
+
async connect() {
|
|
1769
|
+
if (this.connected) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
this.reader.on("line", (line) => {
|
|
1773
|
+
const parsed = parseWireEnvelope(line);
|
|
1774
|
+
if (parsed) {
|
|
1775
|
+
this.onMessage(parsed);
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
this.child.once("error", (error) => {
|
|
1779
|
+
this.onClose(toError(error));
|
|
1780
|
+
});
|
|
1781
|
+
this.child.once("close", () => {
|
|
1782
|
+
this.onClose(new Error("agent-server stdio process closed"));
|
|
1783
|
+
});
|
|
1784
|
+
this.connected = true;
|
|
1785
|
+
}
|
|
1786
|
+
async send(envelope) {
|
|
1787
|
+
if (!this.connected || this.child.stdin.destroyed) {
|
|
1788
|
+
throw new Error("agent-server stdio transport is not connected");
|
|
1789
|
+
}
|
|
1790
|
+
this.child.stdin.write(`${JSON.stringify(envelope)}
|
|
1791
|
+
`);
|
|
1792
|
+
}
|
|
1793
|
+
async close() {
|
|
1794
|
+
this.reader.close();
|
|
1795
|
+
if (!this.child.killed) {
|
|
1796
|
+
this.child.kill();
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
setMessageHandler(handler) {
|
|
1800
|
+
this.onMessage = handler;
|
|
1801
|
+
}
|
|
1802
|
+
setCloseHandler(handler) {
|
|
1803
|
+
this.onClose = handler;
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
async function handleStdioLine(line, connection, onError) {
|
|
1807
|
+
const envelope = parseWireEnvelope(line);
|
|
1808
|
+
if (!envelope) {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (envelope.kind !== "request") {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
try {
|
|
1815
|
+
await connection.handleRequest(envelope);
|
|
1816
|
+
} catch (error) {
|
|
1817
|
+
onError(toError(error));
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function parseWireEnvelope(line) {
|
|
1821
|
+
const trimmed = line.trim();
|
|
1822
|
+
if (!trimmed) {
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
let parsed;
|
|
1826
|
+
try {
|
|
1827
|
+
parsed = JSON.parse(trimmed);
|
|
1828
|
+
} catch {
|
|
1829
|
+
return null;
|
|
1830
|
+
}
|
|
1831
|
+
return isAgentServerWireEnvelope(parsed) ? parsed : null;
|
|
1832
|
+
}
|
|
1833
|
+
function defaultTransportErrorHandler(error) {
|
|
1834
|
+
process.stderr.write(`agent-server stdio transport error: ${error.message}
|
|
1835
|
+
`);
|
|
1836
|
+
}
|
|
1837
|
+
function toError(error) {
|
|
1838
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/transport/websocket.ts
|
|
1842
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
1843
|
+
function startAgentServerWebSocketServer(server, options) {
|
|
1844
|
+
const attached = Boolean(options.httpServer);
|
|
1845
|
+
const wss = options.httpServer ? new WebSocketServer({ server: options.httpServer }) : new WebSocketServer({ port: options.port, host: options.host });
|
|
1846
|
+
const ready = attached ? Promise.resolve() : waitForWebSocketServerReady(wss);
|
|
1847
|
+
const connections = /* @__PURE__ */ new Set();
|
|
1848
|
+
wss.on("connection", (socket) => {
|
|
1849
|
+
const connection = new AgentServerRpcConnection({
|
|
1850
|
+
server,
|
|
1851
|
+
capabilityOverride: {
|
|
1852
|
+
protocol: {
|
|
1853
|
+
transport: "websocket",
|
|
1854
|
+
reconnectable: true,
|
|
1855
|
+
multiClient: true
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
send: async (envelope) => {
|
|
1859
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
1860
|
+
socket.send(JSON.stringify(envelope));
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
connections.add(connection);
|
|
1865
|
+
socket.on("message", (data) => {
|
|
1866
|
+
const envelope = parseWireEnvelope2(data);
|
|
1867
|
+
if (!envelope || envelope.kind !== "request") {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
void connection.handleRequest(envelope);
|
|
1871
|
+
});
|
|
1872
|
+
socket.on("close", () => {
|
|
1873
|
+
connections.delete(connection);
|
|
1874
|
+
void connection.close();
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
return {
|
|
1878
|
+
ready,
|
|
1879
|
+
close: async () => {
|
|
1880
|
+
for (const connection of connections) {
|
|
1881
|
+
await connection.close();
|
|
1882
|
+
}
|
|
1883
|
+
await new Promise((resolve, reject) => {
|
|
1884
|
+
wss.close((error) => {
|
|
1885
|
+
if (error) {
|
|
1886
|
+
reject(error);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
resolve();
|
|
1890
|
+
});
|
|
1891
|
+
});
|
|
1892
|
+
},
|
|
1893
|
+
address: () => {
|
|
1894
|
+
const bound = attached ? options.httpServer.address() : wss.address();
|
|
1895
|
+
if (bound && typeof bound === "object") {
|
|
1896
|
+
return `ws://${formatConnectableHost(bound.address, options.host)}:${bound.port}`;
|
|
1897
|
+
}
|
|
1898
|
+
const host = formatConnectableHost(options.host ?? "127.0.0.1");
|
|
1899
|
+
return `ws://${host}:${options.port ?? 0}`;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
async function connectWebSocketAgentServerClient(options) {
|
|
1904
|
+
const transport = new WebSocketAgentServerTransport(options.url, options.headers);
|
|
1905
|
+
const client = new RemoteAgentServerClient(transport, {
|
|
1906
|
+
requestTimeoutMs: options.requestTimeoutMs
|
|
1907
|
+
});
|
|
1908
|
+
await client.connect();
|
|
1909
|
+
return client;
|
|
1910
|
+
}
|
|
1911
|
+
var WebSocketAgentServerTransport = class {
|
|
1912
|
+
url;
|
|
1913
|
+
headers;
|
|
1914
|
+
socket = null;
|
|
1915
|
+
onMessage = () => {
|
|
1916
|
+
};
|
|
1917
|
+
onClose = () => {
|
|
1918
|
+
};
|
|
1919
|
+
constructor(url, headers) {
|
|
1920
|
+
this.url = url;
|
|
1921
|
+
this.headers = headers;
|
|
1922
|
+
}
|
|
1923
|
+
async connect() {
|
|
1924
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
this.socket = await new Promise((resolve, reject) => {
|
|
1928
|
+
const socket = new WebSocket(this.url, {
|
|
1929
|
+
headers: this.headers
|
|
1930
|
+
});
|
|
1931
|
+
socket.once("open", () => resolve(socket));
|
|
1932
|
+
socket.once("error", (error) => reject(error));
|
|
1933
|
+
});
|
|
1934
|
+
this.socket.on("message", (data) => {
|
|
1935
|
+
const envelope = parseWireEnvelope2(data);
|
|
1936
|
+
if (envelope) {
|
|
1937
|
+
this.onMessage(envelope);
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
this.socket.on("close", () => {
|
|
1941
|
+
this.onClose(new Error("agent-server websocket closed"));
|
|
1942
|
+
this.socket = null;
|
|
1943
|
+
});
|
|
1944
|
+
this.socket.on("error", (error) => {
|
|
1945
|
+
this.onClose(error instanceof Error ? error : new Error(String(error)));
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
async send(envelope) {
|
|
1949
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
1950
|
+
throw new Error("agent-server websocket is not connected");
|
|
1951
|
+
}
|
|
1952
|
+
this.socket.send(JSON.stringify(envelope));
|
|
1953
|
+
}
|
|
1954
|
+
async close() {
|
|
1955
|
+
const socket = this.socket;
|
|
1956
|
+
this.socket = null;
|
|
1957
|
+
if (!socket) {
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
await new Promise((resolve) => {
|
|
1961
|
+
socket.once("close", () => resolve());
|
|
1962
|
+
socket.close();
|
|
1963
|
+
setTimeout(resolve, 250);
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
setMessageHandler(handler) {
|
|
1967
|
+
this.onMessage = handler;
|
|
1968
|
+
}
|
|
1969
|
+
setCloseHandler(handler) {
|
|
1970
|
+
this.onClose = handler;
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
function parseWireEnvelope2(data) {
|
|
1974
|
+
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : Buffer.from(data).toString("utf8");
|
|
1975
|
+
let parsed;
|
|
1976
|
+
try {
|
|
1977
|
+
parsed = JSON.parse(text);
|
|
1978
|
+
} catch {
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
return isAgentServerWireEnvelope(parsed) ? parsed : null;
|
|
1982
|
+
}
|
|
1983
|
+
function formatConnectableHost(address, fallback = "127.0.0.1") {
|
|
1984
|
+
const host = address === "::" || address === "0.0.0.0" ? fallback : address;
|
|
1985
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1986
|
+
}
|
|
1987
|
+
function waitForWebSocketServerReady(wss) {
|
|
1988
|
+
if (wss.address()) {
|
|
1989
|
+
return Promise.resolve();
|
|
1990
|
+
}
|
|
1991
|
+
return new Promise((resolve, reject) => {
|
|
1992
|
+
const onListening = () => {
|
|
1993
|
+
cleanup();
|
|
1994
|
+
resolve();
|
|
1995
|
+
};
|
|
1996
|
+
const onError = (error) => {
|
|
1997
|
+
cleanup();
|
|
1998
|
+
reject(error);
|
|
1999
|
+
};
|
|
2000
|
+
const cleanup = () => {
|
|
2001
|
+
wss.off("listening", onListening);
|
|
2002
|
+
wss.off("error", onError);
|
|
2003
|
+
};
|
|
2004
|
+
wss.once("listening", onListening);
|
|
2005
|
+
wss.once("error", onError);
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/transport/sse.ts
|
|
2010
|
+
function createAgentServerSSEStream(server, sessionId, options) {
|
|
2011
|
+
const encoder = new TextEncoder();
|
|
2012
|
+
let idCounter = 0;
|
|
2013
|
+
return new ReadableStream({
|
|
2014
|
+
start(controller) {
|
|
2015
|
+
controller.enqueue(encoder.encode(":ok\n\n"));
|
|
2016
|
+
const unsubscribe = server.subscribe(
|
|
2017
|
+
(notification) => {
|
|
2018
|
+
const id = String(++idCounter);
|
|
2019
|
+
const event = notification.type;
|
|
2020
|
+
const data = JSON.stringify(notification);
|
|
2021
|
+
controller.enqueue(
|
|
2022
|
+
encoder.encode(`id: ${id}
|
|
2023
|
+
event: ${event}
|
|
2024
|
+
data: ${data}
|
|
2025
|
+
|
|
2026
|
+
`)
|
|
2027
|
+
);
|
|
2028
|
+
},
|
|
2029
|
+
{ sessionId }
|
|
2030
|
+
);
|
|
2031
|
+
const cleanup = () => {
|
|
2032
|
+
unsubscribe();
|
|
2033
|
+
try {
|
|
2034
|
+
controller.close();
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
options?.signal?.addEventListener("abort", cleanup, { once: true });
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
function createEventBusSSEStream(eventBus, channel, options) {
|
|
2043
|
+
const encoder = new TextEncoder();
|
|
2044
|
+
const afterSeq = options?.lastEventId ? parseInt(options.lastEventId, 10) : void 0;
|
|
2045
|
+
return new ReadableStream({
|
|
2046
|
+
start(controller) {
|
|
2047
|
+
controller.enqueue(encoder.encode(":ok\n\n"));
|
|
2048
|
+
const subscription = eventBus.subscribe(channel, {
|
|
2049
|
+
...afterSeq !== void 0 ? { afterSeq } : {}
|
|
2050
|
+
});
|
|
2051
|
+
const pump = async () => {
|
|
2052
|
+
try {
|
|
2053
|
+
for await (const message of subscription) {
|
|
2054
|
+
const data = JSON.stringify(message.data);
|
|
2055
|
+
controller.enqueue(
|
|
2056
|
+
encoder.encode(
|
|
2057
|
+
`id: ${message.seq}
|
|
2058
|
+
event: ${message.type}
|
|
2059
|
+
data: ${data}
|
|
2060
|
+
|
|
2061
|
+
`
|
|
2062
|
+
)
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
} catch {
|
|
2066
|
+
} finally {
|
|
2067
|
+
try {
|
|
2068
|
+
controller.close();
|
|
2069
|
+
} catch {
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
void pump();
|
|
2074
|
+
options?.signal?.addEventListener(
|
|
2075
|
+
"abort",
|
|
2076
|
+
() => {
|
|
2077
|
+
subscription.unsubscribe();
|
|
2078
|
+
},
|
|
2079
|
+
{ once: true }
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
function createAgentServerSSEResponse(server, sessionId, options) {
|
|
2085
|
+
const body = createAgentServerSSEStream(server, sessionId, options);
|
|
2086
|
+
return new Response(body, {
|
|
2087
|
+
status: 200,
|
|
2088
|
+
headers: {
|
|
2089
|
+
"Content-Type": "text/event-stream",
|
|
2090
|
+
"Cache-Control": "no-cache",
|
|
2091
|
+
...options?.headers
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/client/stream-turn.ts
|
|
2097
|
+
async function* streamTurn(client, sessionId, message, options = {}) {
|
|
2098
|
+
const queue = [];
|
|
2099
|
+
let waiter = null;
|
|
2100
|
+
let targetTurnId = null;
|
|
2101
|
+
const unsubscribe = client.subscribe(
|
|
2102
|
+
(notification) => {
|
|
2103
|
+
if (waiter) {
|
|
2104
|
+
const resolve = waiter;
|
|
2105
|
+
waiter = null;
|
|
2106
|
+
resolve(notification);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
queue.push(notification);
|
|
2110
|
+
},
|
|
2111
|
+
{ sessionId }
|
|
2112
|
+
);
|
|
2113
|
+
try {
|
|
2114
|
+
const turn = await client.startTurn(sessionId, message, options);
|
|
2115
|
+
targetTurnId = turn.id;
|
|
2116
|
+
while (true) {
|
|
2117
|
+
const notification = queue.shift() ?? await new Promise((resolve) => {
|
|
2118
|
+
waiter = resolve;
|
|
2119
|
+
});
|
|
2120
|
+
if (!matchesTurnStreamNotification(notification, sessionId, targetTurnId)) {
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
yield notification;
|
|
2124
|
+
if (notification.type === "turn/completed" && notification.turn.id === targetTurnId) {
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
} finally {
|
|
2129
|
+
unsubscribe();
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
function matchesTurnStreamNotification(notification, sessionId, turnId) {
|
|
2133
|
+
switch (notification.type) {
|
|
2134
|
+
case "session/created":
|
|
2135
|
+
return notification.session.id === sessionId;
|
|
2136
|
+
case "session/deleted":
|
|
2137
|
+
return notification.sessionId === sessionId;
|
|
2138
|
+
case "session/branched":
|
|
2139
|
+
case "session/runtime":
|
|
2140
|
+
return notification.sessionId === sessionId;
|
|
2141
|
+
case "team/notification":
|
|
2142
|
+
return false;
|
|
2143
|
+
case "turn/started":
|
|
2144
|
+
case "input/request":
|
|
2145
|
+
case "input/resolved":
|
|
2146
|
+
case "turn/completed":
|
|
2147
|
+
if (notification.type === "turn/started" || notification.type === "turn/completed") {
|
|
2148
|
+
return turnId !== null && notification.turn.id === turnId;
|
|
2149
|
+
}
|
|
2150
|
+
return turnId !== null && (notification.type === "input/request" ? notification.request.turnId === turnId : notification.turnId === turnId);
|
|
2151
|
+
case "turn/event":
|
|
2152
|
+
return turnId !== null && notification.turnId === turnId;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// src/client/in-process-client.ts
|
|
2157
|
+
var InProcessAgentServerClient = class {
|
|
2158
|
+
server;
|
|
2159
|
+
constructor(server) {
|
|
2160
|
+
this.server = server;
|
|
2161
|
+
}
|
|
2162
|
+
getCapabilities() {
|
|
2163
|
+
return this.server.getCapabilities();
|
|
2164
|
+
}
|
|
2165
|
+
getWorkspaceSummary() {
|
|
2166
|
+
return this.server.getWorkspaceSummary();
|
|
2167
|
+
}
|
|
2168
|
+
getSessionRuntimeStatus(sessionId) {
|
|
2169
|
+
return this.server.getSessionRuntimeStatus(sessionId);
|
|
2170
|
+
}
|
|
2171
|
+
getModelState() {
|
|
2172
|
+
return this.server.getModelState();
|
|
2173
|
+
}
|
|
2174
|
+
switchModel(spec) {
|
|
2175
|
+
return this.server.switchModel(spec);
|
|
2176
|
+
}
|
|
2177
|
+
listTools() {
|
|
2178
|
+
return this.server.listTools();
|
|
2179
|
+
}
|
|
2180
|
+
listSkills() {
|
|
2181
|
+
return this.server.listSkills();
|
|
2182
|
+
}
|
|
2183
|
+
listSubAgents() {
|
|
2184
|
+
return this.server.listSubAgents();
|
|
2185
|
+
}
|
|
2186
|
+
listPluginCommands() {
|
|
2187
|
+
return this.server.listPluginCommands();
|
|
2188
|
+
}
|
|
2189
|
+
executePluginCommand(name, args) {
|
|
2190
|
+
return this.server.executePluginCommand(name, args);
|
|
2191
|
+
}
|
|
2192
|
+
compactSessionContext(sessionId) {
|
|
2193
|
+
return this.server.compactSessionContext(sessionId);
|
|
2194
|
+
}
|
|
2195
|
+
undoSessionTurn(sessionId) {
|
|
2196
|
+
return this.server.undoSessionTurn(sessionId);
|
|
2197
|
+
}
|
|
2198
|
+
getSessionTurnDiff(sessionId) {
|
|
2199
|
+
return this.server.getSessionTurnDiff(sessionId);
|
|
2200
|
+
}
|
|
2201
|
+
listSessions() {
|
|
2202
|
+
return this.server.listSessions();
|
|
2203
|
+
}
|
|
2204
|
+
createSession(options) {
|
|
2205
|
+
return this.server.createSession(options);
|
|
2206
|
+
}
|
|
2207
|
+
readSession(sessionId) {
|
|
2208
|
+
return this.server.readSession(sessionId);
|
|
2209
|
+
}
|
|
2210
|
+
deleteSession(sessionId) {
|
|
2211
|
+
return this.server.deleteSession(sessionId);
|
|
2212
|
+
}
|
|
2213
|
+
branchSession(sessionId, options) {
|
|
2214
|
+
return this.server.branchSession(sessionId, options);
|
|
2215
|
+
}
|
|
2216
|
+
startTurn(sessionId, message, options) {
|
|
2217
|
+
return this.server.startTurn(sessionId, message, options);
|
|
2218
|
+
}
|
|
2219
|
+
steerTurn(turnId, message) {
|
|
2220
|
+
return this.server.steerTurn(turnId, message);
|
|
2221
|
+
}
|
|
2222
|
+
followUpTurn(turnId, message) {
|
|
2223
|
+
return this.server.followUpTurn(turnId, message);
|
|
2224
|
+
}
|
|
2225
|
+
listFollowUps(options) {
|
|
2226
|
+
return this.server.listFollowUps(options);
|
|
2227
|
+
}
|
|
2228
|
+
getFollowUp(followUpId) {
|
|
2229
|
+
return this.server.getFollowUp(followUpId);
|
|
2230
|
+
}
|
|
2231
|
+
resolveFollowUp(followUpId, action) {
|
|
2232
|
+
return this.server.resolveFollowUp(followUpId, action);
|
|
2233
|
+
}
|
|
2234
|
+
listTeamNotifications(options) {
|
|
2235
|
+
return this.server.listTeamNotifications(options);
|
|
2236
|
+
}
|
|
2237
|
+
getTurn(turnId) {
|
|
2238
|
+
return this.server.getTurn(turnId);
|
|
2239
|
+
}
|
|
2240
|
+
waitForTurn(turnId, options) {
|
|
2241
|
+
return this.server.waitForTurn(turnId, options);
|
|
2242
|
+
}
|
|
2243
|
+
interruptTurn(turnId) {
|
|
2244
|
+
return this.server.interruptTurn(turnId);
|
|
2245
|
+
}
|
|
2246
|
+
respondToInputRequest(requestId, payload) {
|
|
2247
|
+
return this.server.respondToInputRequest(requestId, payload);
|
|
2248
|
+
}
|
|
2249
|
+
subscribe(listener, options) {
|
|
2250
|
+
return this.server.subscribe(listener, options);
|
|
2251
|
+
}
|
|
2252
|
+
async close() {
|
|
2253
|
+
await this.server.close();
|
|
2254
|
+
}
|
|
2255
|
+
async disconnect() {
|
|
2256
|
+
await this.close();
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
function createInProcessAgentServer(agent) {
|
|
2260
|
+
return new InProcessAgentServer(createAgentServerAdapter(agent));
|
|
2261
|
+
}
|
|
2262
|
+
function createInProcessAgentServerClient(agent) {
|
|
2263
|
+
return new InProcessAgentServerClient(createInProcessAgentServer(agent));
|
|
2264
|
+
}
|
|
2265
|
+
export {
|
|
2266
|
+
AgentServerRpcConnection,
|
|
2267
|
+
InProcessAgentServer,
|
|
2268
|
+
InProcessAgentServerClient,
|
|
2269
|
+
RemoteAgentServerClient,
|
|
2270
|
+
attachAgentServerStdio,
|
|
2271
|
+
connectStdioAgentServerClient,
|
|
2272
|
+
connectWebSocketAgentServerClient,
|
|
2273
|
+
createAgentServerAdapter,
|
|
2274
|
+
createAgentServerSSEResponse,
|
|
2275
|
+
createAgentServerSSEStream,
|
|
2276
|
+
createDefaultAgentServerCapabilities,
|
|
2277
|
+
createEventBusSSEStream,
|
|
2278
|
+
createInProcessAgentServer,
|
|
2279
|
+
createInProcessAgentServerClient,
|
|
2280
|
+
createServerApprovalRequestHandler,
|
|
2281
|
+
createServerHumanInputRequestHandler,
|
|
2282
|
+
createServerInteractiveHandlers,
|
|
2283
|
+
isAgentServerWireEnvelope,
|
|
2284
|
+
matchesNotificationSubscription,
|
|
2285
|
+
mergeAgentServerCapabilities,
|
|
2286
|
+
startAgentServerWebSocketServer,
|
|
2287
|
+
streamTurn
|
|
2288
|
+
};
|