@tylerl0706/ahpx 0.2.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,2384 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/protocol/actions.ts
9
+ var ActionType = /* @__PURE__ */ ((ActionType2) => {
10
+ ActionType2["RootAgentsChanged"] = "root/agentsChanged";
11
+ ActionType2["RootActiveSessionsChanged"] = "root/activeSessionsChanged";
12
+ ActionType2["SessionReady"] = "session/ready";
13
+ ActionType2["SessionCreationFailed"] = "session/creationFailed";
14
+ ActionType2["SessionTurnStarted"] = "session/turnStarted";
15
+ ActionType2["SessionDelta"] = "session/delta";
16
+ ActionType2["SessionResponsePart"] = "session/responsePart";
17
+ ActionType2["SessionToolCallStart"] = "session/toolCallStart";
18
+ ActionType2["SessionToolCallDelta"] = "session/toolCallDelta";
19
+ ActionType2["SessionToolCallReady"] = "session/toolCallReady";
20
+ ActionType2["SessionToolCallConfirmed"] = "session/toolCallConfirmed";
21
+ ActionType2["SessionToolCallComplete"] = "session/toolCallComplete";
22
+ ActionType2["SessionToolCallResultConfirmed"] = "session/toolCallResultConfirmed";
23
+ ActionType2["SessionTurnComplete"] = "session/turnComplete";
24
+ ActionType2["SessionTurnCancelled"] = "session/turnCancelled";
25
+ ActionType2["SessionError"] = "session/error";
26
+ ActionType2["SessionTitleChanged"] = "session/titleChanged";
27
+ ActionType2["SessionUsage"] = "session/usage";
28
+ ActionType2["SessionReasoning"] = "session/reasoning";
29
+ ActionType2["SessionModelChanged"] = "session/modelChanged";
30
+ ActionType2["SessionServerToolsChanged"] = "session/serverToolsChanged";
31
+ ActionType2["SessionActiveClientChanged"] = "session/activeClientChanged";
32
+ ActionType2["SessionActiveClientToolsChanged"] = "session/activeClientToolsChanged";
33
+ ActionType2["SessionPendingMessageSet"] = "session/pendingMessageSet";
34
+ ActionType2["SessionPendingMessageRemoved"] = "session/pendingMessageRemoved";
35
+ ActionType2["SessionQueuedMessagesReordered"] = "session/queuedMessagesReordered";
36
+ ActionType2["SessionCustomizationsChanged"] = "session/customizationsChanged";
37
+ ActionType2["SessionCustomizationToggled"] = "session/customizationToggled";
38
+ ActionType2["SessionTruncated"] = "session/truncated";
39
+ return ActionType2;
40
+ })(ActionType || {});
41
+
42
+ // src/client/protocol.ts
43
+ import { EventEmitter } from "events";
44
+ var RpcError = class extends Error {
45
+ constructor(code, message, data) {
46
+ super(message);
47
+ this.code = code;
48
+ this.data = data;
49
+ this.name = "RpcError";
50
+ }
51
+ };
52
+ var RpcTimeoutError = class extends Error {
53
+ constructor(method, timeoutMs) {
54
+ super(`Request '${method}' timed out after ${timeoutMs}ms`);
55
+ this.method = method;
56
+ this.timeoutMs = timeoutMs;
57
+ this.name = "RpcTimeoutError";
58
+ }
59
+ };
60
+ var ProtocolLayer = class extends EventEmitter {
61
+ constructor(transport, options = {}) {
62
+ super();
63
+ this.transport = transport;
64
+ this.requestTimeout = options.requestTimeout ?? 3e4;
65
+ this.transport.on("message", (data) => this.handleMessage(data));
66
+ }
67
+ nextId = 1;
68
+ pending = /* @__PURE__ */ new Map();
69
+ requestTimeout;
70
+ /**
71
+ * Send a typed JSON-RPC request and await the response.
72
+ */
73
+ async request(method, params, timeoutMs) {
74
+ const id = this.nextId++;
75
+ const timeout = timeoutMs ?? this.requestTimeout;
76
+ return new Promise((resolve, reject) => {
77
+ const timer = setTimeout(() => {
78
+ this.pending.delete(id);
79
+ reject(new RpcTimeoutError(method, timeout));
80
+ }, timeout);
81
+ this.pending.set(id, {
82
+ resolve,
83
+ reject,
84
+ timer
85
+ });
86
+ this.transport.send({
87
+ jsonrpc: "2.0",
88
+ id,
89
+ method,
90
+ params
91
+ });
92
+ });
93
+ }
94
+ /**
95
+ * Send a JSON-RPC notification (no response expected).
96
+ */
97
+ notify(method, params) {
98
+ this.transport.send({
99
+ jsonrpc: "2.0",
100
+ method,
101
+ params
102
+ });
103
+ }
104
+ /**
105
+ * Cancel all pending requests (e.g. on disconnect).
106
+ */
107
+ cancelAll(reason) {
108
+ for (const [id, pending] of this.pending) {
109
+ clearTimeout(pending.timer);
110
+ pending.reject(new Error(reason));
111
+ this.pending.delete(id);
112
+ }
113
+ }
114
+ handleMessage(data) {
115
+ if (!data || typeof data !== "object") return;
116
+ const msg = data;
117
+ if ("id" in msg && typeof msg.id === "number") {
118
+ const pending = this.pending.get(msg.id);
119
+ if (!pending) return;
120
+ this.pending.delete(msg.id);
121
+ clearTimeout(pending.timer);
122
+ if ("error" in msg) {
123
+ const errPayload = msg.error;
124
+ pending.reject(new RpcError(errPayload.code, errPayload.message, errPayload.data));
125
+ } else if ("result" in msg) {
126
+ pending.resolve(msg.result);
127
+ }
128
+ return;
129
+ }
130
+ if ("method" in msg && typeof msg.method === "string") {
131
+ if (msg.method === "action") {
132
+ this.emit("action", msg.params);
133
+ } else if (msg.method === "notification") {
134
+ const params = msg.params;
135
+ this.emit("notification", params.notification);
136
+ }
137
+ }
138
+ }
139
+ };
140
+
141
+ // src/protocol/state.ts
142
+ var PendingMessageKind = /* @__PURE__ */ ((PendingMessageKind2) => {
143
+ PendingMessageKind2["Steering"] = "steering";
144
+ PendingMessageKind2["Queued"] = "queued";
145
+ return PendingMessageKind2;
146
+ })(PendingMessageKind || {});
147
+ var SessionLifecycle = /* @__PURE__ */ ((SessionLifecycle2) => {
148
+ SessionLifecycle2["Creating"] = "creating";
149
+ SessionLifecycle2["Ready"] = "ready";
150
+ SessionLifecycle2["CreationFailed"] = "creationFailed";
151
+ return SessionLifecycle2;
152
+ })(SessionLifecycle || {});
153
+ var SessionStatus = /* @__PURE__ */ ((SessionStatus2) => {
154
+ SessionStatus2["Idle"] = "idle";
155
+ SessionStatus2["InProgress"] = "in-progress";
156
+ SessionStatus2["Error"] = "error";
157
+ return SessionStatus2;
158
+ })(SessionStatus || {});
159
+ var ResponsePartKind = /* @__PURE__ */ ((ResponsePartKind2) => {
160
+ ResponsePartKind2["Markdown"] = "markdown";
161
+ ResponsePartKind2["ContentRef"] = "contentRef";
162
+ ResponsePartKind2["ToolCall"] = "toolCall";
163
+ ResponsePartKind2["Reasoning"] = "reasoning";
164
+ return ResponsePartKind2;
165
+ })(ResponsePartKind || {});
166
+ var ToolCallStatus = /* @__PURE__ */ ((ToolCallStatus2) => {
167
+ ToolCallStatus2["Streaming"] = "streaming";
168
+ ToolCallStatus2["PendingConfirmation"] = "pending-confirmation";
169
+ ToolCallStatus2["Running"] = "running";
170
+ ToolCallStatus2["PendingResultConfirmation"] = "pending-result-confirmation";
171
+ ToolCallStatus2["Completed"] = "completed";
172
+ ToolCallStatus2["Cancelled"] = "cancelled";
173
+ return ToolCallStatus2;
174
+ })(ToolCallStatus || {});
175
+
176
+ // src/client/session-handle.ts
177
+ import { randomUUID } from "crypto";
178
+ import { EventEmitter as EventEmitter2 } from "events";
179
+ var SessionHandle = class extends EventEmitter2 {
180
+ constructor(client, uri, provider, model) {
181
+ super();
182
+ this.client = client;
183
+ this.uri = uri;
184
+ this.provider = provider;
185
+ this.model = model;
186
+ this._onAction = (envelope) => {
187
+ const action = envelope.action;
188
+ if (!("session" in action) || action.session !== this.uri) {
189
+ return;
190
+ }
191
+ this.emit("action", envelope);
192
+ if (action.type === "session/turnComplete" /* SessionTurnComplete */) {
193
+ const session = this.state;
194
+ if (session && session.turns.length > 0) {
195
+ this.emit("turnComplete", session.turns[session.turns.length - 1]);
196
+ }
197
+ } else if (action.type === "session/error" /* SessionError */) {
198
+ const a = action;
199
+ this.emit("error", new Error(a.error.message));
200
+ }
201
+ };
202
+ this._onDisconnect = (_code, reason) => {
203
+ this.emit("error", new Error(`Connection lost: ${reason}`));
204
+ };
205
+ this.client.on("action", this._onAction);
206
+ this.client.on("disconnected", this._onDisconnect);
207
+ }
208
+ uri;
209
+ provider;
210
+ model;
211
+ _disposed = false;
212
+ _activeTurnId;
213
+ _onAction;
214
+ _onDisconnect;
215
+ // ── State accessors ────────────────────────────────────────────────────
216
+ /** Current session state from the state mirror. */
217
+ get state() {
218
+ return this.client.state.getSession(this.uri);
219
+ }
220
+ /** Whether the session is ready for prompts. */
221
+ get isReady() {
222
+ return this.state?.lifecycle === "ready" /* Ready */;
223
+ }
224
+ /** The currently active turn, if any. */
225
+ get activeTurn() {
226
+ return this.state?.activeTurn;
227
+ }
228
+ /** Whether this handle has been disposed. */
229
+ get disposed() {
230
+ return this._disposed;
231
+ }
232
+ // ── Lifecycle ──────────────────────────────────────────────────────────
233
+ /**
234
+ * Wait for the session to reach the "ready" lifecycle state.
235
+ *
236
+ * Resolves immediately if already ready. Rejects on creation failure
237
+ * or timeout.
238
+ */
239
+ async waitForReady(timeout = 3e4) {
240
+ this.ensureNotDisposed();
241
+ if (this.isReady) return;
242
+ const current = this.state;
243
+ if (current?.lifecycle === "creationFailed" /* CreationFailed */) {
244
+ throw new Error(`Session creation failed: ${current.creationError?.message ?? "Unknown error"}`);
245
+ }
246
+ return new Promise((resolve, reject) => {
247
+ const timer = setTimeout(() => {
248
+ cleanup();
249
+ reject(new Error(`Timed out waiting for session to be ready (${timeout}ms)`));
250
+ }, timeout);
251
+ const onAction = (envelope) => {
252
+ const action = envelope.action;
253
+ if (action.type === "session/ready" /* SessionReady */) {
254
+ cleanup();
255
+ resolve();
256
+ } else if (action.type === "session/creationFailed" /* SessionCreationFailed */) {
257
+ cleanup();
258
+ const session = this.state;
259
+ reject(new Error(`Session creation failed: ${session?.creationError?.message ?? "Unknown error"}`));
260
+ }
261
+ };
262
+ const cleanup = () => {
263
+ clearTimeout(timer);
264
+ this.removeListener("action", onAction);
265
+ };
266
+ this.on("action", onAction);
267
+ });
268
+ }
269
+ /**
270
+ * Dispose this session handle and clean up server-side resources.
271
+ */
272
+ async dispose() {
273
+ if (this._disposed) return;
274
+ this._disposed = true;
275
+ this.client.removeListener("action", this._onAction);
276
+ this.client.removeListener("disconnected", this._onDisconnect);
277
+ if (this.client.connected) {
278
+ try {
279
+ await this.client.disposeSession(this.uri);
280
+ } catch {
281
+ }
282
+ }
283
+ this.emit("disposed");
284
+ this.removeAllListeners();
285
+ }
286
+ // ── Turns ──────────────────────────────────────────────────────────────
287
+ /**
288
+ * Send a prompt and wait for the turn to complete.
289
+ *
290
+ * Returns a `TurnResult` when the turn finishes (complete, error, or cancelled).
291
+ * Tool call confirmations and permissions are NOT handled automatically —
292
+ * library consumers should listen to `handle.on('action', ...)` and use
293
+ * `handle.dispatchAction()` for tool/permission responses.
294
+ */
295
+ async sendPrompt(text, options) {
296
+ this.ensureNotDisposed();
297
+ if (!this.isReady) {
298
+ throw new Error("Session is not ready");
299
+ }
300
+ if (this._activeTurnId) {
301
+ throw new Error("A turn is already active");
302
+ }
303
+ const turnId = randomUUID();
304
+ this._activeTurnId = turnId;
305
+ let responseText = "";
306
+ let toolCallCount = 0;
307
+ let usage;
308
+ return new Promise((resolve) => {
309
+ let timer;
310
+ const onAction = (envelope) => {
311
+ const action = envelope.action;
312
+ if ("turnId" in action && action.turnId !== turnId) {
313
+ return;
314
+ }
315
+ switch (action.type) {
316
+ case "session/delta" /* SessionDelta */: {
317
+ const a = action;
318
+ responseText += a.content;
319
+ break;
320
+ }
321
+ case "session/toolCallStart" /* SessionToolCallStart */:
322
+ toolCallCount++;
323
+ break;
324
+ case "session/usage" /* SessionUsage */: {
325
+ const a = action;
326
+ usage = a.usage;
327
+ break;
328
+ }
329
+ case "session/turnComplete" /* SessionTurnComplete */: {
330
+ finish("complete");
331
+ break;
332
+ }
333
+ case "session/error" /* SessionError */: {
334
+ const a = action;
335
+ finish("error", a.error.message);
336
+ break;
337
+ }
338
+ case "session/turnCancelled" /* SessionTurnCancelled */: {
339
+ finish("cancelled");
340
+ break;
341
+ }
342
+ }
343
+ };
344
+ const finish = (state, error) => {
345
+ cleanup();
346
+ resolve({
347
+ turnId,
348
+ responseText,
349
+ toolCalls: toolCallCount,
350
+ usage: usage ? {
351
+ inputTokens: usage.inputTokens ?? 0,
352
+ outputTokens: usage.outputTokens ?? 0,
353
+ model: usage.model
354
+ } : void 0,
355
+ state,
356
+ error
357
+ });
358
+ };
359
+ const cleanup = () => {
360
+ if (timer !== void 0) clearTimeout(timer);
361
+ this.removeListener("action", onAction);
362
+ this._activeTurnId = void 0;
363
+ };
364
+ if (options?.timeout) {
365
+ timer = setTimeout(() => {
366
+ this.cancelTurn().catch(() => {
367
+ });
368
+ cleanup();
369
+ resolve({
370
+ turnId,
371
+ responseText,
372
+ toolCalls: toolCallCount,
373
+ state: "error",
374
+ error: `Turn timed out after ${options.timeout}ms`
375
+ });
376
+ }, options.timeout);
377
+ }
378
+ this.on("action", onAction);
379
+ this.client.dispatchAction({
380
+ type: "session/turnStarted" /* SessionTurnStarted */,
381
+ session: this.uri,
382
+ turnId,
383
+ userMessage: {
384
+ text,
385
+ ...options?.attachments && options.attachments.length > 0 ? { attachments: options.attachments } : {}
386
+ }
387
+ });
388
+ });
389
+ }
390
+ /**
391
+ * Cancel the currently active turn.
392
+ */
393
+ async cancelTurn() {
394
+ this.ensureNotDisposed();
395
+ if (!this._activeTurnId) return;
396
+ this.client.dispatchAction({
397
+ type: "session/turnCancelled" /* SessionTurnCancelled */,
398
+ session: this.uri,
399
+ turnId: this._activeTurnId
400
+ });
401
+ }
402
+ // ── Action dispatch ────────────────────────────────────────────────────
403
+ /**
404
+ * Dispatch a client action for this session.
405
+ *
406
+ * Automatically injects the session URI into the action. Use this for
407
+ * tool call confirmations, permission responses, model changes, etc.
408
+ */
409
+ dispatchAction(action) {
410
+ this.ensureNotDisposed();
411
+ this.client.dispatchAction({
412
+ ...action,
413
+ session: this.uri
414
+ });
415
+ }
416
+ ensureNotDisposed() {
417
+ if (this._disposed) {
418
+ throw new Error("SessionHandle has been disposed");
419
+ }
420
+ }
421
+ };
422
+
423
+ // src/protocol/action-origin.generated.ts
424
+ var IS_CLIENT_DISPATCHABLE = {
425
+ ["root/agentsChanged" /* RootAgentsChanged */]: false,
426
+ ["root/activeSessionsChanged" /* RootActiveSessionsChanged */]: false,
427
+ ["session/ready" /* SessionReady */]: false,
428
+ ["session/creationFailed" /* SessionCreationFailed */]: false,
429
+ ["session/turnStarted" /* SessionTurnStarted */]: true,
430
+ ["session/delta" /* SessionDelta */]: false,
431
+ ["session/responsePart" /* SessionResponsePart */]: false,
432
+ ["session/toolCallStart" /* SessionToolCallStart */]: false,
433
+ ["session/toolCallDelta" /* SessionToolCallDelta */]: false,
434
+ ["session/toolCallReady" /* SessionToolCallReady */]: false,
435
+ ["session/toolCallConfirmed" /* SessionToolCallConfirmed */]: true,
436
+ ["session/toolCallComplete" /* SessionToolCallComplete */]: true,
437
+ ["session/toolCallResultConfirmed" /* SessionToolCallResultConfirmed */]: true,
438
+ ["session/turnComplete" /* SessionTurnComplete */]: false,
439
+ ["session/turnCancelled" /* SessionTurnCancelled */]: true,
440
+ ["session/error" /* SessionError */]: false,
441
+ ["session/titleChanged" /* SessionTitleChanged */]: true,
442
+ ["session/usage" /* SessionUsage */]: false,
443
+ ["session/reasoning" /* SessionReasoning */]: false,
444
+ ["session/modelChanged" /* SessionModelChanged */]: true,
445
+ ["session/serverToolsChanged" /* SessionServerToolsChanged */]: false,
446
+ ["session/activeClientChanged" /* SessionActiveClientChanged */]: true,
447
+ ["session/activeClientToolsChanged" /* SessionActiveClientToolsChanged */]: true,
448
+ ["session/pendingMessageSet" /* SessionPendingMessageSet */]: true,
449
+ ["session/pendingMessageRemoved" /* SessionPendingMessageRemoved */]: true,
450
+ ["session/queuedMessagesReordered" /* SessionQueuedMessagesReordered */]: true,
451
+ ["session/customizationsChanged" /* SessionCustomizationsChanged */]: false,
452
+ ["session/customizationToggled" /* SessionCustomizationToggled */]: true,
453
+ ["session/truncated" /* SessionTruncated */]: true
454
+ };
455
+
456
+ // src/protocol/reducers.ts
457
+ function softAssertNever(value, log7) {
458
+ const msg = `Unhandled action type: ${JSON.stringify(value)}`;
459
+ (log7 ?? console.warn)(msg);
460
+ }
461
+ function tcBase(tc) {
462
+ return {
463
+ toolCallId: tc.toolCallId,
464
+ toolName: tc.toolName,
465
+ displayName: tc.displayName,
466
+ toolClientId: tc.toolClientId,
467
+ _meta: tc._meta
468
+ };
469
+ }
470
+ function endTurn(state, turnId, turnState, summaryStatus, error) {
471
+ if (!state.activeTurn || state.activeTurn.id !== turnId) {
472
+ return state;
473
+ }
474
+ const active = state.activeTurn;
475
+ const responseParts = active.responseParts.map((part) => {
476
+ if (part.kind !== "toolCall" /* ToolCall */) {
477
+ return part;
478
+ }
479
+ const tc = part.toolCall;
480
+ if (tc.status === "completed" /* Completed */ || tc.status === "cancelled" /* Cancelled */) {
481
+ return part;
482
+ }
483
+ return {
484
+ kind: "toolCall" /* ToolCall */,
485
+ toolCall: {
486
+ status: "cancelled" /* Cancelled */,
487
+ ...tcBase(tc),
488
+ invocationMessage: tc.status === "streaming" /* Streaming */ ? tc.invocationMessage ?? "" : tc.invocationMessage,
489
+ toolInput: tc.status === "streaming" /* Streaming */ ? void 0 : tc.toolInput,
490
+ reason: "skipped" /* Skipped */
491
+ }
492
+ };
493
+ });
494
+ const turn = {
495
+ id: active.id,
496
+ userMessage: active.userMessage,
497
+ responseParts,
498
+ usage: active.usage,
499
+ state: turnState,
500
+ error
501
+ };
502
+ return {
503
+ ...state,
504
+ turns: [...state.turns, turn],
505
+ activeTurn: void 0,
506
+ summary: { ...state.summary, status: summaryStatus, modifiedAt: Date.now() }
507
+ };
508
+ }
509
+ function updateToolCallInParts(state, turnId, toolCallId, updater) {
510
+ const activeTurn = state.activeTurn;
511
+ if (!activeTurn || activeTurn.id !== turnId) {
512
+ return state;
513
+ }
514
+ let found = false;
515
+ const responseParts = activeTurn.responseParts.map((part) => {
516
+ if (part.kind === "toolCall" /* ToolCall */ && part.toolCall.toolCallId === toolCallId) {
517
+ const updated = updater(part.toolCall);
518
+ if (updated === part.toolCall) {
519
+ return part;
520
+ }
521
+ found = true;
522
+ return { ...part, toolCall: updated };
523
+ }
524
+ return part;
525
+ });
526
+ if (!found) {
527
+ return state;
528
+ }
529
+ return {
530
+ ...state,
531
+ activeTurn: { ...activeTurn, responseParts }
532
+ };
533
+ }
534
+ function updateResponsePart(state, turnId, partId, updater) {
535
+ const activeTurn = state.activeTurn;
536
+ if (!activeTurn || activeTurn.id !== turnId) {
537
+ return state;
538
+ }
539
+ let found = false;
540
+ const responseParts = activeTurn.responseParts.map((part) => {
541
+ if (!found) {
542
+ const id = part.kind === "toolCall" /* ToolCall */ ? part.toolCall.toolCallId : "id" in part ? part.id : void 0;
543
+ if (id === partId) {
544
+ found = true;
545
+ return updater(part);
546
+ }
547
+ }
548
+ return part;
549
+ });
550
+ if (!found) {
551
+ return state;
552
+ }
553
+ return {
554
+ ...state,
555
+ activeTurn: { ...activeTurn, responseParts }
556
+ };
557
+ }
558
+ function rootReducer(state, action, log7) {
559
+ switch (action.type) {
560
+ case "root/agentsChanged" /* RootAgentsChanged */:
561
+ return { ...state, agents: action.agents };
562
+ case "root/activeSessionsChanged" /* RootActiveSessionsChanged */:
563
+ return { ...state, activeSessions: action.activeSessions };
564
+ default:
565
+ softAssertNever(action, log7);
566
+ return state;
567
+ }
568
+ }
569
+ function sessionReducer(state, action, log7) {
570
+ switch (action.type) {
571
+ // ── Lifecycle ──────────────────────────────────────────────────────────
572
+ case "session/ready" /* SessionReady */:
573
+ return {
574
+ ...state,
575
+ lifecycle: "ready" /* Ready */,
576
+ summary: { ...state.summary, status: "idle" /* Idle */ }
577
+ };
578
+ case "session/creationFailed" /* SessionCreationFailed */:
579
+ return {
580
+ ...state,
581
+ lifecycle: "creationFailed" /* CreationFailed */,
582
+ creationError: action.error
583
+ };
584
+ // ── Turn Lifecycle ────────────────────────────────────────────────────
585
+ case "session/turnStarted" /* SessionTurnStarted */: {
586
+ let next = {
587
+ ...state,
588
+ summary: { ...state.summary, status: "in-progress" /* InProgress */, modifiedAt: Date.now() },
589
+ activeTurn: {
590
+ id: action.turnId,
591
+ userMessage: action.userMessage,
592
+ responseParts: [],
593
+ usage: void 0
594
+ }
595
+ };
596
+ if (action.queuedMessageId) {
597
+ if (next.steeringMessage?.id === action.queuedMessageId) {
598
+ next = { ...next, steeringMessage: void 0 };
599
+ }
600
+ if (next.queuedMessages) {
601
+ const filtered = next.queuedMessages.filter((m) => m.id !== action.queuedMessageId);
602
+ next = { ...next, queuedMessages: filtered.length > 0 ? filtered : void 0 };
603
+ }
604
+ }
605
+ return next;
606
+ }
607
+ case "session/delta" /* SessionDelta */:
608
+ return updateResponsePart(state, action.turnId, action.partId, (part) => {
609
+ if (part.kind === "markdown" /* Markdown */) {
610
+ return { ...part, content: part.content + action.content };
611
+ }
612
+ return part;
613
+ });
614
+ case "session/responsePart" /* SessionResponsePart */:
615
+ if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
616
+ return state;
617
+ }
618
+ return {
619
+ ...state,
620
+ activeTurn: {
621
+ ...state.activeTurn,
622
+ responseParts: [...state.activeTurn.responseParts, action.part]
623
+ }
624
+ };
625
+ case "session/turnComplete" /* SessionTurnComplete */:
626
+ return endTurn(state, action.turnId, "complete" /* Complete */, "idle" /* Idle */);
627
+ case "session/turnCancelled" /* SessionTurnCancelled */:
628
+ return endTurn(state, action.turnId, "cancelled" /* Cancelled */, "idle" /* Idle */);
629
+ case "session/error" /* SessionError */:
630
+ return endTurn(state, action.turnId, "error" /* Error */, "error" /* Error */, action.error);
631
+ // ── Tool Call State Machine ───────────────────────────────────────────
632
+ case "session/toolCallStart" /* SessionToolCallStart */:
633
+ if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
634
+ return state;
635
+ }
636
+ return {
637
+ ...state,
638
+ activeTurn: {
639
+ ...state.activeTurn,
640
+ responseParts: [
641
+ ...state.activeTurn.responseParts,
642
+ {
643
+ kind: "toolCall" /* ToolCall */,
644
+ toolCall: {
645
+ toolCallId: action.toolCallId,
646
+ toolName: action.toolName,
647
+ displayName: action.displayName,
648
+ toolClientId: action.toolClientId,
649
+ _meta: action._meta,
650
+ status: "streaming" /* Streaming */
651
+ }
652
+ }
653
+ ]
654
+ }
655
+ };
656
+ case "session/toolCallDelta" /* SessionToolCallDelta */:
657
+ return updateToolCallInParts(state, action.turnId, action.toolCallId, (tc) => {
658
+ if (tc.status !== "streaming" /* Streaming */) {
659
+ return tc;
660
+ }
661
+ return {
662
+ ...tc,
663
+ partialInput: (tc.partialInput ?? "") + action.content,
664
+ invocationMessage: action.invocationMessage ?? tc.invocationMessage
665
+ };
666
+ });
667
+ case "session/toolCallReady" /* SessionToolCallReady */:
668
+ return updateToolCallInParts(state, action.turnId, action.toolCallId, (tc) => {
669
+ if (tc.status !== "streaming" /* Streaming */ && tc.status !== "running" /* Running */) {
670
+ return tc;
671
+ }
672
+ const base = tcBase(tc);
673
+ if (action.confirmed) {
674
+ return {
675
+ status: "running" /* Running */,
676
+ ...base,
677
+ invocationMessage: action.invocationMessage,
678
+ toolInput: action.toolInput,
679
+ confirmed: action.confirmed
680
+ };
681
+ }
682
+ return {
683
+ status: "pending-confirmation" /* PendingConfirmation */,
684
+ ...base,
685
+ invocationMessage: action.invocationMessage,
686
+ toolInput: action.toolInput,
687
+ confirmationTitle: action.confirmationTitle
688
+ };
689
+ });
690
+ case "session/toolCallConfirmed" /* SessionToolCallConfirmed */:
691
+ return updateToolCallInParts(state, action.turnId, action.toolCallId, (tc) => {
692
+ if (tc.status !== "pending-confirmation" /* PendingConfirmation */) {
693
+ return tc;
694
+ }
695
+ const base = tcBase(tc);
696
+ if (action.approved) {
697
+ return {
698
+ status: "running" /* Running */,
699
+ ...base,
700
+ invocationMessage: tc.invocationMessage,
701
+ toolInput: tc.toolInput,
702
+ confirmed: action.confirmed
703
+ };
704
+ }
705
+ return {
706
+ status: "cancelled" /* Cancelled */,
707
+ ...base,
708
+ invocationMessage: tc.invocationMessage,
709
+ toolInput: tc.toolInput,
710
+ reason: action.reason,
711
+ reasonMessage: action.reasonMessage,
712
+ userSuggestion: action.userSuggestion
713
+ };
714
+ });
715
+ case "session/toolCallComplete" /* SessionToolCallComplete */:
716
+ return updateToolCallInParts(state, action.turnId, action.toolCallId, (tc) => {
717
+ if (tc.status !== "running" /* Running */ && tc.status !== "pending-confirmation" /* PendingConfirmation */) {
718
+ return tc;
719
+ }
720
+ const base = tcBase(tc);
721
+ const confirmed = tc.status === "running" /* Running */ ? tc.confirmed : "not-needed" /* NotNeeded */;
722
+ if (action.requiresResultConfirmation) {
723
+ return {
724
+ status: "pending-result-confirmation" /* PendingResultConfirmation */,
725
+ ...base,
726
+ invocationMessage: tc.invocationMessage,
727
+ toolInput: tc.toolInput,
728
+ confirmed,
729
+ ...action.result
730
+ };
731
+ }
732
+ return {
733
+ status: "completed" /* Completed */,
734
+ ...base,
735
+ invocationMessage: tc.invocationMessage,
736
+ toolInput: tc.toolInput,
737
+ confirmed,
738
+ ...action.result
739
+ };
740
+ });
741
+ case "session/toolCallResultConfirmed" /* SessionToolCallResultConfirmed */:
742
+ return updateToolCallInParts(state, action.turnId, action.toolCallId, (tc) => {
743
+ if (tc.status !== "pending-result-confirmation" /* PendingResultConfirmation */) {
744
+ return tc;
745
+ }
746
+ const base = tcBase(tc);
747
+ if (action.approved) {
748
+ return {
749
+ status: "completed" /* Completed */,
750
+ ...base,
751
+ invocationMessage: tc.invocationMessage,
752
+ toolInput: tc.toolInput,
753
+ confirmed: tc.confirmed,
754
+ success: tc.success,
755
+ pastTenseMessage: tc.pastTenseMessage,
756
+ content: tc.content,
757
+ structuredContent: tc.structuredContent,
758
+ error: tc.error
759
+ };
760
+ }
761
+ return {
762
+ status: "cancelled" /* Cancelled */,
763
+ ...base,
764
+ invocationMessage: tc.invocationMessage,
765
+ toolInput: tc.toolInput,
766
+ reason: "result-denied" /* ResultDenied */
767
+ };
768
+ });
769
+ // ── Metadata ──────────────────────────────────────────────────────────
770
+ case "session/titleChanged" /* SessionTitleChanged */:
771
+ return {
772
+ ...state,
773
+ summary: { ...state.summary, title: action.title, modifiedAt: Date.now() }
774
+ };
775
+ case "session/usage" /* SessionUsage */:
776
+ if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
777
+ return state;
778
+ }
779
+ return {
780
+ ...state,
781
+ activeTurn: { ...state.activeTurn, usage: action.usage }
782
+ };
783
+ case "session/reasoning" /* SessionReasoning */:
784
+ return updateResponsePart(state, action.turnId, action.partId, (part) => {
785
+ if (part.kind === "reasoning" /* Reasoning */) {
786
+ return { ...part, content: part.content + action.content };
787
+ }
788
+ return part;
789
+ });
790
+ case "session/modelChanged" /* SessionModelChanged */:
791
+ return {
792
+ ...state,
793
+ summary: { ...state.summary, model: action.model, modifiedAt: Date.now() }
794
+ };
795
+ case "session/serverToolsChanged" /* SessionServerToolsChanged */:
796
+ return { ...state, serverTools: action.tools };
797
+ case "session/activeClientChanged" /* SessionActiveClientChanged */:
798
+ return {
799
+ ...state,
800
+ activeClient: action.activeClient ?? void 0
801
+ };
802
+ case "session/activeClientToolsChanged" /* SessionActiveClientToolsChanged */:
803
+ if (!state.activeClient) {
804
+ return state;
805
+ }
806
+ return {
807
+ ...state,
808
+ activeClient: { ...state.activeClient, tools: action.tools }
809
+ };
810
+ // ── Customizations ──────────────────────────────────────────────────
811
+ case "session/customizationsChanged" /* SessionCustomizationsChanged */:
812
+ return { ...state, customizations: action.customizations };
813
+ case "session/customizationToggled" /* SessionCustomizationToggled */: {
814
+ const list = state.customizations;
815
+ if (!list) {
816
+ return state;
817
+ }
818
+ const idx = list.findIndex((c) => c.customization.uri === action.uri);
819
+ if (idx < 0) {
820
+ return state;
821
+ }
822
+ const updated = [...list];
823
+ updated[idx] = { ...list[idx], enabled: action.enabled };
824
+ return { ...state, customizations: updated };
825
+ }
826
+ // ── Truncation ────────────────────────────────────────────────────────
827
+ case "session/truncated" /* SessionTruncated */: {
828
+ let turns;
829
+ if (action.turnId === void 0) {
830
+ turns = [];
831
+ } else {
832
+ const idx = state.turns.findIndex((t) => t.id === action.turnId);
833
+ if (idx < 0) {
834
+ return state;
835
+ }
836
+ turns = state.turns.slice(0, idx + 1);
837
+ }
838
+ return {
839
+ ...state,
840
+ turns,
841
+ activeTurn: void 0,
842
+ summary: { ...state.summary, status: "idle" /* Idle */, modifiedAt: Date.now() }
843
+ };
844
+ }
845
+ // ── Pending Messages ──────────────────────────────────────────────────
846
+ case "session/pendingMessageSet" /* SessionPendingMessageSet */: {
847
+ const entry = { id: action.id, userMessage: action.userMessage };
848
+ if (action.kind === "steering" /* Steering */) {
849
+ return { ...state, steeringMessage: entry };
850
+ }
851
+ const existing = state.queuedMessages ?? [];
852
+ const idx = existing.findIndex((m) => m.id === action.id);
853
+ if (idx >= 0) {
854
+ const updated = [...existing];
855
+ updated[idx] = entry;
856
+ return { ...state, queuedMessages: updated };
857
+ }
858
+ return { ...state, queuedMessages: [...existing, entry] };
859
+ }
860
+ case "session/pendingMessageRemoved" /* SessionPendingMessageRemoved */: {
861
+ if (action.kind === "steering" /* Steering */) {
862
+ if (!state.steeringMessage || state.steeringMessage.id !== action.id) {
863
+ return state;
864
+ }
865
+ return { ...state, steeringMessage: void 0 };
866
+ }
867
+ const existing = state.queuedMessages;
868
+ if (!existing) {
869
+ return state;
870
+ }
871
+ const filtered = existing.filter((m) => m.id !== action.id);
872
+ return filtered.length === existing.length ? state : { ...state, queuedMessages: filtered.length > 0 ? filtered : void 0 };
873
+ }
874
+ case "session/queuedMessagesReordered" /* SessionQueuedMessagesReordered */: {
875
+ const existing = state.queuedMessages;
876
+ if (!existing) {
877
+ return state;
878
+ }
879
+ const byId = new Map(existing.map((m) => [m.id, m]));
880
+ const ordered = /* @__PURE__ */ new Set();
881
+ const reordered = action.order.filter((id) => {
882
+ if (byId.has(id) && !ordered.has(id)) {
883
+ ordered.add(id);
884
+ return true;
885
+ }
886
+ return false;
887
+ }).map((id) => byId.get(id));
888
+ for (const m of existing) {
889
+ if (!ordered.has(m.id)) {
890
+ reordered.push(m);
891
+ }
892
+ }
893
+ return { ...state, queuedMessages: reordered };
894
+ }
895
+ default:
896
+ softAssertNever(action, log7);
897
+ return state;
898
+ }
899
+ }
900
+
901
+ // src/client/state.ts
902
+ var ROOT_ACTION_TYPES = /* @__PURE__ */ new Set(["root/agentsChanged" /* RootAgentsChanged */, "root/activeSessionsChanged" /* RootActiveSessionsChanged */]);
903
+ var StateMirror = class {
904
+ rootState = { agents: [] };
905
+ sessions = /* @__PURE__ */ new Map();
906
+ serverSeq = 0;
907
+ /** Current root state (agents, active session count). */
908
+ get root() {
909
+ return this.rootState;
910
+ }
911
+ /** Current server sequence number. */
912
+ get seq() {
913
+ return this.serverSeq;
914
+ }
915
+ /** Get a session state by URI. */
916
+ getSession(uri) {
917
+ return this.sessions.get(uri);
918
+ }
919
+ /** All tracked session URIs. */
920
+ get sessionUris() {
921
+ return [...this.sessions.keys()];
922
+ }
923
+ /**
924
+ * Load a snapshot (from initialize, reconnect, or subscribe).
925
+ */
926
+ applySnapshot(snapshot) {
927
+ if (snapshot.fromSeq > this.serverSeq) {
928
+ this.serverSeq = snapshot.fromSeq;
929
+ }
930
+ if ("agents" in snapshot.state) {
931
+ this.rootState = snapshot.state;
932
+ } else if ("summary" in snapshot.state) {
933
+ const sessionState = snapshot.state;
934
+ this.sessions.set(snapshot.resource, sessionState);
935
+ }
936
+ }
937
+ /**
938
+ * Apply an incoming action envelope from the server.
939
+ */
940
+ applyAction(envelope) {
941
+ this.serverSeq = envelope.serverSeq;
942
+ const action = envelope.action;
943
+ if (ROOT_ACTION_TYPES.has(action.type)) {
944
+ this.rootState = rootReducer(this.rootState, action);
945
+ } else {
946
+ const sessionAction = action;
947
+ const sessionUri = sessionAction.session;
948
+ if (sessionUri) {
949
+ const current = this.sessions.get(sessionUri);
950
+ if (current) {
951
+ this.sessions.set(sessionUri, sessionReducer(current, sessionAction));
952
+ }
953
+ }
954
+ }
955
+ }
956
+ /**
957
+ * Remove a session from tracking (e.g. after dispose or unsubscribe).
958
+ */
959
+ removeSession(uri) {
960
+ this.sessions.delete(uri);
961
+ }
962
+ };
963
+
964
+ // src/client/transport.ts
965
+ import { EventEmitter as EventEmitter3 } from "events";
966
+ import WebSocket from "ws";
967
+ var Transport = class extends EventEmitter3 {
968
+ ws;
969
+ _connected = false;
970
+ get connected() {
971
+ return this._connected;
972
+ }
973
+ /**
974
+ * Open a WebSocket connection to the given URL.
975
+ */
976
+ async connect(url, options = {}) {
977
+ const { connectTimeout = 1e4, headers } = options;
978
+ return new Promise((resolve, reject) => {
979
+ const ws = new WebSocket(url, { headers });
980
+ let settled = false;
981
+ const timer = setTimeout(() => {
982
+ if (!settled) {
983
+ settled = true;
984
+ ws.close();
985
+ reject(new Error(`Connection to ${url} timed out after ${connectTimeout}ms`));
986
+ }
987
+ }, connectTimeout);
988
+ ws.on("open", () => {
989
+ if (settled) return;
990
+ settled = true;
991
+ clearTimeout(timer);
992
+ this.ws = ws;
993
+ this._connected = true;
994
+ this.attachListeners(ws);
995
+ this.emit("open");
996
+ resolve();
997
+ });
998
+ ws.on("error", (err) => {
999
+ if (!settled) {
1000
+ settled = true;
1001
+ clearTimeout(timer);
1002
+ const code = err.code;
1003
+ const detail = err.message || code || "unknown error";
1004
+ reject(new Error(`Connection to ${url} failed: ${detail}`));
1005
+ }
1006
+ });
1007
+ });
1008
+ }
1009
+ /**
1010
+ * Send a JSON-serializable value over the WebSocket.
1011
+ */
1012
+ send(data) {
1013
+ if (!this.ws || !this._connected) {
1014
+ throw new Error("Transport is not connected");
1015
+ }
1016
+ this.ws.send(JSON.stringify(data));
1017
+ }
1018
+ /**
1019
+ * Gracefully close the connection.
1020
+ */
1021
+ close() {
1022
+ if (this.ws) {
1023
+ this._connected = false;
1024
+ this.ws.close();
1025
+ this.ws = void 0;
1026
+ }
1027
+ }
1028
+ attachListeners(ws) {
1029
+ ws.on("message", (raw) => {
1030
+ try {
1031
+ const data = JSON.parse(raw.toString());
1032
+ this.emit("message", data);
1033
+ } catch {
1034
+ this.emit("error", new Error(`Failed to parse message: ${raw.toString().slice(0, 200)}`));
1035
+ }
1036
+ });
1037
+ ws.on("close", (code, reason) => {
1038
+ this._connected = false;
1039
+ this.ws = void 0;
1040
+ this.emit("close", code, reason.toString());
1041
+ });
1042
+ ws.on("error", (err) => {
1043
+ this.emit("error", err);
1044
+ });
1045
+ }
1046
+ };
1047
+
1048
+ // src/client/active-client.ts
1049
+ var ActiveClientManager = class {
1050
+ constructor(client, clientId) {
1051
+ this.client = client;
1052
+ this.clientId = clientId;
1053
+ }
1054
+ /** Sessions where this client is the active client. */
1055
+ activeSessions = /* @__PURE__ */ new Set();
1056
+ /**
1057
+ * Claim active client status for a session.
1058
+ * Dispatches `session/activeClientChanged` with this client's info.
1059
+ */
1060
+ async claimActiveClient(sessionUri, displayName, tools = []) {
1061
+ const activeClient = {
1062
+ clientId: this.clientId,
1063
+ displayName,
1064
+ tools
1065
+ };
1066
+ this.client.dispatchAction({
1067
+ type: "session/activeClientChanged" /* SessionActiveClientChanged */,
1068
+ session: sessionUri,
1069
+ activeClient
1070
+ });
1071
+ this.activeSessions.add(sessionUri);
1072
+ }
1073
+ /**
1074
+ * Release active client status for a session.
1075
+ * Dispatches `session/activeClientChanged` with `null`.
1076
+ */
1077
+ async releaseActiveClient(sessionUri) {
1078
+ this.client.dispatchAction({
1079
+ type: "session/activeClientChanged" /* SessionActiveClientChanged */,
1080
+ session: sessionUri,
1081
+ activeClient: null
1082
+ });
1083
+ this.activeSessions.delete(sessionUri);
1084
+ }
1085
+ /**
1086
+ * Check if this client is the active client for a session.
1087
+ * Uses the local state mirror for the check.
1088
+ */
1089
+ isActiveClient(sessionUri) {
1090
+ const session = this.client.state.getSession(sessionUri);
1091
+ return session?.activeClient?.clientId === this.clientId;
1092
+ }
1093
+ /**
1094
+ * Register (or update) the tools this client provides to a session.
1095
+ * Dispatches `session/activeClientToolsChanged`.
1096
+ * Only valid when this client is the active client.
1097
+ */
1098
+ async registerTools(sessionUri, tools) {
1099
+ this.client.dispatchAction({
1100
+ type: "session/activeClientToolsChanged" /* SessionActiveClientToolsChanged */,
1101
+ session: sessionUri,
1102
+ tools
1103
+ });
1104
+ }
1105
+ /**
1106
+ * Handle a tool call directed at this client.
1107
+ * Dispatches `session/toolCallComplete` with the result.
1108
+ */
1109
+ completeToolCall(sessionUri, turnId, toolCallId, result) {
1110
+ this.client.dispatchAction({
1111
+ type: "session/toolCallComplete" /* SessionToolCallComplete */,
1112
+ session: sessionUri,
1113
+ turnId,
1114
+ toolCallId,
1115
+ result: {
1116
+ success: result.success,
1117
+ pastTenseMessage: result.pastTenseMessage,
1118
+ content: result.content?.map(
1119
+ (c) => ({
1120
+ type: "text" /* Text */,
1121
+ text: c.text
1122
+ })
1123
+ )
1124
+ }
1125
+ });
1126
+ }
1127
+ /**
1128
+ * Get the set of sessions where this client is active.
1129
+ */
1130
+ get sessions() {
1131
+ return this.activeSessions;
1132
+ }
1133
+ };
1134
+
1135
+ // src/client/reconnect.ts
1136
+ import pc from "picocolors";
1137
+ var ReconnectManager = class {
1138
+ constructor(serverUrl, clientId, options = {}) {
1139
+ this.serverUrl = serverUrl;
1140
+ this.clientId = clientId;
1141
+ this.maxRetries = options.maxRetries ?? 5;
1142
+ this.backoffMs = options.backoffMs ?? 1e3;
1143
+ this.maxBackoffMs = options.maxBackoffMs ?? 3e4;
1144
+ this.statusOut = options.statusOut ?? process.stderr;
1145
+ }
1146
+ maxRetries;
1147
+ backoffMs;
1148
+ maxBackoffMs;
1149
+ statusOut;
1150
+ aborted = false;
1151
+ /**
1152
+ * Attempt to reconnect to the AHP server.
1153
+ *
1154
+ * @param client - The AhpClient to reconnect
1155
+ * @param lastSeenServerSeq - Last server sequence number received
1156
+ * @param subscriptions - URIs the client was subscribed to
1157
+ * @returns The reconnect outcome (replay, snapshot, or failed)
1158
+ */
1159
+ async reconnect(client, _lastSeenServerSeq, subscriptions) {
1160
+ this.aborted = false;
1161
+ let attempt = 0;
1162
+ let backoff = this.backoffMs;
1163
+ while (attempt < this.maxRetries && !this.aborted) {
1164
+ attempt++;
1165
+ this.statusOut.write(pc.yellow(`Connection lost. Reconnecting (${attempt}/${this.maxRetries})...
1166
+ `));
1167
+ try {
1168
+ await this.sleep(backoff);
1169
+ if (this.aborted) break;
1170
+ await client.connect(this.serverUrl);
1171
+ for (const uri of subscriptions) {
1172
+ await client.subscribe(uri);
1173
+ }
1174
+ this.statusOut.write(pc.green("Reconnected successfully.\n"));
1175
+ return "snapshot";
1176
+ } catch {
1177
+ backoff = Math.min(backoff * 2, this.maxBackoffMs);
1178
+ const jitter = backoff * 0.25 * (Math.random() * 2 - 1);
1179
+ backoff = Math.max(this.backoffMs, backoff + jitter);
1180
+ }
1181
+ }
1182
+ this.statusOut.write(pc.red(`Failed to reconnect after ${this.maxRetries} attempts.
1183
+ `));
1184
+ return "failed";
1185
+ }
1186
+ /**
1187
+ * Abort any in-progress reconnection attempt.
1188
+ */
1189
+ abort() {
1190
+ this.aborted = true;
1191
+ }
1192
+ /**
1193
+ * Sleep for a given number of milliseconds. Can be aborted.
1194
+ */
1195
+ sleep(ms) {
1196
+ return new Promise((resolve) => {
1197
+ const timer = setTimeout(resolve, ms);
1198
+ const check = setInterval(() => {
1199
+ if (this.aborted) {
1200
+ clearTimeout(timer);
1201
+ clearInterval(check);
1202
+ resolve();
1203
+ }
1204
+ }, 100);
1205
+ setTimeout(() => clearInterval(check), ms + 10);
1206
+ });
1207
+ }
1208
+ };
1209
+
1210
+ // src/client/index.ts
1211
+ import { randomUUID as randomUUID2 } from "crypto";
1212
+ import { EventEmitter as EventEmitter4 } from "events";
1213
+
1214
+ // src/protocol/version/registry.ts
1215
+ var PROTOCOL_VERSION = 1;
1216
+ var ACTION_INTRODUCED_IN = {
1217
+ ["root/agentsChanged" /* RootAgentsChanged */]: 1,
1218
+ ["root/activeSessionsChanged" /* RootActiveSessionsChanged */]: 1,
1219
+ ["session/ready" /* SessionReady */]: 1,
1220
+ ["session/creationFailed" /* SessionCreationFailed */]: 1,
1221
+ ["session/turnStarted" /* SessionTurnStarted */]: 1,
1222
+ ["session/delta" /* SessionDelta */]: 1,
1223
+ ["session/responsePart" /* SessionResponsePart */]: 1,
1224
+ ["session/toolCallStart" /* SessionToolCallStart */]: 1,
1225
+ ["session/toolCallDelta" /* SessionToolCallDelta */]: 1,
1226
+ ["session/toolCallReady" /* SessionToolCallReady */]: 1,
1227
+ ["session/toolCallConfirmed" /* SessionToolCallConfirmed */]: 1,
1228
+ ["session/toolCallComplete" /* SessionToolCallComplete */]: 1,
1229
+ ["session/toolCallResultConfirmed" /* SessionToolCallResultConfirmed */]: 1,
1230
+ ["session/turnComplete" /* SessionTurnComplete */]: 1,
1231
+ ["session/turnCancelled" /* SessionTurnCancelled */]: 1,
1232
+ ["session/error" /* SessionError */]: 1,
1233
+ ["session/titleChanged" /* SessionTitleChanged */]: 1,
1234
+ ["session/usage" /* SessionUsage */]: 1,
1235
+ ["session/reasoning" /* SessionReasoning */]: 1,
1236
+ ["session/modelChanged" /* SessionModelChanged */]: 1,
1237
+ ["session/serverToolsChanged" /* SessionServerToolsChanged */]: 1,
1238
+ ["session/activeClientChanged" /* SessionActiveClientChanged */]: 1,
1239
+ ["session/activeClientToolsChanged" /* SessionActiveClientToolsChanged */]: 1,
1240
+ ["session/pendingMessageSet" /* SessionPendingMessageSet */]: 1,
1241
+ ["session/pendingMessageRemoved" /* SessionPendingMessageRemoved */]: 1,
1242
+ ["session/queuedMessagesReordered" /* SessionQueuedMessagesReordered */]: 1,
1243
+ ["session/customizationsChanged" /* SessionCustomizationsChanged */]: 1,
1244
+ ["session/customizationToggled" /* SessionCustomizationToggled */]: 1,
1245
+ ["session/truncated" /* SessionTruncated */]: 1
1246
+ };
1247
+ var NOTIFICATION_INTRODUCED_IN = {
1248
+ ["notify/sessionAdded" /* SessionAdded */]: 1,
1249
+ ["notify/sessionRemoved" /* SessionRemoved */]: 1,
1250
+ ["notify/authRequired" /* AuthRequired */]: 1
1251
+ };
1252
+
1253
+ // src/client/index.ts
1254
+ var AhpClient = class extends EventEmitter4 {
1255
+ constructor(options = {}) {
1256
+ super();
1257
+ this.options = options;
1258
+ this._clientId = options.clientId ?? randomUUID2();
1259
+ }
1260
+ transport;
1261
+ protocol;
1262
+ _state = new StateMirror();
1263
+ _sessions = /* @__PURE__ */ new Map();
1264
+ _clientId;
1265
+ _clientSeq = 0;
1266
+ _connected = false;
1267
+ /** Whether the client is currently connected. */
1268
+ get connected() {
1269
+ return this._connected;
1270
+ }
1271
+ /** The local state mirror. */
1272
+ get state() {
1273
+ return this._state;
1274
+ }
1275
+ /** The client identifier. */
1276
+ get clientId() {
1277
+ return this._clientId;
1278
+ }
1279
+ /** All active session handles. */
1280
+ get sessions() {
1281
+ return this._sessions;
1282
+ }
1283
+ /**
1284
+ * Connect to an AHP server and perform the initialization handshake.
1285
+ */
1286
+ async connect(url) {
1287
+ const transport = new Transport();
1288
+ const protocol = new ProtocolLayer(transport, this.options);
1289
+ protocol.on("action", (envelope) => {
1290
+ this._state.applyAction(envelope);
1291
+ this.emit("action", envelope);
1292
+ });
1293
+ protocol.on("notification", (notification) => {
1294
+ this.emit("notification", notification);
1295
+ });
1296
+ transport.on("close", (code, reason) => {
1297
+ this._connected = false;
1298
+ protocol.cancelAll("Connection closed");
1299
+ this.emit("disconnected", code, reason);
1300
+ });
1301
+ transport.on("error", (err) => {
1302
+ this.emit("error", err);
1303
+ });
1304
+ await transport.connect(url, this.options);
1305
+ this.transport = transport;
1306
+ this.protocol = protocol;
1307
+ const result = await protocol.request("initialize", {
1308
+ protocolVersion: PROTOCOL_VERSION,
1309
+ clientId: this._clientId,
1310
+ initialSubscriptions: this.options.initialSubscriptions ?? ["agenthost:/root"]
1311
+ });
1312
+ for (const snapshot of result.snapshots) {
1313
+ this._state.applySnapshot(snapshot);
1314
+ }
1315
+ this._connected = true;
1316
+ this.emit("connected", result);
1317
+ return result;
1318
+ }
1319
+ /**
1320
+ * Gracefully disconnect from the server.
1321
+ */
1322
+ async disconnect() {
1323
+ const handles = [...this._sessions.values()];
1324
+ for (const handle of handles) {
1325
+ try {
1326
+ await handle.dispose();
1327
+ } catch {
1328
+ }
1329
+ }
1330
+ this._sessions.clear();
1331
+ if (this.protocol) {
1332
+ this.protocol.cancelAll("Client disconnecting");
1333
+ }
1334
+ if (this.transport) {
1335
+ this.transport.close();
1336
+ this.transport = void 0;
1337
+ this.protocol = void 0;
1338
+ }
1339
+ this._connected = false;
1340
+ }
1341
+ // ── Session management ────────────────────────────────────────────────
1342
+ /**
1343
+ * Create a session and return a handle for interacting with it.
1344
+ *
1345
+ * Creates the session on the server, subscribes to its state, and
1346
+ * (by default) waits for it to reach the "ready" lifecycle state.
1347
+ */
1348
+ async openSession(options = {}) {
1349
+ this.ensureConnected();
1350
+ const {
1351
+ provider: requestedProvider,
1352
+ model,
1353
+ workingDirectory,
1354
+ waitForReady = true,
1355
+ readyTimeout = 3e4
1356
+ } = options;
1357
+ const provider = requestedProvider ?? (this._state.root.agents.length > 0 ? this._state.root.agents[0].provider : void 0);
1358
+ if (!provider) {
1359
+ throw new Error("No agent provider available. Specify one in options or ensure the server has agents.");
1360
+ }
1361
+ const sessionId = randomUUID2();
1362
+ const sessionUri = `${provider}:/${sessionId}`;
1363
+ await this.createSession(sessionUri, provider, model, workingDirectory);
1364
+ await this.subscribe(sessionUri);
1365
+ const handle = new SessionHandle(this, sessionUri, provider, model);
1366
+ this._sessions.set(sessionUri, handle);
1367
+ handle.on("disposed", () => {
1368
+ this._sessions.delete(sessionUri);
1369
+ });
1370
+ if (waitForReady) {
1371
+ try {
1372
+ await handle.waitForReady(readyTimeout);
1373
+ } catch (err) {
1374
+ await handle.dispose().catch(() => {
1375
+ });
1376
+ throw err;
1377
+ }
1378
+ }
1379
+ return handle;
1380
+ }
1381
+ // ── Commands ──────────────────────────────────────────────────────────
1382
+ /**
1383
+ * Create a new session with the specified agent provider.
1384
+ */
1385
+ async createSession(sessionUri, provider, model, workingDirectory) {
1386
+ this.ensureConnected();
1387
+ return this.protocol.request("createSession", {
1388
+ session: sessionUri,
1389
+ provider,
1390
+ model,
1391
+ workingDirectory
1392
+ });
1393
+ }
1394
+ /**
1395
+ * Dispose a session and clean up server-side resources.
1396
+ */
1397
+ async disposeSession(sessionUri) {
1398
+ this.ensureConnected();
1399
+ const result = await this.protocol.request("disposeSession", {
1400
+ session: sessionUri
1401
+ });
1402
+ this._state.removeSession(sessionUri);
1403
+ this._sessions.delete(sessionUri);
1404
+ return result;
1405
+ }
1406
+ /**
1407
+ * List all sessions on the server.
1408
+ */
1409
+ async listSessions() {
1410
+ this.ensureConnected();
1411
+ return this.protocol.request("listSessions", {});
1412
+ }
1413
+ /**
1414
+ * Subscribe to a state resource URI.
1415
+ */
1416
+ async subscribe(resourceUri) {
1417
+ this.ensureConnected();
1418
+ const result = await this.protocol.request("subscribe", {
1419
+ resource: resourceUri
1420
+ });
1421
+ this._state.applySnapshot(result.snapshot);
1422
+ return result;
1423
+ }
1424
+ /**
1425
+ * Unsubscribe from a state resource URI.
1426
+ */
1427
+ unsubscribe(resourceUri) {
1428
+ this.ensureConnected();
1429
+ this.protocol.notify("unsubscribe", { resource: resourceUri });
1430
+ }
1431
+ /**
1432
+ * Fetch historical turns for a session.
1433
+ */
1434
+ async fetchTurns(sessionUri, before, limit) {
1435
+ this.ensureConnected();
1436
+ return this.protocol.request("fetchTurns", {
1437
+ session: sessionUri,
1438
+ before,
1439
+ limit
1440
+ });
1441
+ }
1442
+ /**
1443
+ * Read content by URI (files, tool outputs, etc.).
1444
+ */
1445
+ async resourceRead(uri, encoding) {
1446
+ this.ensureConnected();
1447
+ return this.protocol.request("resourceRead", { uri, ...encoding ? { encoding } : {} });
1448
+ }
1449
+ /**
1450
+ * Write content to a file on the server's filesystem.
1451
+ */
1452
+ async resourceWrite(params) {
1453
+ this.ensureConnected();
1454
+ return this.protocol.request("resourceWrite", params);
1455
+ }
1456
+ /**
1457
+ * List directory entries on the server's filesystem.
1458
+ */
1459
+ async resourceList(uri) {
1460
+ this.ensureConnected();
1461
+ return this.protocol.request("resourceList", { uri });
1462
+ }
1463
+ /**
1464
+ * Copy a resource from one URI to another.
1465
+ */
1466
+ async resourceCopy(params) {
1467
+ this.ensureConnected();
1468
+ return this.protocol.request("resourceCopy", params);
1469
+ }
1470
+ /**
1471
+ * Delete a resource at a URI.
1472
+ */
1473
+ async resourceDelete(params) {
1474
+ this.ensureConnected();
1475
+ return this.protocol.request("resourceDelete", params);
1476
+ }
1477
+ /**
1478
+ * Move (rename) a resource from one URI to another.
1479
+ */
1480
+ async resourceMove(params) {
1481
+ this.ensureConnected();
1482
+ return this.protocol.request("resourceMove", params);
1483
+ }
1484
+ /**
1485
+ * Dispatch a client-originated action to the server.
1486
+ */
1487
+ dispatchAction(action) {
1488
+ this.ensureConnected();
1489
+ this._clientSeq++;
1490
+ this.protocol.notify("dispatchAction", {
1491
+ clientSeq: this._clientSeq,
1492
+ action
1493
+ });
1494
+ }
1495
+ /**
1496
+ * Push an authentication token for a protected resource.
1497
+ */
1498
+ async authenticate(resource, token) {
1499
+ this.ensureConnected();
1500
+ await this.protocol.request("authenticate", { resource, token });
1501
+ }
1502
+ ensureConnected() {
1503
+ if (!this._connected || !this.protocol) {
1504
+ throw new Error("Client is not connected");
1505
+ }
1506
+ }
1507
+ };
1508
+
1509
+ // src/logger.ts
1510
+ import pc2 from "picocolors";
1511
+ var verbose = false;
1512
+ function setVerbose(enabled) {
1513
+ verbose = enabled;
1514
+ }
1515
+ function timestamp() {
1516
+ const now = /* @__PURE__ */ new Date();
1517
+ return [
1518
+ String(now.getHours()).padStart(2, "0"),
1519
+ String(now.getMinutes()).padStart(2, "0"),
1520
+ String(now.getSeconds()).padStart(2, "0")
1521
+ ].join(":");
1522
+ }
1523
+ function formatData(data) {
1524
+ if (!data) return "";
1525
+ const parts = [];
1526
+ for (const [key, value] of Object.entries(data)) {
1527
+ if (value !== void 0) {
1528
+ parts.push(`${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
1529
+ }
1530
+ }
1531
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
1532
+ }
1533
+ function createLogger(component) {
1534
+ const prefix = () => `[${timestamp()} ${component}]`;
1535
+ return {
1536
+ debug(message, data) {
1537
+ if (!verbose) return;
1538
+ process.stderr.write(pc2.dim(`${prefix()} ${message}${formatData(data)}
1539
+ `));
1540
+ },
1541
+ info(message, data) {
1542
+ process.stderr.write(`${prefix()} ${message}${formatData(data)}
1543
+ `);
1544
+ },
1545
+ warn(message, data) {
1546
+ process.stderr.write(pc2.yellow(`${prefix()} ${message}${formatData(data)}
1547
+ `));
1548
+ },
1549
+ error(message, data) {
1550
+ process.stderr.write(pc2.red(`${prefix()} ${message}${formatData(data)}
1551
+ `));
1552
+ }
1553
+ };
1554
+ }
1555
+
1556
+ // src/events/forwarding-formatter.ts
1557
+ var log = createLogger("forwarding-formatter");
1558
+ var ForwardingFormatter = class {
1559
+ inner;
1560
+ forwarders;
1561
+ tags;
1562
+ /** Session URI to attach to forwarded events. Can be set after construction. */
1563
+ sessionUri;
1564
+ constructor(options) {
1565
+ this.inner = options.inner;
1566
+ this.forwarders = options.forwarders;
1567
+ this.sessionUri = options.sessionUri;
1568
+ this.tags = options.tags;
1569
+ }
1570
+ onDelta(text) {
1571
+ this.inner.onDelta(text);
1572
+ this.emit("delta", { content: text });
1573
+ }
1574
+ onReasoning(text) {
1575
+ this.inner.onReasoning(text);
1576
+ this.emit("reasoning", { content: text });
1577
+ }
1578
+ onToolCallStart(id, name) {
1579
+ this.inner.onToolCallStart(id, name);
1580
+ this.emit("tool_call_start", { toolCallId: id, name });
1581
+ }
1582
+ onToolCallDelta(id, paramsDelta) {
1583
+ this.inner.onToolCallDelta(id, paramsDelta);
1584
+ this.emit("tool_call_delta", { toolCallId: id, content: paramsDelta });
1585
+ }
1586
+ onToolCallReady(id, call) {
1587
+ this.inner.onToolCallReady(id, call);
1588
+ this.emit("tool_call_ready", {
1589
+ toolCallId: id,
1590
+ toolName: call.toolName,
1591
+ displayName: call.displayName,
1592
+ invocationMessage: call.invocationMessage,
1593
+ ...call.toolInput !== void 0 ? { toolInput: call.toolInput } : {}
1594
+ });
1595
+ }
1596
+ onToolCallComplete(id, result) {
1597
+ this.inner.onToolCallComplete(id, result);
1598
+ this.emit("tool_call_complete", { toolCallId: id, result });
1599
+ }
1600
+ onToolCallCancelled(id, reason) {
1601
+ this.inner.onToolCallCancelled(id, reason);
1602
+ this.emit("tool_call_cancelled", { toolCallId: id, reason });
1603
+ }
1604
+ onUsage(usage) {
1605
+ this.inner.onUsage(usage);
1606
+ this.emit("usage", { usage });
1607
+ }
1608
+ onTurnComplete(responseText) {
1609
+ this.inner.onTurnComplete(responseText);
1610
+ this.emit("turn_complete", { responseText });
1611
+ }
1612
+ onTurnError(error) {
1613
+ this.inner.onTurnError(error);
1614
+ this.emit("turn_error", { error });
1615
+ }
1616
+ onTurnCancelled() {
1617
+ this.inner.onTurnCancelled();
1618
+ this.emit("turn_cancelled", {});
1619
+ }
1620
+ onTitleChanged(title) {
1621
+ this.inner.onTitleChanged(title);
1622
+ this.emit("title_changed", { title });
1623
+ }
1624
+ /**
1625
+ * Close all forwarders, flushing any buffered events.
1626
+ *
1627
+ * Call this when the session/turn is done to ensure all events
1628
+ * have been delivered.
1629
+ */
1630
+ async close() {
1631
+ await Promise.allSettled(this.forwarders.map((f) => f.close()));
1632
+ }
1633
+ // ── Internal ──────────────────────────────────────────────────────────
1634
+ emit(type, data) {
1635
+ const event = {
1636
+ type,
1637
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1638
+ ...this.tags && Object.keys(this.tags).length > 0 ? { tags: this.tags } : {},
1639
+ data,
1640
+ ...this.sessionUri ? { sessionUri: this.sessionUri } : {}
1641
+ };
1642
+ for (const forwarder of this.forwarders) {
1643
+ forwarder.forward(event).catch((err) => {
1644
+ log.info("forward-error", { type, error: err instanceof Error ? err.message : String(err) });
1645
+ });
1646
+ }
1647
+ }
1648
+ };
1649
+
1650
+ // src/events/webhook-forwarder.ts
1651
+ var log2 = createLogger("webhook-forwarder");
1652
+ var WebhookForwarder = class {
1653
+ url;
1654
+ headers;
1655
+ batchSize;
1656
+ batchIntervalMs;
1657
+ retries;
1658
+ filter;
1659
+ batch = [];
1660
+ flushTimer;
1661
+ closed = false;
1662
+ flushPromise = Promise.resolve();
1663
+ constructor(options) {
1664
+ this.url = options.url;
1665
+ this.headers = options.headers ?? {};
1666
+ this.batchSize = options.batchSize ?? 10;
1667
+ this.batchIntervalMs = options.batchIntervalMs ?? 1e3;
1668
+ this.retries = options.retries ?? 3;
1669
+ this.filter = options.filter && options.filter.length > 0 ? new Set(options.filter) : void 0;
1670
+ }
1671
+ async forward(event) {
1672
+ if (this.closed) return;
1673
+ if (this.filter && !this.filter.has(event.type)) return;
1674
+ this.batch.push(event);
1675
+ if (this.batch.length >= this.batchSize) {
1676
+ this.clearFlushTimer();
1677
+ this.flushPromise = this.flushPromise.then(() => this.flush());
1678
+ } else if (this.flushTimer === void 0) {
1679
+ this.startFlushTimer();
1680
+ }
1681
+ }
1682
+ async close() {
1683
+ if (this.closed) return;
1684
+ this.closed = true;
1685
+ this.clearFlushTimer();
1686
+ await this.flushPromise;
1687
+ await this.flush();
1688
+ }
1689
+ // ── Internal ──────────────────────────────────────────────────────────
1690
+ startFlushTimer() {
1691
+ this.flushTimer = setTimeout(() => {
1692
+ this.flushTimer = void 0;
1693
+ this.flushPromise = this.flushPromise.then(() => this.flush());
1694
+ }, this.batchIntervalMs);
1695
+ }
1696
+ clearFlushTimer() {
1697
+ if (this.flushTimer !== void 0) {
1698
+ clearTimeout(this.flushTimer);
1699
+ this.flushTimer = void 0;
1700
+ }
1701
+ }
1702
+ async flush() {
1703
+ if (this.batch.length === 0) return;
1704
+ const events = this.batch;
1705
+ this.batch = [];
1706
+ await this.sendWithRetry(events);
1707
+ }
1708
+ async sendWithRetry(events) {
1709
+ let lastError;
1710
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
1711
+ try {
1712
+ await this.send(events);
1713
+ return;
1714
+ } catch (err) {
1715
+ lastError = err instanceof Error ? err : new Error(String(err));
1716
+ if (attempt < this.retries) {
1717
+ const delay = Math.min(1e3 * 2 ** attempt, 1e4);
1718
+ log2.info("retry", { attempt: attempt + 1, delay, error: lastError.message });
1719
+ await sleep(delay);
1720
+ }
1721
+ }
1722
+ }
1723
+ log2.info("send-failed", {
1724
+ url: this.url,
1725
+ events: events.length,
1726
+ error: lastError?.message ?? "Unknown error"
1727
+ });
1728
+ }
1729
+ async send(events) {
1730
+ const body = events.map((e) => JSON.stringify(e)).join("\n");
1731
+ const response = await fetch(this.url, {
1732
+ method: "POST",
1733
+ headers: {
1734
+ "Content-Type": "application/x-ndjson",
1735
+ ...this.headers
1736
+ },
1737
+ body
1738
+ });
1739
+ if (!response.ok) {
1740
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1741
+ }
1742
+ }
1743
+ };
1744
+ function sleep(ms) {
1745
+ return new Promise((resolve) => setTimeout(resolve, ms));
1746
+ }
1747
+
1748
+ // src/events/ws-forwarder.ts
1749
+ import WebSocket2 from "ws";
1750
+ var log3 = createLogger("ws-forwarder");
1751
+ var BACKPRESSURE_THRESHOLD = 1024 * 1024;
1752
+ var MAX_BUFFER_SIZE = 1e4;
1753
+ var WebSocketForwarder = class {
1754
+ url;
1755
+ headers;
1756
+ reconnect;
1757
+ filter;
1758
+ ws;
1759
+ buffer = [];
1760
+ closed = false;
1761
+ reconnecting = false;
1762
+ reconnectTimer;
1763
+ reconnectAttempt = 0;
1764
+ connectPromise;
1765
+ constructor(options) {
1766
+ this.url = options.url;
1767
+ this.headers = options.headers ?? {};
1768
+ this.reconnect = options.reconnect ?? true;
1769
+ this.filter = options.filter && options.filter.length > 0 ? new Set(options.filter) : void 0;
1770
+ this.connectPromise = this.connect();
1771
+ }
1772
+ async forward(event) {
1773
+ if (this.closed) return;
1774
+ if (this.filter && !this.filter.has(event.type)) return;
1775
+ if (this.connectPromise) {
1776
+ try {
1777
+ await this.connectPromise;
1778
+ } catch {
1779
+ }
1780
+ }
1781
+ if (this.isConnected()) {
1782
+ this.sendOrBuffer(event);
1783
+ } else {
1784
+ this.addToBuffer(event);
1785
+ }
1786
+ }
1787
+ async close() {
1788
+ if (this.closed) return;
1789
+ this.closed = true;
1790
+ if (this.reconnectTimer !== void 0) {
1791
+ clearTimeout(this.reconnectTimer);
1792
+ this.reconnectTimer = void 0;
1793
+ }
1794
+ if (this.connectPromise) {
1795
+ try {
1796
+ await this.connectPromise;
1797
+ } catch {
1798
+ }
1799
+ }
1800
+ if (this.isConnected()) {
1801
+ this.drainBuffer();
1802
+ }
1803
+ if (this.ws) {
1804
+ this.ws.removeAllListeners();
1805
+ if (this.ws.readyState === WebSocket2.OPEN || this.ws.readyState === WebSocket2.CONNECTING) {
1806
+ this.ws.close(1e3, "Forwarder closed");
1807
+ }
1808
+ this.ws = void 0;
1809
+ }
1810
+ this.buffer = [];
1811
+ }
1812
+ // ── Internal ──────────────────────────────────────────────────────────
1813
+ connect() {
1814
+ return new Promise((resolve, reject) => {
1815
+ try {
1816
+ this.ws = new WebSocket2(this.url, { headers: this.headers });
1817
+ } catch (err) {
1818
+ this.connectPromise = void 0;
1819
+ reject(err);
1820
+ return;
1821
+ }
1822
+ this.ws.on("open", () => {
1823
+ log3.info("connected", { url: this.url });
1824
+ this.reconnectAttempt = 0;
1825
+ this.reconnecting = false;
1826
+ this.connectPromise = void 0;
1827
+ this.drainBuffer();
1828
+ resolve();
1829
+ });
1830
+ this.ws.on("close", (code, reason) => {
1831
+ log3.info("disconnected", { url: this.url, code, reason: reason.toString() });
1832
+ if (!this.closed && this.reconnect) {
1833
+ this.scheduleReconnect();
1834
+ }
1835
+ });
1836
+ this.ws.on("error", (err) => {
1837
+ log3.info("error", { url: this.url, error: err.message });
1838
+ if (this.connectPromise) {
1839
+ this.connectPromise = void 0;
1840
+ reject(err);
1841
+ }
1842
+ });
1843
+ });
1844
+ }
1845
+ scheduleReconnect() {
1846
+ if (this.closed || this.reconnecting) return;
1847
+ this.reconnecting = true;
1848
+ const delay = Math.min(1e3 * 2 ** this.reconnectAttempt, 3e4);
1849
+ this.reconnectAttempt++;
1850
+ log3.info("reconnecting", { attempt: this.reconnectAttempt, delay });
1851
+ this.reconnectTimer = setTimeout(() => {
1852
+ this.reconnectTimer = void 0;
1853
+ if (!this.closed) {
1854
+ this.connectPromise = this.connect().catch(() => {
1855
+ });
1856
+ }
1857
+ }, delay);
1858
+ }
1859
+ isConnected() {
1860
+ return this.ws?.readyState === WebSocket2.OPEN;
1861
+ }
1862
+ sendOrBuffer(event) {
1863
+ if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) {
1864
+ this.addToBuffer(event);
1865
+ return;
1866
+ }
1867
+ if (this.ws.bufferedAmount > BACKPRESSURE_THRESHOLD) {
1868
+ log3.info("backpressure", { bufferedAmount: this.ws.bufferedAmount });
1869
+ this.addToBuffer(event);
1870
+ return;
1871
+ }
1872
+ try {
1873
+ this.ws.send(JSON.stringify(event));
1874
+ } catch {
1875
+ this.addToBuffer(event);
1876
+ }
1877
+ }
1878
+ addToBuffer(event) {
1879
+ if (this.buffer.length >= MAX_BUFFER_SIZE) {
1880
+ this.buffer.shift();
1881
+ }
1882
+ this.buffer.push(event);
1883
+ }
1884
+ drainBuffer() {
1885
+ while (this.buffer.length > 0 && this.isConnected()) {
1886
+ const event = this.buffer.shift();
1887
+ try {
1888
+ this.ws.send(JSON.stringify(event));
1889
+ } catch {
1890
+ this.buffer.unshift(event);
1891
+ break;
1892
+ }
1893
+ }
1894
+ }
1895
+ };
1896
+
1897
+ // src/fleet/health.ts
1898
+ var HealthChecker = class {
1899
+ _defaultTimeout;
1900
+ constructor(options) {
1901
+ this._defaultTimeout = options?.timeout ?? 1e4;
1902
+ }
1903
+ /** Check a single server's health. */
1904
+ async check(url, name, options) {
1905
+ const start = performance.now();
1906
+ const client = new AhpClient({
1907
+ connectTimeout: options?.timeout ?? this._defaultTimeout
1908
+ });
1909
+ try {
1910
+ const result = await client.connect(url);
1911
+ if (options?.token) {
1912
+ await client.authenticate(url, options.token);
1913
+ }
1914
+ const latencyMs = performance.now() - start;
1915
+ const root = client.state.root;
1916
+ return {
1917
+ name,
1918
+ url,
1919
+ status: "healthy",
1920
+ latencyMs,
1921
+ protocolVersion: result.protocolVersion,
1922
+ agents: root.agents.map((a) => ({
1923
+ provider: a.provider,
1924
+ models: a.models.map((m) => m.id)
1925
+ })),
1926
+ activeSessions: root.activeSessions ?? 0,
1927
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
1928
+ };
1929
+ } catch (err) {
1930
+ const latencyMs = performance.now() - start;
1931
+ return {
1932
+ name,
1933
+ url,
1934
+ status: "unreachable",
1935
+ latencyMs,
1936
+ agents: [],
1937
+ activeSessions: 0,
1938
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1939
+ error: err instanceof Error ? err.message : String(err)
1940
+ };
1941
+ } finally {
1942
+ try {
1943
+ await client.disconnect();
1944
+ } catch {
1945
+ }
1946
+ }
1947
+ }
1948
+ /** Check all servers concurrently. */
1949
+ async checkAll(connections) {
1950
+ return Promise.all(connections.map((c) => this.check(c.url, c.name, c.token ? { token: c.token } : void 0)));
1951
+ }
1952
+ };
1953
+
1954
+ // src/session/store.ts
1955
+ import { randomUUID as randomUUID3 } from "crypto";
1956
+ import * as fs from "fs/promises";
1957
+ import * as os from "os";
1958
+ import * as path from "path";
1959
+ var log4 = createLogger("session-store");
1960
+ var MAX_LOCAL_TURNS = 100;
1961
+ var PREVIEW_MAX_LEN = 200;
1962
+ function truncatePreview(str, maxLen = PREVIEW_MAX_LEN) {
1963
+ if (str.length <= maxLen) return str;
1964
+ return `${str.slice(0, maxLen - 1)}\u2026`;
1965
+ }
1966
+ function buildTurnSummary(result) {
1967
+ return {
1968
+ turnId: result.turnId,
1969
+ userMessage: truncatePreview(result.userMessage),
1970
+ responsePreview: truncatePreview(result.responseText || "(no response)"),
1971
+ toolCallCount: result.toolCalls,
1972
+ tokenUsage: result.usage ? { input: result.usage.inputTokens, output: result.usage.outputTokens, model: result.usage.model } : void 0,
1973
+ state: result.state,
1974
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1975
+ };
1976
+ }
1977
+ var SessionStore = class {
1978
+ sessionsDir;
1979
+ constructor(configDir) {
1980
+ const dir = configDir ?? path.join(os.homedir(), ".ahpx");
1981
+ this.sessionsDir = path.join(dir, "sessions");
1982
+ }
1983
+ /** Ensure the sessions directory exists. */
1984
+ async ensureDir() {
1985
+ await fs.mkdir(this.sessionsDir, { recursive: true, mode: 448 });
1986
+ }
1987
+ /** Path to a session record file. Validates ID to prevent path traversal. */
1988
+ filePath(id) {
1989
+ this.validateId(id);
1990
+ return path.join(this.sessionsDir, `${id}.json`);
1991
+ }
1992
+ /** Validate that a session ID is safe for use in file paths (no traversal). */
1993
+ validateId(id) {
1994
+ if (!id || id.includes("/") || id.includes("\\") || id.includes("..") || id.includes("\0")) {
1995
+ throw new Error(`Invalid session ID: "${id}". IDs must not contain path separators or traversal sequences.`);
1996
+ }
1997
+ }
1998
+ /**
1999
+ * Save a session record to disk. Creates a new file or overwrites an existing one.
2000
+ * Uses atomic writes (temp file + rename).
2001
+ */
2002
+ async save(record) {
2003
+ this.validateId(record.id);
2004
+ await this.ensureDir();
2005
+ const tmp = path.join(this.sessionsDir, `.${record.id}.${randomUUID3()}.tmp`);
2006
+ await fs.writeFile(tmp, `${JSON.stringify(record, null, " ")}
2007
+ `, { mode: 384, encoding: "utf-8" });
2008
+ await fs.rename(tmp, this.filePath(record.id));
2009
+ }
2010
+ /** Get a session record by ID, or undefined if not found. */
2011
+ async get(id) {
2012
+ try {
2013
+ const raw = await fs.readFile(this.filePath(id), "utf-8");
2014
+ return JSON.parse(raw);
2015
+ } catch (err) {
2016
+ if (err.code === "ENOENT") {
2017
+ return void 0;
2018
+ }
2019
+ throw err;
2020
+ }
2021
+ }
2022
+ /**
2023
+ * List session records, optionally filtered.
2024
+ * Returns records sorted by createdAt descending (newest first).
2025
+ */
2026
+ async list(filter) {
2027
+ await this.ensureDir();
2028
+ let entries;
2029
+ try {
2030
+ entries = await fs.readdir(this.sessionsDir);
2031
+ } catch (err) {
2032
+ if (err.code === "ENOENT") {
2033
+ return [];
2034
+ }
2035
+ throw err;
2036
+ }
2037
+ const records = [];
2038
+ for (const entry of entries) {
2039
+ if (!entry.endsWith(".json")) continue;
2040
+ try {
2041
+ const raw = await fs.readFile(path.join(this.sessionsDir, entry), "utf-8");
2042
+ const record = JSON.parse(raw);
2043
+ if (filter?.status && record.status !== filter.status) continue;
2044
+ if (filter?.serverName && record.serverName !== filter.serverName) continue;
2045
+ if (filter?.workingDirectory && record.workingDirectory !== filter.workingDirectory) continue;
2046
+ if (filter?.name && record.name !== filter.name) continue;
2047
+ records.push(record);
2048
+ } catch (err) {
2049
+ log4.warn("skipping corrupt session file", { file: entry, error: String(err) });
2050
+ }
2051
+ }
2052
+ records.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
2053
+ return records;
2054
+ }
2055
+ /**
2056
+ * Update specific fields on an existing session record.
2057
+ * Returns the updated record, or undefined if not found.
2058
+ */
2059
+ async update(id, updates) {
2060
+ const existing = await this.get(id);
2061
+ if (!existing) return void 0;
2062
+ const updated = { ...existing, ...updates };
2063
+ await this.save(updated);
2064
+ return updated;
2065
+ }
2066
+ /**
2067
+ * Soft-close a session: set status='closed' and record the timestamp.
2068
+ * The record is preserved for history.
2069
+ * Returns the updated record, or undefined if not found.
2070
+ */
2071
+ async close(id) {
2072
+ return this.update(id, {
2073
+ status: "closed",
2074
+ closedAt: (/* @__PURE__ */ new Date()).toISOString()
2075
+ });
2076
+ }
2077
+ /**
2078
+ * Append a turn summary to a session record's local history.
2079
+ * Caps the history at MAX_LOCAL_TURNS entries (oldest removed first).
2080
+ * Returns the updated record, or undefined if not found.
2081
+ */
2082
+ async appendTurn(id, turn) {
2083
+ const record = await this.get(id);
2084
+ if (!record) return void 0;
2085
+ const turns = record.turns ? [...record.turns] : [];
2086
+ turns.push(turn);
2087
+ while (turns.length > MAX_LOCAL_TURNS) {
2088
+ turns.shift();
2089
+ }
2090
+ return this.update(id, { turns });
2091
+ }
2092
+ /**
2093
+ * Find a session by scope: matching serverName, workingDirectory, and optional name.
2094
+ * Only returns active sessions.
2095
+ */
2096
+ async getByScope(options) {
2097
+ const records = await this.list({
2098
+ status: "active",
2099
+ serverName: options.serverName,
2100
+ workingDirectory: options.workingDirectory,
2101
+ ...options.name ? { name: options.name } : {}
2102
+ });
2103
+ if (options.name) {
2104
+ return records.find((r) => r.name === options.name);
2105
+ }
2106
+ return records[0];
2107
+ }
2108
+ };
2109
+
2110
+ // src/auth/handler.ts
2111
+ import { randomUUID as randomUUID4 } from "crypto";
2112
+ import * as fs2 from "fs/promises";
2113
+ import * as os2 from "os";
2114
+ import * as path2 from "path";
2115
+ var log5 = createLogger("auth");
2116
+ function authFilePath(configDir) {
2117
+ const dir = configDir ?? path2.join(os2.homedir(), ".ahpx");
2118
+ return path2.join(dir, "auth.json");
2119
+ }
2120
+ var AuthHandler = class {
2121
+ constructor(client, options = {}) {
2122
+ this.client = client;
2123
+ this.configDir = options.configDir ?? path2.join(os2.homedir(), ".ahpx");
2124
+ this.explicitToken = options.token;
2125
+ this.interactive = options.interactive ?? !!process.stdin.isTTY;
2126
+ }
2127
+ configDir;
2128
+ explicitToken;
2129
+ interactive;
2130
+ /**
2131
+ * Handle an authRequired notification — find a token and authenticate.
2132
+ * Returns true if authentication succeeded, false otherwise.
2133
+ */
2134
+ async handleAuthRequired(resource) {
2135
+ const resourceUri = resource.resource;
2136
+ if (this.explicitToken) {
2137
+ return this.tryAuthenticate(resourceUri, this.explicitToken);
2138
+ }
2139
+ const envToken = process.env.AHPX_TOKEN;
2140
+ if (envToken) {
2141
+ return this.tryAuthenticate(resourceUri, envToken);
2142
+ }
2143
+ const stored = await this.loadToken(resourceUri);
2144
+ if (stored) {
2145
+ const success = await this.tryAuthenticate(resourceUri, stored);
2146
+ if (success) return true;
2147
+ }
2148
+ if (this.interactive) {
2149
+ const token = await this.promptForToken(resource);
2150
+ if (token) {
2151
+ const success = await this.tryAuthenticate(resourceUri, token);
2152
+ if (success) {
2153
+ await this.storeToken(resourceUri, token);
2154
+ }
2155
+ return success;
2156
+ }
2157
+ }
2158
+ return false;
2159
+ }
2160
+ /**
2161
+ * Store a token for a resource in ~/.ahpx/auth.json.
2162
+ * File is created with 0600 permissions.
2163
+ */
2164
+ async storeToken(resourceUri, token) {
2165
+ const filePath = authFilePath(this.configDir);
2166
+ await fs2.mkdir(path2.dirname(filePath), { recursive: true, mode: 448 });
2167
+ let store = {};
2168
+ try {
2169
+ const raw = await fs2.readFile(filePath, "utf-8");
2170
+ store = JSON.parse(raw);
2171
+ } catch (err) {
2172
+ if (err.code !== "ENOENT") {
2173
+ log5.warn("corrupt auth file, starting fresh", { error: String(err) });
2174
+ }
2175
+ }
2176
+ store[resourceUri] = {
2177
+ token,
2178
+ storedAt: (/* @__PURE__ */ new Date()).toISOString()
2179
+ };
2180
+ const tmp = `${filePath}.${randomUUID4()}.tmp`;
2181
+ await fs2.writeFile(tmp, `${JSON.stringify(store, null, " ")}
2182
+ `, { mode: 384 });
2183
+ await fs2.rename(tmp, filePath);
2184
+ try {
2185
+ await fs2.chmod(filePath, 384);
2186
+ } catch {
2187
+ }
2188
+ }
2189
+ /**
2190
+ * Load a stored token for a resource.
2191
+ */
2192
+ async loadToken(resourceUri) {
2193
+ try {
2194
+ const raw = await fs2.readFile(authFilePath(this.configDir), "utf-8");
2195
+ const store = JSON.parse(raw);
2196
+ return store[resourceUri]?.token;
2197
+ } catch (err) {
2198
+ if (err.code !== "ENOENT") {
2199
+ log5.warn("skipping corrupt auth file", { error: String(err) });
2200
+ }
2201
+ return void 0;
2202
+ }
2203
+ }
2204
+ /**
2205
+ * Try to authenticate with the server.
2206
+ */
2207
+ async tryAuthenticate(resource, token) {
2208
+ try {
2209
+ await this.client.authenticate(resource, token);
2210
+ return true;
2211
+ } catch {
2212
+ return false;
2213
+ }
2214
+ }
2215
+ /**
2216
+ * Prompt the user for a token interactively.
2217
+ */
2218
+ promptForToken(resource) {
2219
+ const name = resource.resource_name ?? resource.resource;
2220
+ return new Promise((resolve) => {
2221
+ const readline = __require("readline");
2222
+ const rl = readline.createInterface({
2223
+ input: process.stdin,
2224
+ output: process.stderr
2225
+ });
2226
+ rl.question(`Token for ${name}: `, (answer) => {
2227
+ rl.close();
2228
+ const token = answer.trim();
2229
+ resolve(token || void 0);
2230
+ });
2231
+ rl.on("close", () => resolve(void 0));
2232
+ });
2233
+ }
2234
+ };
2235
+
2236
+ // src/session/persistence.ts
2237
+ var log6 = createLogger("persistence");
2238
+ var SESSION_NOT_FOUND = -32001;
2239
+ var SessionPersistence = class {
2240
+ constructor(store) {
2241
+ this.store = store;
2242
+ }
2243
+ /**
2244
+ * Resume a locally-stored session on a connected server.
2245
+ *
2246
+ * Subscribes to the session URI and verifies it is still alive. Returns
2247
+ * a ResumeOutcome indicating whether the session was found.
2248
+ */
2249
+ async resume(record, client) {
2250
+ try {
2251
+ await client.subscribe(record.sessionUri);
2252
+ log6.info("session resumed", { sessionUri: record.sessionUri });
2253
+ return { status: "resumed" };
2254
+ } catch (err) {
2255
+ if (err instanceof RpcError && err.code === SESSION_NOT_FOUND) {
2256
+ log6.info("session not found on server", { sessionUri: record.sessionUri });
2257
+ return { status: "not_found" };
2258
+ }
2259
+ const message = err instanceof Error ? err.message : String(err);
2260
+ log6.info("resume failed", { sessionUri: record.sessionUri, error: message });
2261
+ return { status: "error", message };
2262
+ }
2263
+ }
2264
+ /**
2265
+ * Save a turn summary to the local session record after a prompt completes.
2266
+ */
2267
+ async saveTurn(recordId, result) {
2268
+ const summary = buildTurnSummary(result);
2269
+ return this.store.appendTurn(recordId, summary);
2270
+ }
2271
+ /**
2272
+ * Sync local session records for a server with the server's actual state.
2273
+ *
2274
+ * Compares locally-active records against the server's session list to
2275
+ * find sessions that were added remotely, disposed remotely, or updated.
2276
+ */
2277
+ async sync(client, serverName) {
2278
+ const result = { added: [], removed: [], updated: [] };
2279
+ const serverSessions = await client.listSessions();
2280
+ const serverUris = new Set(serverSessions.items.map((s) => s.resource));
2281
+ const serverMap = new Map(serverSessions.items.map((s) => [s.resource, s]));
2282
+ const localRecords = await this.store.list({ status: "active", serverName });
2283
+ const localUris = new Set(localRecords.map((r) => r.sessionUri));
2284
+ for (const uri of serverUris) {
2285
+ if (!localUris.has(uri)) {
2286
+ result.added.push(uri);
2287
+ }
2288
+ }
2289
+ for (const record of localRecords) {
2290
+ if (!serverUris.has(record.sessionUri)) {
2291
+ result.removed.push(record.id);
2292
+ await this.store.close(record.id);
2293
+ log6.info("closed stale local record", { id: record.id, sessionUri: record.sessionUri });
2294
+ }
2295
+ }
2296
+ for (const record of localRecords) {
2297
+ const serverSession = serverMap.get(record.sessionUri);
2298
+ if (!serverSession) continue;
2299
+ let changed = false;
2300
+ const updates = {};
2301
+ if (serverSession.title && serverSession.title !== record.title) {
2302
+ updates.title = serverSession.title;
2303
+ changed = true;
2304
+ }
2305
+ if (changed) {
2306
+ await this.store.update(record.id, updates);
2307
+ result.updated.push(record.id);
2308
+ }
2309
+ }
2310
+ return result;
2311
+ }
2312
+ };
2313
+
2314
+ // src/uri.ts
2315
+ function hasScheme(input) {
2316
+ return /^[a-zA-Z][a-zA-Z0-9+.-]+:/.test(input);
2317
+ }
2318
+ function ensureFileUri(pathOrUri) {
2319
+ if (!pathOrUri) return pathOrUri;
2320
+ if (hasScheme(pathOrUri)) return pathOrUri;
2321
+ const normalized = pathOrUri.replace(/\\/g, "/");
2322
+ if (/^[a-zA-Z]:\//.test(normalized) || /^[a-zA-Z]:$/.test(normalized)) {
2323
+ return `file:///${encodeFilePathComponents(normalized)}`;
2324
+ }
2325
+ if (normalized.startsWith("/")) {
2326
+ return `file://${encodeFilePathComponents(normalized)}`;
2327
+ }
2328
+ return pathOrUri;
2329
+ }
2330
+ function fileUriToDisplayPath(uri) {
2331
+ if (!uri.startsWith("file:///")) return uri;
2332
+ try {
2333
+ const parsed = new URL(uri);
2334
+ const pathname = decodeURIComponent(parsed.pathname);
2335
+ if (/^\/[a-zA-Z]:/.test(pathname)) {
2336
+ return pathname.slice(1);
2337
+ }
2338
+ return pathname;
2339
+ } catch {
2340
+ const pathname = decodeURIComponent(uri.slice("file://".length));
2341
+ if (/^\/[a-zA-Z]:/.test(pathname)) {
2342
+ return pathname.slice(1);
2343
+ }
2344
+ return pathname;
2345
+ }
2346
+ }
2347
+ function encodeFilePathComponents(filePath) {
2348
+ return filePath.split("/").map((component) => {
2349
+ if (!component) return component;
2350
+ if (/^[a-zA-Z]:$/.test(component)) return component;
2351
+ return encodeURIComponent(component);
2352
+ }).join("/");
2353
+ }
2354
+
2355
+ export {
2356
+ ActionType,
2357
+ RpcError,
2358
+ RpcTimeoutError,
2359
+ ProtocolLayer,
2360
+ PendingMessageKind,
2361
+ SessionLifecycle,
2362
+ SessionStatus,
2363
+ ResponsePartKind,
2364
+ ToolCallStatus,
2365
+ SessionHandle,
2366
+ StateMirror,
2367
+ Transport,
2368
+ ActiveClientManager,
2369
+ ReconnectManager,
2370
+ AhpClient,
2371
+ setVerbose,
2372
+ createLogger,
2373
+ ForwardingFormatter,
2374
+ WebhookForwarder,
2375
+ WebSocketForwarder,
2376
+ HealthChecker,
2377
+ truncatePreview,
2378
+ buildTurnSummary,
2379
+ SessionStore,
2380
+ AuthHandler,
2381
+ SessionPersistence,
2382
+ ensureFileUri,
2383
+ fileUriToDisplayPath
2384
+ };