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