@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.
- package/ARCHITECTURE.md +290 -0
- package/README.md +822 -0
- package/dist/adapters/http.d.ts +56 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +950 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/client/client.d.ts +240 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +1605 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/state.d.ts +72 -0
- package/dist/client/state.d.ts.map +1 -0
- package/dist/client/state.js +385 -0
- package/dist/client/state.js.map +1 -0
- package/dist/conformance/index.d.ts +41 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +924 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/agui-codec.d.ts +28 -0
- package/dist/protocol/agui-codec.d.ts.map +1 -0
- package/dist/protocol/agui-codec.js +250 -0
- package/dist/protocol/agui-codec.js.map +1 -0
- package/dist/protocol/agui.d.ts +144 -0
- package/dist/protocol/agui.d.ts.map +1 -0
- package/dist/protocol/agui.js +253 -0
- package/dist/protocol/agui.js.map +1 -0
- package/dist/protocol/compatibility.d.ts +35 -0
- package/dist/protocol/compatibility.d.ts.map +1 -0
- package/dist/protocol/compatibility.js +160 -0
- package/dist/protocol/compatibility.js.map +1 -0
- package/dist/protocol/errors.d.ts +24 -0
- package/dist/protocol/errors.d.ts.map +1 -0
- package/dist/protocol/errors.js +77 -0
- package/dist/protocol/errors.js.map +1 -0
- package/dist/protocol/index.d.ts +982 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +56 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/validation.d.ts +94 -0
- package/dist/protocol/validation.d.ts.map +1 -0
- package/dist/protocol/validation.js +1871 -0
- package/dist/protocol/validation.js.map +1 -0
- package/dist/protocol/version.d.ts +7 -0
- package/dist/protocol/version.d.ts.map +1 -0
- package/dist/protocol/version.js +9 -0
- package/dist/protocol/version.js.map +1 -0
- package/dist/react/chat.d.ts +34 -0
- package/dist/react/chat.d.ts.map +1 -0
- package/dist/react/chat.js +23 -0
- package/dist/react/chat.js.map +1 -0
- package/dist/react/components.d.ts +96 -0
- package/dist/react/components.d.ts.map +1 -0
- package/dist/react/components.js +1300 -0
- package/dist/react/components.js.map +1 -0
- package/dist/react/context.d.ts +259 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +359 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/headless.d.ts +3 -0
- package/dist/react/headless.d.ts.map +1 -0
- package/dist/react/headless.js +3 -0
- package/dist/react/headless.js.map +1 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/root.d.ts +58 -0
- package/dist/react/root.d.ts.map +1 -0
- package/dist/react/root.js +151 -0
- package/dist/react/root.js.map +1 -0
- package/dist/react/streaming-text.d.ts +19 -0
- package/dist/react/streaming-text.d.ts.map +1 -0
- package/dist/react/streaming-text.js +259 -0
- package/dist/react/streaming-text.js.map +1 -0
- package/dist/react/styles.css +1477 -0
- 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
|