@agent-native/agentkit 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.
Files changed (88) hide show
  1. package/ARCHITECTURE.md +290 -0
  2. package/README.md +822 -0
  3. package/dist/adapters/http.d.ts +56 -0
  4. package/dist/adapters/http.d.ts.map +1 -0
  5. package/dist/adapters/http.js +950 -0
  6. package/dist/adapters/http.js.map +1 -0
  7. package/dist/adapters/index.d.ts +2 -0
  8. package/dist/adapters/index.d.ts.map +1 -0
  9. package/dist/adapters/index.js +2 -0
  10. package/dist/adapters/index.js.map +1 -0
  11. package/dist/client/client.d.ts +240 -0
  12. package/dist/client/client.d.ts.map +1 -0
  13. package/dist/client/client.js +1605 -0
  14. package/dist/client/client.js.map +1 -0
  15. package/dist/client/index.d.ts +3 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +3 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/state.d.ts +72 -0
  20. package/dist/client/state.d.ts.map +1 -0
  21. package/dist/client/state.js +385 -0
  22. package/dist/client/state.js.map +1 -0
  23. package/dist/conformance/index.d.ts +41 -0
  24. package/dist/conformance/index.d.ts.map +1 -0
  25. package/dist/conformance/index.js +924 -0
  26. package/dist/conformance/index.js.map +1 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/protocol/agui-codec.d.ts +28 -0
  32. package/dist/protocol/agui-codec.d.ts.map +1 -0
  33. package/dist/protocol/agui-codec.js +250 -0
  34. package/dist/protocol/agui-codec.js.map +1 -0
  35. package/dist/protocol/agui.d.ts +144 -0
  36. package/dist/protocol/agui.d.ts.map +1 -0
  37. package/dist/protocol/agui.js +253 -0
  38. package/dist/protocol/agui.js.map +1 -0
  39. package/dist/protocol/compatibility.d.ts +35 -0
  40. package/dist/protocol/compatibility.d.ts.map +1 -0
  41. package/dist/protocol/compatibility.js +160 -0
  42. package/dist/protocol/compatibility.js.map +1 -0
  43. package/dist/protocol/errors.d.ts +24 -0
  44. package/dist/protocol/errors.d.ts.map +1 -0
  45. package/dist/protocol/errors.js +77 -0
  46. package/dist/protocol/errors.js.map +1 -0
  47. package/dist/protocol/index.d.ts +982 -0
  48. package/dist/protocol/index.d.ts.map +1 -0
  49. package/dist/protocol/index.js +56 -0
  50. package/dist/protocol/index.js.map +1 -0
  51. package/dist/protocol/validation.d.ts +94 -0
  52. package/dist/protocol/validation.d.ts.map +1 -0
  53. package/dist/protocol/validation.js +1871 -0
  54. package/dist/protocol/validation.js.map +1 -0
  55. package/dist/protocol/version.d.ts +7 -0
  56. package/dist/protocol/version.d.ts.map +1 -0
  57. package/dist/protocol/version.js +9 -0
  58. package/dist/protocol/version.js.map +1 -0
  59. package/dist/react/chat.d.ts +34 -0
  60. package/dist/react/chat.d.ts.map +1 -0
  61. package/dist/react/chat.js +23 -0
  62. package/dist/react/chat.js.map +1 -0
  63. package/dist/react/components.d.ts +96 -0
  64. package/dist/react/components.d.ts.map +1 -0
  65. package/dist/react/components.js +1300 -0
  66. package/dist/react/components.js.map +1 -0
  67. package/dist/react/context.d.ts +259 -0
  68. package/dist/react/context.d.ts.map +1 -0
  69. package/dist/react/context.js +359 -0
  70. package/dist/react/context.js.map +1 -0
  71. package/dist/react/headless.d.ts +3 -0
  72. package/dist/react/headless.d.ts.map +1 -0
  73. package/dist/react/headless.js +3 -0
  74. package/dist/react/headless.js.map +1 -0
  75. package/dist/react/index.d.ts +6 -0
  76. package/dist/react/index.d.ts.map +1 -0
  77. package/dist/react/index.js +6 -0
  78. package/dist/react/index.js.map +1 -0
  79. package/dist/react/root.d.ts +58 -0
  80. package/dist/react/root.d.ts.map +1 -0
  81. package/dist/react/root.js +151 -0
  82. package/dist/react/root.js.map +1 -0
  83. package/dist/react/streaming-text.d.ts +19 -0
  84. package/dist/react/streaming-text.d.ts.map +1 -0
  85. package/dist/react/streaming-text.js +259 -0
  86. package/dist/react/streaming-text.js.map +1 -0
  87. package/dist/react/styles.css +1477 -0
  88. package/package.json +112 -0
