@wyrd-company/ahp-server 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.
@@ -0,0 +1,51 @@
1
+ import { type Message, type StateAction } from '@microsoft/agent-host-protocol';
2
+ import type { AhpServerOptions, ServerTransport } from './types.js';
3
+ export declare class AhpServer {
4
+ private readonly options;
5
+ private readonly providers;
6
+ private readonly store;
7
+ private readonly supportedProtocolVersions;
8
+ private readonly resources;
9
+ private readonly textDecoder;
10
+ private readonly connections;
11
+ private readonly activeClientToolCalls;
12
+ private serverSeq;
13
+ constructor(options: AhpServerOptions);
14
+ accept(transport: ServerTransport): Promise<void>;
15
+ private decode;
16
+ private decodeText;
17
+ private handleMessage;
18
+ private handleRequest;
19
+ private dispatchRequest;
20
+ private handleNotification;
21
+ private initialize;
22
+ private reconnect;
23
+ private subscribe;
24
+ private listSessions;
25
+ private resolveSessionConfig;
26
+ private createSession;
27
+ private disposeSession;
28
+ private fetchTurns;
29
+ private dispatchAction;
30
+ private acceptClientSessionAction;
31
+ private syncActiveClientTools;
32
+ private reportActiveClientToolInvocation;
33
+ private clearActiveClientForConnection;
34
+ private deleteActiveClientToolCalls;
35
+ private completeActiveClientToolCall;
36
+ private queueAgentTurn;
37
+ private startAgentTurn;
38
+ private snapshot;
39
+ private publishRootNotification;
40
+ private publishRootAction;
41
+ private applySessionAction;
42
+ private publishAction;
43
+ private firstProvider;
44
+ private requireSession;
45
+ private isRequest;
46
+ private isResponse;
47
+ }
48
+ export declare function textMessage(text: string): Message;
49
+ export declare function markdownResponsePart(partId: string): StateAction;
50
+ export declare function completeTurn(turnId: string): StateAction;
51
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,OAAO,EAiBZ,KAAK,WAAW,EAMjB,MAAM,gCAAgC,CAAC;AAKxC,OAAO,KAAK,EAIV,gBAAgB,EAKhB,eAAe,EAIhB,MAAM,YAAY,CAAC;AAwCpB,qBAAa,SAAS;IAUR,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;IAC9D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAiD;IACvF,OAAO,CAAC,SAAS,CAAK;gBAEO,OAAO,EAAE,gBAAgB;IAWhD,MAAM,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BvD,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,UAAU;YAIJ,aAAa;YAWb,aAAa;YASb,eAAe;YA2Cf,kBAAkB;IAehC,OAAO,CAAC,UAAU;IA2BlB,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,YAAY;YAIN,oBAAoB;YAcpB,aAAa;YAwEb,cAAc;IAa5B,OAAO,CAAC,UAAU;YAOJ,cAAc;IA0C5B,OAAO,CAAC,yBAAyB;YAkCnB,qBAAqB;IAInC,OAAO,CAAC,gCAAgC;YA6C1B,8BAA8B;IAkB5C,OAAO,CAAC,2BAA2B;IASnC,OAAO,CAAC,4BAA4B;IAUpC,OAAO,CAAC,cAAc;YAIR,cAAc;IAwC5B,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,UAAU;CAGnB;AAwDD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAUhE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAKxD"}
@@ -0,0 +1,625 @@
1
+ import { SUPPORTED_PROTOCOL_VERSIONS, rootReducer, sessionReducer, } from '@microsoft/agent-host-protocol';
2
+ import { AhpServerError, JsonRpcErrorCodes, toJsonRpcError } from './errors.js';
3
+ import { FileResourceService } from './resources.js';
4
+ import { InMemorySessionStore } from './store.js';
5
+ const ROOT_URI = 'ahp-root://';
6
+ const ACTION = {
7
+ RootActiveSessionsChanged: 'root/activeSessionsChanged',
8
+ SessionReady: 'session/ready',
9
+ SessionCreationFailed: 'session/creationFailed',
10
+ SessionTurnStarted: 'session/turnStarted',
11
+ SessionPendingMessageSet: 'session/pendingMessageSet',
12
+ SessionTurnCancelled: 'session/turnCancelled',
13
+ SessionError: 'session/error',
14
+ SessionResponsePart: 'session/responsePart',
15
+ SessionTurnComplete: 'session/turnComplete',
16
+ SessionActiveClientChanged: 'session/activeClientChanged',
17
+ SessionActiveClientToolsChanged: 'session/activeClientToolsChanged',
18
+ SessionToolCallStart: 'session/toolCallStart',
19
+ SessionToolCallReady: 'session/toolCallReady',
20
+ SessionToolCallComplete: 'session/toolCallComplete',
21
+ SessionToolCallContentChanged: 'session/toolCallContentChanged',
22
+ SessionToolCallResultConfirmed: 'session/toolCallResultConfirmed',
23
+ };
24
+ export class AhpServer {
25
+ options;
26
+ providers = new Map();
27
+ store;
28
+ supportedProtocolVersions;
29
+ resources;
30
+ textDecoder = new TextDecoder();
31
+ connections = new Set();
32
+ activeClientToolCalls = new Map();
33
+ serverSeq = 0;
34
+ constructor(options) {
35
+ this.options = options;
36
+ for (const provider of options.providers) {
37
+ this.providers.set(provider.agent.provider, provider);
38
+ }
39
+ this.store = options.store ?? new InMemorySessionStore(options.providers.map(p => p.agent));
40
+ this.supportedProtocolVersions = [...(options.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS)];
41
+ this.resources = new FileResourceService({
42
+ roots: options.resourceRoots ?? (options.defaultDirectory ? [options.defaultDirectory] : undefined),
43
+ });
44
+ }
45
+ async accept(transport) {
46
+ const connection = {
47
+ transport,
48
+ initialized: false,
49
+ subscriptions: new Set(),
50
+ send: async (message) => {
51
+ await transport.send(message);
52
+ },
53
+ };
54
+ this.connections.add(connection);
55
+ try {
56
+ while (true) {
57
+ const inbound = await transport.recv();
58
+ if (inbound === null) {
59
+ return;
60
+ }
61
+ const message = this.decode(inbound);
62
+ await this.handleMessage(connection, message);
63
+ }
64
+ }
65
+ finally {
66
+ await this.clearActiveClientForConnection(connection);
67
+ this.connections.delete(connection);
68
+ await transport.close();
69
+ }
70
+ }
71
+ decode(frame) {
72
+ if (frame.kind === 'parsed') {
73
+ return frame.message;
74
+ }
75
+ if (frame.kind === 'binary') {
76
+ return this.decodeText(this.textDecoder.decode(frame.data));
77
+ }
78
+ return this.decodeText(frame.text);
79
+ }
80
+ decodeText(text) {
81
+ return JSON.parse(text);
82
+ }
83
+ async handleMessage(connection, message) {
84
+ if (this.isResponse(message)) {
85
+ return;
86
+ }
87
+ if (this.isRequest(message)) {
88
+ await this.handleRequest(connection, message);
89
+ return;
90
+ }
91
+ await this.handleNotification(connection, message);
92
+ }
93
+ async handleRequest(connection, request) {
94
+ try {
95
+ const result = await this.dispatchRequest(connection, request.method, request.params);
96
+ await connection.send({ jsonrpc: '2.0', id: request.id, result });
97
+ }
98
+ catch (error) {
99
+ await connection.send({ jsonrpc: '2.0', id: request.id, error: toJsonRpcError(error) });
100
+ }
101
+ }
102
+ async dispatchRequest(connection, method, params) {
103
+ switch (method) {
104
+ case 'initialize':
105
+ return this.initialize(connection, params);
106
+ case 'ping':
107
+ return null;
108
+ case 'reconnect':
109
+ return this.reconnect(connection, params);
110
+ case 'subscribe':
111
+ return this.subscribe(connection, params);
112
+ case 'listSessions':
113
+ return this.listSessions();
114
+ case 'resourceRead':
115
+ return this.resources.read(params);
116
+ case 'resourceWrite':
117
+ return this.resources.write(params);
118
+ case 'resourceList':
119
+ return this.resources.list(params);
120
+ case 'resourceCopy':
121
+ return this.resources.copy(params);
122
+ case 'resourceDelete':
123
+ return this.resources.delete(params);
124
+ case 'resourceMove':
125
+ return this.resources.move(params);
126
+ case 'resourceResolve':
127
+ return this.resources.resolve(params);
128
+ case 'resourceMkdir':
129
+ return this.resources.mkdir(params);
130
+ case 'resolveSessionConfig':
131
+ return this.resolveSessionConfig(params);
132
+ case 'createSession':
133
+ return this.createSession(connection, params);
134
+ case 'disposeSession':
135
+ return this.disposeSession(params);
136
+ case 'fetchTurns':
137
+ return this.fetchTurns(params);
138
+ case 'completions':
139
+ return { items: [] };
140
+ default:
141
+ throw new AhpServerError(JsonRpcErrorCodes.MethodNotFound, `method not found: ${method}`);
142
+ }
143
+ }
144
+ async handleNotification(connection, notification) {
145
+ switch (notification.method) {
146
+ case 'unsubscribe': {
147
+ const { channel } = notification.params;
148
+ connection.subscriptions.delete(channel);
149
+ return;
150
+ }
151
+ case 'dispatchAction':
152
+ await this.dispatchAction(connection, notification.params);
153
+ return;
154
+ default:
155
+ return;
156
+ }
157
+ }
158
+ initialize(connection, params) {
159
+ const protocolVersion = params.protocolVersions.find(version => this.supportedProtocolVersions.includes(version));
160
+ if (!protocolVersion) {
161
+ throw new AhpServerError(JsonRpcErrorCodes.UnsupportedProtocolVersion, `unsupported protocol versions: ${params.protocolVersions.join(', ')}`);
162
+ }
163
+ connection.clientId = params.clientId;
164
+ connection.initialized = true;
165
+ const snapshots = (params.initialSubscriptions ?? []).map(uri => {
166
+ connection.subscriptions.add(uri);
167
+ return this.snapshot(uri);
168
+ });
169
+ return {
170
+ protocolVersion,
171
+ serverSeq: this.serverSeq,
172
+ snapshots,
173
+ ...(this.options.defaultDirectory ? { defaultDirectory: this.options.defaultDirectory } : {}),
174
+ };
175
+ }
176
+ reconnect(connection, params) {
177
+ connection.clientId = params.clientId;
178
+ connection.initialized = true;
179
+ connection.subscriptions.clear();
180
+ for (const uri of params.subscriptions) {
181
+ connection.subscriptions.add(uri);
182
+ }
183
+ return {
184
+ type: 'snapshot',
185
+ snapshots: params.subscriptions.map(uri => this.snapshot(uri)),
186
+ };
187
+ }
188
+ subscribe(connection, params) {
189
+ connection.subscriptions.add(params.channel);
190
+ return { snapshot: this.snapshot(params.channel) };
191
+ }
192
+ listSessions() {
193
+ return { items: this.store.listSessions() };
194
+ }
195
+ async resolveSessionConfig(params) {
196
+ const provider = params.provider ? this.providers.get(params.provider) : this.firstProvider();
197
+ if (!provider) {
198
+ throw new AhpServerError(JsonRpcErrorCodes.ProviderNotFound, `provider not found: ${params.provider ?? '(default)'}`);
199
+ }
200
+ if (!provider.resolveSessionConfig) {
201
+ return { schema: { type: 'object', properties: {} }, values: params.config ?? {} };
202
+ }
203
+ return provider.resolveSessionConfig({
204
+ workingDirectory: params.workingDirectory,
205
+ config: params.config,
206
+ });
207
+ }
208
+ async createSession(connection, params) {
209
+ if (this.store.getSession(params.channel)) {
210
+ throw new AhpServerError(JsonRpcErrorCodes.SessionAlreadyExists, `session already exists: ${params.channel}`);
211
+ }
212
+ const provider = params.provider ? this.providers.get(params.provider) : this.firstProvider();
213
+ if (!provider) {
214
+ throw new AhpServerError(JsonRpcErrorCodes.ProviderNotFound, `provider not found: ${params.provider ?? '(default)'}`);
215
+ }
216
+ if (params.activeClient && params.activeClient.clientId !== connection.clientId) {
217
+ throw new AhpServerError(JsonRpcErrorCodes.PermissionDenied, 'active client must match initialized client id');
218
+ }
219
+ const now = Date.now();
220
+ const state = {
221
+ summary: {
222
+ resource: params.channel,
223
+ provider: provider.agent.provider,
224
+ title: 'New Session',
225
+ status: 1,
226
+ createdAt: now,
227
+ modifiedAt: now,
228
+ ...(params.model ? { model: params.model } : {}),
229
+ ...(params.agent ? { agent: params.agent } : {}),
230
+ ...(params.workingDirectory ? { workingDirectory: params.workingDirectory } : {}),
231
+ },
232
+ lifecycle: 'creating',
233
+ turns: [],
234
+ ...(params.config ? {
235
+ config: {
236
+ schema: { type: 'object', properties: {} },
237
+ values: params.config,
238
+ },
239
+ } : {}),
240
+ ...(params.activeClient ? { activeClient: params.activeClient } : {}),
241
+ };
242
+ const stored = { uri: params.channel, state };
243
+ this.store.addSession(stored);
244
+ this.publishRootNotification('root/sessionAdded', { channel: ROOT_URI, summary: state.summary });
245
+ this.publishRootAction({ type: ACTION.RootActiveSessionsChanged, activeSessions: this.store.listSessions().length });
246
+ try {
247
+ stored.agentSession = await provider.createSession({
248
+ sessionUri: params.channel,
249
+ providerId: provider.agent.provider,
250
+ workingDirectory: params.workingDirectory,
251
+ model: params.model,
252
+ config: params.config,
253
+ activeClientId: connection.clientId,
254
+ activeClientTools: activeClientToolsFromState(state),
255
+ activeClientToolSink: {
256
+ reportInvocation: invocation => {
257
+ return this.reportActiveClientToolInvocation(params.channel, invocation);
258
+ },
259
+ },
260
+ });
261
+ await this.syncActiveClientTools(stored);
262
+ this.applySessionAction(params.channel, { type: ACTION.SessionReady });
263
+ }
264
+ catch (error) {
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ this.applySessionAction(params.channel, {
267
+ type: ACTION.SessionCreationFailed,
268
+ error: { errorType: 'provider.createSession', message },
269
+ });
270
+ throw new AhpServerError(JsonRpcErrorCodes.InternalError, `provider.createSession failed: ${message}`);
271
+ }
272
+ return null;
273
+ }
274
+ async disposeSession(params) {
275
+ const session = this.store.removeSession(params.channel);
276
+ if (!session) {
277
+ throw new AhpServerError(JsonRpcErrorCodes.NotFound, `session not found: ${params.channel}`);
278
+ }
279
+ session.abortController?.abort();
280
+ await session.agentSession?.dispose?.();
281
+ this.deleteActiveClientToolCalls(params.channel);
282
+ this.publishRootNotification('root/sessionRemoved', { channel: ROOT_URI, session: params.channel });
283
+ this.publishRootAction({ type: ACTION.RootActiveSessionsChanged, activeSessions: this.store.listSessions().length });
284
+ return null;
285
+ }
286
+ fetchTurns(params) {
287
+ const session = this.requireSession(params.channel);
288
+ const turns = session.state.turns;
289
+ const limit = params.limit ?? turns.length;
290
+ return { turns: turns.slice(Math.max(0, turns.length - limit)), hasMore: false };
291
+ }
292
+ async dispatchAction(connection, params) {
293
+ const origin = connection.clientId
294
+ ? { clientId: connection.clientId, clientSeq: params.clientSeq }
295
+ : undefined;
296
+ if (params.channel === ROOT_URI) {
297
+ this.publishRootAction(params.action, origin);
298
+ return;
299
+ }
300
+ const session = this.requireSession(params.channel);
301
+ if (!this.acceptClientSessionAction(connection, session, params.action)) {
302
+ return;
303
+ }
304
+ this.applySessionAction(params.channel, params.action, origin);
305
+ if (params.action.type === ACTION.SessionTurnStarted) {
306
+ this.queueAgentTurn(session, params.action.turnId, params.action.message);
307
+ }
308
+ else if (params.action.type === ACTION.SessionPendingMessageSet) {
309
+ this.queueAgentTurn(session, params.action.id, params.action.message);
310
+ }
311
+ else if (params.action.type === ACTION.SessionTurnCancelled) {
312
+ session.abortController?.abort(params.action.turnId);
313
+ await session.agentSession?.cancel?.(params.action.turnId);
314
+ }
315
+ else if (params.action.type === ACTION.SessionActiveClientChanged ||
316
+ params.action.type === ACTION.SessionActiveClientToolsChanged) {
317
+ if (params.action.type === ACTION.SessionActiveClientChanged && !params.action.activeClient) {
318
+ this.deleteActiveClientToolCalls(params.channel);
319
+ }
320
+ await this.syncActiveClientTools(session);
321
+ }
322
+ else if (params.action.type === ACTION.SessionToolCallComplete &&
323
+ !params.action.requiresResultConfirmation) {
324
+ this.completeActiveClientToolCall(params.channel, params.action.toolCallId, params.action.result);
325
+ }
326
+ else if (params.action.type === ACTION.SessionToolCallResultConfirmed) {
327
+ this.activeClientToolCalls.delete(activeClientToolCallKey(params.channel, params.action.toolCallId));
328
+ }
329
+ }
330
+ acceptClientSessionAction(connection, session, action) {
331
+ if (action.type === ACTION.SessionActiveClientChanged) {
332
+ const currentClientId = session.state.activeClient?.clientId;
333
+ const requestedClientId = action.activeClient?.clientId;
334
+ if (!connection.clientId) {
335
+ return false;
336
+ }
337
+ if (requestedClientId && requestedClientId !== connection.clientId) {
338
+ return false;
339
+ }
340
+ if (currentClientId && currentClientId !== connection.clientId) {
341
+ return false;
342
+ }
343
+ return true;
344
+ }
345
+ if (action.type === ACTION.SessionActiveClientToolsChanged) {
346
+ return Boolean(connection.clientId && session.state.activeClient?.clientId === connection.clientId);
347
+ }
348
+ if (isClientToolResultAction(action)) {
349
+ const record = this.activeClientToolCalls.get(activeClientToolCallKey(session.uri, action.toolCallId));
350
+ if (!record) {
351
+ if (clientContributorFromSessionState(session.state, action.toolCallId)) {
352
+ return false;
353
+ }
354
+ return true;
355
+ }
356
+ return connection.clientId === record.clientId && session.state.activeClient?.clientId === record.clientId;
357
+ }
358
+ return true;
359
+ }
360
+ async syncActiveClientTools(session) {
361
+ await session.agentSession?.setActiveClientTools?.(activeClientToolsFromState(session.state));
362
+ }
363
+ reportActiveClientToolInvocation(sessionUri, invocation) {
364
+ const session = this.store.getSession(sessionUri);
365
+ const activeClient = session?.state.activeClient;
366
+ if (!session || !activeClient) {
367
+ throw new Error(`session has no active client: ${sessionUri}`);
368
+ }
369
+ const tool = activeClient.tools.find(candidate => candidate.name === invocation.toolName);
370
+ if (!tool) {
371
+ throw new Error(`active client tool not found: ${invocation.toolName}`);
372
+ }
373
+ const completion = new Promise((resolve, reject) => {
374
+ this.activeClientToolCalls.set(activeClientToolCallKey(sessionUri, invocation.toolCallId), {
375
+ sessionUri,
376
+ turnId: invocation.turnId,
377
+ toolCallId: invocation.toolCallId,
378
+ toolName: invocation.toolName,
379
+ clientId: activeClient.clientId,
380
+ resolve,
381
+ reject,
382
+ });
383
+ });
384
+ const displayName = invocation.displayName ?? tool.title ?? tool.name;
385
+ this.applySessionAction(sessionUri, {
386
+ type: ACTION.SessionToolCallStart,
387
+ turnId: invocation.turnId,
388
+ toolCallId: invocation.toolCallId,
389
+ toolName: invocation.toolName,
390
+ displayName,
391
+ contributor: { kind: 'client', clientId: activeClient.clientId },
392
+ ...(invocation._meta ? { _meta: invocation._meta } : {}),
393
+ });
394
+ this.applySessionAction(sessionUri, {
395
+ type: ACTION.SessionToolCallReady,
396
+ turnId: invocation.turnId,
397
+ toolCallId: invocation.toolCallId,
398
+ invocationMessage: invocation.invocationMessage ?? displayName,
399
+ ...(invocation.toolInput ? { toolInput: invocation.toolInput } : {}),
400
+ confirmed: 'not-needed',
401
+ ...(invocation._meta ? { _meta: invocation._meta } : {}),
402
+ });
403
+ return completion;
404
+ }
405
+ async clearActiveClientForConnection(connection) {
406
+ if (!connection.clientId) {
407
+ return;
408
+ }
409
+ for (const summary of this.store.listSessions()) {
410
+ const session = this.store.getSession(summary.resource);
411
+ if (!session || session.state.activeClient?.clientId !== connection.clientId) {
412
+ continue;
413
+ }
414
+ this.applySessionAction(session.uri, {
415
+ type: ACTION.SessionActiveClientChanged,
416
+ activeClient: null,
417
+ });
418
+ this.deleteActiveClientToolCalls(session.uri);
419
+ await this.syncActiveClientTools(session);
420
+ }
421
+ }
422
+ deleteActiveClientToolCalls(sessionUri) {
423
+ for (const [key, record] of this.activeClientToolCalls.entries()) {
424
+ if (record.sessionUri === sessionUri) {
425
+ this.activeClientToolCalls.delete(key);
426
+ record.reject(new Error(`active client tool call cancelled for session ${sessionUri}`));
427
+ }
428
+ }
429
+ }
430
+ completeActiveClientToolCall(sessionUri, toolCallId, result) {
431
+ const key = activeClientToolCallKey(sessionUri, toolCallId);
432
+ const record = this.activeClientToolCalls.get(key);
433
+ if (!record) {
434
+ return;
435
+ }
436
+ this.activeClientToolCalls.delete(key);
437
+ record.resolve(result);
438
+ }
439
+ queueAgentTurn(session, turnId, message) {
440
+ void this.startAgentTurn(session, turnId, message);
441
+ }
442
+ async startAgentTurn(session, turnId, message) {
443
+ if (!session.agentSession) {
444
+ this.applySessionAction(session.uri, {
445
+ type: ACTION.SessionError,
446
+ turnId,
447
+ error: { errorType: 'session.notReady', message: 'session backend is not ready' },
448
+ });
449
+ return;
450
+ }
451
+ session.abortController?.abort('new turn started');
452
+ const abortController = new AbortController();
453
+ session.abortController = abortController;
454
+ const sink = {
455
+ emit: action => this.applySessionAction(session.uri, action),
456
+ fail: error => {
457
+ this.applySessionAction(session.uri, {
458
+ type: ACTION.SessionError,
459
+ turnId,
460
+ error: { errorType: 'agent.turn', message: error.message },
461
+ });
462
+ },
463
+ };
464
+ try {
465
+ await session.agentSession.sendUserMessage(message, sink, abortController.signal, turnId);
466
+ }
467
+ catch (error) {
468
+ if (abortController.signal.aborted) {
469
+ return;
470
+ }
471
+ const messageText = error instanceof Error ? error.message : String(error);
472
+ this.applySessionAction(session.uri, {
473
+ type: ACTION.SessionError,
474
+ turnId,
475
+ error: { errorType: 'agent.turn', message: messageText },
476
+ });
477
+ }
478
+ }
479
+ snapshot(uri) {
480
+ if (uri === ROOT_URI) {
481
+ return {
482
+ resource: ROOT_URI,
483
+ state: this.store.getRootState(),
484
+ fromSeq: this.serverSeq,
485
+ };
486
+ }
487
+ const session = this.requireSession(uri);
488
+ return {
489
+ resource: uri,
490
+ state: session.state,
491
+ fromSeq: this.serverSeq,
492
+ };
493
+ }
494
+ publishRootNotification(method, params) {
495
+ const notification = { jsonrpc: '2.0', method, params };
496
+ for (const connection of this.connections) {
497
+ if (connection.subscriptions.has(ROOT_URI)) {
498
+ void connection.send(notification);
499
+ }
500
+ }
501
+ }
502
+ publishRootAction(action, origin) {
503
+ const root = this.store.getRootState();
504
+ const updated = rootReducer(root, action);
505
+ if ('setAgents' in this.store && typeof this.store.setAgents === 'function') {
506
+ this.store.setAgents(updated.agents);
507
+ }
508
+ this.publishAction(ROOT_URI, action, origin);
509
+ }
510
+ applySessionAction(uri, action, origin) {
511
+ const session = this.store.updateSession(uri, stored => {
512
+ stored.state = sessionReducer(stored.state, action);
513
+ stored.state.summary.modifiedAt = Date.now();
514
+ });
515
+ this.publishAction(uri, action, origin);
516
+ this.publishRootNotification('root/sessionSummaryChanged', {
517
+ channel: ROOT_URI,
518
+ session: uri,
519
+ changes: session.state.summary,
520
+ });
521
+ }
522
+ publishAction(channel, action, origin) {
523
+ const envelope = {
524
+ channel,
525
+ action,
526
+ serverSeq: ++this.serverSeq,
527
+ origin,
528
+ };
529
+ const notification = {
530
+ jsonrpc: '2.0',
531
+ method: 'action',
532
+ params: envelope,
533
+ };
534
+ for (const connection of this.connections) {
535
+ if (connection.subscriptions.has(channel)) {
536
+ void connection.send(notification);
537
+ }
538
+ }
539
+ }
540
+ firstProvider() {
541
+ return this.providers.values().next().value;
542
+ }
543
+ requireSession(uri) {
544
+ const session = this.store.getSession(uri);
545
+ if (!session) {
546
+ throw new AhpServerError(JsonRpcErrorCodes.NotFound, `session not found: ${uri}`);
547
+ }
548
+ return session;
549
+ }
550
+ isRequest(message) {
551
+ return 'method' in message && 'id' in message;
552
+ }
553
+ isResponse(message) {
554
+ return 'id' in message && ('result' in message || 'error' in message) && !('method' in message);
555
+ }
556
+ }
557
+ function activeClientToolsFromState(state) {
558
+ return state.activeClient
559
+ ? { clientId: state.activeClient.clientId, tools: state.activeClient.tools }
560
+ : undefined;
561
+ }
562
+ function isClientToolResultAction(action) {
563
+ return action.type === ACTION.SessionToolCallComplete ||
564
+ action.type === ACTION.SessionToolCallContentChanged ||
565
+ action.type === ACTION.SessionToolCallResultConfirmed;
566
+ }
567
+ function activeClientToolCallKey(sessionUri, toolCallId) {
568
+ return `${sessionUri}\u0000${toolCallId}`;
569
+ }
570
+ function clientContributorFromSessionState(state, toolCallId) {
571
+ for (const part of responsePartsFromSessionState(state)) {
572
+ if (!isRecord(part) || part.kind !== 'toolCall' || !isRecord(part.toolCall)) {
573
+ continue;
574
+ }
575
+ const toolCall = part.toolCall;
576
+ if (toolCall.toolCallId !== toolCallId || !isRecord(toolCall.contributor)) {
577
+ continue;
578
+ }
579
+ const contributor = toolCall.contributor;
580
+ if (contributor.kind === 'client' && typeof contributor.clientId === 'string') {
581
+ return contributor.clientId;
582
+ }
583
+ }
584
+ return undefined;
585
+ }
586
+ function responsePartsFromSessionState(state) {
587
+ const parts = [];
588
+ const stateRecord = state;
589
+ const activeTurn = stateRecord.activeTurn;
590
+ if (isRecord(activeTurn) && Array.isArray(activeTurn.responseParts)) {
591
+ parts.push(...activeTurn.responseParts);
592
+ }
593
+ if (Array.isArray(stateRecord.turns)) {
594
+ for (const turn of stateRecord.turns) {
595
+ if (isRecord(turn) && Array.isArray(turn.responseParts)) {
596
+ parts.push(...turn.responseParts);
597
+ }
598
+ }
599
+ }
600
+ return parts;
601
+ }
602
+ function isRecord(value) {
603
+ return typeof value === 'object' && value !== null;
604
+ }
605
+ export function textMessage(text) {
606
+ return { text, origin: { kind: 'user' } };
607
+ }
608
+ export function markdownResponsePart(partId) {
609
+ return {
610
+ type: ACTION.SessionResponsePart,
611
+ turnId: partId.split(':')[0] ?? partId,
612
+ part: {
613
+ kind: 'markdown',
614
+ id: partId,
615
+ content: '',
616
+ },
617
+ };
618
+ }
619
+ export function completeTurn(turnId) {
620
+ return {
621
+ type: ACTION.SessionTurnComplete,
622
+ turnId,
623
+ };
624
+ }
625
+ //# sourceMappingURL=server.js.map