@runtypelabs/cloudflare-agents 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,891 @@
1
+ import { n as operationSpanId, t as createRunIdentity } from "./chunk-BuRpWjxB.mjs";
2
+ import { DurableObject } from "cloudflare:workers";
3
+ //#region package.json
4
+ var version = "0.1.1";
5
+ //#endregion
6
+ //#region src/otlp.ts
7
+ function otlpValue(value) {
8
+ if (typeof value === "string") return { stringValue: value };
9
+ if (typeof value === "boolean") return { boolValue: value };
10
+ return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
11
+ }
12
+ function attributes(values) {
13
+ return Object.entries(values).map(([key, value]) => ({
14
+ key,
15
+ value: otlpValue(value)
16
+ }));
17
+ }
18
+ /** Encodes one immutable Cloudflare Agents submission trace as OTLP/JSON. */
19
+ function encodeTraceBatch(spans, identity) {
20
+ return JSON.stringify({ resourceSpans: [{
21
+ resource: { attributes: attributes({
22
+ "runtype.agent.id": identity.agentId,
23
+ "runtype.adapter.name": "cloudflare-agents",
24
+ "runtype.adapter.version": identity.adapterVersion
25
+ }) },
26
+ scopeSpans: [{
27
+ scope: {
28
+ name: "runtype.cloudflare-agents",
29
+ version: identity.adapterVersion
30
+ },
31
+ spans: spans.map((span) => ({
32
+ traceId: span.traceId,
33
+ spanId: span.spanId,
34
+ ...span.parentSpanId ? { parentSpanId: span.parentSpanId } : {},
35
+ name: span.name,
36
+ startTimeUnixNano: span.startTimeUnixNano,
37
+ endTimeUnixNano: span.endTimeUnixNano,
38
+ attributes: attributes(span.attributes),
39
+ ...span.status ? { status: {
40
+ code: span.status.code,
41
+ ...span.status.message ? { message: span.status.message } : {}
42
+ } } : {}
43
+ }))
44
+ }]
45
+ }] });
46
+ }
47
+ //#endregion
48
+ //#region src/sender.ts
49
+ /** Builds the one outbound OTLP request without exposing delivery state. */
50
+ function createOtlpDeliveryRequest(options) {
51
+ return new Request(`${options.baseUrl}/v1/otel/v1/traces`, {
52
+ method: "POST",
53
+ redirect: "manual",
54
+ signal: options.signal,
55
+ headers: {
56
+ authorization: `Bearer ${options.apiKey}`,
57
+ "content-type": "application/json",
58
+ "x-runtype-agent-id": options.agentId
59
+ },
60
+ body: options.body.slice().buffer
61
+ });
62
+ }
63
+ //#endregion
64
+ //#region src/session.ts
65
+ const MAX_MODEL_STEPS = 128;
66
+ const MAX_TOOL_CALLS = 256;
67
+ function nanos(timestamp) {
68
+ return (BigInt(Math.trunc(timestamp)) * 1000000n).toString();
69
+ }
70
+ function contentAttribute(value) {
71
+ return value === void 0 ? void 0 : JSON.stringify(value);
72
+ }
73
+ function inputAttributes(input) {
74
+ if (!input) return {};
75
+ const system = contentAttribute(input.system);
76
+ const combined = [...typeof input.system === "string" ? [{
77
+ role: "system",
78
+ content: input.system
79
+ }] : Array.isArray(input.system) ? input.system : [], ...Array.isArray(input.messages) ? input.messages : []];
80
+ const messages = combined.length > 0 ? JSON.stringify(combined) : contentAttribute(input.messages);
81
+ return {
82
+ ...system === void 0 ? {} : { "gen_ai.system_instructions": system },
83
+ ...messages === void 0 ? {} : { "gen_ai.input.messages": messages }
84
+ };
85
+ }
86
+ function errorMessage(value) {
87
+ if (typeof value === "string") return value;
88
+ if (value && !Array.isArray(value) && typeof value === "object" && typeof value.message === "string") return value.message;
89
+ }
90
+ function outputAttributes(output) {
91
+ if (output === void 0) return {};
92
+ return { "gen_ai.output.messages": JSON.stringify([{
93
+ role: "assistant",
94
+ content: output
95
+ }]) };
96
+ }
97
+ async function span(session, operationId, name, startedAt, endedAt, attributes) {
98
+ return {
99
+ traceId: session.identity.traceId,
100
+ spanId: await operationSpanId(session.identity.traceId, operationId),
101
+ parentSpanId: session.identity.rootSpanId,
102
+ name,
103
+ startTimeUnixNano: nanos(startedAt),
104
+ endTimeUnixNano: nanos(Math.max(startedAt, endedAt)),
105
+ attributes: {
106
+ "gen_ai.conversation.id": session.identity.conversationId,
107
+ ...attributes
108
+ }
109
+ };
110
+ }
111
+ /** Creates the immutable start marker and state for one accepted submission. */
112
+ async function beginSession(identity, exchangeId, now) {
113
+ if (!Number.isSafeInteger(now) || now <= 0) throw new Error("Invalid adapter timestamp");
114
+ const session = {
115
+ version: 1,
116
+ identity,
117
+ exchangeId,
118
+ startedAt: now,
119
+ models: [],
120
+ tools: []
121
+ };
122
+ return {
123
+ session,
124
+ spans: [await span(session, "accepted", "runtype.adapter.accepted", now, now, { "runtype.execution.id": identity.traceId })]
125
+ };
126
+ }
127
+ /** Reduce sanitized observations; persist returned state and spans in one transaction. */
128
+ async function reduceObservation(previous, observation, now) {
129
+ if (!Number.isSafeInteger(now) || now <= 0) throw new Error("Invalid adapter timestamp");
130
+ const session = structuredClone(previous);
131
+ const unchanged = () => ({
132
+ session,
133
+ spans: []
134
+ });
135
+ const missing = () => {
136
+ session.telemetryIncomplete = true;
137
+ return {
138
+ ...unchanged(),
139
+ diagnostic: "missing_operation_start"
140
+ };
141
+ };
142
+ if (observation.turn.submissionId !== session.identity.submissionId || observation.turn.conversationId !== session.identity.conversationId || observation.turn.exchangeId !== session.exchangeId) return {
143
+ ...unchanged(),
144
+ diagnostic: "identity_conflict"
145
+ };
146
+ if (session.terminal) return unchanged();
147
+ if (observation.kind === "incomplete") {
148
+ session.telemetryIncomplete = true;
149
+ session.terminal = "incomplete";
150
+ return {
151
+ ...unchanged(),
152
+ diagnostic: "incomplete"
153
+ };
154
+ }
155
+ if (observation.kind === "unsupported" || observation.turn.continuation) {
156
+ session.terminal = "unsupported";
157
+ return {
158
+ ...unchanged(),
159
+ diagnostic: "unsupported"
160
+ };
161
+ }
162
+ if (observation.kind === "accepted") return unchanged();
163
+ if (observation.kind === "model_input") {
164
+ if (observation.stepNumber === void 0) return unchanged();
165
+ if (session.models.some((model) => model.index === observation.stepNumber)) return unchanged();
166
+ if (session.models.length >= MAX_MODEL_STEPS) return {
167
+ ...unchanged(),
168
+ diagnostic: "session_limit"
169
+ };
170
+ session.models.push({
171
+ index: observation.stepNumber,
172
+ startedAt: now,
173
+ input: observation
174
+ });
175
+ return unchanged();
176
+ }
177
+ if (observation.kind === "model_step") {
178
+ const model = session.models.find((item) => item.index === observation.stepNumber);
179
+ if (!model) return missing();
180
+ if (model.result) return unchanged();
181
+ model.result = observation;
182
+ return {
183
+ session,
184
+ spans: [await span(session, `chat:${model.index}`, `chat ${observation.model.modelId}`, model.startedAt, now, {
185
+ "gen_ai.operation.name": "chat",
186
+ "gen_ai.request.model": observation.model.modelId,
187
+ "gen_ai.provider.name": observation.model.provider,
188
+ ...observation.usage.inputTokens === void 0 ? {} : { "gen_ai.usage.input_tokens": observation.usage.inputTokens },
189
+ ...observation.usage.outputTokens === void 0 ? {} : { "gen_ai.usage.output_tokens": observation.usage.outputTokens },
190
+ "runtype.iteration": model.index,
191
+ ...inputAttributes(model.input),
192
+ ...outputAttributes(observation.output)
193
+ })]
194
+ };
195
+ }
196
+ if (observation.kind === "tool_started") {
197
+ const id = observation.toolCall.toolCallId;
198
+ if (!id) return missing();
199
+ if (session.tools.some((tool) => tool.id === id)) return unchanged();
200
+ if (session.tools.length >= MAX_TOOL_CALLS) return {
201
+ ...unchanged(),
202
+ diagnostic: "session_limit"
203
+ };
204
+ session.tools.push({
205
+ id,
206
+ startedAt: now,
207
+ input: observation,
208
+ ended: false
209
+ });
210
+ return unchanged();
211
+ }
212
+ if (observation.kind === "tool_finished") {
213
+ const tool = session.tools.find((item) => item.id === observation.toolCall.toolCallId);
214
+ if (!tool) return missing();
215
+ if (tool.ended) return unchanged();
216
+ tool.ended = true;
217
+ const name = tool.input.toolCall.toolName ?? "unknown";
218
+ const parameters = contentAttribute(tool.input.toolCall.input);
219
+ const result = contentAttribute(observation.output);
220
+ const durationMs = Math.max(0, Math.trunc(observation.durationMs));
221
+ const ended = await span(session, `tool:${tool.id}`, `execute_tool ${name}`, Math.max(1, now - durationMs), now, {
222
+ "gen_ai.operation.name": "execute_tool",
223
+ "gen_ai.tool.call.id": tool.id,
224
+ "gen_ai.tool.name": name,
225
+ ...parameters === void 0 ? {} : { "gen_ai.tool.call.arguments": parameters },
226
+ ...result === void 0 ? {} : { "gen_ai.tool.call.result": result }
227
+ });
228
+ const message = errorMessage(observation.error);
229
+ ended.status = {
230
+ code: observation.success ? 1 : 2,
231
+ ...message === void 0 ? {} : { message }
232
+ };
233
+ return {
234
+ session,
235
+ spans: [ended]
236
+ };
237
+ }
238
+ if (observation.kind === "finished" && (session.models.some((model) => !model.result) || session.tools.some((tool) => !tool.ended))) {
239
+ session.telemetryIncomplete = true;
240
+ session.terminal = "incomplete";
241
+ return {
242
+ ...unchanged(),
243
+ diagnostic: "incomplete"
244
+ };
245
+ }
246
+ if (session.telemetryIncomplete) {
247
+ session.terminal = "unsupported";
248
+ return {
249
+ ...unchanged(),
250
+ diagnostic: "unsupported"
251
+ };
252
+ }
253
+ session.terminal = observation.kind;
254
+ const steps = session.models.filter((model) => model.result).sort((a, b) => a.index - b.index);
255
+ const root = await span(session, "root", "invoke_agent", session.startedAt, now, {
256
+ "gen_ai.operation.name": "invoke_agent",
257
+ "runtype.execution.id": session.identity.traceId,
258
+ "runtype.stop_reason": observation.kind === "finished" ? "stop" : observation.kind === "aborted" ? "cancelled" : "error",
259
+ ...steps.length > 0 && steps.every((model) => model.result?.usage.inputTokens !== void 0) ? { "gen_ai.usage.input_tokens": steps.reduce((sum, model) => sum + model.result.usage.inputTokens, 0) } : {},
260
+ ...steps.length > 0 && steps.every((model) => model.result?.usage.outputTokens !== void 0) ? { "gen_ai.usage.output_tokens": steps.reduce((sum, model) => sum + model.result.usage.outputTokens, 0) } : {},
261
+ ...inputAttributes(steps[0]?.input),
262
+ ...outputAttributes(steps.at(-1)?.result?.output)
263
+ });
264
+ delete root.parentSpanId;
265
+ const message = "error" in observation ? errorMessage(observation.error) : void 0;
266
+ root.status = {
267
+ code: observation.kind === "finished" ? 1 : 2,
268
+ ...message === void 0 ? {} : { message }
269
+ };
270
+ return {
271
+ session,
272
+ spans: [root]
273
+ };
274
+ }
275
+ //#endregion
276
+ //#region src/outbox.ts
277
+ const OUTBOX_RETENTION_MS = 6048e5;
278
+ const OUTBOX_LEASE_MS = 6e4;
279
+ const outboxStatsKey = "outbox:stats";
280
+ const knownFailureCodes = /* @__PURE__ */ new Set([
281
+ "network_error",
282
+ "network_abort_error",
283
+ "network_type_error",
284
+ "network_unknown_error",
285
+ "partial_success",
286
+ "invalid_success_response",
287
+ "success_response_too_large"
288
+ ]);
289
+ function emptyStats() {
290
+ return {
291
+ expired: 0,
292
+ rejected: 0,
293
+ failureCounts: {}
294
+ };
295
+ }
296
+ function safeFailureCode(code) {
297
+ return /^http_[0-9]{3}$/.test(code) || knownFailureCodes.has(code) ? code : "other";
298
+ }
299
+ const MAX_RESPONSE_BYTES = 16384;
300
+ const pendingKey = (id) => `outbox:pending:${id}`;
301
+ const completedKey = (id) => `outbox:completed:${id}`;
302
+ const hashBytes = async (body) => {
303
+ const copy = body.slice().buffer;
304
+ const digest = await crypto.subtle.digest("SHA-256", copy);
305
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
306
+ };
307
+ function validBatch(batch) {
308
+ return batch.version === 1 && batch.deliveryId.length > 0 && Number.isSafeInteger(batch.createdAt) && batch.createdAt > 0;
309
+ }
310
+ function retryAt(now, attempts) {
311
+ return now + Math.min(3e5, 1e4 * 2 ** Math.min(9, Math.max(0, attempts - 1)));
312
+ }
313
+ async function incrementStat(transaction, key) {
314
+ const stats = {
315
+ ...emptyStats(),
316
+ ...await transaction.get(outboxStatsKey)
317
+ };
318
+ stats[key] += 1;
319
+ await transaction.put(outboxStatsKey, stats);
320
+ }
321
+ async function recordFailure(transaction, code) {
322
+ const stats = {
323
+ ...emptyStats(),
324
+ ...await transaction.get(outboxStatsKey)
325
+ };
326
+ const safe = safeFailureCode(code);
327
+ stats.failureCounts[safe] = (stats.failureCounts[safe] ?? 0) + 1;
328
+ await transaction.put(outboxStatsKey, stats);
329
+ }
330
+ async function queueTotals(transaction, now) {
331
+ const entries = await transaction.list("outbox:pending:");
332
+ let rows = 0;
333
+ let bytes = 0;
334
+ for (const { key, value } of entries) {
335
+ if (value.expiresAt <= now) {
336
+ await transaction.delete(key);
337
+ await incrementStat(transaction, "expired");
338
+ continue;
339
+ }
340
+ rows += 1;
341
+ bytes += value.body.byteLength;
342
+ }
343
+ return {
344
+ rows,
345
+ bytes
346
+ };
347
+ }
348
+ async function pruneCompleted(transaction, now) {
349
+ for (const { key, value } of await transaction.list("outbox:completed:")) if (value.expiresAt <= now) await transaction.delete(key);
350
+ }
351
+ /** Transactional immutable OTLP queue. The caller may combine this with session state in one transaction. */
352
+ var DurableOtlpOutbox = class {
353
+ storage;
354
+ constructor(storage) {
355
+ this.storage = storage;
356
+ }
357
+ async enqueue(batch, now) {
358
+ return this.storage.transaction((transaction) => this.enqueueTransaction(transaction, batch, now));
359
+ }
360
+ async enqueueTransaction(transaction, batch, now) {
361
+ const totals = await queueTotals(transaction, now);
362
+ await pruneCompleted(transaction, now);
363
+ const completedRows = (await transaction.list("outbox:completed:")).length;
364
+ if (!validBatch(batch)) return {
365
+ accepted: false,
366
+ code: "invalid_payload",
367
+ queuedRows: totals.rows,
368
+ queuedBytes: totals.bytes
369
+ };
370
+ if (batch.body.byteLength > 262144) return {
371
+ accepted: false,
372
+ code: "payload_too_large",
373
+ queuedRows: totals.rows,
374
+ queuedBytes: totals.bytes
375
+ };
376
+ const bodyHash = await hashBytes(batch.body);
377
+ const completed = await transaction.get(completedKey(batch.deliveryId));
378
+ if (completed && completed.expiresAt > now) {
379
+ if (completed.bodyHash !== bodyHash) return {
380
+ accepted: false,
381
+ code: "invalid_payload",
382
+ queuedRows: totals.rows,
383
+ queuedBytes: totals.bytes
384
+ };
385
+ return {
386
+ accepted: true,
387
+ duplicate: true,
388
+ queuedAt: now
389
+ };
390
+ }
391
+ if (completed) await transaction.delete(completedKey(batch.deliveryId));
392
+ const existing = await transaction.get(pendingKey(batch.deliveryId));
393
+ if (existing) {
394
+ if (existing.bodyHash !== bodyHash) return {
395
+ accepted: false,
396
+ code: "invalid_payload",
397
+ queuedRows: totals.rows,
398
+ queuedBytes: totals.bytes
399
+ };
400
+ return {
401
+ accepted: true,
402
+ duplicate: true,
403
+ queuedAt: existing.createdAt
404
+ };
405
+ }
406
+ if (totals.rows + completedRows >= 1e3 || totals.bytes + batch.body.byteLength > 16777216) return {
407
+ accepted: false,
408
+ code: "outbox_full",
409
+ queuedRows: totals.rows,
410
+ queuedBytes: totals.bytes
411
+ };
412
+ await transaction.put(pendingKey(batch.deliveryId), {
413
+ version: 1,
414
+ deliveryId: batch.deliveryId,
415
+ body: batch.body,
416
+ bodyHash,
417
+ createdAt: batch.createdAt,
418
+ expiresAt: now + OUTBOX_RETENTION_MS,
419
+ attempts: 0,
420
+ nextAttemptAt: now
421
+ });
422
+ return {
423
+ accepted: true,
424
+ duplicate: false,
425
+ queuedAt: now
426
+ };
427
+ }
428
+ async claim(now, limit = 16) {
429
+ return this.storage.transaction((transaction) => this.claimTransaction(transaction, now, limit));
430
+ }
431
+ async claimTransaction(transaction, now, limit = 16) {
432
+ const entries = await transaction.list("outbox:pending:");
433
+ const claimed = [];
434
+ for (const { key, value } of entries.sort((a, b) => a.value.nextAttemptAt - b.value.nextAttemptAt)) {
435
+ if (claimed.length === limit) break;
436
+ if (value.expiresAt <= now) {
437
+ await transaction.delete(key);
438
+ await incrementStat(transaction, "expired");
439
+ continue;
440
+ }
441
+ if (value.nextAttemptAt > now || (value.leaseUntil ?? 0) > now) continue;
442
+ const leaseToken = crypto.randomUUID();
443
+ value.leaseToken = leaseToken;
444
+ value.leaseUntil = now + OUTBOX_LEASE_MS;
445
+ value.attempts += 1;
446
+ await transaction.put(key, value);
447
+ claimed.push({
448
+ version: 1,
449
+ deliveryId: value.deliveryId,
450
+ body: value.body,
451
+ createdAt: value.createdAt,
452
+ leaseToken,
453
+ attempts: value.attempts
454
+ });
455
+ }
456
+ return claimed;
457
+ }
458
+ async settle(delivery, result, now) {
459
+ await this.storage.transaction((transaction) => this.settleTransaction(transaction, delivery, result, now));
460
+ }
461
+ async settleTransaction(transaction, delivery, result, now) {
462
+ const key = pendingKey(delivery.deliveryId);
463
+ const entry = await transaction.get(key);
464
+ if (!entry || entry.leaseToken !== delivery.leaseToken) return;
465
+ if (result.kind === "success") {
466
+ await transaction.delete(key);
467
+ await transaction.put(completedKey(entry.deliveryId), {
468
+ version: 1,
469
+ bodyHash: entry.bodyHash,
470
+ expiresAt: now + OUTBOX_RETENTION_MS
471
+ });
472
+ return;
473
+ }
474
+ if (result.kind === "reject" || entry.expiresAt <= now) {
475
+ await transaction.delete(key);
476
+ await incrementStat(transaction, result.kind === "reject" ? "rejected" : "expired");
477
+ if (result.kind === "reject") await recordFailure(transaction, result.code);
478
+ return;
479
+ }
480
+ entry.leaseToken = void 0;
481
+ entry.leaseUntil = void 0;
482
+ entry.lastErrorCode = safeFailureCode(result.code);
483
+ await recordFailure(transaction, result.code);
484
+ entry.nextAttemptAt = retryAt(now, entry.attempts);
485
+ await transaction.put(key, entry);
486
+ }
487
+ async stats() {
488
+ return this.storage.transaction(async (transaction) => ({
489
+ ...emptyStats(),
490
+ ...await transaction.get(outboxStatsKey)
491
+ }));
492
+ }
493
+ async health(now) {
494
+ return this.storage.transaction(async (transaction) => {
495
+ let pendingRows = 0;
496
+ let pendingBytes = 0;
497
+ let earliestRetryAt;
498
+ const failureCounts = {};
499
+ for (const { key, value } of await transaction.list("outbox:pending:")) {
500
+ if (value.expiresAt <= now) {
501
+ await transaction.delete(key);
502
+ await incrementStat(transaction, "expired");
503
+ continue;
504
+ }
505
+ pendingRows += 1;
506
+ pendingBytes += value.body.byteLength;
507
+ const due = Math.max(value.nextAttemptAt, value.leaseUntil ?? 0);
508
+ earliestRetryAt = earliestRetryAt === void 0 ? due : Math.min(earliestRetryAt, due);
509
+ if (value.lastErrorCode) failureCounts[value.lastErrorCode] = (failureCounts[value.lastErrorCode] ?? 0) + 1;
510
+ }
511
+ return {
512
+ pendingRows,
513
+ pendingBytes,
514
+ ...earliestRetryAt === void 0 ? {} : { earliestRetryAt },
515
+ failureCounts
516
+ };
517
+ });
518
+ }
519
+ async nextDue(now) {
520
+ return this.storage.transaction((transaction) => this.nextDueTransaction(transaction, now));
521
+ }
522
+ async nextDueTransaction(transaction, now) {
523
+ const entries = await transaction.list("outbox:pending:");
524
+ let earliest;
525
+ for (const { key, value } of entries) {
526
+ if (value.expiresAt <= now) {
527
+ await transaction.delete(key);
528
+ await incrementStat(transaction, "expired");
529
+ continue;
530
+ }
531
+ const due = Math.max(value.nextAttemptAt, value.leaseUntil ?? 0);
532
+ earliest = earliest === void 0 ? due : Math.min(earliest, due);
533
+ }
534
+ for (const { key, value } of await transaction.list("outbox:completed:")) if (value.expiresAt <= now) await transaction.delete(key);
535
+ else earliest = earliest === void 0 ? value.expiresAt : Math.min(earliest, value.expiresAt);
536
+ return earliest;
537
+ }
538
+ };
539
+ async function boundedResponseText(response) {
540
+ const declared = Number(response.headers.get("content-length"));
541
+ if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) return void 0;
542
+ if (!response.body) return "";
543
+ const reader = response.body.getReader();
544
+ const chunks = [];
545
+ let bytes = 0;
546
+ while (true) {
547
+ const next = await reader.read();
548
+ if (next.done) break;
549
+ bytes += next.value.byteLength;
550
+ if (bytes > MAX_RESPONSE_BYTES) {
551
+ await reader.cancel();
552
+ return;
553
+ }
554
+ chunks.push(next.value);
555
+ }
556
+ const joined = new Uint8Array(bytes);
557
+ let offset = 0;
558
+ for (const chunk of chunks) {
559
+ joined.set(chunk, offset);
560
+ offset += chunk.byteLength;
561
+ }
562
+ return new TextDecoder().decode(joined);
563
+ }
564
+ async function classifyDeliveryResponse(response) {
565
+ if (response.status !== 200) {
566
+ if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403 || response.status === 408 || response.status === 429 || response.status >= 500) return {
567
+ kind: "retry",
568
+ code: `http_${response.status}`
569
+ };
570
+ return {
571
+ kind: "reject",
572
+ code: `http_${response.status}`
573
+ };
574
+ }
575
+ const body = await boundedResponseText(response);
576
+ if (body === void 0) return {
577
+ kind: "retry",
578
+ code: "success_response_too_large"
579
+ };
580
+ if (body.length === 0) return { kind: "success" };
581
+ try {
582
+ const parsed = JSON.parse(body);
583
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
584
+ kind: "retry",
585
+ code: "invalid_success_response"
586
+ };
587
+ const keys = Object.keys(parsed);
588
+ if (keys.length === 0) return { kind: "success" };
589
+ if (keys.length !== 1 || !("partialSuccess" in parsed)) return {
590
+ kind: "retry",
591
+ code: "invalid_success_response"
592
+ };
593
+ const partial = parsed.partialSuccess;
594
+ if (typeof partial !== "object" || partial === null || Array.isArray(partial)) return {
595
+ kind: "retry",
596
+ code: "invalid_success_response"
597
+ };
598
+ const entries = Object.entries(partial);
599
+ if (entries.every(([key, value]) => key.startsWith("rejected") && typeof value === "number" && value === 0)) return { kind: "success" };
600
+ return entries.some(([key, value]) => key.startsWith("rejected") && typeof value === "number" && value > 0) || entries.length > 0 ? {
601
+ kind: "retry",
602
+ code: "partial_success"
603
+ } : { kind: "success" };
604
+ } catch {
605
+ return {
606
+ kind: "retry",
607
+ code: "invalid_success_response"
608
+ };
609
+ }
610
+ }
611
+ //#endregion
612
+ //#region src/durable-object.ts
613
+ const SESSION_RETENTION_MS = OUTBOX_RETENTION_MS;
614
+ const SESSION_MAX_BYTES = 65536;
615
+ const SESSION_MAX_ROWS = 1e3;
616
+ const SESSION_MAX_BYTES_TOTAL = 16777216;
617
+ const diagnosticsKey = "adapter:diagnostics";
618
+ const sessionKey = (traceId) => `adapter:session:${traceId}`;
619
+ var CloudflareOutboxStorage = class {
620
+ storage;
621
+ constructor(storage) {
622
+ this.storage = storage;
623
+ }
624
+ transaction(callback) {
625
+ return this.storage.transaction(async (storage) => callback(this.transactionView(storage)));
626
+ }
627
+ transactionView(storage) {
628
+ return {
629
+ get: (key) => storage.get(key),
630
+ put: async (key, value) => {
631
+ await storage.put(key, value);
632
+ },
633
+ delete: async (key) => {
634
+ await storage.delete(key);
635
+ },
636
+ list: async (prefix) => Array.from((await storage.list({ prefix })).entries()).map(([key, value]) => ({
637
+ key,
638
+ value
639
+ }))
640
+ };
641
+ }
642
+ };
643
+ /** A companion DO; it never reads or modifies an application Agent's alarm. */
644
+ var RuntypeOtlpOutbox = class extends DurableObject {
645
+ outbox;
646
+ storage;
647
+ constructor(ctx, env) {
648
+ super(ctx, env);
649
+ this.storage = new CloudflareOutboxStorage(ctx.storage);
650
+ this.outbox = new DurableOtlpOutbox(this.storage);
651
+ }
652
+ async observe(observation) {
653
+ const identity = await createRunIdentity({
654
+ agentId: this.env.RUNTYPE_AGENT_ID,
655
+ submissionId: observation.turn.submissionId,
656
+ conversationId: observation.turn.conversationId
657
+ });
658
+ const now = Date.now();
659
+ return this.ctx.storage.transaction(async (transaction) => {
660
+ const view = this.storage.transactionView(transaction);
661
+ const key = sessionKey(identity.traceId);
662
+ const stored = await view.get(key);
663
+ if (stored?.expiresAt !== void 0 && stored.expiresAt <= now) {
664
+ await this.recordDiagnostic(view, "missingSession");
665
+ return {
666
+ accepted: false,
667
+ duplicate: false,
668
+ diagnostic: "missing_session"
669
+ };
670
+ }
671
+ const live = stored;
672
+ if (!live && observation.kind !== "accepted") {
673
+ await this.recordDiagnostic(view, "missingSession");
674
+ return {
675
+ accepted: false,
676
+ duplicate: false,
677
+ diagnostic: "missing_session"
678
+ };
679
+ }
680
+ const reduction = live ? await reduceObservation(live.session, observation, now) : await beginSession(identity, observation.turn.exchangeId, now);
681
+ if (reduction.diagnostic === "identity_conflict" || reduction.diagnostic === "session_limit" || reduction.diagnostic === "missing_operation_start") {
682
+ if (live && reduction.diagnostic !== "identity_conflict") await this.markIncomplete(view, key, live);
683
+ await this.recordDiagnostic(view, this.diagnosticKey(reduction.diagnostic));
684
+ return {
685
+ accepted: false,
686
+ duplicate: false,
687
+ diagnostic: reduction.diagnostic
688
+ };
689
+ }
690
+ const expiresAt = live?.expiresAt ?? reduction.session.startedAt + SESSION_RETENTION_MS;
691
+ if (!await this.admitSession(view, key, reduction.session, expiresAt, now)) {
692
+ if (live) await this.markIncomplete(view, key, live);
693
+ await this.recordDiagnostic(view, "sessionLimit");
694
+ return {
695
+ accepted: false,
696
+ duplicate: false,
697
+ diagnostic: "session_limit"
698
+ };
699
+ }
700
+ const batch = reduction.spans.length === 0 ? void 0 : await this.batch(identity.traceId, reduction.spans, now);
701
+ if (batch) {
702
+ const receipt = await this.outbox.enqueueTransaction(view, batch, now);
703
+ if (!receipt.accepted) {
704
+ if (live) await this.markIncomplete(view, key, live);
705
+ await this.recordDiagnostic(view, "outboxRefused");
706
+ return {
707
+ accepted: false,
708
+ duplicate: false,
709
+ diagnostic: receipt.code,
710
+ queuedRows: receipt.queuedRows,
711
+ queuedBytes: receipt.queuedBytes
712
+ };
713
+ }
714
+ if (receipt.duplicate) await this.recordDiagnostic(view, "duplicate");
715
+ }
716
+ await view.put(key, {
717
+ version: 1,
718
+ expiresAt,
719
+ session: reduction.session
720
+ });
721
+ if (reduction.diagnostic) await this.recordDiagnostic(view, this.diagnosticKey(reduction.diagnostic));
722
+ else await this.recordDiagnostic(view, "accepted");
723
+ await this.armTransaction(transaction, view, now);
724
+ return {
725
+ accepted: true,
726
+ duplicate: batch === void 0,
727
+ ...reduction.diagnostic === void 0 ? {} : { diagnostic: reduction.diagnostic }
728
+ };
729
+ });
730
+ }
731
+ async diagnostics() {
732
+ const diagnostics = await this.ctx.storage.get(diagnosticsKey) ?? {
733
+ accepted: 0,
734
+ duplicate: 0,
735
+ missingSession: 0,
736
+ identityConflict: 0,
737
+ sessionLimit: 0,
738
+ missingOperationStart: 0,
739
+ incomplete: 0,
740
+ unsupported: 0,
741
+ outboxRefused: 0,
742
+ expired: 0,
743
+ rejected: 0,
744
+ pendingRows: 0,
745
+ pendingBytes: 0,
746
+ failureCounts: {}
747
+ };
748
+ const outbox = await this.outbox.stats();
749
+ const health = await this.outbox.health(Date.now());
750
+ return {
751
+ ...diagnostics,
752
+ expired: outbox.expired,
753
+ rejected: outbox.rejected,
754
+ pendingRows: health.pendingRows,
755
+ pendingBytes: health.pendingBytes,
756
+ ...health.earliestRetryAt === void 0 ? {} : { earliestRetryAt: health.earliestRetryAt },
757
+ failureCounts: outbox.failureCounts
758
+ };
759
+ }
760
+ async enqueue(batch) {
761
+ const now = Date.now();
762
+ return this.ctx.storage.transaction(async (transaction) => {
763
+ const view = this.storage.transactionView(transaction);
764
+ const receipt = await this.outbox.enqueueTransaction(view, batch, now);
765
+ if (receipt.accepted) await this.armTransaction(transaction, view, now);
766
+ return receipt;
767
+ });
768
+ }
769
+ async alarm() {
770
+ const now = Date.now();
771
+ const claimed = await this.ctx.storage.transaction(async (transaction) => {
772
+ await transaction.setAlarm(now + 1e3);
773
+ return this.outbox.claimTransaction(this.storage.transactionView(transaction), now, 1);
774
+ });
775
+ for (const delivery of claimed) await this.send(delivery);
776
+ await this.ctx.storage.transaction(async (transaction) => {
777
+ const view = this.storage.transactionView(transaction);
778
+ await this.armTransaction(transaction, view, Date.now());
779
+ });
780
+ }
781
+ async markIncomplete(transaction, key, stored) {
782
+ await transaction.put(key, {
783
+ ...stored,
784
+ session: {
785
+ ...stored.session,
786
+ telemetryIncomplete: true
787
+ }
788
+ });
789
+ }
790
+ async admitSession(transaction, key, session, expiresAt, now) {
791
+ const encoded = new TextEncoder().encode(JSON.stringify({
792
+ version: 1,
793
+ expiresAt,
794
+ session
795
+ }));
796
+ if (encoded.byteLength > SESSION_MAX_BYTES) return false;
797
+ let rows = 0;
798
+ let bytes = 0;
799
+ for (const item of await transaction.list("adapter:session:")) {
800
+ if (item.key === key) continue;
801
+ if (item.value.expiresAt <= now) {
802
+ await transaction.delete(item.key);
803
+ continue;
804
+ }
805
+ rows += 1;
806
+ bytes += new TextEncoder().encode(JSON.stringify(item.value)).byteLength;
807
+ }
808
+ return rows < SESSION_MAX_ROWS && bytes + encoded.byteLength <= SESSION_MAX_BYTES_TOTAL;
809
+ }
810
+ async batch(traceId, spans, createdAt) {
811
+ const encoded = new TextEncoder().encode(encodeTraceBatch(spans, {
812
+ agentId: this.env.RUNTYPE_AGENT_ID,
813
+ adapterVersion: version
814
+ }));
815
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", encoded.slice().buffer));
816
+ return {
817
+ version: 1,
818
+ deliveryId: `${traceId}:${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`,
819
+ body: encoded,
820
+ createdAt
821
+ };
822
+ }
823
+ diagnosticKey(diagnostic) {
824
+ if (diagnostic === "identity_conflict") return "identityConflict";
825
+ if (diagnostic === "session_limit") return "sessionLimit";
826
+ if (diagnostic === "missing_operation_start") return "missingOperationStart";
827
+ if (diagnostic === "incomplete") return "incomplete";
828
+ return "unsupported";
829
+ }
830
+ async recordDiagnostic(transaction, key) {
831
+ const current = await transaction.get(diagnosticsKey) ?? {
832
+ accepted: 0,
833
+ duplicate: 0,
834
+ missingSession: 0,
835
+ identityConflict: 0,
836
+ sessionLimit: 0,
837
+ missingOperationStart: 0,
838
+ incomplete: 0,
839
+ unsupported: 0,
840
+ outboxRefused: 0,
841
+ expired: 0,
842
+ rejected: 0,
843
+ pendingRows: 0,
844
+ pendingBytes: 0,
845
+ failureCounts: {}
846
+ };
847
+ current[key] += 1;
848
+ await transaction.put(diagnosticsKey, current);
849
+ }
850
+ async send(delivery) {
851
+ let result;
852
+ const controller = new AbortController();
853
+ const timeout = setTimeout(() => controller.abort(), 2e4);
854
+ try {
855
+ result = await classifyDeliveryResponse(await fetch(createOtlpDeliveryRequest({
856
+ baseUrl: this.env.RUNTYPE_BASE_URL,
857
+ apiKey: this.env.RUNTYPE_API_KEY,
858
+ agentId: this.env.RUNTYPE_AGENT_ID,
859
+ body: delivery.body,
860
+ signal: controller.signal
861
+ })));
862
+ } catch (error) {
863
+ result = {
864
+ kind: "retry",
865
+ code: this.networkFailureCode(error)
866
+ };
867
+ } finally {
868
+ clearTimeout(timeout);
869
+ }
870
+ await this.ctx.storage.transaction(async (transaction) => {
871
+ const view = this.storage.transactionView(transaction);
872
+ await this.outbox.settleTransaction(view, delivery, result, Date.now());
873
+ await this.armTransaction(transaction, view, Date.now());
874
+ });
875
+ }
876
+ networkFailureCode(error) {
877
+ if (error instanceof Error && error.name === "AbortError") return "network_abort_error";
878
+ if (error instanceof Error && error.name === "TypeError") return "network_type_error";
879
+ return "network_unknown_error";
880
+ }
881
+ async armTransaction(storage, view, now) {
882
+ let due = await this.outbox.nextDueTransaction(view, now);
883
+ for (const item of await view.list("adapter:session:")) if (item.value.expiresAt <= now) await view.delete(item.key);
884
+ else due = due === void 0 ? item.value.expiresAt : Math.min(due, item.value.expiresAt);
885
+ if (due === void 0) await storage.deleteAlarm();
886
+ else await storage.setAlarm(Math.max(now + 1e3, due));
887
+ }
888
+ };
889
+ //#endregion
890
+ export { RuntypeOtlpOutbox };
891
+