@@ -0,0 +1,1605 @@
1
+ import { AgentProtocolValidationError, AgentKitProtocolError, createRequestAbortedError, createAgentKitProtocolVersionOffer, parseAgentEvent, projectAgentCapabilities, resumeEntryFromApproval, } from "../protocol/index.js";
2
+ import { classifyAgentEvent, createAgentThreadState, reduceAgentEvent, } from "./state.js";
3
+ /**
4
+ * Creates the deterministic AgentKit controller used by every UI binding.
5
+ * Prefer this factory at application boundaries; the class remains public for
6
+ * dependency injection, extension, and test harnesses.
7
+ */
8
+ export function createAgentKitClient(options) {
9
+ return new AgentKitClient(options);
10
+ }
11
+ function defaultCreateId(prefix) {
12
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
13
+ return `${prefix}-${crypto.randomUUID()}`;
14
+ }
15
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
16
+ }
17
+ function errorProperty(error, property) {
18
+ return typeof error === "object" && error !== null
19
+ ? error[property]
20
+ : undefined;
21
+ }
22
+ function errorIsRetryable(error, fallback = true) {
23
+ const explicit = errorProperty(error, "retryable");
24
+ if (typeof explicit === "boolean")
25
+ return explicit;
26
+ if (error instanceof AgentKitCapabilityError ||
27
+ error instanceof AgentKitOperationError ||
28
+ error instanceof AgentKitDisposedError ||
29
+ error instanceof AgentProtocolValidationError ||
30
+ (error instanceof Error && error.name === "AbortError")) {
31
+ return false;
32
+ }
33
+ const status = errorProperty(error, "status");
34
+ if (typeof status === "number") {
35
+ return (status === 408 ||
36
+ status === 425 ||
37
+ status === 429 ||
38
+ (status >= 500 && status !== 501 && status !== 505));
39
+ }
40
+ return fallback;
41
+ }
42
+ function isAbortFailure(error) {
43
+ return error instanceof Error && error.name === "AbortError";
44
+ }
45
+ function toError(error, code = "agentkit_client_error") {
46
+ const errorCode = errorProperty(error, "code");
47
+ const correlationId = errorProperty(error, "correlationId");
48
+ const details = errorProperty(error, "details");
49
+ return {
50
+ code: typeof errorCode === "string" ? errorCode : code,
51
+ message: error instanceof Error ? error.message : String(error),
52
+ retryable: errorIsRetryable(error),
53
+ ...(typeof correlationId === "string" ? { correlationId } : {}),
54
+ ...(details === undefined ? {} : { details }),
55
+ };
56
+ }
57
+ export class AgentKitCapabilityError extends Error {
58
+ capability;
59
+ state;
60
+ correlationId;
61
+ details;
62
+ code;
63
+ retryable;
64
+ constructor(capability, state = "unsupported", message, retryable = state === "unavailable", correlationId, details) {
65
+ super(message ??
66
+ `This AgentKit controller does not support the ${capability} capability.`);
67
+ this.capability = capability;
68
+ this.state = state;
69
+ this.correlationId = correlationId;
70
+ this.details = details;
71
+ this.name = "AgentKitCapabilityError";
72
+ this.code =
73
+ state === "unavailable"
74
+ ? "capability_unavailable"
75
+ : "capability_unsupported";
76
+ this.retryable = retryable;
77
+ }
78
+ }
79
+ export class AgentKitOperationError extends Error {
80
+ operation;
81
+ code = "operation_unsupported";
82
+ retryable = false;
83
+ constructor(operation) {
84
+ super(`This AgentKit controller does not support ${operation}.`);
85
+ this.operation = operation;
86
+ this.name = "AgentKitOperationError";
87
+ }
88
+ }
89
+ export class AgentKitDisposedError extends Error {
90
+ code = "client_disposed";
91
+ retryable = false;
92
+ constructor() {
93
+ super("This AgentKit client has been disposed.");
94
+ this.name = "AgentKitDisposedError";
95
+ }
96
+ }
97
+ export class AgentKitHandshakeError extends Error {
98
+ code;
99
+ details;
100
+ correlationId;
101
+ retryable = false;
102
+ constructor(code, message, details, correlationId) {
103
+ super(message);
104
+ this.code = code;
105
+ this.details = details;
106
+ this.correlationId = correlationId;
107
+ this.name = "AgentKitHandshakeError";
108
+ }
109
+ }
110
+ class AgentKitConsumerStoppedError extends Error {
111
+ reason;
112
+ constructor(reason) {
113
+ super(`AgentKit run subscription stopped: ${reason}.`);
114
+ this.reason = reason;
115
+ this.name = "AgentKitConsumerStoppedError";
116
+ }
117
+ }
118
+ async function defaultUploadDriver(target, file, context) {
119
+ const body = target.fields
120
+ ? (() => {
121
+ const data = new FormData();
122
+ for (const [key, value] of Object.entries(target.fields)) {
123
+ data.set(key, value);
124
+ }
125
+ data.set("file", file.body, file.name);
126
+ return data;
127
+ })()
128
+ : file.body;
129
+ const response = await fetch(target.url, {
130
+ method: target.method,
131
+ headers: target.headers,
132
+ body,
133
+ signal: context?.signal,
134
+ });
135
+ if (!response.ok) {
136
+ throw new Error(`Upload failed with ${response.status}.`);
137
+ }
138
+ }
139
+ export class AgentKitClient {
140
+ transport;
141
+ createId;
142
+ now;
143
+ reconnectAttempts;
144
+ reconnectDelay;
145
+ onError;
146
+ onIntegrityReport;
147
+ upload;
148
+ ownsTransport;
149
+ retainActiveRunsOnThreadRelease;
150
+ listeners = new Set();
151
+ consumers = new Map();
152
+ consumerAbortControllers = new Map();
153
+ threadLoads = new Map();
154
+ threadLeaseCounts = new Map();
155
+ queuePromotions = new Set();
156
+ requestAbortController = new AbortController();
157
+ capabilitiesLoad;
158
+ shutdownPromise;
159
+ disposed = false;
160
+ snapshot;
161
+ constructor(options) {
162
+ this.transport = options.transport;
163
+ this.ownsTransport = options.transportOwnership === "owned";
164
+ this.retainActiveRunsOnThreadRelease =
165
+ options.retainActiveRunsOnThreadRelease ?? false;
166
+ this.createId = options.createId ?? defaultCreateId;
167
+ this.now = options.now ?? (() => new Date().toISOString());
168
+ this.reconnectAttempts = options.reconnect?.attempts ?? 3;
169
+ this.reconnectDelay =
170
+ options.reconnect?.delayMs ??
171
+ ((attempt) => Math.min(250 * 2 ** attempt, 4_000));
172
+ this.onError = options.onError;
173
+ this.onIntegrityReport = options.onIntegrityReport;
174
+ this.upload = options.upload ?? defaultUploadDriver;
175
+ this.snapshot = {
176
+ connection: "idle",
177
+ capabilities: options.transport.capabilities ?? {},
178
+ capabilitiesStatus: options.transport.discoverCapabilities
179
+ ? "unknown"
180
+ : "ready",
181
+ threads: {},
182
+ revision: 0,
183
+ };
184
+ }
185
+ createRequestContext(context) {
186
+ const lifetimeSignal = this.requestAbortController.signal;
187
+ const signal = context?.signal
188
+ ? context.signal === lifetimeSignal
189
+ ? lifetimeSignal
190
+ : AbortSignal.any([lifetimeSignal, context.signal])
191
+ : lifetimeSignal;
192
+ const requestContext = {
193
+ signal,
194
+ correlationId: context?.correlationId ?? defaultCreateId("request"),
195
+ };
196
+ this.assertRequestActive(requestContext);
197
+ return requestContext;
198
+ }
199
+ assertRequestActive(context) {
200
+ if (context.signal?.aborted) {
201
+ throw new AgentKitProtocolError(createRequestAbortedError({ correlationId: context.correlationId }));
202
+ }
203
+ }
204
+ invokeRequest(context, operation) {
205
+ this.assertRequestActive(context);
206
+ const signal = context.signal;
207
+ return new Promise((resolve, reject) => {
208
+ let settled = false;
209
+ const finish = (callback) => {
210
+ if (settled)
211
+ return;
212
+ settled = true;
213
+ signal?.removeEventListener("abort", onAbort);
214
+ callback();
215
+ };
216
+ const abortError = () => new AgentKitProtocolError(createRequestAbortedError({ correlationId: context.correlationId }));
217
+ const onAbort = () => finish(() => reject(abortError()));
218
+ signal?.addEventListener("abort", onAbort, { once: true });
219
+ void Promise.resolve()
220
+ .then(() => operation(context))
221
+ .then((value) => {
222
+ if (signal?.aborted)
223
+ onAbort();
224
+ else
225
+ finish(() => resolve(value));
226
+ }, (error) => {
227
+ if (signal?.aborted || isAbortFailure(error)) {
228
+ finish(() => reject(abortError()));
229
+ }
230
+ else {
231
+ finish(() => reject(error));
232
+ }
233
+ });
234
+ });
235
+ }
236
+ getSnapshot = () => this.snapshot;
237
+ subscribe = (listener) => {
238
+ this.listeners.add(listener);
239
+ return () => this.listeners.delete(listener);
240
+ };
241
+ getThread(threadId) {
242
+ return this.snapshot.threads[threadId] ?? createAgentThreadState(threadId);
243
+ }
244
+ async openThread(threadId, context) {
245
+ this.assertActive();
246
+ this.threadLeaseCounts.set(threadId, (this.threadLeaseCounts.get(threadId) ?? 0) + 1);
247
+ let released = false;
248
+ const release = () => {
249
+ if (released)
250
+ return;
251
+ released = true;
252
+ this.releaseThreadLease(threadId);
253
+ };
254
+ try {
255
+ await this.loadThread(threadId, context);
256
+ this.assertActive();
257
+ return {
258
+ threadId,
259
+ getSnapshot: () => this.getThread(threadId),
260
+ release,
261
+ dispose: release,
262
+ };
263
+ }
264
+ catch (error) {
265
+ release();
266
+ throw error;
267
+ }
268
+ }
269
+ async loadThread(threadId, context) {
270
+ this.assertActive();
271
+ const requestContext = this.createRequestContext(context);
272
+ const existing = this.threadLoads.get(threadId);
273
+ if (existing) {
274
+ return this.invokeRequest(requestContext, () => existing);
275
+ }
276
+ const load = this.loadThreadProjection(threadId, requestContext);
277
+ this.threadLoads.set(threadId, load);
278
+ try {
279
+ return await load;
280
+ }
281
+ finally {
282
+ if (this.threadLoads.get(threadId) === load) {
283
+ this.threadLoads.delete(threadId);
284
+ }
285
+ }
286
+ }
287
+ async loadThreadProjection(threadId, requestContext, preserveRuntimeProjection = false) {
288
+ const baseline = this.snapshot.threads[threadId] ?? createAgentThreadState(threadId);
289
+ if (!this.snapshot.threads[threadId])
290
+ this.setThread(threadId, baseline);
291
+ this.setConnection("connecting");
292
+ try {
293
+ await this.ensureCapabilities(requestContext);
294
+ this.assertActive();
295
+ if (!this.transport.getThreadSnapshot && !this.transport.getThread) {
296
+ throw new AgentKitOperationError("thread reads");
297
+ }
298
+ const getThreadSnapshot = this.transport.getThreadSnapshot;
299
+ const snapshot = getThreadSnapshot
300
+ ? await this.invokeRequest(requestContext, (context) => getThreadSnapshot({ threadId }, context))
301
+ : undefined;
302
+ this.assertActive();
303
+ const runSnapshots = new Map((snapshot?.runs ?? []).map((run) => [run.id, run]));
304
+ const missingRunIds = (snapshot?.activeRunIds ?? []).filter((runId) => !runSnapshots.has(runId));
305
+ const getRun = this.transport.getRun;
306
+ const activeRuns = missingRunIds.length
307
+ ? await Promise.all(missingRunIds.map(async (runId) => getRun
308
+ ? await this.invokeRequest(requestContext, (context) => getRun({ threadId, runId }, context))
309
+ : undefined))
310
+ : [];
311
+ this.assertActive();
312
+ missingRunIds.forEach((runId, index) => {
313
+ const run = activeRuns[index];
314
+ runSnapshots.set(runId, run ?? this.runSnapshot(threadId, runId));
315
+ });
316
+ const hydratedRuns = Object.fromEntries([...runSnapshots.entries()].map(([runId, run]) => [
317
+ runId,
318
+ this.runState(runId, run),
319
+ ]));
320
+ const activeRunIds = Array.from(new Set([
321
+ ...(snapshot?.activeRunIds ?? []),
322
+ ...Object.values(hydratedRuns)
323
+ .filter((run) => !this.isTerminalStatus(run.status))
324
+ .map((run) => run.id),
325
+ ])).filter((runId) => !this.isTerminalStatus(hydratedRuns[runId]?.status ?? "running"));
326
+ let thread;
327
+ if (snapshot) {
328
+ thread = this.hydrateThread(snapshot, hydratedRuns, activeRunIds);
329
+ }
330
+ else if (getThreadSnapshot) {
331
+ // A null durable snapshot is an authoritative missing thread, which is
332
+ // also the expected initial state for a client-generated new-chat id.
333
+ // Only transports without snapshot support need the legacy split reads.
334
+ thread = createAgentThreadState(threadId);
335
+ }
336
+ else {
337
+ const listQueuedMessages = this.transport.listQueuedMessages;
338
+ const getThread = this.transport.getThread;
339
+ const [queuedMessages, loadedThread] = await Promise.all([
340
+ listQueuedMessages
341
+ ? this.invokeRequest(requestContext, (context) => listQueuedMessages({ threadId }, context))
342
+ : Promise.resolve([]),
343
+ getThread
344
+ ? this.invokeRequest(requestContext, (context) => getThread({ threadId }, context))
345
+ : Promise.resolve(undefined),
346
+ ]);
347
+ this.assertActive();
348
+ thread = {
349
+ ...createAgentThreadState(threadId),
350
+ thread: loadedThread === null ? undefined : loadedThread,
351
+ queuedMessages,
352
+ };
353
+ }
354
+ if (preserveRuntimeProjection) {
355
+ const reconciliation = this.reconcileMessages(baseline.messages, thread.messages);
356
+ const runtimeProjection = this.remapThreadMessageReferences(baseline, reconciliation.idRemap);
357
+ const authoritative = {
358
+ thread: thread.thread,
359
+ messages: reconciliation.messages,
360
+ // Queue mutations are already optimistic and independently durable.
361
+ // A completed-run snapshot can lag the accepted steer/remove write,
362
+ // so it must not resurrect work the client has already promoted.
363
+ queuedMessages: baseline.queuedMessages,
364
+ };
365
+ thread = {
366
+ ...this.mergeLoadedThread(createAgentThreadState(threadId), runtimeProjection, thread),
367
+ ...authoritative,
368
+ };
369
+ }
370
+ const current = this.getThread(threadId);
371
+ if (current !== baseline) {
372
+ thread = this.mergeLoadedThread(baseline, current, thread);
373
+ }
374
+ this.setThread(threadId, thread);
375
+ this.setConnection("connected");
376
+ for (const runId of thread.activeRunIds) {
377
+ void this.resubscribeRun(threadId, runId).catch(() => {
378
+ // The consumer records the typed stream error in the client snapshot.
379
+ });
380
+ }
381
+ return thread;
382
+ }
383
+ catch (error) {
384
+ if (this.disposed)
385
+ throw error;
386
+ if (this.snapshot.capabilitiesStatus === "loading") {
387
+ this.patch({ capabilitiesStatus: "error" });
388
+ }
389
+ this.fail(error, "thread_load_failed");
390
+ throw error;
391
+ }
392
+ }
393
+ async getCapabilities(context) {
394
+ this.assertActive();
395
+ return this.ensureCapabilities(this.createRequestContext(context));
396
+ }
397
+ async createThread(input, context) {
398
+ this.assertActive();
399
+ const requestContext = this.createRequestContext(context);
400
+ await this.ensureCapabilities(requestContext);
401
+ const createThread = this.transport.createThread;
402
+ if (!createThread) {
403
+ throw new AgentKitOperationError("thread creation");
404
+ }
405
+ const thread = await this.invokeRequest(requestContext, (context) => createThread(input, context));
406
+ this.assertActive();
407
+ const current = this.getThread(thread.id);
408
+ this.setThread(thread.id, { ...current, thread });
409
+ return thread;
410
+ }
411
+ async listThreads(input, context) {
412
+ this.assertActive();
413
+ const requestContext = this.createRequestContext(context);
414
+ await this.ensureCapabilities(requestContext);
415
+ const listThreads = this.transport.listThreads;
416
+ if (!listThreads) {
417
+ throw new AgentKitOperationError("thread listing");
418
+ }
419
+ const result = await this.invokeRequest(requestContext, (context) => listThreads(input, context));
420
+ this.assertActive();
421
+ return result;
422
+ }
423
+ async sendMessage(input, context) {
424
+ this.assertActive();
425
+ const requestContext = this.createRequestContext(context);
426
+ await this.ensureCapabilities(requestContext);
427
+ if (input.attachments?.length) {
428
+ await this.requireCapability("attachments", requestContext);
429
+ }
430
+ if (input.options?.model) {
431
+ await this.requireCapability("modelSelection", requestContext);
432
+ }
433
+ if (input.options?.toolChoice) {
434
+ await this.requireCapability("toolSelection", requestContext);
435
+ }
436
+ this.assertActive();
437
+ const message = {
438
+ id: this.createId("message"),
439
+ role: "user",
440
+ createdAt: this.now(),
441
+ status: "complete",
442
+ parts: [{ type: "text", text: input.text }, ...(input.attachments ?? [])],
443
+ metadata: input.metadata,
444
+ };
445
+ const current = this.getThread(input.threadId);
446
+ this.setThread(input.threadId, {
447
+ ...current,
448
+ messages: [...current.messages, message],
449
+ suggestions: [],
450
+ });
451
+ this.setConnection("connecting");
452
+ try {
453
+ const result = await this.invokeRequest(requestContext, (context) => this.transport.startRun({
454
+ threadId: input.threadId,
455
+ messages: [...current.messages, message],
456
+ options: input.options,
457
+ metadata: input.metadata,
458
+ }, context));
459
+ this.assertActive();
460
+ if (result.capabilities) {
461
+ this.patch({
462
+ capabilities: result.capabilities,
463
+ capabilitiesStatus: "ready",
464
+ });
465
+ }
466
+ this.markRunStarted(input.threadId, result.runId);
467
+ const completed = this.consume(input.threadId, result.runId);
468
+ this.trackConsumer(input.threadId, result.runId, completed);
469
+ return {
470
+ runId: result.runId,
471
+ completed,
472
+ cancel: () => this.cancelRun(input.threadId, result.runId),
473
+ };
474
+ }
475
+ catch (error) {
476
+ if (this.disposed)
477
+ throw error;
478
+ const failed = this.getThread(input.threadId);
479
+ this.setThread(input.threadId, {
480
+ ...failed,
481
+ messages: failed.messages.map((candidate) => candidate.id === message.id
482
+ ? { ...candidate, status: "error" }
483
+ : candidate),
484
+ });
485
+ this.fail(error, "run_start_failed");
486
+ throw error;
487
+ }
488
+ }
489
+ resubscribeRun(threadId, runId) {
490
+ this.assertActive();
491
+ const status = this.getThread(threadId).runs[runId]?.status;
492
+ if (status && this.isTerminalStatus(status))
493
+ return Promise.resolve();
494
+ const key = this.runKey(threadId, runId);
495
+ const existing = this.consumers.get(key);
496
+ if (existing)
497
+ return existing;
498
+ const consumer = this.consume(threadId, runId);
499
+ this.trackConsumer(threadId, runId, consumer);
500
+ return consumer;
501
+ }
502
+ async resolveApproval(input, context) {
503
+ this.assertActive();
504
+ const requestContext = this.createRequestContext(context);
505
+ await this.requireCapability("approvals", requestContext);
506
+ const resumeRun = this.transport.resumeRun;
507
+ const resolveApproval = this.transport.resolveApproval;
508
+ if (!resumeRun && !resolveApproval) {
509
+ throw new AgentKitCapabilityError("approvals");
510
+ }
511
+ if (!resumeRun) {
512
+ await this.invokeRequest(requestContext, (context) => resolveApproval(input, context));
513
+ this.assertActive();
514
+ return;
515
+ }
516
+ const result = await this.invokeRequest(requestContext, (context) => resumeRun({
517
+ threadId: input.threadId,
518
+ runId: input.runId,
519
+ resume: [resumeEntryFromApproval(input)],
520
+ }, context));
521
+ this.assertActive();
522
+ if (result.runId !== input.runId) {
523
+ this.retireInterruptedRun(input.threadId, input.runId);
524
+ }
525
+ // A runtime that suspends rather than terminates answers with the run that
526
+ // was already streaming, so adopting it blindly would open a second reader
527
+ // on one stream.
528
+ const key = this.runKey(input.threadId, result.runId);
529
+ if (this.consumers.has(key))
530
+ return;
531
+ this.markRunStarted(input.threadId, result.runId);
532
+ this.trackConsumer(input.threadId, result.runId, this.consume(input.threadId, result.runId));
533
+ }
534
+ async resolveConnectionRequest(input, context) {
535
+ this.assertActive();
536
+ const requestContext = this.createRequestContext(context);
537
+ await this.requireCapability("connectionRequests", requestContext);
538
+ const resolveConnectionRequest = this.transport.resolveConnectionRequest;
539
+ if (!resolveConnectionRequest) {
540
+ throw new AgentKitCapabilityError("connectionRequests");
541
+ }
542
+ await this.invokeRequest(requestContext, (context) => resolveConnectionRequest(input, context));
543
+ this.assertActive();
544
+ const key = this.runKey(input.threadId, input.runId);
545
+ const existingConsumer = this.consumers.get(key);
546
+ if (existingConsumer)
547
+ await existingConsumer.catch(() => undefined);
548
+ if (this.consumers.has(key))
549
+ return;
550
+ this.trackConsumer(input.threadId, input.runId, this.consume(input.threadId, input.runId));
551
+ }
552
+ async invokeAction(invocation, context) {
553
+ this.assertActive();
554
+ const requestContext = this.createRequestContext(context);
555
+ await this.requireCapability("actions", requestContext);
556
+ const invokeAction = this.transport.invokeAction;
557
+ if (!invokeAction) {
558
+ throw new AgentKitCapabilityError("actions");
559
+ }
560
+ const thread = this.getThread(invocation.threadId);
561
+ this.setThread(invocation.threadId, {
562
+ ...thread,
563
+ actions: {
564
+ ...thread.actions,
565
+ [invocation.id]: { invocation },
566
+ },
567
+ });
568
+ try {
569
+ const result = await this.invokeRequest(requestContext, (context) => invokeAction({ invocation }, context));
570
+ this.assertActive();
571
+ const current = this.getThread(invocation.threadId);
572
+ this.setThread(invocation.threadId, {
573
+ ...current,
574
+ actions: {
575
+ ...current.actions,
576
+ [invocation.id]: {
577
+ ...current.actions[invocation.id],
578
+ invocation,
579
+ result,
580
+ },
581
+ },
582
+ });
583
+ return result;
584
+ }
585
+ catch (error) {
586
+ if (this.disposed)
587
+ throw error;
588
+ const current = this.getThread(invocation.threadId);
589
+ this.setThread(invocation.threadId, {
590
+ ...current,
591
+ actions: {
592
+ ...current.actions,
593
+ [invocation.id]: {
594
+ ...current.actions[invocation.id],
595
+ invocation,
596
+ result: {
597
+ invocationId: invocation.id,
598
+ status: "failed",
599
+ error: toError(error, "action_failed"),
600
+ },
601
+ },
602
+ },
603
+ });
604
+ throw error;
605
+ }
606
+ }
607
+ async uploadFiles(threadId, files, context) {
608
+ this.assertActive();
609
+ const requestContext = this.createRequestContext(context);
610
+ await this.requireCapability("uploads", requestContext);
611
+ return Promise.all(files.map(async (file) => {
612
+ const target = await this.createUpload(threadId, {
613
+ name: file.name,
614
+ mediaType: file.mediaType,
615
+ size: file.size,
616
+ purpose: "message",
617
+ }, requestContext);
618
+ try {
619
+ await this.invokeRequest(requestContext, (context) => this.upload(target, file, context));
620
+ this.assertActive();
621
+ return await this.completeUpload(threadId, target.uploadId, requestContext);
622
+ }
623
+ catch (error) {
624
+ if (this.transport.cancelUpload) {
625
+ const cleanupContext = this.createRequestContext();
626
+ await Promise.allSettled([
627
+ this.transport.cancelUpload({
628
+ threadId,
629
+ uploadId: target.uploadId,
630
+ }, cleanupContext),
631
+ ]);
632
+ }
633
+ throw error;
634
+ }
635
+ }));
636
+ }
637
+ async createUpload(threadId, descriptor, context) {
638
+ this.assertActive();
639
+ const requestContext = this.createRequestContext(context);
640
+ await this.requireCapability("uploads", requestContext);
641
+ const createUpload = this.transport.createUpload;
642
+ if (!createUpload) {
643
+ throw new AgentKitCapabilityError("uploads");
644
+ }
645
+ const target = await this.invokeRequest(requestContext, (context) => createUpload({ threadId, descriptor }, context));
646
+ this.assertActive();
647
+ return target;
648
+ }
649
+ async completeUpload(threadId, uploadId, context) {
650
+ this.assertActive();
651
+ const requestContext = this.createRequestContext(context);
652
+ await this.requireCapability("uploads", requestContext);
653
+ const completeUpload = this.transport.completeUpload;
654
+ if (!completeUpload) {
655
+ throw new AgentKitCapabilityError("uploads");
656
+ }
657
+ const file = await this.invokeRequest(requestContext, (context) => completeUpload({ threadId, uploadId }, context));
658
+ this.assertActive();
659
+ return file;
660
+ }
661
+ async cancelUpload(threadId, uploadId, context) {
662
+ this.assertActive();
663
+ const requestContext = this.createRequestContext(context);
664
+ await this.requireCapability("uploads", requestContext);
665
+ const cancelUpload = this.transport.cancelUpload;
666
+ if (!cancelUpload) {
667
+ throw new AgentKitCapabilityError("uploads");
668
+ }
669
+ await this.invokeRequest(requestContext, (context) => cancelUpload({ threadId, uploadId }, context));
670
+ this.assertActive();
671
+ }
672
+ async queueMessage(input, context) {
673
+ this.assertActive();
674
+ const requestContext = this.createRequestContext(context);
675
+ await this.requireCapability("messageQueue", requestContext);
676
+ if (input.attachments?.length) {
677
+ await this.requireCapability("attachments", requestContext);
678
+ }
679
+ const queueMessage = this.transport.queueMessage;
680
+ if (!queueMessage) {
681
+ throw new AgentKitCapabilityError("messageQueue");
682
+ }
683
+ const result = await this.invokeRequest(requestContext, (context) => queueMessage({
684
+ threadId: input.threadId,
685
+ text: input.text,
686
+ attachments: input.attachments,
687
+ metadata: input.metadata,
688
+ }, context));
689
+ this.assertActive();
690
+ const thread = this.getThread(input.threadId);
691
+ this.setThread(input.threadId, {
692
+ ...thread,
693
+ queuedMessages: [...thread.queuedMessages, result.message],
694
+ });
695
+ return result.message;
696
+ }
697
+ async removeQueuedMessage(threadId, messageId, context) {
698
+ this.assertActive();
699
+ const requestContext = this.createRequestContext(context);
700
+ await this.requireCapability("messageQueue", requestContext);
701
+ const removeQueuedMessage = this.transport.removeQueuedMessage;
702
+ if (!removeQueuedMessage) {
703
+ throw new AgentKitCapabilityError("messageQueue");
704
+ }
705
+ const previous = this.getThread(threadId);
706
+ const removedIndex = previous.queuedMessages.findIndex((message) => message.id === messageId);
707
+ const removed = previous.queuedMessages[removedIndex];
708
+ this.setThread(threadId, {
709
+ ...previous,
710
+ queuedMessages: previous.queuedMessages.filter((message) => message.id !== messageId),
711
+ });
712
+ try {
713
+ await this.invokeRequest(requestContext, (context) => removeQueuedMessage({ threadId, messageId }, context));
714
+ this.assertActive();
715
+ }
716
+ catch (error) {
717
+ if (this.disposed)
718
+ throw error;
719
+ if (removed) {
720
+ const current = this.getThread(threadId);
721
+ if (!current.queuedMessages.some((message) => message.id === messageId)) {
722
+ const queuedMessages = [...current.queuedMessages];
723
+ queuedMessages.splice(Math.min(removedIndex, queuedMessages.length), 0, removed);
724
+ this.setThread(threadId, { ...current, queuedMessages });
725
+ }
726
+ }
727
+ throw error;
728
+ }
729
+ }
730
+ async steerQueuedMessage(threadId, messageId, context) {
731
+ this.assertActive();
732
+ const requestContext = this.createRequestContext(context);
733
+ await this.requireCapability("messageQueue", requestContext);
734
+ const steerQueuedMessage = this.transport.steerQueuedMessage;
735
+ if (!steerQueuedMessage) {
736
+ throw new AgentKitCapabilityError("messageQueue");
737
+ }
738
+ const previous = this.getThread(threadId);
739
+ const queued = previous.queuedMessages.find((message) => message.id === messageId);
740
+ if (!queued)
741
+ throw new Error(`Unknown queued message: ${messageId}`);
742
+ const message = {
743
+ id: queued.id,
744
+ role: "user",
745
+ createdAt: queued.createdAt,
746
+ status: "complete",
747
+ parts: [
748
+ { type: "text", text: queued.text },
749
+ ...(queued.attachments ?? []),
750
+ ],
751
+ metadata: queued.metadata,
752
+ };
753
+ const previousConnection = this.snapshot.connection;
754
+ const previousError = this.snapshot.error;
755
+ this.setThread(threadId, {
756
+ ...previous,
757
+ messages: [...previous.messages, message],
758
+ queuedMessages: previous.queuedMessages.filter((candidate) => candidate.id !== messageId),
759
+ suggestions: [],
760
+ });
761
+ this.setConnection("connecting");
762
+ try {
763
+ const result = await this.invokeRequest(requestContext, (context) => steerQueuedMessage({ threadId, messageId }, context));
764
+ this.assertActive();
765
+ if (!result) {
766
+ this.setConnection("connected");
767
+ return;
768
+ }
769
+ if (result.capabilities) {
770
+ this.patch({
771
+ capabilities: result.capabilities,
772
+ capabilitiesStatus: "ready",
773
+ });
774
+ }
775
+ this.markRunStarted(threadId, result.runId);
776
+ const completed = this.consume(threadId, result.runId);
777
+ this.trackConsumer(threadId, result.runId, completed);
778
+ return {
779
+ runId: result.runId,
780
+ completed,
781
+ cancel: () => this.cancelRun(threadId, result.runId),
782
+ };
783
+ }
784
+ catch (error) {
785
+ if (this.disposed)
786
+ throw error;
787
+ const current = this.getThread(threadId);
788
+ const queuedMessages = current.queuedMessages.some((candidate) => candidate.id === queued.id)
789
+ ? current.queuedMessages
790
+ : [queued, ...current.queuedMessages];
791
+ this.setThread(threadId, {
792
+ ...current,
793
+ messages: current.messages.filter((candidate) => candidate.id !== message.id),
794
+ queuedMessages,
795
+ });
796
+ this.patch({ connection: previousConnection, error: previousError });
797
+ this.report(error, "queue_steer_failed");
798
+ throw error;
799
+ }
800
+ }
801
+ async cancelRun(threadId, runId, context) {
802
+ this.assertActive();
803
+ const requestContext = this.createRequestContext(context);
804
+ await this.invokeRequest(requestContext, (context) => this.transport.cancelRun({ threadId, runId }, context));
805
+ this.assertActive();
806
+ const status = this.getThread(threadId).runs[runId]?.status;
807
+ if (status && this.isTerminalStatus(status))
808
+ return;
809
+ this.markRunCancelled(threadId, runId);
810
+ const key = this.runKey(threadId, runId);
811
+ const hasOtherConsumers = [...this.consumers.keys()].some((candidate) => candidate !== key);
812
+ this.stopConsumer(threadId, runId, "cancelled");
813
+ if (!hasOtherConsumers)
814
+ this.setConnection("connected");
815
+ }
816
+ async submitFeedback(threadId, messageId, value, options, context) {
817
+ this.assertActive();
818
+ const requestContext = this.createRequestContext(context);
819
+ await this.requireCapability("feedback", requestContext);
820
+ const submitFeedback = this.transport.submitFeedback;
821
+ if (!submitFeedback) {
822
+ throw new AgentKitCapabilityError("feedback");
823
+ }
824
+ await this.invokeRequest(requestContext, (context) => submitFeedback({
825
+ threadId,
826
+ messageId,
827
+ value,
828
+ ...options,
829
+ }, context));
830
+ this.assertActive();
831
+ }
832
+ async forkThread(threadId, fromMessageId, options, context) {
833
+ this.assertActive();
834
+ const requestContext = this.createRequestContext(context);
835
+ await this.requireCapability("threadForking", requestContext);
836
+ const forkThread = this.transport.forkThread;
837
+ if (!forkThread) {
838
+ throw new AgentKitCapabilityError("threadForking");
839
+ }
840
+ const forked = await this.invokeRequest(requestContext, (context) => forkThread({
841
+ threadId,
842
+ fromMessageId,
843
+ ...options,
844
+ }, context));
845
+ this.assertActive();
846
+ const current = this.getThread(forked.id);
847
+ this.setThread(forked.id, { ...current, thread: forked });
848
+ return forked;
849
+ }
850
+ async updateThread(threadId, patch, context) {
851
+ this.assertActive();
852
+ const requestContext = this.createRequestContext(context);
853
+ await this.ensureCapabilities(requestContext);
854
+ const updateThread = this.transport.updateThread;
855
+ if (!updateThread) {
856
+ throw new AgentKitOperationError("thread updates");
857
+ }
858
+ const updated = await this.invokeRequest(requestContext, (context) => updateThread({ ...patch, threadId }, context));
859
+ this.assertActive();
860
+ const current = this.getThread(threadId);
861
+ this.setThread(threadId, { ...current, thread: updated });
862
+ return updated;
863
+ }
864
+ async deleteThread(threadId, context) {
865
+ this.assertActive();
866
+ const requestContext = this.createRequestContext(context);
867
+ await this.ensureCapabilities(requestContext);
868
+ const deleteThread = this.transport.deleteThread;
869
+ if (!deleteThread) {
870
+ throw new AgentKitOperationError("thread deletion");
871
+ }
872
+ await this.invokeRequest(requestContext, (context) => deleteThread({ threadId }, context));
873
+ this.assertActive();
874
+ this.stopThreadConsumers(threadId, "deleted");
875
+ const threads = { ...this.snapshot.threads };
876
+ delete threads[threadId];
877
+ this.patch({ threads });
878
+ }
879
+ shutdown() {
880
+ if (this.shutdownPromise)
881
+ return this.shutdownPromise;
882
+ this.disposed = true;
883
+ this.requestAbortController.abort(new AgentKitDisposedError());
884
+ for (const controller of this.consumerAbortControllers.values()) {
885
+ controller.abort(new AgentKitConsumerStoppedError("disposed"));
886
+ }
887
+ this.consumerAbortControllers.clear();
888
+ this.threadLeaseCounts.clear();
889
+ this.patch({ connection: "offline" });
890
+ this.listeners.clear();
891
+ this.consumers.clear();
892
+ this.shutdownPromise = this.ownsTransport
893
+ ? (async () => {
894
+ await this.transport.dispose?.();
895
+ })()
896
+ : Promise.resolve();
897
+ return this.shutdownPromise;
898
+ }
899
+ dispose() {
900
+ return this.shutdown();
901
+ }
902
+ trackConsumer(threadId, runId, completed) {
903
+ this.consumers.set(this.runKey(threadId, runId), completed);
904
+ // `consume` reports failures through snapshot state and `onError` before it
905
+ // rejects. Observe internally-owned continuations here so a caller that
906
+ // cannot receive their completion promise never triggers an unhandled
907
+ // rejection; APIs that return `completed` still expose the original promise.
908
+ void completed.catch(() => undefined);
909
+ }
910
+ async consume(threadId, runId) {
911
+ const key = this.runKey(threadId, runId);
912
+ const abortController = new AbortController();
913
+ this.consumerAbortControllers.set(key, abortController);
914
+ let attempt = 0;
915
+ let interruptedForContinuation = [
916
+ "awaiting_approval",
917
+ "awaiting_input",
918
+ ].includes(this.getThread(threadId).runs[runId]?.status ?? "");
919
+ try {
920
+ while (true) {
921
+ const afterSequence = this.getThread(threadId).runs[runId]?.lastSequence ?? 0;
922
+ let terminalEvent;
923
+ try {
924
+ this.setConnection(attempt === 0 ? "connected" : "reconnecting");
925
+ for await (const value of this.transport.subscribeToRun({
926
+ threadId,
927
+ runId,
928
+ afterSequence,
929
+ signal: abortController.signal,
930
+ })) {
931
+ if (abortController.signal.reason instanceof
932
+ AgentKitConsumerStoppedError) {
933
+ return;
934
+ }
935
+ const event = parseAgentEvent(value);
936
+ if (event.threadId !== threadId) {
937
+ throw new AgentProtocolValidationError("event.threadId", "must match the subscribed thread");
938
+ }
939
+ if (event.runId !== runId) {
940
+ throw new AgentProtocolValidationError("event.runId", "must match the subscribed run");
941
+ }
942
+ this.applyEvent(event);
943
+ if (event.type === "approval.requested" ||
944
+ event.type === "connection.requested") {
945
+ interruptedForContinuation = true;
946
+ }
947
+ else if (event.type === "approval.resolved") {
948
+ interruptedForContinuation = false;
949
+ }
950
+ else if (event.type === "connection.updated" &&
951
+ (event.request.status === "connected" ||
952
+ event.request.status === "declined" ||
953
+ event.request.status === "failed")) {
954
+ interruptedForContinuation = false;
955
+ }
956
+ if (event.type === "run.completed" ||
957
+ event.type === "run.failed" ||
958
+ event.type === "run.cancelled") {
959
+ terminalEvent = event;
960
+ }
961
+ }
962
+ if (!terminalEvent) {
963
+ if (interruptedForContinuation) {
964
+ this.setConnection("connected");
965
+ return;
966
+ }
967
+ this.reportIntegrity({
968
+ code: "run_missing_terminal",
969
+ threadId,
970
+ runId,
971
+ });
972
+ throw new Error(`AgentKit run ${runId} ended without a terminal event.`);
973
+ }
974
+ this.setConnection("connected");
975
+ if (terminalEvent.type === "run.completed") {
976
+ if (!this.threadLoads.has(threadId) &&
977
+ (this.transport.getThreadSnapshot || this.transport.getThread)) {
978
+ await this.loadThreadProjection(threadId, this.createRequestContext(), true);
979
+ }
980
+ this.scheduleQueuePromotion(threadId);
981
+ }
982
+ return;
983
+ }
984
+ catch (error) {
985
+ if (abortController.signal.reason instanceof
986
+ AgentKitConsumerStoppedError) {
987
+ return;
988
+ }
989
+ if (abortController.signal.aborted ||
990
+ !errorIsRetryable(error) ||
991
+ attempt >= this.reconnectAttempts) {
992
+ throw error;
993
+ }
994
+ attempt += 1;
995
+ this.setConnection("reconnecting");
996
+ await this.waitForReconnect(this.reconnectDelay(attempt), abortController.signal);
997
+ }
998
+ }
999
+ }
1000
+ catch (error) {
1001
+ if (abortController.signal.reason instanceof AgentKitConsumerStoppedError) {
1002
+ return;
1003
+ }
1004
+ this.fail(error, "run_stream_failed");
1005
+ throw error;
1006
+ }
1007
+ finally {
1008
+ this.consumers.delete(key);
1009
+ this.consumerAbortControllers.delete(key);
1010
+ }
1011
+ }
1012
+ waitForReconnect(duration, signal) {
1013
+ return new Promise((resolve, reject) => {
1014
+ const onAbort = () => {
1015
+ clearTimeout(timeout);
1016
+ reject(signal.reason ?? this.abortError());
1017
+ };
1018
+ const timeout = setTimeout(() => {
1019
+ signal.removeEventListener("abort", onAbort);
1020
+ resolve();
1021
+ }, duration);
1022
+ signal.addEventListener("abort", onAbort, { once: true });
1023
+ });
1024
+ }
1025
+ abortError() {
1026
+ const error = new Error("Aborted");
1027
+ error.name = "AbortError";
1028
+ return error;
1029
+ }
1030
+ async ensureCapabilities(context) {
1031
+ this.assertRequestActive(context);
1032
+ if (this.snapshot.capabilitiesStatus === "ready" &&
1033
+ !this.capabilitiesExpired()) {
1034
+ return this.snapshot.capabilities;
1035
+ }
1036
+ if (this.capabilitiesLoad) {
1037
+ return this.invokeRequest(context, () => this.capabilitiesLoad);
1038
+ }
1039
+ this.patch({ capabilitiesStatus: "loading" });
1040
+ const loadContext = this.createRequestContext({
1041
+ correlationId: context.correlationId,
1042
+ });
1043
+ const load = (async () => {
1044
+ if (this.transport.discoverCapabilities) {
1045
+ const discoverCapabilities = this.transport.discoverCapabilities;
1046
+ const discovery = await this.invokeRequest(loadContext, (requestContext) => discoverCapabilities({ protocol: createAgentKitProtocolVersionOffer() }, requestContext));
1047
+ if (discovery.protocol.status === "incompatible") {
1048
+ throw new AgentKitHandshakeError(discovery.protocol.error.code, discovery.protocol.error.message, discovery.protocol.error.details, discovery.protocol.error.correlationId);
1049
+ }
1050
+ const capabilities = discovery.legacy ?? projectAgentCapabilities(discovery);
1051
+ this.patch({
1052
+ capabilities,
1053
+ capabilityDiscovery: discovery,
1054
+ capabilitiesStatus: "ready",
1055
+ });
1056
+ return capabilities;
1057
+ }
1058
+ const capabilities = this.transport.capabilities ?? {};
1059
+ this.patch({ capabilities, capabilitiesStatus: "ready" });
1060
+ return capabilities;
1061
+ })().catch((error) => {
1062
+ if (!this.disposed)
1063
+ this.patch({ capabilitiesStatus: "error" });
1064
+ throw error;
1065
+ });
1066
+ this.capabilitiesLoad = load;
1067
+ try {
1068
+ return await this.invokeRequest(context, () => load);
1069
+ }
1070
+ finally {
1071
+ if (this.capabilitiesLoad === load)
1072
+ this.capabilitiesLoad = undefined;
1073
+ }
1074
+ }
1075
+ capabilitiesExpired() {
1076
+ const expiresAt = this.snapshot.capabilityDiscovery?.expiresAt;
1077
+ if (!expiresAt)
1078
+ return false;
1079
+ const expiration = Date.parse(expiresAt);
1080
+ const current = Date.parse(this.now());
1081
+ return (Number.isFinite(expiration) &&
1082
+ Number.isFinite(current) &&
1083
+ expiration <= current);
1084
+ }
1085
+ hydrateThread(snapshot, runs, activeRunIds) {
1086
+ let hydrated = createAgentThreadState(snapshot.id);
1087
+ for (const event of snapshot.events ?? []) {
1088
+ hydrated = reduceAgentEvent(hydrated, event);
1089
+ }
1090
+ const snapshotApprovals = Object.fromEntries((snapshot.approvals ?? [])
1091
+ .filter((approval) => approval.status === "pending")
1092
+ .map((approval) => [approval.request.id, approval.request]));
1093
+ const snapshotApprovalRunIds = Object.fromEntries((snapshot.approvals ?? [])
1094
+ .filter((approval) => approval.status === "pending" && approval.runId !== undefined)
1095
+ .map((approval) => [approval.request.id, approval.runId]));
1096
+ const snapshotConnectionRequests = Object.fromEntries((snapshot.connectionRequests ?? []).map((entry) => [
1097
+ entry.request.id,
1098
+ entry.request,
1099
+ ]));
1100
+ const snapshotConnectionRequestRunIds = Object.fromEntries((snapshot.connectionRequests ?? [])
1101
+ .filter((entry) => entry.runId !== undefined)
1102
+ .map((entry) => [entry.request.id, entry.runId]));
1103
+ const snapshotWidgets = Object.fromEntries((snapshot.widgets ?? []).map((entry) => [entry.widget.id, entry.widget]));
1104
+ const snapshotWidgetMessageIds = Object.fromEntries((snapshot.widgets ?? []).map((entry) => [
1105
+ entry.widget.id,
1106
+ entry.messageId,
1107
+ ]));
1108
+ const mergedRuns = this.mergeRuns(hydrated.runs, runs);
1109
+ const currentActiveRunIds = activeRunIds.filter((runId) => !this.isTerminalStatus(mergedRuns[runId]?.status ?? "running"));
1110
+ return {
1111
+ ...hydrated,
1112
+ thread: snapshot,
1113
+ // The snapshot projection is authoritative. The event log rebuilds all
1114
+ // rich, non-message state without replaying text deltas twice.
1115
+ messages: snapshot.messages,
1116
+ queuedMessages: snapshot.queuedMessages ?? hydrated.queuedMessages,
1117
+ runs: mergedRuns,
1118
+ activeRunIds: currentActiveRunIds,
1119
+ tools: {
1120
+ ...hydrated.tools,
1121
+ ...Object.fromEntries((snapshot.toolCalls ?? []).map((tool) => [tool.id, tool])),
1122
+ },
1123
+ activities: {
1124
+ ...hydrated.activities,
1125
+ ...Object.fromEntries((snapshot.activities ?? []).map((activity) => [
1126
+ activity.id,
1127
+ activity,
1128
+ ])),
1129
+ },
1130
+ tasks: {
1131
+ ...hydrated.tasks,
1132
+ ...Object.fromEntries((snapshot.tasks ?? []).map((task) => [task.id, task])),
1133
+ },
1134
+ approvals: { ...hydrated.approvals, ...snapshotApprovals },
1135
+ approvalRunIds: {
1136
+ ...hydrated.approvalRunIds,
1137
+ ...snapshotApprovalRunIds,
1138
+ },
1139
+ connectionRequests: {
1140
+ ...hydrated.connectionRequests,
1141
+ ...snapshotConnectionRequests,
1142
+ },
1143
+ connectionRequestRunIds: {
1144
+ ...hydrated.connectionRequestRunIds,
1145
+ ...snapshotConnectionRequestRunIds,
1146
+ },
1147
+ widgets: { ...hydrated.widgets, ...snapshotWidgets },
1148
+ widgetMessageIds: {
1149
+ ...hydrated.widgetMessageIds,
1150
+ ...snapshotWidgetMessageIds,
1151
+ },
1152
+ agents: {
1153
+ ...hydrated.agents,
1154
+ ...Object.fromEntries((snapshot.agents ?? []).map((agent) => [agent.id, agent])),
1155
+ },
1156
+ agentInteractions: snapshot.interactions ?? hydrated.agentInteractions,
1157
+ artifacts: snapshot.artifacts ?? hydrated.artifacts,
1158
+ suggestions: snapshot.suggestions ?? hydrated.suggestions,
1159
+ };
1160
+ }
1161
+ async requireCapability(capability, context) {
1162
+ const capabilities = await this.ensureCapabilities(context);
1163
+ this.assertActive();
1164
+ const descriptor = this.snapshot.capabilityDiscovery?.capabilities.find((candidate) => candidate.id === capability);
1165
+ if (descriptor?.state === "unsupported") {
1166
+ throw this.capabilityError(descriptor);
1167
+ }
1168
+ if (descriptor?.state === "unavailable") {
1169
+ throw this.capabilityError(descriptor);
1170
+ }
1171
+ if (this.snapshot.capabilityDiscovery && !descriptor) {
1172
+ throw new AgentKitCapabilityError(capability, "unavailable", `The ${JSON.stringify(capability)} capability was not included in discovery.`, true);
1173
+ }
1174
+ if (capabilities[capability] === false) {
1175
+ throw new AgentKitCapabilityError(capability);
1176
+ }
1177
+ }
1178
+ capabilityError(descriptor) {
1179
+ const unavailable = descriptor.state === "unavailable";
1180
+ return new AgentKitCapabilityError(descriptor.id, unavailable ? "unavailable" : "unsupported", descriptor.error?.message ?? descriptor.description, descriptor.error?.retryable ?? unavailable, descriptor.error?.correlationId, descriptor.error?.details);
1181
+ }
1182
+ mergeLoadedThread(baseline, current, loaded) {
1183
+ const runs = current.runs === baseline.runs
1184
+ ? loaded.runs
1185
+ : this.mergeRuns(loaded.runs, current.runs);
1186
+ const activeRunIds = Array.from(new Set([...loaded.activeRunIds, ...current.activeRunIds])).filter((runId) => !this.isTerminalStatus(runs[runId]?.status ?? "running"));
1187
+ return {
1188
+ ...loaded,
1189
+ thread: current.thread === baseline.thread ? loaded.thread : current.thread,
1190
+ messages: current.messages === baseline.messages
1191
+ ? loaded.messages
1192
+ : this.mergeItemsById(loaded.messages, current.messages),
1193
+ queuedMessages: current.queuedMessages === baseline.queuedMessages
1194
+ ? loaded.queuedMessages
1195
+ : this.mergeQueuedMessages(baseline.queuedMessages, loaded.queuedMessages, current.queuedMessages),
1196
+ runs,
1197
+ activeRunIds,
1198
+ events: current.events === baseline.events
1199
+ ? loaded.events
1200
+ : this.mergeEvents(loaded.events, current.events),
1201
+ agents: current.agents === baseline.agents
1202
+ ? loaded.agents
1203
+ : { ...loaded.agents, ...current.agents },
1204
+ agentInteractions: current.agentInteractions === baseline.agentInteractions
1205
+ ? loaded.agentInteractions
1206
+ : this.mergeItemsById(loaded.agentInteractions, current.agentInteractions),
1207
+ activities: current.activities === baseline.activities
1208
+ ? loaded.activities
1209
+ : { ...loaded.activities, ...current.activities },
1210
+ tasks: current.tasks === baseline.tasks
1211
+ ? loaded.tasks
1212
+ : { ...loaded.tasks, ...current.tasks },
1213
+ tools: current.tools === baseline.tools
1214
+ ? loaded.tools
1215
+ : { ...loaded.tools, ...current.tools },
1216
+ approvals: current.approvals === baseline.approvals
1217
+ ? loaded.approvals
1218
+ : current.approvals,
1219
+ approvalRunIds: current.approvalRunIds === baseline.approvalRunIds
1220
+ ? loaded.approvalRunIds
1221
+ : current.approvalRunIds,
1222
+ connectionRequests: current.connectionRequests === baseline.connectionRequests
1223
+ ? loaded.connectionRequests
1224
+ : { ...loaded.connectionRequests, ...current.connectionRequests },
1225
+ connectionRequestRunIds: current.connectionRequestRunIds === baseline.connectionRequestRunIds
1226
+ ? loaded.connectionRequestRunIds
1227
+ : {
1228
+ ...loaded.connectionRequestRunIds,
1229
+ ...current.connectionRequestRunIds,
1230
+ },
1231
+ artifacts: current.artifacts === baseline.artifacts
1232
+ ? loaded.artifacts
1233
+ : this.mergeItemsById(loaded.artifacts, current.artifacts),
1234
+ widgets: current.widgets === baseline.widgets
1235
+ ? loaded.widgets
1236
+ : { ...loaded.widgets, ...current.widgets },
1237
+ widgetMessageIds: current.widgetMessageIds === baseline.widgetMessageIds
1238
+ ? loaded.widgetMessageIds
1239
+ : { ...loaded.widgetMessageIds, ...current.widgetMessageIds },
1240
+ annotations: current.annotations === baseline.annotations
1241
+ ? loaded.annotations
1242
+ : { ...loaded.annotations, ...current.annotations },
1243
+ annotationMessageIds: current.annotationMessageIds === baseline.annotationMessageIds
1244
+ ? loaded.annotationMessageIds
1245
+ : {
1246
+ ...loaded.annotationMessageIds,
1247
+ ...current.annotationMessageIds,
1248
+ },
1249
+ suggestions: current.suggestions === baseline.suggestions
1250
+ ? loaded.suggestions
1251
+ : current.suggestions,
1252
+ actions: current.actions === baseline.actions
1253
+ ? loaded.actions
1254
+ : { ...loaded.actions, ...current.actions },
1255
+ uploads: current.uploads === baseline.uploads
1256
+ ? loaded.uploads
1257
+ : { ...loaded.uploads, ...current.uploads },
1258
+ };
1259
+ }
1260
+ mergeRuns(first, second) {
1261
+ const merged = { ...first };
1262
+ for (const [runId, run] of Object.entries(second)) {
1263
+ const existing = merged[runId];
1264
+ if (!existing || run.lastSequence >= existing.lastSequence) {
1265
+ merged[runId] = run;
1266
+ }
1267
+ }
1268
+ return merged;
1269
+ }
1270
+ mergeQueuedMessages(baseline, loaded, current) {
1271
+ const currentIds = new Set(current.map((message) => message.id));
1272
+ const removedIds = new Set(baseline
1273
+ .filter((message) => !currentIds.has(message.id))
1274
+ .map((message) => message.id));
1275
+ return this.mergeItemsById(loaded.filter((message) => !removedIds.has(message.id)), current);
1276
+ }
1277
+ mergeItemsById(first, second) {
1278
+ const merged = [...first];
1279
+ const indexById = new Map(merged.map((item, index) => [item.id, index]));
1280
+ for (const item of second) {
1281
+ const index = indexById.get(item.id);
1282
+ if (index === undefined) {
1283
+ indexById.set(item.id, merged.length);
1284
+ merged.push(item);
1285
+ }
1286
+ else {
1287
+ merged[index] = item;
1288
+ }
1289
+ }
1290
+ return merged;
1291
+ }
1292
+ reconcileMessages(current, durable) {
1293
+ if (durable.length === 0) {
1294
+ return { messages: current, idRemap: new Map() };
1295
+ }
1296
+ const durableIds = new Set(durable.map((message) => message.id));
1297
+ const currentIds = new Set(current.map((message) => message.id));
1298
+ const unmatchedContent = new Map();
1299
+ for (const message of durable) {
1300
+ // An explicit durable identity already present in the local projection
1301
+ // owns its match. Content fallback is only for still-unidentified work.
1302
+ if (currentIds.has(message.id))
1303
+ continue;
1304
+ const key = this.messageContentKey(message);
1305
+ unmatchedContent.set(key, [
1306
+ ...(unmatchedContent.get(key) ?? []),
1307
+ message,
1308
+ ]);
1309
+ }
1310
+ const optimistic = [];
1311
+ const idRemap = new Map();
1312
+ for (const message of current) {
1313
+ if (durableIds.has(message.id))
1314
+ continue;
1315
+ const key = this.messageContentKey(message);
1316
+ const matches = unmatchedContent.get(key);
1317
+ const durableMatch = matches?.shift();
1318
+ if (durableMatch) {
1319
+ idRemap.set(message.id, durableMatch.id);
1320
+ }
1321
+ else {
1322
+ optimistic.push(message);
1323
+ }
1324
+ }
1325
+ return { messages: [...durable, ...optimistic], idRemap };
1326
+ }
1327
+ remapThreadMessageReferences(thread, idRemap) {
1328
+ if (idRemap.size === 0)
1329
+ return thread;
1330
+ const remap = (messageId) => messageId ? (idRemap.get(messageId) ?? messageId) : undefined;
1331
+ const events = thread.events.map((event) => {
1332
+ switch (event.type) {
1333
+ case "message.created":
1334
+ case "message.completed":
1335
+ return {
1336
+ ...event,
1337
+ message: {
1338
+ ...event.message,
1339
+ id: remap(event.message.id),
1340
+ },
1341
+ };
1342
+ case "message.delta":
1343
+ case "reasoning.delta":
1344
+ return { ...event, messageId: remap(event.messageId) };
1345
+ case "tool.started":
1346
+ case "tool.updated":
1347
+ return {
1348
+ ...event,
1349
+ toolCall: {
1350
+ ...event.toolCall,
1351
+ ...(event.toolCall.messageId
1352
+ ? { messageId: remap(event.toolCall.messageId) }
1353
+ : {}),
1354
+ },
1355
+ };
1356
+ case "widget.created":
1357
+ case "widget.updated":
1358
+ case "annotation.created":
1359
+ case "annotation.updated":
1360
+ return event.messageId
1361
+ ? { ...event, messageId: remap(event.messageId) }
1362
+ : event;
1363
+ case "action.started":
1364
+ return event.invocation.messageId
1365
+ ? {
1366
+ ...event,
1367
+ invocation: {
1368
+ ...event.invocation,
1369
+ messageId: remap(event.invocation.messageId),
1370
+ },
1371
+ }
1372
+ : event;
1373
+ default:
1374
+ return event;
1375
+ }
1376
+ });
1377
+ const remapRecord = (record) => Object.fromEntries(Object.entries(record).map(([id, messageId]) => [
1378
+ id,
1379
+ remap(messageId),
1380
+ ]));
1381
+ return {
1382
+ ...thread,
1383
+ events,
1384
+ tools: Object.fromEntries(Object.entries(thread.tools).map(([id, tool]) => [
1385
+ id,
1386
+ tool.messageId ? { ...tool, messageId: remap(tool.messageId) } : tool,
1387
+ ])),
1388
+ widgetMessageIds: remapRecord(thread.widgetMessageIds),
1389
+ annotationMessageIds: remapRecord(thread.annotationMessageIds),
1390
+ actions: Object.fromEntries(Object.entries(thread.actions).map(([id, action]) => [
1391
+ id,
1392
+ action.invocation?.messageId
1393
+ ? {
1394
+ ...action,
1395
+ invocation: {
1396
+ ...action.invocation,
1397
+ messageId: remap(action.invocation.messageId),
1398
+ },
1399
+ }
1400
+ : action,
1401
+ ])),
1402
+ };
1403
+ }
1404
+ messageContentKey(message) {
1405
+ const text = message.parts
1406
+ .filter((part) => part.type === "text")
1407
+ .map((part) => part.text)
1408
+ .join("");
1409
+ return text
1410
+ ? JSON.stringify([message.role, "text", text])
1411
+ : JSON.stringify([message.role, "parts", message.parts]);
1412
+ }
1413
+ mergeEvents(first, second) {
1414
+ const merged = [...first];
1415
+ const keys = new Set(first.map((event) => `${event.threadId}\u0000${event.runId}\u0000${event.sequence}`));
1416
+ for (const event of second) {
1417
+ const key = `${event.threadId}\u0000${event.runId}\u0000${event.sequence}`;
1418
+ if (!keys.has(key)) {
1419
+ keys.add(key);
1420
+ merged.push(event);
1421
+ }
1422
+ }
1423
+ return merged;
1424
+ }
1425
+ releaseThreadLease(threadId) {
1426
+ const count = this.threadLeaseCounts.get(threadId) ?? 0;
1427
+ if (count <= 1) {
1428
+ this.threadLeaseCounts.delete(threadId);
1429
+ if (!this.retainActiveRunsOnThreadRelease) {
1430
+ this.stopThreadConsumers(threadId, "released");
1431
+ }
1432
+ return;
1433
+ }
1434
+ this.threadLeaseCounts.set(threadId, count - 1);
1435
+ }
1436
+ stopThreadConsumers(threadId, reason) {
1437
+ for (const [key, controller] of this.consumerAbortControllers) {
1438
+ if (key.startsWith(`${threadId}\u0000`)) {
1439
+ controller.abort(new AgentKitConsumerStoppedError(reason));
1440
+ }
1441
+ }
1442
+ }
1443
+ stopConsumer(threadId, runId, reason) {
1444
+ this.consumerAbortControllers
1445
+ .get(this.runKey(threadId, runId))
1446
+ ?.abort(new AgentKitConsumerStoppedError(reason));
1447
+ }
1448
+ markRunCancelled(threadId, runId) {
1449
+ const thread = this.getThread(threadId);
1450
+ const run = thread.runs[runId] ?? this.runState(runId);
1451
+ const activeRunIds = thread.activeRunIds.filter((id) => id !== runId);
1452
+ this.setThread(threadId, {
1453
+ ...thread,
1454
+ runs: {
1455
+ ...thread.runs,
1456
+ [runId]: {
1457
+ ...run,
1458
+ status: "cancelled",
1459
+ completedAt: this.now(),
1460
+ },
1461
+ },
1462
+ activeRunIds,
1463
+ });
1464
+ }
1465
+ markRunStarted(threadId, runId) {
1466
+ const thread = this.getThread(threadId);
1467
+ const run = thread.runs[runId] ?? this.runState(runId);
1468
+ const activeRunIds = Array.from(new Set([...thread.activeRunIds, runId]));
1469
+ this.setThread(threadId, {
1470
+ ...thread,
1471
+ runs: {
1472
+ ...thread.runs,
1473
+ [runId]: { ...run, status: "running" },
1474
+ },
1475
+ activeRunIds,
1476
+ });
1477
+ }
1478
+ retireInterruptedRun(threadId, runId) {
1479
+ const thread = this.getThread(threadId);
1480
+ this.setThread(threadId, {
1481
+ ...thread,
1482
+ activeRunIds: thread.activeRunIds.filter((id) => id !== runId),
1483
+ });
1484
+ }
1485
+ runKey(threadId, runId) {
1486
+ return `${threadId}\u0000${runId}`;
1487
+ }
1488
+ runState(runId, run) {
1489
+ return {
1490
+ id: runId,
1491
+ status: run?.status ?? "running",
1492
+ lastSequence: run?.lastSequence ?? 0,
1493
+ startedAt: run?.startedAt,
1494
+ completedAt: run?.completedAt,
1495
+ usage: run?.usage,
1496
+ error: run?.error,
1497
+ };
1498
+ }
1499
+ runSnapshot(threadId, runId) {
1500
+ return {
1501
+ id: runId,
1502
+ threadId,
1503
+ status: "running",
1504
+ lastSequence: 0,
1505
+ };
1506
+ }
1507
+ isTerminalStatus(status) {
1508
+ return ["completed", "failed", "cancelled"].includes(status);
1509
+ }
1510
+ scheduleQueuePromotion(threadId) {
1511
+ const thread = this.getThread(threadId);
1512
+ if (this.queuePromotions.has(threadId))
1513
+ return;
1514
+ const queued = thread.queuedMessages[0];
1515
+ if (!queued)
1516
+ return;
1517
+ // Reached only after a terminal event, so a queued follow-up that cannot
1518
+ // be promoted here is stranded rather than merely waiting.
1519
+ if (!this.transport.steerQueuedMessage) {
1520
+ this.reportIntegrity({
1521
+ code: "queue_promotion_dropped",
1522
+ threadId,
1523
+ reason: "transport-cannot-steer",
1524
+ });
1525
+ return;
1526
+ }
1527
+ if (thread.activeRunIds.length > 0) {
1528
+ this.reportIntegrity({
1529
+ code: "queue_promotion_dropped",
1530
+ threadId,
1531
+ reason: "run-still-active",
1532
+ });
1533
+ return;
1534
+ }
1535
+ this.queuePromotions.add(threadId);
1536
+ void this.steerQueuedMessage(threadId, queued.id)
1537
+ .catch(() => {
1538
+ // `steerQueuedMessage` already restores state and reports the failure.
1539
+ })
1540
+ .finally(() => this.queuePromotions.delete(threadId));
1541
+ }
1542
+ assertActive() {
1543
+ if (this.disposed) {
1544
+ throw new AgentKitDisposedError();
1545
+ }
1546
+ }
1547
+ applyEvent(event) {
1548
+ const thread = this.getThread(event.threadId);
1549
+ const admission = classifyAgentEvent(thread, event);
1550
+ if (admission.status === "duplicate") {
1551
+ this.reportIntegrity({
1552
+ code: "duplicate_event",
1553
+ threadId: event.threadId,
1554
+ runId: event.runId,
1555
+ expectedSequence: admission.lastSequence + 1,
1556
+ receivedSequence: event.sequence,
1557
+ });
1558
+ }
1559
+ if (admission.status === "gap") {
1560
+ this.reportIntegrity({
1561
+ code: "sequence_gap",
1562
+ threadId: event.threadId,
1563
+ runId: event.runId,
1564
+ expectedSequence: admission.expectedSequence,
1565
+ receivedSequence: admission.receivedSequence,
1566
+ });
1567
+ }
1568
+ this.setThread(event.threadId, reduceAgentEvent(thread, event));
1569
+ }
1570
+ setThread(threadId, thread) {
1571
+ this.patch({
1572
+ threads: { ...this.snapshot.threads, [threadId]: thread },
1573
+ error: undefined,
1574
+ });
1575
+ }
1576
+ setConnection(connection) {
1577
+ if (this.snapshot.connection !== connection)
1578
+ this.patch({ connection });
1579
+ }
1580
+ fail(error, code) {
1581
+ const agentError = toError(error, code);
1582
+ this.patch({ connection: "error", error: agentError });
1583
+ this.onError?.(agentError);
1584
+ }
1585
+ report(error, code) {
1586
+ this.onError?.(toError(error, code));
1587
+ }
1588
+ reportIntegrity(report) {
1589
+ try {
1590
+ this.onIntegrityReport?.(report);
1591
+ // coercion-ok: a counter must never affect the stream it counts.
1592
+ }
1593
+ catch { }
1594
+ }
1595
+ patch(patch) {
1596
+ this.snapshot = {
1597
+ ...this.snapshot,
1598
+ ...patch,
1599
+ revision: this.snapshot.revision + 1,
1600
+ };
1601
+ for (const listener of this.listeners)
1602
+ listener();
1603
+ }
1604
+ }
1605
+ //# sourceMappingURL=client.js.map