@cybernetyx1/atlasflow-runtime 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/LICENSE +18 -0
- package/README.md +56 -0
- package/dist/adapter/index.d.ts +1 -0
- package/dist/adapter/index.js +0 -0
- package/dist/channel-Dv3Hv1ee.d.ts +634 -0
- package/dist/chunk-4DU4GJ2X.js +347 -0
- package/dist/chunk-HO6QHSUS.js +85 -0
- package/dist/chunk-M4JW76IL.js +1161 -0
- package/dist/chunk-RF6W3TKJ.js +2762 -0
- package/dist/chunk-S7RZJMCF.js +47 -0
- package/dist/cloudflare/index.d.ts +109 -0
- package/dist/cloudflare/index.js +212 -0
- package/dist/command-kxrqWIH7.d.ts +204 -0
- package/dist/index-UFTgKRK4.d.ts +589 -0
- package/dist/index.d.ts +739 -0
- package/dist/index.js +965 -0
- package/dist/node/index.d.ts +65 -0
- package/dist/node/index.js +251 -0
- package/dist/providers.d.ts +40 -0
- package/dist/providers.js +26 -0
- package/dist/routing/index.d.ts +256 -0
- package/dist/routing/index.js +2184 -0
- package/package.json +68 -0
- package/schemas/persona-manifest.v1.schema.json +258 -0
|
@@ -0,0 +1,2184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
advanceWorkflow,
|
|
3
|
+
approveGate,
|
|
4
|
+
cronMatches,
|
|
5
|
+
decideToolApprovalState,
|
|
6
|
+
dispatchWorkflow,
|
|
7
|
+
genId,
|
|
8
|
+
getDefaultPersistence,
|
|
9
|
+
invokeAgent,
|
|
10
|
+
isChannelDefinition,
|
|
11
|
+
isToolApprovalRequiredError,
|
|
12
|
+
recoverRuns,
|
|
13
|
+
startWorkflow,
|
|
14
|
+
waitingGate,
|
|
15
|
+
waitingToolApproval
|
|
16
|
+
} from "../chunk-RF6W3TKJ.js";
|
|
17
|
+
import "../chunk-4DU4GJ2X.js";
|
|
18
|
+
import {
|
|
19
|
+
isJsonStreamContentType,
|
|
20
|
+
normalizeStreamContentType,
|
|
21
|
+
streamMessageBytes
|
|
22
|
+
} from "../chunk-M4JW76IL.js";
|
|
23
|
+
import "../chunk-S7RZJMCF.js";
|
|
24
|
+
import "../chunk-HO6QHSUS.js";
|
|
25
|
+
|
|
26
|
+
// src/routing/index.ts
|
|
27
|
+
import { Hono } from "hono";
|
|
28
|
+
import { streamSSE } from "hono/streaming";
|
|
29
|
+
|
|
30
|
+
// src/event-offset.ts
|
|
31
|
+
var COMPONENT_PAD = 16;
|
|
32
|
+
var ZERO_COMPONENT = "0".repeat(COMPONENT_PAD);
|
|
33
|
+
function formatEventOffset(index) {
|
|
34
|
+
if (index === -1) return "-1";
|
|
35
|
+
return `${ZERO_COMPONENT}_${String(index).padStart(COMPONENT_PAD, "0")}`;
|
|
36
|
+
}
|
|
37
|
+
function parseEventOffset(offset) {
|
|
38
|
+
if (offset === "-1") return -1;
|
|
39
|
+
if (/^-?\d+$/.test(offset)) return Number(offset);
|
|
40
|
+
const match = /^\d+_(\d+)$/.exec(offset);
|
|
41
|
+
if (!match) throw new Error(`Invalid event stream offset: ${offset}`);
|
|
42
|
+
return Number(match[1]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/routing/index.ts
|
|
46
|
+
var STREAM_OFFSET_HEADER = "Stream-Next-Offset";
|
|
47
|
+
var STREAM_CURSOR_HEADER = "Stream-Cursor";
|
|
48
|
+
var STREAM_UP_TO_DATE_HEADER = "Stream-Up-To-Date";
|
|
49
|
+
var STREAM_CLOSED_HEADER = "Stream-Closed";
|
|
50
|
+
var STREAM_SEQ_HEADER = "Stream-Seq";
|
|
51
|
+
var STREAM_TTL_HEADER = "Stream-TTL";
|
|
52
|
+
var STREAM_EXPIRES_AT_HEADER = "Stream-Expires-At";
|
|
53
|
+
var STREAM_FORKED_FROM_HEADER = "Stream-Forked-From";
|
|
54
|
+
var STREAM_FORK_OFFSET_HEADER = "Stream-Fork-Offset";
|
|
55
|
+
var STREAM_SSE_DATA_ENCODING_HEADER = "stream-sse-data-encoding";
|
|
56
|
+
var PRODUCER_ID_HEADER = "Producer-Id";
|
|
57
|
+
var PRODUCER_EPOCH_HEADER = "Producer-Epoch";
|
|
58
|
+
var PRODUCER_SEQ_HEADER = "Producer-Seq";
|
|
59
|
+
var PRODUCER_EXPECTED_SEQ_HEADER = "Producer-Expected-Seq";
|
|
60
|
+
var PRODUCER_RECEIVED_SEQ_HEADER = "Producer-Received-Seq";
|
|
61
|
+
var STREAM_EXPOSE_HEADERS = `${STREAM_OFFSET_HEADER}, ${STREAM_CURSOR_HEADER}, ${STREAM_UP_TO_DATE_HEADER}, ${STREAM_CLOSED_HEADER}, ${STREAM_SEQ_HEADER}, ${STREAM_TTL_HEADER}, ${STREAM_EXPIRES_AT_HEADER}, ${STREAM_FORKED_FROM_HEADER}, ${STREAM_FORK_OFFSET_HEADER}, ${STREAM_SSE_DATA_ENCODING_HEADER}, ${PRODUCER_ID_HEADER}, ${PRODUCER_EPOCH_HEADER}, ${PRODUCER_SEQ_HEADER}, ${PRODUCER_EXPECTED_SEQ_HEADER}, ${PRODUCER_RECEIVED_SEQ_HEADER}, etag, content-type, location`;
|
|
62
|
+
var DURABLE_STREAM_LONG_POLL_MS = 25e3;
|
|
63
|
+
var DEFAULT_SUBSCRIPTION_LEASE_TTL_MS = 3e4;
|
|
64
|
+
var MIN_SUBSCRIPTION_LEASE_TTL_MS = 1e3;
|
|
65
|
+
var MAX_SUBSCRIPTION_LEASE_TTL_MS = 6e5;
|
|
66
|
+
var RATE_LIMIT_BUCKETS = /* @__PURE__ */ new Map();
|
|
67
|
+
var nextRateLimitScope = 0;
|
|
68
|
+
function requireApiKey(options = {}) {
|
|
69
|
+
return async (c, next) => {
|
|
70
|
+
const env = requestEnv(c);
|
|
71
|
+
const expected = options.token ?? env.ATLASFLOW_API_KEY;
|
|
72
|
+
if (!expected) {
|
|
73
|
+
const open = typeof options.allowUnconfigured === "function" ? options.allowUnconfigured(env) : options.allowUnconfigured === true;
|
|
74
|
+
if (open) return next();
|
|
75
|
+
return c.json(
|
|
76
|
+
{
|
|
77
|
+
ok: false,
|
|
78
|
+
error: {
|
|
79
|
+
code: "auth_unconfigured",
|
|
80
|
+
message: "API-key auth is enabled but no token is configured. Set ATLASFLOW_API_KEY (or ATLASFLOW_MODE=local for open local development)."
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
503
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const header = c.req.header(options.header ?? "authorization") ?? "";
|
|
87
|
+
const presented = header.replace(/^Bearer\s+/i, "");
|
|
88
|
+
if (!timingSafeEqualStr(presented, expected)) {
|
|
89
|
+
const scoped = await verifyRequestScopedToken(presented, c, env, expected, options.scopedTokens);
|
|
90
|
+
if (scoped.ok) return next();
|
|
91
|
+
return c.json({ ok: false, error: { code: "unauthorized", message: "invalid or missing API key" } }, 401);
|
|
92
|
+
}
|
|
93
|
+
return next();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function mintScopedToken(options) {
|
|
97
|
+
if (!options.secret) throw new Error("mintScopedToken: secret is required");
|
|
98
|
+
const now = Math.floor((options.now?.() ?? Date.now()) / 1e3);
|
|
99
|
+
const exp = options.expiresAt instanceof Date ? Math.floor(options.expiresAt.getTime() / 1e3) : typeof options.expiresAt === "number" ? Math.floor(options.expiresAt) : now + (options.ttlSeconds ?? 300);
|
|
100
|
+
if (exp <= now) throw new Error("mintScopedToken: expiration must be in the future");
|
|
101
|
+
const claims = {
|
|
102
|
+
v: 1,
|
|
103
|
+
iat: now,
|
|
104
|
+
exp,
|
|
105
|
+
...options.subject ? { sub: options.subject } : {},
|
|
106
|
+
...options.audience ? { aud: options.audience } : {},
|
|
107
|
+
...options.scopes?.length ? { scopes: [...new Set(options.scopes)].sort() } : {},
|
|
108
|
+
...options.agents?.length ? { agents: [...new Set(options.agents)].sort() } : {},
|
|
109
|
+
...options.instanceIds?.length ? { instanceIds: [...new Set(options.instanceIds)].sort() } : {}
|
|
110
|
+
};
|
|
111
|
+
const payload = base64UrlEncodeJson(claims);
|
|
112
|
+
const signature = await signScopedToken(options.secret, payload);
|
|
113
|
+
return `afst.v1.${payload}.${signature}`;
|
|
114
|
+
}
|
|
115
|
+
function scopedTokensFromEnv(env = defaultEnv()) {
|
|
116
|
+
const enabled = (env.ATLASFLOW_BROWSER_TOKENS ?? "").toLowerCase() === "true";
|
|
117
|
+
const secret = env.ATLASFLOW_BROWSER_TOKEN_SECRET;
|
|
118
|
+
if (!enabled && !secret) return false;
|
|
119
|
+
return {
|
|
120
|
+
...secret ? { secret } : {},
|
|
121
|
+
...env.ATLASFLOW_BROWSER_TOKEN_AUDIENCE ? { audience: env.ATLASFLOW_BROWSER_TOKEN_AUDIENCE } : {}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function corsFromEnv(env = defaultEnv()) {
|
|
125
|
+
const raw = env.ATLASFLOW_CORS_ORIGINS?.trim();
|
|
126
|
+
if (!raw) return false;
|
|
127
|
+
const origins = raw === "*" ? "*" : raw.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
128
|
+
if (origins !== "*" && origins.length === 0) return false;
|
|
129
|
+
return { origins };
|
|
130
|
+
}
|
|
131
|
+
function rateLimitFromEnv(env = defaultEnv()) {
|
|
132
|
+
const enabled = (env.ATLASFLOW_RATE_LIMIT_ENABLED ?? "true").trim().toLowerCase();
|
|
133
|
+
if (["0", "false", "off", "no"].includes(enabled)) return false;
|
|
134
|
+
const windowMs = positiveInt(env.ATLASFLOW_RATE_LIMIT_WINDOW_MS, 6e4);
|
|
135
|
+
const limit = positiveInt(env.ATLASFLOW_RATE_LIMIT_RPM, 120);
|
|
136
|
+
if (limit <= 0) return false;
|
|
137
|
+
return {
|
|
138
|
+
scope: env.ATLASFLOW_RATE_LIMIT_SCOPE?.trim() || "default",
|
|
139
|
+
limit,
|
|
140
|
+
windowMs,
|
|
141
|
+
burst: positiveInt(env.ATLASFLOW_RATE_LIMIT_BURST, Math.max(1, Math.min(limit, 30))),
|
|
142
|
+
maxKeys: positiveInt(env.ATLASFLOW_RATE_LIMIT_MAX_KEYS, 1e4)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function rateLimit(source = {}) {
|
|
146
|
+
const localScope = `app:${++nextRateLimitScope}`;
|
|
147
|
+
let requestCount = 0;
|
|
148
|
+
return async (c, next) => {
|
|
149
|
+
const env = requestEnv(c);
|
|
150
|
+
const options = resolveRateLimitOptions(source, env, localScope);
|
|
151
|
+
if (!options) return next();
|
|
152
|
+
const url = new URL(c.req.url);
|
|
153
|
+
const route = rateLimitRoute(c.req.method, url.pathname);
|
|
154
|
+
if (!route) return next();
|
|
155
|
+
const tokenHash = rateLimitTokenHash(c.req.raw);
|
|
156
|
+
const input = { request: c.req.raw, url, env, route, clientIp: clientIp(c.req.raw), tokenHash };
|
|
157
|
+
const key = `${options.scope}:${options.key?.(input) ?? `${input.clientIp}:${tokenHash}:${route}`}`;
|
|
158
|
+
if (!key) return next();
|
|
159
|
+
const decision = consumeRateLimitBucket(RATE_LIMIT_BUCKETS, key, options);
|
|
160
|
+
if (!decision.ok) return rateLimitResponse(decision);
|
|
161
|
+
if (++requestCount % 500 === 0) pruneRateLimitBuckets(RATE_LIMIT_BUCKETS, options);
|
|
162
|
+
await next();
|
|
163
|
+
c.res.headers.set("x-ratelimit-limit", String(decision.limit));
|
|
164
|
+
c.res.headers.set("x-ratelimit-remaining", String(decision.remaining));
|
|
165
|
+
c.res.headers.set("x-ratelimit-reset", String(decision.resetSeconds));
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function timingSafeEqualStr(a, b) {
|
|
169
|
+
if (a.length !== b.length) return false;
|
|
170
|
+
let mismatch = 0;
|
|
171
|
+
for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
172
|
+
return mismatch === 0;
|
|
173
|
+
}
|
|
174
|
+
function requestEnv(c) {
|
|
175
|
+
const base = defaultEnv();
|
|
176
|
+
const workerEnv = c.env ?? {};
|
|
177
|
+
return { ...base, ...workerEnv };
|
|
178
|
+
}
|
|
179
|
+
function defaultEnv() {
|
|
180
|
+
return typeof process !== "undefined" && process.env ? process.env : {};
|
|
181
|
+
}
|
|
182
|
+
function positiveInt(raw, fallback) {
|
|
183
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
184
|
+
const parsed = Number(raw);
|
|
185
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
186
|
+
}
|
|
187
|
+
function resolveRateLimitOptions(source, env, fallbackScope) {
|
|
188
|
+
const resolved = typeof source === "function" ? source(env) : source;
|
|
189
|
+
if (!resolved) return false;
|
|
190
|
+
const scope = resolved.scope?.trim() || fallbackScope;
|
|
191
|
+
const limit = Math.max(1, Math.floor(resolved.limit ?? 120));
|
|
192
|
+
const windowMs = Math.max(1, Math.floor(resolved.windowMs ?? 6e4));
|
|
193
|
+
const burst = Math.max(1, Math.floor(resolved.burst ?? Math.min(limit, 30)));
|
|
194
|
+
const maxKeys = Math.max(1, Math.floor(resolved.maxKeys ?? 1e4));
|
|
195
|
+
return { scope, limit, windowMs, burst, maxKeys, key: resolved.key };
|
|
196
|
+
}
|
|
197
|
+
function rateLimitRoute(method, pathname) {
|
|
198
|
+
const normalizedMethod = method.toUpperCase();
|
|
199
|
+
if (normalizedMethod === "OPTIONS") return void 0;
|
|
200
|
+
if ((normalizedMethod === "GET" || normalizedMethod === "HEAD") && (pathname === "/" || pathname === "/health" || pathname === "/docs")) return void 0;
|
|
201
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
202
|
+
if (!parts.length) return void 0;
|
|
203
|
+
if (parts[0] === "channels") return `channels:${parts[1] ?? "*"}`;
|
|
204
|
+
if (parts[0] === "agents") return `agents:${parts[1] ?? "*"}`;
|
|
205
|
+
if (parts[0] === "sessions") return "sessions";
|
|
206
|
+
if (parts[0] === "runs") return normalizedMethod === "POST" ? `runs:${parts[1] ?? "*"}` : "runs:read";
|
|
207
|
+
if (parts[0] === "workflows") return `workflows:${parts[1] ?? "*"}`;
|
|
208
|
+
if (parts[0] === "streams") return normalizedMethod === "GET" || normalizedMethod === "HEAD" ? "streams:read" : "streams:write";
|
|
209
|
+
if (parts[0] === "admin") return "admin";
|
|
210
|
+
return `${normalizedMethod}:${parts[0]}`;
|
|
211
|
+
}
|
|
212
|
+
function clientIp(request) {
|
|
213
|
+
const cf = request.headers.get("cf-connecting-ip")?.trim();
|
|
214
|
+
if (cf) return cf;
|
|
215
|
+
const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
|
216
|
+
if (forwarded) return forwarded;
|
|
217
|
+
return request.headers.get("x-real-ip")?.trim() || "unknown";
|
|
218
|
+
}
|
|
219
|
+
function rateLimitTokenHash(request) {
|
|
220
|
+
const token = bearerTokenFromHeader(request.headers.get("authorization")) ?? request.headers.get("x-atlasflow-channel-token") ?? "";
|
|
221
|
+
return token ? shortHash(token) : "anon";
|
|
222
|
+
}
|
|
223
|
+
function bearerTokenFromHeader(value) {
|
|
224
|
+
const trimmed = value?.trim();
|
|
225
|
+
if (!trimmed) return void 0;
|
|
226
|
+
return trimmed.replace(/^Bearer\s+/i, "");
|
|
227
|
+
}
|
|
228
|
+
function shortHash(value) {
|
|
229
|
+
let hash = 2166136261;
|
|
230
|
+
for (let i = 0; i < value.length; i++) {
|
|
231
|
+
hash ^= value.charCodeAt(i);
|
|
232
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
233
|
+
}
|
|
234
|
+
return hash.toString(16).padStart(8, "0");
|
|
235
|
+
}
|
|
236
|
+
function consumeRateLimitBucket(buckets, key, options) {
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
const refillPerMs = options.limit / options.windowMs;
|
|
239
|
+
const existing = buckets.get(key);
|
|
240
|
+
const bucket = existing ?? { tokens: options.burst, updatedAt: now };
|
|
241
|
+
if (existing) {
|
|
242
|
+
const elapsed = Math.max(0, now - bucket.updatedAt);
|
|
243
|
+
bucket.tokens = Math.min(options.burst, bucket.tokens + elapsed * refillPerMs);
|
|
244
|
+
bucket.updatedAt = now;
|
|
245
|
+
}
|
|
246
|
+
if (bucket.tokens < 1) {
|
|
247
|
+
buckets.set(key, bucket);
|
|
248
|
+
const retrySeconds = Math.max(1, Math.ceil((1 - bucket.tokens) / refillPerMs / 1e3));
|
|
249
|
+
return { ok: false, limit: options.limit, retrySeconds, resetSeconds: retrySeconds };
|
|
250
|
+
}
|
|
251
|
+
bucket.tokens -= 1;
|
|
252
|
+
buckets.set(key, bucket);
|
|
253
|
+
const resetSeconds = Math.max(1, Math.ceil((options.burst - bucket.tokens) / refillPerMs / 1e3));
|
|
254
|
+
return { ok: true, limit: options.limit, remaining: Math.floor(bucket.tokens), resetSeconds };
|
|
255
|
+
}
|
|
256
|
+
function pruneRateLimitBuckets(buckets, options) {
|
|
257
|
+
if (buckets.size <= options.maxKeys) return;
|
|
258
|
+
const cutoff = Date.now() - options.windowMs * 2;
|
|
259
|
+
for (const [key, bucket] of buckets) {
|
|
260
|
+
if (bucket.updatedAt < cutoff) buckets.delete(key);
|
|
261
|
+
if (buckets.size <= options.maxKeys) return;
|
|
262
|
+
}
|
|
263
|
+
for (const key of buckets.keys()) {
|
|
264
|
+
buckets.delete(key);
|
|
265
|
+
if (buckets.size <= options.maxKeys) return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function rateLimitResponse(decision) {
|
|
269
|
+
return new Response(JSON.stringify({ ok: false, error: { code: "rate_limited", message: "Too many requests. Retry later." } }), {
|
|
270
|
+
status: 429,
|
|
271
|
+
headers: {
|
|
272
|
+
"content-type": "application/json",
|
|
273
|
+
"retry-after": String(decision.retrySeconds),
|
|
274
|
+
"x-ratelimit-limit": String(decision.limit),
|
|
275
|
+
"x-ratelimit-remaining": "0",
|
|
276
|
+
"x-ratelimit-reset": String(decision.resetSeconds)
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async function verifyRequestScopedToken(token, c, env, apiKey, source) {
|
|
281
|
+
const options = resolveScopedTokenOptions(source, env);
|
|
282
|
+
if (!options) return { ok: false, message: "scoped tokens are not enabled" };
|
|
283
|
+
const secret = options.secret ?? env.ATLASFLOW_BROWSER_TOKEN_SECRET ?? apiKey;
|
|
284
|
+
if (!secret) return { ok: false, message: "scoped token secret is not configured" };
|
|
285
|
+
const claims = await verifyScopedToken(token, secret, options);
|
|
286
|
+
if (!claims.ok) return claims;
|
|
287
|
+
const access = scopedTokenAllowsRequest(claims.claims, c);
|
|
288
|
+
return access.ok ? { ok: true } : { ok: false, message: access.message };
|
|
289
|
+
}
|
|
290
|
+
function resolveScopedTokenOptions(source, env) {
|
|
291
|
+
const resolved = typeof source === "function" ? source(env) : source;
|
|
292
|
+
if (!resolved) return false;
|
|
293
|
+
if (resolved === true) return {};
|
|
294
|
+
return resolved;
|
|
295
|
+
}
|
|
296
|
+
async function verifyScopedToken(token, secret, options) {
|
|
297
|
+
const parts = token.split(".");
|
|
298
|
+
if (parts.length !== 4 || parts[0] !== "afst" || parts[1] !== "v1") return { ok: false, message: "not an AtlasFlow scoped token" };
|
|
299
|
+
const payload = parts[2];
|
|
300
|
+
const signature = parts[3];
|
|
301
|
+
const expected = await signScopedToken(secret, payload);
|
|
302
|
+
if (!timingSafeEqualStr(signature, expected)) return { ok: false, message: "invalid scoped token signature" };
|
|
303
|
+
const claims = parseScopedTokenPayload(payload);
|
|
304
|
+
if (!claims) return { ok: false, message: "invalid scoped token payload" };
|
|
305
|
+
const now = Math.floor((options.now?.() ?? Date.now()) / 1e3);
|
|
306
|
+
const leeway = options.leewaySeconds ?? 15;
|
|
307
|
+
if (claims.exp + leeway < now) return { ok: false, message: "scoped token expired" };
|
|
308
|
+
if (options.audience && claims.aud !== options.audience) return { ok: false, message: "scoped token audience mismatch" };
|
|
309
|
+
return { ok: true, claims };
|
|
310
|
+
}
|
|
311
|
+
function parseScopedTokenPayload(payload) {
|
|
312
|
+
try {
|
|
313
|
+
const raw = JSON.parse(base64UrlDecodeText(payload));
|
|
314
|
+
if (raw.v !== 1 || typeof raw.exp !== "number" || !Number.isFinite(raw.exp)) return void 0;
|
|
315
|
+
for (const key of ["scopes", "agents", "instanceIds"]) {
|
|
316
|
+
const value = raw[key];
|
|
317
|
+
if (value !== void 0 && (!Array.isArray(value) || value.some((item) => typeof item !== "string"))) return void 0;
|
|
318
|
+
}
|
|
319
|
+
if (raw.sub !== void 0 && typeof raw.sub !== "string") return void 0;
|
|
320
|
+
if (raw.aud !== void 0 && typeof raw.aud !== "string") return void 0;
|
|
321
|
+
if (raw.iat !== void 0 && typeof raw.iat !== "number") return void 0;
|
|
322
|
+
return raw;
|
|
323
|
+
} catch {
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function scopedTokenAllowsRequest(claims, c) {
|
|
328
|
+
const route = scopedRequest(c);
|
|
329
|
+
if (!route) return { ok: false, message: "scoped token cannot access this route" };
|
|
330
|
+
const scopes = new Set(claims.scopes ?? []);
|
|
331
|
+
if (!scopes.has("*") && !route.scopes.some((scope) => scopes.has(scope))) {
|
|
332
|
+
return { ok: false, message: `scoped token lacks ${route.scopes[0]} access` };
|
|
333
|
+
}
|
|
334
|
+
if (route.agent && claims.agents?.length && !claims.agents.includes(route.agent)) {
|
|
335
|
+
return { ok: false, message: `scoped token cannot access agent ${route.agent}` };
|
|
336
|
+
}
|
|
337
|
+
if (route.instanceId && claims.instanceIds?.length && !claims.instanceIds.includes(route.instanceId)) {
|
|
338
|
+
return { ok: false, message: `scoped token cannot access instance ${route.instanceId}` };
|
|
339
|
+
}
|
|
340
|
+
return { ok: true };
|
|
341
|
+
}
|
|
342
|
+
function scopedRequest(c) {
|
|
343
|
+
const url = new URL(c.req.url);
|
|
344
|
+
const method = c.req.method.toUpperCase();
|
|
345
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
346
|
+
if (parts[0] === "agents") {
|
|
347
|
+
if (parts.length === 1 && method === "GET") return { scopes: ["agents:list"] };
|
|
348
|
+
if (parts.length >= 3 && method === "POST") {
|
|
349
|
+
return { scopes: ["agents:invoke"], agent: decodeURIComponent(parts[1]), instanceId: decodeURIComponent(parts[2]) };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (parts[0] === "runs") {
|
|
353
|
+
if (parts.length === 2 && method === "POST") return { scopes: ["runs:write"], agent: decodeURIComponent(parts[1]) };
|
|
354
|
+
if (parts.length === 2 && method === "GET") return { scopes: ["runs:read"] };
|
|
355
|
+
if (parts.length === 3 && parts[2] === "events" && (method === "GET" || method === "HEAD")) return { scopes: ["runs:read"] };
|
|
356
|
+
if (parts.length === 3 && parts[2] === "approve" && method === "POST") return { scopes: ["runs:approve"] };
|
|
357
|
+
}
|
|
358
|
+
if (parts[0] === "workflows" && parts.length === 2 && method === "POST") return { scopes: ["runs:write"], agent: decodeURIComponent(parts[1]) };
|
|
359
|
+
if (parts[0] === "streams") {
|
|
360
|
+
if (method === "GET" || method === "HEAD") return { scopes: ["streams:read"] };
|
|
361
|
+
if (method === "PUT" || method === "POST" || method === "DELETE") return { scopes: ["streams:write"] };
|
|
362
|
+
}
|
|
363
|
+
if (parts[0] === "admin") return { scopes: ["admin:*"] };
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
async function signScopedToken(secret, payload) {
|
|
367
|
+
const bytes = new TextEncoder().encode(secret);
|
|
368
|
+
const data = new TextEncoder().encode(payload);
|
|
369
|
+
const subtle = webCryptoSubtle();
|
|
370
|
+
const key = await subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
371
|
+
const signature = await subtle.sign("HMAC", key, data);
|
|
372
|
+
return base64UrlEncodeBytes(new Uint8Array(signature));
|
|
373
|
+
}
|
|
374
|
+
function webCryptoSubtle() {
|
|
375
|
+
const candidate = globalThis.crypto?.subtle;
|
|
376
|
+
if (!candidate) throw new Error("Scoped browser tokens require Web Crypto support.");
|
|
377
|
+
return candidate;
|
|
378
|
+
}
|
|
379
|
+
function base64UrlEncodeJson(value) {
|
|
380
|
+
return base64UrlEncodeBytes(new TextEncoder().encode(JSON.stringify(value)));
|
|
381
|
+
}
|
|
382
|
+
function base64UrlEncodeBytes(bytes) {
|
|
383
|
+
let binary = "";
|
|
384
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
385
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
|
|
386
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
387
|
+
}
|
|
388
|
+
function base64UrlDecodeText(value) {
|
|
389
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
390
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
391
|
+
const binary = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("binary");
|
|
392
|
+
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
|
|
393
|
+
return new TextDecoder().decode(bytes);
|
|
394
|
+
}
|
|
395
|
+
function corsMiddleware(source) {
|
|
396
|
+
return async (c, next) => {
|
|
397
|
+
const cors = resolveCorsOptions(source, requestEnv(c));
|
|
398
|
+
if (!cors) return next();
|
|
399
|
+
const headers = corsResponseHeaders(c, cors);
|
|
400
|
+
if (c.req.method.toUpperCase() === "OPTIONS") {
|
|
401
|
+
return new Response(null, { status: headers ? 204 : 403, headers: headers ?? void 0 });
|
|
402
|
+
}
|
|
403
|
+
await next();
|
|
404
|
+
if (!headers) return;
|
|
405
|
+
for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function resolveCorsOptions(source, env) {
|
|
409
|
+
return typeof source === "function" ? source(env) ?? false : source;
|
|
410
|
+
}
|
|
411
|
+
function corsResponseHeaders(c, options) {
|
|
412
|
+
const origin = c.req.header("origin");
|
|
413
|
+
if (!origin) return void 0;
|
|
414
|
+
const allowOrigin = allowedCorsOrigin(origin, options);
|
|
415
|
+
if (!allowOrigin) return void 0;
|
|
416
|
+
const requestedHeaders = c.req.header("access-control-request-headers");
|
|
417
|
+
const headers = {
|
|
418
|
+
"access-control-allow-origin": allowOrigin,
|
|
419
|
+
"access-control-allow-methods": (options.methods ?? ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"]).join(", "),
|
|
420
|
+
"access-control-allow-headers": requestedHeaders ?? (options.headers ?? defaultCorsHeaders()).join(", "),
|
|
421
|
+
"access-control-expose-headers": [.../* @__PURE__ */ new Set([STREAM_EXPOSE_HEADERS, ...options.exposeHeaders ?? []])].join(", "),
|
|
422
|
+
vary: "Origin"
|
|
423
|
+
};
|
|
424
|
+
if (options.credentials) headers["access-control-allow-credentials"] = "true";
|
|
425
|
+
if (options.maxAgeSeconds !== void 0) headers["access-control-max-age"] = String(options.maxAgeSeconds);
|
|
426
|
+
return headers;
|
|
427
|
+
}
|
|
428
|
+
function allowedCorsOrigin(origin, options) {
|
|
429
|
+
if (options.origins === "*") return options.credentials ? origin : "*";
|
|
430
|
+
return options.origins.includes(origin) ? origin : void 0;
|
|
431
|
+
}
|
|
432
|
+
function defaultCorsHeaders() {
|
|
433
|
+
return [
|
|
434
|
+
"authorization",
|
|
435
|
+
"content-type",
|
|
436
|
+
"last-event-id",
|
|
437
|
+
STREAM_SEQ_HEADER,
|
|
438
|
+
STREAM_CLOSED_HEADER,
|
|
439
|
+
STREAM_TTL_HEADER,
|
|
440
|
+
STREAM_EXPIRES_AT_HEADER,
|
|
441
|
+
STREAM_FORKED_FROM_HEADER,
|
|
442
|
+
STREAM_FORK_OFFSET_HEADER,
|
|
443
|
+
PRODUCER_ID_HEADER,
|
|
444
|
+
PRODUCER_EPOCH_HEADER,
|
|
445
|
+
PRODUCER_SEQ_HEADER,
|
|
446
|
+
PRODUCER_EXPECTED_SEQ_HEADER
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
function keepAlive(c, promise) {
|
|
450
|
+
try {
|
|
451
|
+
c.executionCtx?.waitUntil?.(promise);
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function mountChannels(app, channels, ctx) {
|
|
456
|
+
for (const [name, channel] of Object.entries(channels)) {
|
|
457
|
+
const path = channelPath(name, channel);
|
|
458
|
+
const methods = channelMethods(channel);
|
|
459
|
+
for (const method of methods) {
|
|
460
|
+
app.on(method, path, (c) => dispatchChannel(c, name, channel, ctx));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function dispatchChannel(c, name, channel, ctx) {
|
|
465
|
+
const result = await channel.handle({
|
|
466
|
+
channel: name,
|
|
467
|
+
request: c.req.raw,
|
|
468
|
+
url: new URL(c.req.url),
|
|
469
|
+
env: requestEnv(c),
|
|
470
|
+
json: () => safeJsonValue(c.req.raw),
|
|
471
|
+
text: () => c.req.text()
|
|
472
|
+
});
|
|
473
|
+
if (result instanceof Response) return result;
|
|
474
|
+
const envelope = channelEnvelope(result);
|
|
475
|
+
if (!envelope.ok) return c.json({ ok: false, error: { code: "bad_channel_result", message: envelope.message } }, 500);
|
|
476
|
+
const dispatch = envelope.dispatch;
|
|
477
|
+
const agentName = dispatch.agent ?? ctx.defaultAgentName;
|
|
478
|
+
if (!agentName) return c.json({ ok: false, error: { code: "ambiguous_agent", message: "Channel dispatch requires an agent when more than one agent is deployed." } }, 400);
|
|
479
|
+
const agent = ctx.agents[agentName];
|
|
480
|
+
if (!agent) return c.json({ ok: false, error: { code: "unknown_agent", message: `No agent "${agentName}"` } }, 404);
|
|
481
|
+
const images = promptImages(dispatch.images);
|
|
482
|
+
if (!images.ok) return c.json({ ok: false, error: { code: "bad_request", message: images.message } }, 400);
|
|
483
|
+
const common = {
|
|
484
|
+
agent,
|
|
485
|
+
agentName,
|
|
486
|
+
instanceId: dispatch.continuationToken,
|
|
487
|
+
runId: dispatch.runId,
|
|
488
|
+
message: dispatch.message,
|
|
489
|
+
payload: dispatch.payload,
|
|
490
|
+
images: images.value,
|
|
491
|
+
persistence: ctx.persistence,
|
|
492
|
+
env: requestEnv(c),
|
|
493
|
+
...ctx.shared
|
|
494
|
+
};
|
|
495
|
+
if (dispatch.mode === "sync") {
|
|
496
|
+
try {
|
|
497
|
+
const run = await invokeAgent({ ...common, signal: c.req.raw.signal });
|
|
498
|
+
return c.json({ ok: true, channel: name, continuationToken: dispatch.continuationToken, runId: run.runId, data: run.text, result: run.data, usage: run.usage });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
if (isToolApprovalRequiredError(err)) return toolApprovalWaitingResponse(c, err);
|
|
501
|
+
return c.json({ ok: false, ...errorPayload(err) }, 500);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const { runId, done } = await dispatchWorkflow(common);
|
|
505
|
+
keepAlive(c, done);
|
|
506
|
+
return c.json(
|
|
507
|
+
{
|
|
508
|
+
ok: true,
|
|
509
|
+
channel: name,
|
|
510
|
+
continuationToken: dispatch.continuationToken,
|
|
511
|
+
runId,
|
|
512
|
+
status: "queued",
|
|
513
|
+
...channelResponseFields(envelope.response),
|
|
514
|
+
...streamCoordinates(c, runId)
|
|
515
|
+
},
|
|
516
|
+
channelStatus(envelope.status)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
function routeContext(agents, persistence, shared, defaultEnv2) {
|
|
520
|
+
const resolve = (name, opts = {}) => {
|
|
521
|
+
const agent = agents[name];
|
|
522
|
+
if (!agent) throw new Error(`No agent "${name}"`);
|
|
523
|
+
return {
|
|
524
|
+
agent,
|
|
525
|
+
agentName: name,
|
|
526
|
+
instanceId: opts.instanceId ?? opts.runId ?? genId("run"),
|
|
527
|
+
message: opts.message,
|
|
528
|
+
payload: opts.payload,
|
|
529
|
+
env: opts.env ?? defaultEnv2,
|
|
530
|
+
signal: opts.signal,
|
|
531
|
+
runId: opts.runId,
|
|
532
|
+
onEvent: opts.onEvent,
|
|
533
|
+
persistence,
|
|
534
|
+
...shared
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
return {
|
|
538
|
+
agents,
|
|
539
|
+
persistence,
|
|
540
|
+
invoke: (name, opts) => invokeAgent(resolve(name, opts)),
|
|
541
|
+
dispatch: (name, opts) => dispatchWorkflow(resolve(name, opts))
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function createApp(options) {
|
|
545
|
+
const app = new Hono();
|
|
546
|
+
const persistence = options.persistence ?? getDefaultPersistence();
|
|
547
|
+
const agents = options.agents;
|
|
548
|
+
const personas = options.personas ?? options.subagents;
|
|
549
|
+
const shared = { baseInstructions: options.baseInstructions, sharedPersonas: personas, engine: options.engine, durability: options.durability };
|
|
550
|
+
const workflows = normalizeWorkflows(options.workflows);
|
|
551
|
+
const channels = normalizeChannels(options.channels);
|
|
552
|
+
const defaultAgentName = singleAgentName(agents);
|
|
553
|
+
const workflowCtx = (env, mount) => buildWorkflowCtx(options, persistence, env, mount);
|
|
554
|
+
app.onError(
|
|
555
|
+
(err, c) => c.json({ ok: false, error: { code: "internal_error", message: err instanceof Error ? err.message : String(err) } }, 500)
|
|
556
|
+
);
|
|
557
|
+
if (options.cors) app.use("*", corsMiddleware(options.cors));
|
|
558
|
+
if (options.rateLimit) app.use("*", rateLimit(options.rateLimit));
|
|
559
|
+
options.routes?.(app, routeContext(agents, persistence, shared));
|
|
560
|
+
mountChannels(app, channels, { agents, persistence, shared, defaultAgentName });
|
|
561
|
+
app.get("/", (c) => c.json({ name: "atlasflow", agents: Object.keys(agents), docs: "/docs" }));
|
|
562
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
563
|
+
app.get(
|
|
564
|
+
"/docs",
|
|
565
|
+
(c) => c.json({
|
|
566
|
+
name: "AtlasFlow HTTP API",
|
|
567
|
+
version: 1,
|
|
568
|
+
auth: "Bearer token in the `authorization` header (ATLASFLOW_API_KEY). /health and /docs are open; everything else is gated.",
|
|
569
|
+
endpoints: [
|
|
570
|
+
{ method: "GET", path: "/health", auth: false, returns: '{ "status": "ok" }' },
|
|
571
|
+
{ method: "GET", path: "/docs", auth: false, returns: "this document" },
|
|
572
|
+
{ method: "GET", path: "/agents", auth: true, returns: "{ items: [{ name, triggers }], workflows: [name], channels: [name] } \u2014 every persona/agent, authored workflow, and ingress channel deployed here" },
|
|
573
|
+
{
|
|
574
|
+
method: "POST",
|
|
575
|
+
path: "/channels/:name",
|
|
576
|
+
auth: false,
|
|
577
|
+
body: "channel-specific webhook/event body",
|
|
578
|
+
returns: "202 { runId, continuationToken } for background dispatch, or 200 { runId, data, result, usage } when the channel requests sync mode. Channel handlers perform their own platform signature verification."
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
method: "POST",
|
|
582
|
+
path: "/agents/:name/:id",
|
|
583
|
+
auth: true,
|
|
584
|
+
body: '{ "message"?: string, "payload"?: any, "images"?: [{ "data": "base64-or-data-url", "mime_type"?: "image/png" }] }',
|
|
585
|
+
modes: {
|
|
586
|
+
sync: "default \u2014 returns { ok, runId, data (final text), result (structured), usage }",
|
|
587
|
+
stream: 'send header "accept: text/event-stream" \u2014 Server-Sent Events (run_start, text_delta, tool_call, rule_triggered, ... then a terminal `result` event)',
|
|
588
|
+
background: 'send header "x-webhook: true" \u2014 returns 202 { runId }; poll /admin/runs/:runId'
|
|
589
|
+
},
|
|
590
|
+
notes: ":id is the conversation instance \u2014 reuse it to continue a session, change it for a fresh one"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
method: "POST",
|
|
594
|
+
path: "/sessions/:id/messages",
|
|
595
|
+
auth: true,
|
|
596
|
+
body: '{ "message"?: string, "payload"?: any, "images"?: [{ "data": "base64-or-data-url", "mime_type"?: "image/png" }] }',
|
|
597
|
+
returns: "Single-agent alias for /agents/:name/:id. Available only when this deployment has exactly one agent."
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
method: "POST",
|
|
601
|
+
path: "/runs?wait=result",
|
|
602
|
+
auth: true,
|
|
603
|
+
body: '{ "message"?: string, "payload"?: any, "images"?: [{ "data": "base64-or-data-url", "mime_type"?: "image/png" }], "wait"?: boolean }',
|
|
604
|
+
returns: "Single-agent alias for /runs/:name. Available only when this deployment has exactly one agent."
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
method: "POST",
|
|
608
|
+
path: "/runs/:name?wait=result",
|
|
609
|
+
auth: true,
|
|
610
|
+
body: '{ "message"?: string, "payload"?: any, "images"?: [{ "data": "base64-or-data-url", "mime_type"?: "image/png" }], "wait"?: boolean }',
|
|
611
|
+
returns: '202 { runId, status: "queued" } for background dispatch, 202 { runId, status: "waiting_approval", approval } when an approval-marked tool parks, or 200 { runId, data, result, usage } when wait mode completes. For an authored workflow name: { runId, status: "waiting_approval", gate } when parked at a human gate. Steps read the payload as {{steps.payload.text}}.'
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
method: "GET",
|
|
615
|
+
path: "/runs/:runId?meta",
|
|
616
|
+
auth: true,
|
|
617
|
+
returns: "RunRecord: { runId, agent, status, result, error, kind, state, images? }"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
method: "GET/HEAD",
|
|
621
|
+
path: "/runs/:runId/events?offset=<opaque>&follow=true",
|
|
622
|
+
auth: true,
|
|
623
|
+
returns: "AtlasFlow envelope JSON with ?format=atlas, legacy SSE with Accept: text/event-stream, or Durable Streams read metadata/body when called as a plain stream endpoint. Reconnect with Last-Event-ID or ?offset=<event.offset>; numeric ?after=N remains supported for legacy callers."
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
method: "PUT/GET/HEAD/POST/DELETE",
|
|
627
|
+
path: "/streams/*",
|
|
628
|
+
auth: true,
|
|
629
|
+
returns: "Writable Durable Streams-compatible public streams. Create with PUT, duplicate creates return 409 CONFLICT_EXISTS, fork with Stream-Forked-From/Stream-Fork-Offset, append or close with POST, coordinate writers with Stream-Seq or Producer-* headers, read with ?offset=<opaque>, inspect with HEAD, and delete with DELETE. Pull-wake and HTTPS-only webhook subscriptions live under /streams/__ds/subscriptions/*; webhook callbacks block localhost/private destinations, DNS resolutions to private addresses, and sensitive headers by default. Run event streams remain framework-owned and read-only."
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
method: "POST",
|
|
633
|
+
path: "/runs/:runId/approve",
|
|
634
|
+
auth: true,
|
|
635
|
+
body: '{ "approved": boolean, "gate"?: string, "approval"?: string, "note"?: string }',
|
|
636
|
+
returns: "{ runId, status, data?, approval? } \u2014 records the human decision on a workflow gate or approval-marked tool; approval resumes execution"
|
|
637
|
+
},
|
|
638
|
+
{ method: "GET", path: "/admin/runs?limit=N", auth: true, returns: "{ items: RunRecord[] } \u2014 newest first" },
|
|
639
|
+
{ method: "GET", path: "/admin/runs/:runId", auth: true, returns: "RunRecord: { runId, agent, status (queued|running|waiting_approval|success|failed|interrupted), result, error, kind, state }" },
|
|
640
|
+
{ method: "GET", path: "/admin/runs/:runId/events", auth: true, returns: "{ items: AtlasEvent[] } \u2014 the full event log: tool_call/tool_result, rule_triggered, turn usage, task/skill activity" },
|
|
641
|
+
{ method: "GET", path: "/admin/metrics", auth: true, returns: "{ ok, metrics } when an in-process metrics observer is configured" }
|
|
642
|
+
],
|
|
643
|
+
sdk: "npm: @cybernetyx1/atlasflow-sdk \u2014 createAtlasFlowClient({ baseUrl, apiKey })"
|
|
644
|
+
})
|
|
645
|
+
);
|
|
646
|
+
if (options.auth) {
|
|
647
|
+
app.use("/agents/*", options.auth);
|
|
648
|
+
app.use("/sessions/*", options.auth);
|
|
649
|
+
app.use("/runs", options.auth);
|
|
650
|
+
app.use("/runs/*", options.auth);
|
|
651
|
+
app.use("/workflows/*", options.auth);
|
|
652
|
+
app.use("/admin/*", options.auth);
|
|
653
|
+
app.use("/streams/*", options.auth);
|
|
654
|
+
}
|
|
655
|
+
app.get(
|
|
656
|
+
"/agents",
|
|
657
|
+
(c) => c.json({
|
|
658
|
+
items: Object.keys(agents).map((name) => ({ name, triggers: options.triggers?.[name] ?? { webhook: true } })),
|
|
659
|
+
workflows: Object.keys(workflows),
|
|
660
|
+
channels: Object.keys(channels)
|
|
661
|
+
})
|
|
662
|
+
);
|
|
663
|
+
const invokeInteractive = async (c, name, id) => {
|
|
664
|
+
if (!name) return c.json({ ok: false, error: { code: "ambiguous_agent", message: "This route is only available when exactly one agent is deployed." } }, 400);
|
|
665
|
+
const agent = agents[name];
|
|
666
|
+
if (!agent) return c.json({ ok: false, error: { code: "unknown_agent", message: `No agent "${name}"` } }, 404);
|
|
667
|
+
const body = await safeJson(c.req.raw);
|
|
668
|
+
const message = typeof body?.message === "string" ? body.message : void 0;
|
|
669
|
+
const images = promptImages(body?.images);
|
|
670
|
+
if (!images.ok) return c.json({ ok: false, error: { code: "bad_request", message: images.message } }, 400);
|
|
671
|
+
const common = { agent, agentName: name, instanceId: id, message, payload: body?.payload, images: images.value, persistence, env: requestEnv(c), ...shared };
|
|
672
|
+
if (c.req.header("x-webhook") === "true") {
|
|
673
|
+
const { runId, done } = await dispatchWorkflow(common);
|
|
674
|
+
keepAlive(c, done);
|
|
675
|
+
return c.json({ ok: true, runId, status: "queued", ...streamCoordinates(c, runId) }, 202);
|
|
676
|
+
}
|
|
677
|
+
if (c.req.header("accept")?.includes("text/event-stream")) {
|
|
678
|
+
return streamSSE(c, async (stream) => {
|
|
679
|
+
const abort = new AbortController();
|
|
680
|
+
stream.onAbort(() => abort.abort(new Error("client disconnected")));
|
|
681
|
+
const queue = [];
|
|
682
|
+
let done = false;
|
|
683
|
+
let wake;
|
|
684
|
+
const invocation = invokeAgent({ ...common, signal: abort.signal, onEvent: (e) => {
|
|
685
|
+
queue.push(e);
|
|
686
|
+
wake?.();
|
|
687
|
+
} }).finally(() => {
|
|
688
|
+
done = true;
|
|
689
|
+
wake?.();
|
|
690
|
+
});
|
|
691
|
+
while (!done || queue.length > 0) {
|
|
692
|
+
const e = queue.shift();
|
|
693
|
+
if (e) await stream.writeSSE({ event: e.type, data: JSON.stringify(e) });
|
|
694
|
+
else await new Promise((r) => wake = r);
|
|
695
|
+
}
|
|
696
|
+
const result = await invocation.catch((err) => ({ error: errorPayload(err).error }));
|
|
697
|
+
await stream.writeSSE({ event: "result", data: JSON.stringify({ type: "result", ...result }) });
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const result = await invokeAgent({ ...common, signal: c.req.raw.signal });
|
|
702
|
+
return c.json({ ok: true, runId: result.runId, data: result.text, result: result.data, usage: result.usage });
|
|
703
|
+
} catch (err) {
|
|
704
|
+
if (isToolApprovalRequiredError(err)) return toolApprovalWaitingResponse(c, err);
|
|
705
|
+
return c.json({ ok: false, ...errorPayload(err) }, 500);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
app.post("/agents/:name/:id", (c) => invokeInteractive(c, c.req.param("name"), c.req.param("id")));
|
|
709
|
+
app.post("/sessions/:id/messages", (c) => invokeInteractive(c, defaultAgentName, c.req.param("id")));
|
|
710
|
+
const dispatchRun = async (c) => {
|
|
711
|
+
const name = c.req.param("name") ?? defaultAgentName;
|
|
712
|
+
if (!name) return c.json({ ok: false, error: { code: "ambiguous_agent", message: "POST /runs is only available when exactly one agent is deployed. Use /runs/:name instead." } }, 400);
|
|
713
|
+
const body = await safeJson(c.req.raw);
|
|
714
|
+
const mount = workflows[name];
|
|
715
|
+
if (mount) {
|
|
716
|
+
const result = await startWorkflow(mount.def, workflowCtx(requestEnv(c), mount), { payload: body?.payload });
|
|
717
|
+
return c.json(
|
|
718
|
+
{ ok: result.status !== "failed", runId: result.runId, status: result.status, ...streamCoordinates(c, result.runId), ...result.gate ? { gate: result.gate } : {} },
|
|
719
|
+
result.status === "failed" ? 500 : 202
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const agent = agents[name];
|
|
723
|
+
if (!agent) return c.json({ ok: false, error: { code: "unknown_agent", message: `No agent or workflow "${name}"` } }, 404);
|
|
724
|
+
const message = typeof body?.message === "string" ? body.message : void 0;
|
|
725
|
+
const images = promptImages(body?.images);
|
|
726
|
+
if (!images.ok) return c.json({ ok: false, error: { code: "bad_request", message: images.message } }, 400);
|
|
727
|
+
const waitQuery = c.req.query("wait");
|
|
728
|
+
const wait = body?.wait === true || waitQuery === "result" || waitQuery === "true";
|
|
729
|
+
const runId = genId("run");
|
|
730
|
+
const common = {
|
|
731
|
+
agent,
|
|
732
|
+
agentName: name,
|
|
733
|
+
instanceId: runId,
|
|
734
|
+
runId,
|
|
735
|
+
payload: body?.payload,
|
|
736
|
+
images: images.value,
|
|
737
|
+
message,
|
|
738
|
+
persistence,
|
|
739
|
+
env: requestEnv(c),
|
|
740
|
+
...shared
|
|
741
|
+
};
|
|
742
|
+
if (wait) {
|
|
743
|
+
try {
|
|
744
|
+
const result = await invokeAgent(common);
|
|
745
|
+
return c.json({ ok: true, runId: result.runId, result: result.data, data: result.text, usage: result.usage });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
if (isToolApprovalRequiredError(err)) return toolApprovalWaitingResponse(c, err);
|
|
748
|
+
return c.json({ ok: false, ...errorPayload(err) }, 500);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const { done } = await dispatchWorkflow(common);
|
|
752
|
+
keepAlive(c, done);
|
|
753
|
+
return c.json({ ok: true, runId, status: "queued", ...streamCoordinates(c, runId) }, 202);
|
|
754
|
+
};
|
|
755
|
+
app.post("/runs", dispatchRun);
|
|
756
|
+
app.post("/runs/:name", dispatchRun);
|
|
757
|
+
app.post("/workflows/:name", dispatchRun);
|
|
758
|
+
app.get("/runs/:runId", async (c) => {
|
|
759
|
+
const run = await persistence.runs.get(c.req.param("runId"));
|
|
760
|
+
return run ? c.json(run) : c.json({ ok: false, error: { code: "not_found", message: "no such run" } }, 404);
|
|
761
|
+
});
|
|
762
|
+
app.on("HEAD", "/runs/:runId/events", async (c) => {
|
|
763
|
+
const runId = c.req.param("runId");
|
|
764
|
+
const run = await persistence.runs.get(runId);
|
|
765
|
+
if (!run) return new Response(null, { status: 404, headers: { "content-type": "application/json" } });
|
|
766
|
+
const events = (await persistence.runs.events(runId)).sort((a, b) => a.index - b.index);
|
|
767
|
+
const offset = tailOffset(events);
|
|
768
|
+
const closed = isEventStreamClosed(run, events);
|
|
769
|
+
return new Response(null, {
|
|
770
|
+
status: 200,
|
|
771
|
+
headers: eventStreamHeaders({ runId, startOffset: "-1", offset, upToDate: true, closed, cursor: streamCursor(runId, offset, closed), head: true })
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
app.get("/runs/:runId/events", async (c) => {
|
|
775
|
+
const runId = c.req.param("runId");
|
|
776
|
+
let run = await persistence.runs.get(runId);
|
|
777
|
+
if (!run) return c.json({ ok: false, error: { code: "not_found", message: "no such run" } }, 404);
|
|
778
|
+
let allEvents = (await persistence.runs.events(runId)).sort((a, b) => a.index - b.index);
|
|
779
|
+
if (c.req.method === "HEAD") {
|
|
780
|
+
const offset = tailOffset(allEvents);
|
|
781
|
+
const closed2 = isEventStreamClosed(run, allEvents);
|
|
782
|
+
return new Response(null, {
|
|
783
|
+
status: 200,
|
|
784
|
+
headers: eventStreamHeaders({ runId, startOffset: "-1", offset, upToDate: true, closed: closed2, cursor: streamCursor(runId, offset, closed2), head: true })
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
const cursor = eventCursor(c, runId, allEvents);
|
|
788
|
+
if (!cursor.ok) return c.json({ ok: false, error: cursor.error }, 400);
|
|
789
|
+
const after = cursor.value;
|
|
790
|
+
const limit = eventLimit(c);
|
|
791
|
+
const durableMode = durableStreamMode(c);
|
|
792
|
+
const live = c.req.query("live");
|
|
793
|
+
if ((live === "long-poll" || live === "sse") && c.req.query("offset") == null && c.req.query("cursor") == null && c.req.header("last-event-id") == null) {
|
|
794
|
+
return durableMode ? new Response("live reads require offset", { status: 400, headers: { "content-type": "text/plain" } }) : c.json({ ok: false, error: { code: "bad_request", message: "live reads require offset" } }, 400);
|
|
795
|
+
}
|
|
796
|
+
if (durableMode && live === "sse") return streamDurableEvents(c, persistence, run, allEvents, after);
|
|
797
|
+
if (durableMode && live === "long-poll" && isCaughtUp(after, allEvents)) {
|
|
798
|
+
let closed2 = isEventStreamClosed(run, allEvents);
|
|
799
|
+
let timedOut = false;
|
|
800
|
+
if (!closed2) {
|
|
801
|
+
const waited = await waitForRunEvents(persistence, runId, after, longPollTimeoutMs(c));
|
|
802
|
+
run = waited.run ?? run;
|
|
803
|
+
allEvents = waited.events;
|
|
804
|
+
closed2 = isEventStreamClosed(run, allEvents);
|
|
805
|
+
timedOut = waited.timedOut;
|
|
806
|
+
}
|
|
807
|
+
if (timedOut || closed2 && isCaughtUp(after, allEvents)) {
|
|
808
|
+
const offset = tailOffset(allEvents, after);
|
|
809
|
+
return new Response(null, {
|
|
810
|
+
status: 204,
|
|
811
|
+
headers: eventStreamHeaders({ runId, startOffset: formatEventOffset(after), offset, upToDate: true, closed: closed2, cursor: streamCursor(runId, offset, closed2) })
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const closed = isEventStreamClosed(run, allEvents);
|
|
816
|
+
if (durableMode) {
|
|
817
|
+
const { items, nextOffset, upToDate } = selectEventsAfter(allEvents, after, { limit, tail: eventTail(c) });
|
|
818
|
+
return jsonResponse(items.map(withOffset), {
|
|
819
|
+
headers: eventStreamHeaders({ runId, startOffset: formatEventOffset(after), offset: nextOffset, upToDate, closed: closed && upToDate, cursor: live === "long-poll" ? streamCursor(runId, nextOffset, closed && upToDate) : void 0 })
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (!c.req.header("accept")?.includes("text/event-stream")) {
|
|
823
|
+
const { items, nextOffset, upToDate } = selectEventsAfter(allEvents, after, { limit, tail: eventTail(c) });
|
|
824
|
+
return jsonResponse(
|
|
825
|
+
{ items: items.map(withOffset), nextOffset, upToDate, closed },
|
|
826
|
+
{ headers: eventStreamHeaders({ runId, startOffset: formatEventOffset(after), offset: nextOffset, upToDate, closed: closed && upToDate }) }
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
const follow = c.req.query("follow") !== "false";
|
|
830
|
+
return streamSSE(c, async (stream) => {
|
|
831
|
+
let cursor2 = after;
|
|
832
|
+
let aborted = false;
|
|
833
|
+
let terminalPolls = 0;
|
|
834
|
+
stream.onAbort(() => {
|
|
835
|
+
aborted = true;
|
|
836
|
+
});
|
|
837
|
+
for (; ; ) {
|
|
838
|
+
const events = (await persistence.runs.events(runId)).filter((e) => e.index > cursor2).sort((a, b) => a.index - b.index);
|
|
839
|
+
for (const event of events) {
|
|
840
|
+
cursor2 = event.index;
|
|
841
|
+
await stream.writeSSE({ id: formatEventOffset(event.index), event: event.type, data: JSON.stringify(withOffset(event)) });
|
|
842
|
+
}
|
|
843
|
+
const latest = await persistence.runs.get(runId);
|
|
844
|
+
if (aborted || !follow || !latest) break;
|
|
845
|
+
const sawTerminalEvent = events.some((event) => event.type === "run_end" || event.type === "run_error");
|
|
846
|
+
if (latest.status !== "running" && latest.status !== "queued") {
|
|
847
|
+
if (sawTerminalEvent || terminalPolls++ >= 3) break;
|
|
848
|
+
await routeSleep(50);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
terminalPolls = 0;
|
|
852
|
+
await routeSleep(250);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
app.put("/streams/__ds/subscriptions/:subscriptionId", async (c) => {
|
|
857
|
+
const subscriptions = persistence.subscriptions;
|
|
858
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
859
|
+
const input = await subscriptionCreateInput(c);
|
|
860
|
+
if (!input.ok) return subscriptionJsonError(400, "INVALID_REQUEST", input.message);
|
|
861
|
+
const result = await subscriptions.createOrConfirm(c.req.param("subscriptionId"), input.value);
|
|
862
|
+
if (!result.ok) return subscriptionStoreError(result);
|
|
863
|
+
return c.json(subscriptionResponse(result.value.subscription), result.value.created ? 201 : 200);
|
|
864
|
+
});
|
|
865
|
+
app.get("/streams/__ds/subscriptions/:subscriptionId", async (c) => {
|
|
866
|
+
const subscriptions = persistence.subscriptions;
|
|
867
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
868
|
+
const subscription = await subscriptions.get(c.req.param("subscriptionId"));
|
|
869
|
+
return subscription ? c.json(subscriptionResponse(subscription)) : subscriptionJsonError(404, "SUBSCRIPTION_NOT_FOUND", "Subscription not found");
|
|
870
|
+
});
|
|
871
|
+
app.delete("/streams/__ds/subscriptions/:subscriptionId", async (c) => {
|
|
872
|
+
const subscriptions = persistence.subscriptions;
|
|
873
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
874
|
+
await subscriptions.delete(c.req.param("subscriptionId"));
|
|
875
|
+
return new Response(null, { status: 204 });
|
|
876
|
+
});
|
|
877
|
+
app.post("/streams/__ds/subscriptions/:subscriptionId/streams", async (c) => {
|
|
878
|
+
const subscriptions = persistence.subscriptions;
|
|
879
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
880
|
+
const body = await readJsonObject(c.req.raw);
|
|
881
|
+
const streams = body?.streams;
|
|
882
|
+
if (!Array.isArray(streams) || streams.length === 0 || streams.some((stream) => typeof stream !== "string" || stream.length === 0)) {
|
|
883
|
+
return subscriptionJsonError(400, "INVALID_REQUEST", "streams must be a non-empty string array");
|
|
884
|
+
}
|
|
885
|
+
const ok = await subscriptions.addExplicitStreams(c.req.param("subscriptionId"), streams);
|
|
886
|
+
return ok ? new Response(null, { status: 204 }) : subscriptionJsonError(404, "SUBSCRIPTION_NOT_FOUND", "Subscription not found");
|
|
887
|
+
});
|
|
888
|
+
app.delete("/streams/__ds/subscriptions/:subscriptionId/streams/*", async (c) => {
|
|
889
|
+
const subscriptions = persistence.subscriptions;
|
|
890
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
891
|
+
const streamPath = subscriptionStreamRoutePath(c);
|
|
892
|
+
const ok = await subscriptions.removeExplicitStream(c.req.param("subscriptionId"), streamPath);
|
|
893
|
+
return ok ? new Response(null, { status: 204 }) : subscriptionJsonError(404, "SUBSCRIPTION_NOT_FOUND", "Subscription not found");
|
|
894
|
+
});
|
|
895
|
+
app.post("/streams/__ds/subscriptions/:subscriptionId/claim", async (c) => {
|
|
896
|
+
const subscriptions = persistence.subscriptions;
|
|
897
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
898
|
+
const body = await readJsonObject(c.req.raw);
|
|
899
|
+
const worker = body?.worker;
|
|
900
|
+
if (typeof worker !== "string" || worker.length === 0) {
|
|
901
|
+
return subscriptionJsonError(400, "INVALID_REQUEST", "worker must be a non-empty string");
|
|
902
|
+
}
|
|
903
|
+
const result = await subscriptions.claim(c.req.param("subscriptionId"), worker);
|
|
904
|
+
if (!result.ok) return subscriptionStoreError(result);
|
|
905
|
+
return c.json(subscriptionClaimResponse(result.value), 200);
|
|
906
|
+
});
|
|
907
|
+
app.post("/streams/__ds/subscriptions/:subscriptionId/ack", async (c) => {
|
|
908
|
+
const subscriptions = persistence.subscriptions;
|
|
909
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
910
|
+
const token = bearerToken(c);
|
|
911
|
+
if (!token) return subscriptionJsonError(401, "TOKEN_INVALID", "Missing or malformed Authorization header");
|
|
912
|
+
const input = await readJsonObject(c.req.raw);
|
|
913
|
+
const result = await subscriptions.ack(c.req.param("subscriptionId"), token, subscriptionAckInput(input));
|
|
914
|
+
if (!result.ok) return subscriptionStoreError(result);
|
|
915
|
+
return c.json({ ok: true, next_wake: result.value.nextWake }, 200);
|
|
916
|
+
});
|
|
917
|
+
app.post("/streams/__ds/subscriptions/:subscriptionId/release", async (c) => {
|
|
918
|
+
const subscriptions = persistence.subscriptions;
|
|
919
|
+
if (!subscriptions) return subscriptionJsonError(501, "INVALID_REQUEST", "Durable Streams subscriptions are not configured");
|
|
920
|
+
const token = bearerToken(c);
|
|
921
|
+
if (!token) return subscriptionJsonError(401, "TOKEN_INVALID", "Missing or malformed Authorization header");
|
|
922
|
+
const input = await readJsonObject(c.req.raw);
|
|
923
|
+
const result = await subscriptions.release(c.req.param("subscriptionId"), token, subscriptionAckInput(input));
|
|
924
|
+
if (!result.ok) return subscriptionStoreError(result);
|
|
925
|
+
return new Response(null, { status: 204 });
|
|
926
|
+
});
|
|
927
|
+
app.on("HEAD", "/streams/*", async (c) => {
|
|
928
|
+
const path = publicStreamPath(c);
|
|
929
|
+
if (!path.ok) return streamText(path.message, path.status);
|
|
930
|
+
const record = await persistence.streams.get(path.value);
|
|
931
|
+
if (!record) return streamText("Stream not found", 404);
|
|
932
|
+
return new Response(null, {
|
|
933
|
+
status: 200,
|
|
934
|
+
headers: publicStreamHeaders({
|
|
935
|
+
path: path.value,
|
|
936
|
+
contentType: record.contentType,
|
|
937
|
+
startOffset: "-1",
|
|
938
|
+
offset: record.currentOffset,
|
|
939
|
+
upToDate: true,
|
|
940
|
+
closed: record.closed,
|
|
941
|
+
cursor: publicStreamCursorToken(path.value, record.currentOffset, record.closed),
|
|
942
|
+
head: true,
|
|
943
|
+
ttlSeconds: record.ttlSeconds,
|
|
944
|
+
expiresAt: record.expiresAt
|
|
945
|
+
})
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
app.put("/streams/*", async (c) => {
|
|
949
|
+
const path = publicStreamPath(c);
|
|
950
|
+
if (!path.ok) return streamText(path.message, path.status);
|
|
951
|
+
const existing = await persistence.streams.get(path.value);
|
|
952
|
+
if (existing) {
|
|
953
|
+
return streamJsonError(
|
|
954
|
+
"CONFLICT_EXISTS",
|
|
955
|
+
"Stream already exists",
|
|
956
|
+
409,
|
|
957
|
+
publicStreamHeaders({
|
|
958
|
+
path: path.value,
|
|
959
|
+
contentType: existing.contentType,
|
|
960
|
+
startOffset: "-1",
|
|
961
|
+
offset: existing.currentOffset,
|
|
962
|
+
upToDate: true,
|
|
963
|
+
closed: existing.closed,
|
|
964
|
+
ttlSeconds: existing.ttlSeconds,
|
|
965
|
+
expiresAt: existing.expiresAt
|
|
966
|
+
})
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const parsed = streamCreateHeaders(c);
|
|
970
|
+
if (!parsed.ok) return streamText(parsed.message, 400);
|
|
971
|
+
const fork = streamForkHeaders(c);
|
|
972
|
+
if (!fork.ok) return streamText(fork.message, 400);
|
|
973
|
+
const body = new Uint8Array(await c.req.raw.arrayBuffer());
|
|
974
|
+
try {
|
|
975
|
+
const { record, created } = await persistence.streams.create(path.value, {
|
|
976
|
+
contentType: parsed.contentType,
|
|
977
|
+
ttlSeconds: parsed.ttlSeconds,
|
|
978
|
+
expiresAt: parsed.expiresAt,
|
|
979
|
+
initialData: body.length ? body : void 0,
|
|
980
|
+
closed: parsed.closed,
|
|
981
|
+
fork: fork.fork
|
|
982
|
+
});
|
|
983
|
+
if (!created) {
|
|
984
|
+
return streamJsonError(
|
|
985
|
+
"CONFLICT_EXISTS",
|
|
986
|
+
"Stream already exists",
|
|
987
|
+
409,
|
|
988
|
+
publicStreamHeaders({
|
|
989
|
+
path: path.value,
|
|
990
|
+
contentType: record.contentType,
|
|
991
|
+
startOffset: "-1",
|
|
992
|
+
offset: record.currentOffset,
|
|
993
|
+
upToDate: true,
|
|
994
|
+
closed: record.closed,
|
|
995
|
+
ttlSeconds: record.ttlSeconds,
|
|
996
|
+
expiresAt: record.expiresAt
|
|
997
|
+
})
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (body.length > 0) {
|
|
1001
|
+
const messages = await persistence.streams.messages(path.value);
|
|
1002
|
+
await persistence.subscriptions?.notifyStreamAppend(path.value, messages[messages.length - 1]);
|
|
1003
|
+
}
|
|
1004
|
+
const headers = publicStreamHeaders({
|
|
1005
|
+
path: path.value,
|
|
1006
|
+
contentType: record.contentType,
|
|
1007
|
+
startOffset: "-1",
|
|
1008
|
+
offset: record.currentOffset,
|
|
1009
|
+
upToDate: true,
|
|
1010
|
+
closed: record.closed,
|
|
1011
|
+
ttlSeconds: record.ttlSeconds,
|
|
1012
|
+
expiresAt: record.expiresAt
|
|
1013
|
+
});
|
|
1014
|
+
if (created) headers.location = new URL(`/streams/${encodePublicStreamPath(path.value)}`, c.req.url).toString();
|
|
1015
|
+
return new Response(null, { status: created ? 201 : 200, headers });
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
return streamError(err);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
app.get("/streams/*", async (c) => {
|
|
1021
|
+
const path = publicStreamPath(c);
|
|
1022
|
+
if (!path.ok) return streamText(path.message, path.status);
|
|
1023
|
+
let record = await persistence.streams.get(path.value);
|
|
1024
|
+
if (!record) return streamText("Stream not found", 404);
|
|
1025
|
+
let messages = await persistence.streams.messages(path.value);
|
|
1026
|
+
if (c.req.method === "HEAD") {
|
|
1027
|
+
return new Response(null, {
|
|
1028
|
+
status: 200,
|
|
1029
|
+
headers: publicStreamHeaders({
|
|
1030
|
+
path: path.value,
|
|
1031
|
+
contentType: record.contentType,
|
|
1032
|
+
startOffset: "-1",
|
|
1033
|
+
offset: record.currentOffset,
|
|
1034
|
+
upToDate: true,
|
|
1035
|
+
closed: record.closed,
|
|
1036
|
+
cursor: publicStreamCursorToken(path.value, record.currentOffset, record.closed),
|
|
1037
|
+
head: true,
|
|
1038
|
+
ttlSeconds: record.ttlSeconds,
|
|
1039
|
+
expiresAt: record.expiresAt
|
|
1040
|
+
})
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
const cursor = publicStreamCursor(c, record, path.value);
|
|
1044
|
+
if (!cursor.ok) return streamText(cursor.message, 400);
|
|
1045
|
+
const after = cursor.value;
|
|
1046
|
+
const live = c.req.query("live");
|
|
1047
|
+
if ((live === "long-poll" || live === "sse") && c.req.query("offset") == null && c.req.query("cursor") == null && c.req.header("last-event-id") == null) {
|
|
1048
|
+
return streamText("live reads require offset", 400);
|
|
1049
|
+
}
|
|
1050
|
+
if (live === "sse") return streamPublicDurableStream(c, persistence, path.value, record, messages, after);
|
|
1051
|
+
if (live === "long-poll" && isStreamCaughtUp(after, messages)) {
|
|
1052
|
+
let timedOut = false;
|
|
1053
|
+
if (!record.closed) {
|
|
1054
|
+
const waited = await waitForPublicStreamMessages(persistence, path.value, after, longPollTimeoutMs(c));
|
|
1055
|
+
record = waited.record ?? record;
|
|
1056
|
+
messages = waited.messages;
|
|
1057
|
+
timedOut = waited.timedOut;
|
|
1058
|
+
}
|
|
1059
|
+
if (timedOut || record.closed && isStreamCaughtUp(after, messages)) {
|
|
1060
|
+
const offset = publicStreamTailOffset(messages, after);
|
|
1061
|
+
return new Response(null, {
|
|
1062
|
+
status: 204,
|
|
1063
|
+
headers: publicStreamHeaders({
|
|
1064
|
+
path: path.value,
|
|
1065
|
+
contentType: record.contentType,
|
|
1066
|
+
startOffset: formatEventOffset(after),
|
|
1067
|
+
offset,
|
|
1068
|
+
upToDate: true,
|
|
1069
|
+
closed: record.closed,
|
|
1070
|
+
cursor: publicStreamCursorToken(path.value, offset, record.closed),
|
|
1071
|
+
ttlSeconds: record.ttlSeconds,
|
|
1072
|
+
expiresAt: record.expiresAt
|
|
1073
|
+
})
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const { items, nextOffset, upToDate } = selectStreamMessagesAfter(messages, after, eventLimit(c));
|
|
1078
|
+
return new Response(publicStreamBody(record, items), {
|
|
1079
|
+
status: 200,
|
|
1080
|
+
headers: publicStreamHeaders({
|
|
1081
|
+
path: path.value,
|
|
1082
|
+
contentType: record.contentType,
|
|
1083
|
+
startOffset: formatEventOffset(after),
|
|
1084
|
+
offset: nextOffset,
|
|
1085
|
+
upToDate,
|
|
1086
|
+
closed: record.closed && upToDate,
|
|
1087
|
+
cursor: live === "long-poll" ? publicStreamCursorToken(path.value, nextOffset, record.closed && upToDate) : void 0,
|
|
1088
|
+
ttlSeconds: record.ttlSeconds,
|
|
1089
|
+
expiresAt: record.expiresAt
|
|
1090
|
+
})
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
app.post("/streams/*", async (c) => {
|
|
1094
|
+
const path = publicStreamPath(c);
|
|
1095
|
+
if (!path.ok) return streamText(path.message, path.status);
|
|
1096
|
+
const producer = streamProducerHeaders(c);
|
|
1097
|
+
if (!producer.ok) return streamText(producer.message, 400);
|
|
1098
|
+
const seq = streamSeqHeader(c);
|
|
1099
|
+
if (!seq.ok) return streamText(seq.message, 400);
|
|
1100
|
+
const close = c.req.header(STREAM_CLOSED_HEADER)?.toLowerCase() === "true";
|
|
1101
|
+
const body = new Uint8Array(await c.req.raw.arrayBuffer());
|
|
1102
|
+
const record = await persistence.streams.get(path.value);
|
|
1103
|
+
if (!record) return streamText("Stream not found", 404);
|
|
1104
|
+
if (body.length === 0 && close) {
|
|
1105
|
+
if (seq.value !== void 0) return streamText(`${STREAM_SEQ_HEADER} requires a non-empty append body`, 400);
|
|
1106
|
+
if (producer.producer) {
|
|
1107
|
+
const closed2 = await persistence.streams.closeWithProducer(path.value, producer.producer);
|
|
1108
|
+
if (!closed2) return streamText("Stream not found", 404);
|
|
1109
|
+
return streamProducerWriteResponse({ path: path.value, producer: producer.producer, result: closed2, acceptedStatus: 200 });
|
|
1110
|
+
}
|
|
1111
|
+
const closed = await persistence.streams.close(path.value);
|
|
1112
|
+
if (!closed) return streamText("Stream not found", 404);
|
|
1113
|
+
return new Response(null, {
|
|
1114
|
+
status: 204,
|
|
1115
|
+
headers: publicStreamHeaders({
|
|
1116
|
+
path: path.value,
|
|
1117
|
+
contentType: closed.contentType,
|
|
1118
|
+
startOffset: "-1",
|
|
1119
|
+
offset: closed.currentOffset,
|
|
1120
|
+
upToDate: true,
|
|
1121
|
+
closed: true,
|
|
1122
|
+
ttlSeconds: closed.ttlSeconds,
|
|
1123
|
+
expiresAt: closed.expiresAt
|
|
1124
|
+
})
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (body.length === 0) return streamText("Empty body", 400);
|
|
1128
|
+
if (record.closed && !producer.producer) {
|
|
1129
|
+
return streamText(
|
|
1130
|
+
"Stream is closed",
|
|
1131
|
+
409,
|
|
1132
|
+
publicStreamHeaders({ path: path.value, contentType: record.contentType, startOffset: "-1", offset: record.currentOffset, upToDate: true, closed: true })
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const contentType = c.req.header("content-type");
|
|
1136
|
+
if (!contentType) return streamText("Content-Type header is required", 400);
|
|
1137
|
+
try {
|
|
1138
|
+
if (producer.producer) {
|
|
1139
|
+
const appended2 = await persistence.streams.appendWithProducer(path.value, body, { contentType, close, producer: producer.producer, seq: seq.value });
|
|
1140
|
+
if (!appended2) return streamText("Stream not found", 404);
|
|
1141
|
+
if (appended2.message && appended2.producerResult?.status === "accepted") await persistence.subscriptions?.notifyStreamAppend(path.value, appended2.message);
|
|
1142
|
+
return streamProducerWriteResponse({
|
|
1143
|
+
path: path.value,
|
|
1144
|
+
producer: producer.producer,
|
|
1145
|
+
result: appended2,
|
|
1146
|
+
acceptedStatus: 200,
|
|
1147
|
+
offset: appended2.message?.offset
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
const appended = await persistence.streams.append(path.value, body, { contentType, close, seq: seq.value });
|
|
1151
|
+
if (!appended) return streamText("Stream not found", 404);
|
|
1152
|
+
await persistence.subscriptions?.notifyStreamAppend(path.value, appended.message);
|
|
1153
|
+
return new Response(null, {
|
|
1154
|
+
status: 204,
|
|
1155
|
+
headers: publicStreamHeaders({
|
|
1156
|
+
path: path.value,
|
|
1157
|
+
contentType: appended.record.contentType,
|
|
1158
|
+
startOffset: "-1",
|
|
1159
|
+
offset: appended.message.offset,
|
|
1160
|
+
upToDate: true,
|
|
1161
|
+
closed: appended.record.closed,
|
|
1162
|
+
ttlSeconds: appended.record.ttlSeconds,
|
|
1163
|
+
expiresAt: appended.record.expiresAt
|
|
1164
|
+
})
|
|
1165
|
+
});
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
const latest = await persistence.streams.get(path.value);
|
|
1168
|
+
return streamError(
|
|
1169
|
+
err,
|
|
1170
|
+
latest ? publicStreamHeaders({ path: path.value, contentType: latest.contentType, startOffset: "-1", offset: latest.currentOffset, upToDate: true, closed: latest.closed }) : void 0
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
app.delete("/streams/*", async (c) => {
|
|
1175
|
+
const path = publicStreamPath(c);
|
|
1176
|
+
if (!path.ok) return streamText(path.message, path.status);
|
|
1177
|
+
const deleted = await persistence.streams.delete(path.value);
|
|
1178
|
+
if (deleted) await persistence.subscriptions?.notifyStreamDelete(path.value);
|
|
1179
|
+
return deleted ? new Response(null, { status: 204 }) : streamText("Stream not found", 404);
|
|
1180
|
+
});
|
|
1181
|
+
app.post("/runs/:runId/approve", async (c) => {
|
|
1182
|
+
const runId = c.req.param("runId");
|
|
1183
|
+
const record = await persistence.runs.get(runId);
|
|
1184
|
+
const body = await safeJson(c.req.raw);
|
|
1185
|
+
if (!record) return c.json({ ok: false, error: { code: "not_found", message: "no such run" } }, 404);
|
|
1186
|
+
if (record.kind === "workflow") {
|
|
1187
|
+
const mount = workflows[record.agent];
|
|
1188
|
+
if (!mount) return c.json({ ok: false, error: { code: "unknown_workflow", message: `workflow "${record.agent}" is not registered` } }, 404);
|
|
1189
|
+
const gate = body?.gate ?? waitingGate(mount.def, record)?.id;
|
|
1190
|
+
if (!gate || typeof body?.approved !== "boolean") {
|
|
1191
|
+
return c.json({ ok: false, error: { code: "bad_request", message: 'body must include {"approved": true|false} (and optionally "gate", "note")' } }, 400);
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
const result = await approveGate(mount.def, workflowCtx(requestEnv(c), mount), runId, { gate, approved: body.approved, note: body?.note });
|
|
1195
|
+
return c.json({ ok: result.status !== "failed", runId, status: result.status, ...result.gate ? { gate: result.gate } : {}, ...result.text !== void 0 ? { data: result.text } : {} });
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
return c.json({ ok: false, error: { code: "approve_failed", message: err instanceof Error ? err.message : String(err) } }, 409);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const pending = waitingToolApproval(record);
|
|
1201
|
+
if (!pending) return c.json({ ok: false, error: { code: "not_waiting_approval", message: `Run "${runId}" has no pending tool approval.` } }, 409);
|
|
1202
|
+
if (typeof body?.approved !== "boolean") {
|
|
1203
|
+
return c.json({ ok: false, error: { code: "bad_request", message: 'body must include {"approved": true|false} (and optionally "approval", "note")' } }, 400);
|
|
1204
|
+
}
|
|
1205
|
+
const agent = agents[record.agent];
|
|
1206
|
+
if (!agent) return c.json({ ok: false, error: { code: "unknown_agent", message: `agent "${record.agent}" is not registered` } }, 404);
|
|
1207
|
+
try {
|
|
1208
|
+
const { state, approval } = decideToolApprovalState(record, {
|
|
1209
|
+
approval: typeof body?.approval === "string" ? body.approval : pending.id,
|
|
1210
|
+
approved: body.approved,
|
|
1211
|
+
note: body.note
|
|
1212
|
+
});
|
|
1213
|
+
if (!body.approved) {
|
|
1214
|
+
const endedAt = Date.now();
|
|
1215
|
+
await persistence.runs.update(runId, {
|
|
1216
|
+
status: "failed",
|
|
1217
|
+
state,
|
|
1218
|
+
error: { code: "tool_rejected", message: `Tool "${approval.tool}" was rejected${approval.note ? `: ${approval.note}` : "."}` },
|
|
1219
|
+
endedAt
|
|
1220
|
+
});
|
|
1221
|
+
await appendRouteEvent(persistence, runId, { type: "tool_approval_decided", approval: approval.id, tool: approval.tool, approved: false, runId, timestamp: endedAt });
|
|
1222
|
+
await appendRouteEvent(persistence, runId, { type: "run_error", code: "tool_rejected", message: `Tool "${approval.tool}" was rejected.`, runId, timestamp: endedAt });
|
|
1223
|
+
await appendRouteEvent(persistence, runId, { type: "run_end", ok: false, runId, timestamp: endedAt });
|
|
1224
|
+
return c.json({ ok: false, runId, status: "failed", approval: publicToolApproval(approval) }, 409);
|
|
1225
|
+
}
|
|
1226
|
+
await persistence.runs.update(runId, { status: "running", state, error: void 0, endedAt: void 0 });
|
|
1227
|
+
try {
|
|
1228
|
+
const result = await invokeAgent({
|
|
1229
|
+
agent,
|
|
1230
|
+
agentName: record.agent,
|
|
1231
|
+
instanceId: record.instanceId,
|
|
1232
|
+
runId,
|
|
1233
|
+
runResume: true,
|
|
1234
|
+
message: record.message,
|
|
1235
|
+
payload: record.payload,
|
|
1236
|
+
images: record.images,
|
|
1237
|
+
persistence,
|
|
1238
|
+
env: requestEnv(c),
|
|
1239
|
+
...shared
|
|
1240
|
+
});
|
|
1241
|
+
return c.json({ ok: true, runId, status: "success", data: result.text, result: result.data, usage: result.usage });
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
if (isToolApprovalRequiredError(err)) return toolApprovalWaitingResponse(c, err);
|
|
1244
|
+
return c.json({ ok: false, ...errorPayload(err) }, 500);
|
|
1245
|
+
}
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
return c.json({ ok: false, error: { code: "approve_failed", message: err instanceof Error ? err.message : String(err) } }, 409);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
app.get("/admin/agents", (c) => c.json({ items: Object.keys(agents).map((name) => ({ name })) }));
|
|
1251
|
+
app.get("/admin/runs", async (c) => {
|
|
1252
|
+
const status = runStatus(c.req.query("status"));
|
|
1253
|
+
if (status === false) return c.json({ ok: false, error: { code: "bad_request", message: "status must be queued, running, waiting_approval, success, failed, or interrupted" } }, 400);
|
|
1254
|
+
const limit = runListLimit(c);
|
|
1255
|
+
const agent = nonEmpty(c.req.query("agent"));
|
|
1256
|
+
return c.json({ items: await persistence.runs.list({ limit, ...status ? { status } : {}, ...agent ? { agent } : {} }) });
|
|
1257
|
+
});
|
|
1258
|
+
app.get("/admin/runs/:runId", async (c) => {
|
|
1259
|
+
const run = await persistence.runs.get(c.req.param("runId"));
|
|
1260
|
+
return run ? c.json(run) : c.json({ ok: false, error: { code: "not_found", message: "no such run" } }, 404);
|
|
1261
|
+
});
|
|
1262
|
+
app.get("/admin/runs/:runId/events", async (c) => {
|
|
1263
|
+
return c.json({ items: await persistence.runs.events(c.req.param("runId")) });
|
|
1264
|
+
});
|
|
1265
|
+
if (options.metrics) {
|
|
1266
|
+
app.get("/admin/metrics", (c) => c.json({ ok: true, metrics: options.metrics.snapshot() }));
|
|
1267
|
+
}
|
|
1268
|
+
return app;
|
|
1269
|
+
}
|
|
1270
|
+
async function safeJson(req) {
|
|
1271
|
+
try {
|
|
1272
|
+
return await req.json();
|
|
1273
|
+
} catch {
|
|
1274
|
+
return void 0;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
async function safeJsonValue(req) {
|
|
1278
|
+
try {
|
|
1279
|
+
return await req.json();
|
|
1280
|
+
} catch {
|
|
1281
|
+
return void 0;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function promptImages(value) {
|
|
1285
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
1286
|
+
if (!Array.isArray(value)) return { ok: false, message: "images must be an array" };
|
|
1287
|
+
const images = [];
|
|
1288
|
+
for (const [index, item] of value.entries()) {
|
|
1289
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return { ok: false, message: `images[${index}] must be an object` };
|
|
1290
|
+
const record = item;
|
|
1291
|
+
const rawData = typeof record.data === "string" ? record.data : typeof record.data_base64 === "string" ? record.data_base64 : void 0;
|
|
1292
|
+
if (!rawData) return { ok: false, message: `images[${index}].data must be a base64 string or data URL` };
|
|
1293
|
+
const parsed = parseImageData(rawData);
|
|
1294
|
+
const mimeType = parsed.mimeType ?? (typeof record.mime_type === "string" ? record.mime_type : typeof record.mimeType === "string" ? record.mimeType : void 0);
|
|
1295
|
+
if (!mimeType || !/^image\/[A-Za-z0-9.+-]+$/.test(mimeType)) return { ok: false, message: `images[${index}].mime_type must be an image MIME type` };
|
|
1296
|
+
if (!isLikelyBase64(parsed.data)) return { ok: false, message: `images[${index}].data must be base64 encoded` };
|
|
1297
|
+
images.push({ data: parsed.data, mimeType });
|
|
1298
|
+
}
|
|
1299
|
+
return { ok: true, value: images };
|
|
1300
|
+
}
|
|
1301
|
+
function parseImageData(value) {
|
|
1302
|
+
const trimmed = value.trim();
|
|
1303
|
+
const match = /^data:([^;,]+)(?:;[^,]*)?;base64,(.*)$/is.exec(trimmed);
|
|
1304
|
+
if (match) return { mimeType: match[1], data: match[2].replace(/\s/g, "") };
|
|
1305
|
+
return { data: trimmed.replace(/\s/g, "") };
|
|
1306
|
+
}
|
|
1307
|
+
function isLikelyBase64(value) {
|
|
1308
|
+
return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
1309
|
+
}
|
|
1310
|
+
async function readJsonObject(req) {
|
|
1311
|
+
try {
|
|
1312
|
+
const value = await req.json();
|
|
1313
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1314
|
+
} catch {
|
|
1315
|
+
return void 0;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function durableStreamMode(c) {
|
|
1319
|
+
const format = c.req.query("format");
|
|
1320
|
+
if (format === "atlas") return false;
|
|
1321
|
+
if (format === "stream") return true;
|
|
1322
|
+
const live = c.req.query("live");
|
|
1323
|
+
if (live === "long-poll" || live === "sse") return true;
|
|
1324
|
+
return (c.req.query("offset") != null || c.req.query("cursor") != null) && c.req.query("limit") == null && c.req.query("tail") == null && c.req.query("follow") == null && c.req.query("after") == null && c.req.query("since") == null;
|
|
1325
|
+
}
|
|
1326
|
+
function jsonResponse(body, options = {}) {
|
|
1327
|
+
return new Response(JSON.stringify(body), {
|
|
1328
|
+
status: options.status ?? 200,
|
|
1329
|
+
headers: { "content-type": "application/json", ...options.headers ?? {} }
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
function subscriptionJsonError(status, code, message, extra = {}) {
|
|
1333
|
+
return jsonResponse({ error: { code, message, ...extra } }, { status });
|
|
1334
|
+
}
|
|
1335
|
+
function subscriptionStoreError(result) {
|
|
1336
|
+
if (result.ok) throw new Error("subscriptionStoreError called with an ok result");
|
|
1337
|
+
const { code, message, currentHolder, generation } = result.error;
|
|
1338
|
+
return jsonResponse(
|
|
1339
|
+
{
|
|
1340
|
+
error: {
|
|
1341
|
+
code,
|
|
1342
|
+
message,
|
|
1343
|
+
...currentHolder !== void 0 ? { current_holder: currentHolder } : {},
|
|
1344
|
+
...generation !== void 0 ? { generation } : {}
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
{ status: result.status }
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
async function subscriptionCreateInput(c) {
|
|
1351
|
+
const body = await readJsonObject(c.req.raw);
|
|
1352
|
+
if (!body) return { ok: false, message: "Request body must be a JSON object" };
|
|
1353
|
+
if (body.type !== "pull-wake" && body.type !== "webhook") return { ok: false, message: 'type must be "pull-wake" or "webhook"' };
|
|
1354
|
+
const pattern = typeof body.pattern === "string" && body.pattern.length > 0 ? normalizeSubscriptionRoutePath(body.pattern) : void 0;
|
|
1355
|
+
const streams = Array.isArray(body.streams) && body.streams.length > 0 ? body.streams.map((stream) => typeof stream === "string" ? normalizeSubscriptionRoutePath(stream) : null) : [];
|
|
1356
|
+
if (streams.some((stream) => stream === null)) return { ok: false, message: "streams must contain only strings" };
|
|
1357
|
+
if (!pattern && streams.length === 0) return { ok: false, message: "At least one of pattern or streams is required" };
|
|
1358
|
+
if (body.type === "webhook") {
|
|
1359
|
+
const webhookUrl = typeof body.webhook_url === "string" ? body.webhook_url : typeof body.url === "string" ? body.url : void 0;
|
|
1360
|
+
if (!webhookUrl || !isHttpUrl(webhookUrl)) return { ok: false, message: "webhook subscriptions require an http(s) webhook_url" };
|
|
1361
|
+
const headers = subscriptionWebhookHeaders(body.webhook_headers ?? body.headers);
|
|
1362
|
+
if (!headers.ok) return { ok: false, message: headers.message };
|
|
1363
|
+
return {
|
|
1364
|
+
ok: true,
|
|
1365
|
+
value: {
|
|
1366
|
+
type: "webhook",
|
|
1367
|
+
...pattern ? { pattern } : {},
|
|
1368
|
+
streams,
|
|
1369
|
+
webhookUrl,
|
|
1370
|
+
...Object.keys(headers.value).length > 0 ? { webhookHeaders: headers.value } : {},
|
|
1371
|
+
...typeof body.description === "string" ? { description: body.description } : {}
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
const wakeStream = typeof body.wake_stream === "string" && body.wake_stream.length > 0 ? normalizeSubscriptionRoutePath(body.wake_stream) : void 0;
|
|
1376
|
+
if (!wakeStream) return { ok: false, message: "pull-wake subscriptions require wake_stream" };
|
|
1377
|
+
const leaseTtl = body.lease_ttl_ms === void 0 ? DEFAULT_SUBSCRIPTION_LEASE_TTL_MS : body.lease_ttl_ms;
|
|
1378
|
+
if (typeof leaseTtl !== "number" || !Number.isInteger(leaseTtl) || leaseTtl < MIN_SUBSCRIPTION_LEASE_TTL_MS || leaseTtl > MAX_SUBSCRIPTION_LEASE_TTL_MS) {
|
|
1379
|
+
return { ok: false, message: "lease_ttl_ms must be an integer from 1000 to 600000" };
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
ok: true,
|
|
1383
|
+
value: {
|
|
1384
|
+
type: "pull-wake",
|
|
1385
|
+
...pattern ? { pattern } : {},
|
|
1386
|
+
streams,
|
|
1387
|
+
wakeStream,
|
|
1388
|
+
leaseTtlMs: leaseTtl,
|
|
1389
|
+
...typeof body.description === "string" ? { description: body.description } : {}
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function subscriptionResponse(record) {
|
|
1394
|
+
return {
|
|
1395
|
+
id: record.id,
|
|
1396
|
+
subscription_id: record.id,
|
|
1397
|
+
type: record.type,
|
|
1398
|
+
pattern: record.pattern,
|
|
1399
|
+
streams: record.streams.map((stream) => ({
|
|
1400
|
+
path: stream.path,
|
|
1401
|
+
link_type: stream.linkTypes.includes("explicit") ? "explicit" : "glob",
|
|
1402
|
+
acked_offset: stream.ackedOffset
|
|
1403
|
+
})),
|
|
1404
|
+
wake_stream: record.wakeStream,
|
|
1405
|
+
lease_ttl_ms: record.leaseTtlMs,
|
|
1406
|
+
webhook_url: record.webhookUrl,
|
|
1407
|
+
webhook_last_success_at: record.webhookLastSuccessAt,
|
|
1408
|
+
webhook_last_error: record.webhookLastError,
|
|
1409
|
+
created_at: record.createdAt,
|
|
1410
|
+
status: record.status,
|
|
1411
|
+
description: record.description
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function subscriptionClaimResponse(claim) {
|
|
1415
|
+
return {
|
|
1416
|
+
wake_id: claim.wakeId,
|
|
1417
|
+
generation: claim.generation,
|
|
1418
|
+
token: claim.token,
|
|
1419
|
+
streams: claim.streams.map(subscriptionStreamInfoResponse),
|
|
1420
|
+
lease_ttl_ms: claim.leaseTtlMs
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function subscriptionStreamInfoResponse(stream) {
|
|
1424
|
+
return {
|
|
1425
|
+
path: stream.path,
|
|
1426
|
+
link_type: stream.linkType,
|
|
1427
|
+
acked_offset: stream.ackedOffset,
|
|
1428
|
+
tail_offset: stream.tailOffset,
|
|
1429
|
+
has_pending: stream.hasPending
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function subscriptionAckInput(input) {
|
|
1433
|
+
const body = input ?? {};
|
|
1434
|
+
const rawAcks = Array.isArray(body.acks) ? body.acks : void 0;
|
|
1435
|
+
return {
|
|
1436
|
+
wakeId: typeof body.wake_id === "string" ? body.wake_id : typeof body.wakeId === "string" ? body.wakeId : void 0,
|
|
1437
|
+
generation: typeof body.generation === "number" && Number.isInteger(body.generation) ? body.generation : void 0,
|
|
1438
|
+
acks: rawAcks?.map((ack) => {
|
|
1439
|
+
const item = ack && typeof ack === "object" ? ack : {};
|
|
1440
|
+
return {
|
|
1441
|
+
...typeof item.stream === "string" ? { stream: item.stream } : {},
|
|
1442
|
+
...typeof item.path === "string" ? { path: item.path } : {},
|
|
1443
|
+
offset: typeof item.offset === "string" ? item.offset : ""
|
|
1444
|
+
};
|
|
1445
|
+
}),
|
|
1446
|
+
done: body.done === true
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function bearerToken(c) {
|
|
1450
|
+
const header = c.req.header("subscription-token") ?? c.req.header("x-subscription-token") ?? c.req.header("authorization");
|
|
1451
|
+
if (!header) return null;
|
|
1452
|
+
return header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header;
|
|
1453
|
+
}
|
|
1454
|
+
function errorPayload(err) {
|
|
1455
|
+
return {
|
|
1456
|
+
error: {
|
|
1457
|
+
code: typeof err?.code === "string" ? err.code : "agent_error",
|
|
1458
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function toolApprovalWaitingResponse(c, err) {
|
|
1463
|
+
return c.json({ ok: true, runId: err.runId, status: "waiting_approval", approval: publicToolApproval(err.approval) }, 202);
|
|
1464
|
+
}
|
|
1465
|
+
function publicToolApproval(approval) {
|
|
1466
|
+
return {
|
|
1467
|
+
id: approval.id,
|
|
1468
|
+
tool: approval.tool,
|
|
1469
|
+
arguments: approval.arguments,
|
|
1470
|
+
...approval.reason ? { reason: approval.reason } : {},
|
|
1471
|
+
requestedAt: approval.requestedAt,
|
|
1472
|
+
status: approval.status
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
async function appendRouteEvent(persistence, runId, event) {
|
|
1476
|
+
const index = await persistence.runs.eventCount?.(runId) ?? (await persistence.runs.events(runId)).length;
|
|
1477
|
+
await persistence.runs.appendEvent(runId, { ...event, index, timestamp: event.timestamp ?? Date.now() });
|
|
1478
|
+
}
|
|
1479
|
+
function channelEnvelope(result) {
|
|
1480
|
+
const record = objectRecord(result);
|
|
1481
|
+
if (!record) return { ok: false, message: "channel handler must return a dispatch envelope or Response" };
|
|
1482
|
+
const dispatch = objectRecord(record.dispatch) ? record.dispatch : record;
|
|
1483
|
+
if (typeof dispatch.continuationToken !== "string" || !dispatch.continuationToken.trim()) {
|
|
1484
|
+
return { ok: false, message: "channel dispatch must include a non-empty continuationToken" };
|
|
1485
|
+
}
|
|
1486
|
+
if (dispatch.agent !== void 0 && typeof dispatch.agent !== "string") return { ok: false, message: "channel dispatch agent must be a string" };
|
|
1487
|
+
if (dispatch.message !== void 0 && typeof dispatch.message !== "string") return { ok: false, message: "channel dispatch message must be a string" };
|
|
1488
|
+
if (dispatch.runId !== void 0 && typeof dispatch.runId !== "string") return { ok: false, message: "channel dispatch runId must be a string" };
|
|
1489
|
+
if (dispatch.mode !== void 0 && dispatch.mode !== "background" && dispatch.mode !== "sync") {
|
|
1490
|
+
return { ok: false, message: "channel dispatch mode must be background or sync" };
|
|
1491
|
+
}
|
|
1492
|
+
const status = typeof record.status === "number" && Number.isInteger(record.status) ? record.status : void 0;
|
|
1493
|
+
return { ok: true, dispatch, response: "dispatch" in record ? record.response : void 0, status };
|
|
1494
|
+
}
|
|
1495
|
+
function channelResponseFields(value) {
|
|
1496
|
+
if (value === void 0) return {};
|
|
1497
|
+
const record = objectRecord(value);
|
|
1498
|
+
return record ? { response: record } : { response: value };
|
|
1499
|
+
}
|
|
1500
|
+
function channelStatus(status) {
|
|
1501
|
+
switch (status) {
|
|
1502
|
+
case 200:
|
|
1503
|
+
case 201:
|
|
1504
|
+
case 202:
|
|
1505
|
+
case 400:
|
|
1506
|
+
case 401:
|
|
1507
|
+
case 403:
|
|
1508
|
+
case 404:
|
|
1509
|
+
case 409:
|
|
1510
|
+
case 422:
|
|
1511
|
+
case 500:
|
|
1512
|
+
return status;
|
|
1513
|
+
default:
|
|
1514
|
+
return 202;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function objectRecord(value) {
|
|
1518
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1519
|
+
}
|
|
1520
|
+
function subscriptionStreamRoutePath(c) {
|
|
1521
|
+
const marker = `/streams/__ds/subscriptions/${encodeURIComponent(c.req.param("subscriptionId") ?? "")}/streams/`;
|
|
1522
|
+
const pathname = new URL(c.req.url).pathname;
|
|
1523
|
+
const raw = pathname.startsWith(marker) ? pathname.slice(marker.length) : "";
|
|
1524
|
+
return normalizeSubscriptionRoutePath(decodeURIComponent(raw));
|
|
1525
|
+
}
|
|
1526
|
+
function normalizeSubscriptionRoutePath(path) {
|
|
1527
|
+
return path.replace(/^\/+/, "").replace(/\/+$/, "").split("/").filter((part) => part.length > 0 && part !== "." && part !== "..").join("/");
|
|
1528
|
+
}
|
|
1529
|
+
function isHttpUrl(value) {
|
|
1530
|
+
try {
|
|
1531
|
+
const url = new URL(value);
|
|
1532
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
1533
|
+
} catch {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function subscriptionWebhookHeaders(value) {
|
|
1538
|
+
if (value === void 0) return { ok: true, value: {} };
|
|
1539
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, message: "webhook headers must be an object" };
|
|
1540
|
+
const headers = {};
|
|
1541
|
+
for (const [key, headerValue] of Object.entries(value)) {
|
|
1542
|
+
if (typeof headerValue !== "string") return { ok: false, message: "webhook headers must contain only string values" };
|
|
1543
|
+
if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(key)) return { ok: false, message: `Invalid webhook header name: ${key}` };
|
|
1544
|
+
headers[key] = headerValue;
|
|
1545
|
+
}
|
|
1546
|
+
return { ok: true, value: headers };
|
|
1547
|
+
}
|
|
1548
|
+
async function streamDurableEvents(c, persistence, run, initialEvents, after) {
|
|
1549
|
+
const runId = run.runId;
|
|
1550
|
+
return streamSSE(c, async (stream) => {
|
|
1551
|
+
let cursor = after;
|
|
1552
|
+
let aborted = false;
|
|
1553
|
+
let lastControlAt = 0;
|
|
1554
|
+
stream.onAbort(() => {
|
|
1555
|
+
aborted = true;
|
|
1556
|
+
});
|
|
1557
|
+
let latest = run;
|
|
1558
|
+
let knownEvents = initialEvents;
|
|
1559
|
+
for (; ; ) {
|
|
1560
|
+
const events = knownEvents.filter((event) => event.index > cursor).sort((a, b) => a.index - b.index);
|
|
1561
|
+
for (const event of events) {
|
|
1562
|
+
cursor = event.index;
|
|
1563
|
+
await stream.writeSSE({ event: "data", data: JSON.stringify(withOffset(event)) });
|
|
1564
|
+
}
|
|
1565
|
+
latest = await persistence.runs.get(runId) ?? void 0;
|
|
1566
|
+
const closed = !latest || isEventStreamClosed(latest, knownEvents);
|
|
1567
|
+
const offset = formatEventOffset(cursor);
|
|
1568
|
+
const upToDate = isCaughtUp(cursor, knownEvents);
|
|
1569
|
+
const now = Date.now();
|
|
1570
|
+
if (events.length > 0 || closed || lastControlAt === 0 || now - lastControlAt > 15e3) {
|
|
1571
|
+
const control = { streamNextOffset: offset };
|
|
1572
|
+
if (closed && upToDate) control.streamClosed = true;
|
|
1573
|
+
else {
|
|
1574
|
+
control.streamCursor = streamCursor(runId, offset, false);
|
|
1575
|
+
if (upToDate) control.upToDate = true;
|
|
1576
|
+
}
|
|
1577
|
+
await stream.writeSSE({ event: "control", data: JSON.stringify(control) });
|
|
1578
|
+
lastControlAt = now;
|
|
1579
|
+
}
|
|
1580
|
+
if (aborted || closed && upToDate) break;
|
|
1581
|
+
await routeSleep(250);
|
|
1582
|
+
knownEvents = (await persistence.runs.events(runId)).sort((a, b) => a.index - b.index);
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
async function streamPublicDurableStream(c, persistence, path, initialRecord, initialMessages, after) {
|
|
1587
|
+
const binarySse = !isSseTextCompatibleStreamContentType(initialRecord.contentType);
|
|
1588
|
+
if (binarySse) c.header(STREAM_SSE_DATA_ENCODING_HEADER, "base64");
|
|
1589
|
+
return streamSSE(c, async (stream) => {
|
|
1590
|
+
let cursor = after;
|
|
1591
|
+
let aborted = false;
|
|
1592
|
+
let lastControlAt = 0;
|
|
1593
|
+
let record = initialRecord;
|
|
1594
|
+
let messages = initialMessages;
|
|
1595
|
+
stream.onAbort(() => {
|
|
1596
|
+
aborted = true;
|
|
1597
|
+
});
|
|
1598
|
+
for (; ; ) {
|
|
1599
|
+
const next = messages.filter((message) => message.index > cursor).sort((a, b) => a.index - b.index);
|
|
1600
|
+
for (const message of next) {
|
|
1601
|
+
cursor = message.index;
|
|
1602
|
+
for (const data of publicStreamSseData(record, message, binarySse)) {
|
|
1603
|
+
await stream.writeSSE({ event: "data", data });
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
record = await persistence.streams.get(path) ?? record;
|
|
1607
|
+
const offset = formatEventOffset(cursor);
|
|
1608
|
+
const upToDate = isStreamCaughtUp(cursor, messages);
|
|
1609
|
+
const now = Date.now();
|
|
1610
|
+
if (next.length > 0 || record.closed || lastControlAt === 0 || now - lastControlAt > 15e3) {
|
|
1611
|
+
const control = { streamNextOffset: offset };
|
|
1612
|
+
if (record.closed && upToDate) control.streamClosed = true;
|
|
1613
|
+
else {
|
|
1614
|
+
control.streamCursor = publicStreamCursorToken(path, offset, false);
|
|
1615
|
+
if (upToDate) control.upToDate = true;
|
|
1616
|
+
}
|
|
1617
|
+
await stream.writeSSE({ event: "control", data: JSON.stringify(control) });
|
|
1618
|
+
lastControlAt = now;
|
|
1619
|
+
}
|
|
1620
|
+
if (aborted || record.closed && upToDate) break;
|
|
1621
|
+
await routeSleep(250);
|
|
1622
|
+
messages = await persistence.streams.messages(path);
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
async function waitForPublicStreamMessages(persistence, path, after, timeoutMs) {
|
|
1627
|
+
const deadline = Date.now() + timeoutMs;
|
|
1628
|
+
let record = await persistence.streams.get(path) ?? void 0;
|
|
1629
|
+
let messages = await persistence.streams.messages(path);
|
|
1630
|
+
while (Date.now() < deadline && record && !record.closed && isStreamCaughtUp(after, messages)) {
|
|
1631
|
+
await routeSleep(100);
|
|
1632
|
+
record = await persistence.streams.get(path) ?? void 0;
|
|
1633
|
+
messages = await persistence.streams.messages(path);
|
|
1634
|
+
}
|
|
1635
|
+
return { record, messages, timedOut: isStreamCaughtUp(after, messages) && (!record || !record.closed) };
|
|
1636
|
+
}
|
|
1637
|
+
async function waitForRunEvents(persistence, runId, after, timeoutMs) {
|
|
1638
|
+
const deadline = Date.now() + timeoutMs;
|
|
1639
|
+
let run = await persistence.runs.get(runId) ?? void 0;
|
|
1640
|
+
let events = (await persistence.runs.events(runId)).sort((a, b) => a.index - b.index);
|
|
1641
|
+
while (Date.now() < deadline && run && !isEventStreamClosed(run, events) && isCaughtUp(after, events)) {
|
|
1642
|
+
await routeSleep(100);
|
|
1643
|
+
run = await persistence.runs.get(runId) ?? void 0;
|
|
1644
|
+
events = (await persistence.runs.events(runId)).sort((a, b) => a.index - b.index);
|
|
1645
|
+
}
|
|
1646
|
+
return { run, events, timedOut: isCaughtUp(after, events) && (!run || !isEventStreamClosed(run, events)) };
|
|
1647
|
+
}
|
|
1648
|
+
function eventCursor(c, runId, events) {
|
|
1649
|
+
const cursor = c.req.query("cursor");
|
|
1650
|
+
if (cursor != null) {
|
|
1651
|
+
try {
|
|
1652
|
+
const value = parseScopedCursor(cursor, "run", runId);
|
|
1653
|
+
const rawOffset = c.req.query("offset");
|
|
1654
|
+
if (rawOffset && rawOffset !== "now" && parseEventOffset(rawOffset) !== value) {
|
|
1655
|
+
return {
|
|
1656
|
+
ok: false,
|
|
1657
|
+
error: {
|
|
1658
|
+
code: "invalid_offset",
|
|
1659
|
+
message: "event stream cursor does not match offset"
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
return { ok: true, value };
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
return {
|
|
1666
|
+
ok: false,
|
|
1667
|
+
error: {
|
|
1668
|
+
code: "invalid_offset",
|
|
1669
|
+
message: err instanceof Error ? err.message : `Invalid event stream cursor: ${cursor}`
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const raw = c.req.header("last-event-id") ?? c.req.query("offset") ?? c.req.query("after") ?? c.req.query("since");
|
|
1675
|
+
if (raw === "now") return { ok: true, value: events?.at(-1)?.index ?? -1 };
|
|
1676
|
+
if (raw == null) return { ok: true, value: -1 };
|
|
1677
|
+
try {
|
|
1678
|
+
return { ok: true, value: parseEventOffset(raw) };
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
return {
|
|
1681
|
+
ok: false,
|
|
1682
|
+
error: {
|
|
1683
|
+
code: "invalid_offset",
|
|
1684
|
+
message: err instanceof Error ? err.message : `Invalid event stream offset: ${raw}`
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function eventLimit(c) {
|
|
1690
|
+
const raw = Number(c.req.query("limit") ?? "1000");
|
|
1691
|
+
if (!Number.isFinite(raw) || raw <= 0) return 1e3;
|
|
1692
|
+
return Math.min(Math.floor(raw), 1e3);
|
|
1693
|
+
}
|
|
1694
|
+
function eventTail(c) {
|
|
1695
|
+
const raw = c.req.query("tail");
|
|
1696
|
+
if (raw == null) return void 0;
|
|
1697
|
+
const n = Number(raw);
|
|
1698
|
+
return Number.isFinite(n) && n > 0 ? Math.min(Math.floor(n), 1e3) : void 0;
|
|
1699
|
+
}
|
|
1700
|
+
function longPollTimeoutMs(c) {
|
|
1701
|
+
const raw = Number(c.req.query("timeout") ?? DURABLE_STREAM_LONG_POLL_MS);
|
|
1702
|
+
if (!Number.isFinite(raw) || raw <= 0) return DURABLE_STREAM_LONG_POLL_MS;
|
|
1703
|
+
return Math.min(Math.floor(raw), DURABLE_STREAM_LONG_POLL_MS);
|
|
1704
|
+
}
|
|
1705
|
+
function publicStreamPath(c) {
|
|
1706
|
+
const prefix = "/streams/";
|
|
1707
|
+
const encoded = new URL(c.req.url).pathname.slice(prefix.length);
|
|
1708
|
+
if (!encoded) return { ok: false, status: 400, message: "stream path is required" };
|
|
1709
|
+
let value;
|
|
1710
|
+
try {
|
|
1711
|
+
value = decodeURIComponent(encoded);
|
|
1712
|
+
} catch {
|
|
1713
|
+
return { ok: false, status: 400, message: "stream path is not valid percent-encoding" };
|
|
1714
|
+
}
|
|
1715
|
+
return validatePublicStreamPath(value);
|
|
1716
|
+
}
|
|
1717
|
+
function validatePublicStreamPath(value) {
|
|
1718
|
+
const parts = value.split("/");
|
|
1719
|
+
if (value.length > 512 || parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
1720
|
+
return { ok: false, status: 400, message: "stream path is invalid" };
|
|
1721
|
+
}
|
|
1722
|
+
if (value === "__ds" || value.startsWith("__ds/")) {
|
|
1723
|
+
return { ok: false, status: 400, message: "stream path uses a reserved prefix" };
|
|
1724
|
+
}
|
|
1725
|
+
return { ok: true, value };
|
|
1726
|
+
}
|
|
1727
|
+
function publicStreamReferencePath(raw, requestUrl) {
|
|
1728
|
+
const value = raw.trim();
|
|
1729
|
+
if (!value) return { ok: false, message: `${STREAM_FORKED_FROM_HEADER} is required` };
|
|
1730
|
+
let encoded = value;
|
|
1731
|
+
if (value.startsWith("/") || /^[a-z][a-z\d+.-]*:/i.test(value)) {
|
|
1732
|
+
let pathname;
|
|
1733
|
+
try {
|
|
1734
|
+
pathname = new URL(value, requestUrl).pathname;
|
|
1735
|
+
} catch {
|
|
1736
|
+
return { ok: false, message: `${STREAM_FORKED_FROM_HEADER} is not a valid URL` };
|
|
1737
|
+
}
|
|
1738
|
+
const marker = "/streams/";
|
|
1739
|
+
const index = pathname.indexOf(marker);
|
|
1740
|
+
if (index < 0) return { ok: false, message: `${STREAM_FORKED_FROM_HEADER} must reference a /streams/* path` };
|
|
1741
|
+
encoded = pathname.slice(index + marker.length);
|
|
1742
|
+
}
|
|
1743
|
+
let decoded;
|
|
1744
|
+
try {
|
|
1745
|
+
decoded = decodeURIComponent(encoded);
|
|
1746
|
+
} catch {
|
|
1747
|
+
return { ok: false, message: `${STREAM_FORKED_FROM_HEADER} is not valid percent-encoding` };
|
|
1748
|
+
}
|
|
1749
|
+
const path = validatePublicStreamPath(decoded);
|
|
1750
|
+
return path.ok ? path : { ok: false, message: path.message };
|
|
1751
|
+
}
|
|
1752
|
+
function encodePublicStreamPath(path) {
|
|
1753
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
1754
|
+
}
|
|
1755
|
+
function streamCreateHeaders(c) {
|
|
1756
|
+
const ttlRaw = c.req.header(STREAM_TTL_HEADER);
|
|
1757
|
+
const expiresAt = c.req.header(STREAM_EXPIRES_AT_HEADER);
|
|
1758
|
+
if (ttlRaw && expiresAt) return { ok: false, message: `Cannot specify both ${STREAM_TTL_HEADER} and ${STREAM_EXPIRES_AT_HEADER}` };
|
|
1759
|
+
let ttlSeconds;
|
|
1760
|
+
if (ttlRaw !== void 0) {
|
|
1761
|
+
if (!/^(0|[1-9]\d*)$/.test(ttlRaw)) return { ok: false, message: `Invalid ${STREAM_TTL_HEADER} value` };
|
|
1762
|
+
ttlSeconds = Number(ttlRaw);
|
|
1763
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 0) return { ok: false, message: `Invalid ${STREAM_TTL_HEADER} value` };
|
|
1764
|
+
}
|
|
1765
|
+
if (expiresAt !== void 0 && Number.isNaN(new Date(expiresAt).getTime())) {
|
|
1766
|
+
return { ok: false, message: `Invalid ${STREAM_EXPIRES_AT_HEADER} timestamp` };
|
|
1767
|
+
}
|
|
1768
|
+
return {
|
|
1769
|
+
ok: true,
|
|
1770
|
+
...c.req.header("content-type") !== void 0 ? { contentType: normalizeStreamContentType(c.req.header("content-type") ?? void 0) } : {},
|
|
1771
|
+
...ttlSeconds !== void 0 ? { ttlSeconds } : {},
|
|
1772
|
+
...expiresAt !== void 0 ? { expiresAt } : {},
|
|
1773
|
+
closed: c.req.header(STREAM_CLOSED_HEADER)?.toLowerCase() === "true"
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function streamForkHeaders(c) {
|
|
1777
|
+
const from = c.req.header(STREAM_FORKED_FROM_HEADER);
|
|
1778
|
+
const offset = c.req.header(STREAM_FORK_OFFSET_HEADER);
|
|
1779
|
+
if (from === void 0) {
|
|
1780
|
+
if (offset !== void 0) return { ok: false, message: `${STREAM_FORK_OFFSET_HEADER} requires ${STREAM_FORKED_FROM_HEADER}` };
|
|
1781
|
+
return { ok: true };
|
|
1782
|
+
}
|
|
1783
|
+
const path = publicStreamReferencePath(from, c.req.url);
|
|
1784
|
+
if (!path.ok) return path;
|
|
1785
|
+
return { ok: true, fork: { path: path.value, ...offset !== void 0 ? { offset } : {} } };
|
|
1786
|
+
}
|
|
1787
|
+
function streamProducerHeaders(c) {
|
|
1788
|
+
const producerId = c.req.header(PRODUCER_ID_HEADER);
|
|
1789
|
+
const epochRaw = c.req.header(PRODUCER_EPOCH_HEADER);
|
|
1790
|
+
const seqRaw = c.req.header(PRODUCER_SEQ_HEADER);
|
|
1791
|
+
const supplied = [producerId, epochRaw, seqRaw].filter((value) => value !== void 0).length;
|
|
1792
|
+
if (supplied === 0) return { ok: true };
|
|
1793
|
+
if (supplied !== 3) {
|
|
1794
|
+
return { ok: false, message: `${PRODUCER_ID_HEADER}, ${PRODUCER_EPOCH_HEADER}, and ${PRODUCER_SEQ_HEADER} must be supplied together` };
|
|
1795
|
+
}
|
|
1796
|
+
const id = producerId?.trim();
|
|
1797
|
+
if (!id) return { ok: false, message: `Invalid ${PRODUCER_ID_HEADER} value` };
|
|
1798
|
+
const epoch = parseProducerInteger(epochRaw, PRODUCER_EPOCH_HEADER);
|
|
1799
|
+
if (!epoch.ok) return epoch;
|
|
1800
|
+
const seq = parseProducerInteger(seqRaw, PRODUCER_SEQ_HEADER);
|
|
1801
|
+
if (!seq.ok) return seq;
|
|
1802
|
+
return { ok: true, producer: { producerId: id, epoch: epoch.value, seq: seq.value } };
|
|
1803
|
+
}
|
|
1804
|
+
function streamSeqHeader(c) {
|
|
1805
|
+
const seq = c.req.header(STREAM_SEQ_HEADER);
|
|
1806
|
+
if (seq === void 0) return { ok: true };
|
|
1807
|
+
if (seq.length === 0) return { ok: false, message: `Invalid ${STREAM_SEQ_HEADER} value` };
|
|
1808
|
+
return { ok: true, value: seq };
|
|
1809
|
+
}
|
|
1810
|
+
function parseProducerInteger(value, header) {
|
|
1811
|
+
if (value === void 0 || !/^(0|[1-9]\d*)$/.test(value)) return { ok: false, message: `Invalid ${header} value` };
|
|
1812
|
+
const parsed = Number(value);
|
|
1813
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return { ok: false, message: `Invalid ${header} value` };
|
|
1814
|
+
return { ok: true, value: parsed };
|
|
1815
|
+
}
|
|
1816
|
+
function publicStreamCursor(c, record, path) {
|
|
1817
|
+
const cursor = c.req.query("cursor");
|
|
1818
|
+
if (cursor != null) {
|
|
1819
|
+
try {
|
|
1820
|
+
const value = parseScopedCursor(cursor, "stream", encodePublicStreamPath(path));
|
|
1821
|
+
const rawOffset = c.req.query("offset");
|
|
1822
|
+
if (rawOffset && rawOffset !== "now" && parsePublicStreamOffset(rawOffset) !== value) return { ok: false, message: "stream cursor does not match offset" };
|
|
1823
|
+
return { ok: true, value };
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
return { ok: false, message: err instanceof Error ? err.message : `Invalid stream cursor: ${cursor}` };
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const raw = c.req.header("last-event-id") ?? c.req.query("offset");
|
|
1829
|
+
if (raw === "now") return { ok: true, value: parsePublicStreamOffset(record.currentOffset) };
|
|
1830
|
+
if (raw == null) return { ok: true, value: -1 };
|
|
1831
|
+
try {
|
|
1832
|
+
return { ok: true, value: parsePublicStreamOffset(raw) };
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
return { ok: false, message: err instanceof Error ? err.message : `Invalid stream offset: ${raw}` };
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
function parsePublicStreamOffset(offset) {
|
|
1838
|
+
try {
|
|
1839
|
+
return parseEventOffset(offset);
|
|
1840
|
+
} catch {
|
|
1841
|
+
throw new Error(`Invalid stream offset: ${offset}`);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function parseScopedCursor(cursor, kind, scope) {
|
|
1845
|
+
const prefix = `${kind}:${scope}:`;
|
|
1846
|
+
if (!cursor.startsWith(prefix)) throw new Error(`Invalid ${kind} cursor scope`);
|
|
1847
|
+
let offset = cursor.slice(prefix.length);
|
|
1848
|
+
if (offset.endsWith(":closed")) offset = offset.slice(0, -":closed".length);
|
|
1849
|
+
return parseEventOffset(offset);
|
|
1850
|
+
}
|
|
1851
|
+
function selectStreamMessagesAfter(messages, after, limit) {
|
|
1852
|
+
const source = messages.filter((message) => message.index > after);
|
|
1853
|
+
const items = source.slice(0, limit);
|
|
1854
|
+
const lastDelivered = items.at(-1)?.index ?? after;
|
|
1855
|
+
return { items, nextOffset: formatEventOffset(lastDelivered), upToDate: items.length >= source.length };
|
|
1856
|
+
}
|
|
1857
|
+
function publicStreamTailOffset(messages, fallback = -1) {
|
|
1858
|
+
return formatEventOffset(messages.at(-1)?.index ?? fallback);
|
|
1859
|
+
}
|
|
1860
|
+
function isStreamCaughtUp(after, messages) {
|
|
1861
|
+
return (messages.at(-1)?.index ?? -1) <= after;
|
|
1862
|
+
}
|
|
1863
|
+
function publicStreamBody(record, messages) {
|
|
1864
|
+
if (isJsonStreamContentType(record.contentType)) {
|
|
1865
|
+
return JSON.stringify(messages.flatMap(publicStreamJsonValues));
|
|
1866
|
+
}
|
|
1867
|
+
const chunks = messages.map(streamMessageBytes);
|
|
1868
|
+
const size = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
1869
|
+
const merged = new Uint8Array(size);
|
|
1870
|
+
let offset = 0;
|
|
1871
|
+
for (const chunk of chunks) {
|
|
1872
|
+
merged.set(chunk, offset);
|
|
1873
|
+
offset += chunk.byteLength;
|
|
1874
|
+
}
|
|
1875
|
+
return merged;
|
|
1876
|
+
}
|
|
1877
|
+
function publicStreamJsonValues(message) {
|
|
1878
|
+
const parsed = JSON.parse(new TextDecoder().decode(streamMessageBytes(message)));
|
|
1879
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
1880
|
+
}
|
|
1881
|
+
function publicStreamSseData(record, message, binarySse = false) {
|
|
1882
|
+
if (binarySse) return [bytesToBase64(streamMessageBytes(message))];
|
|
1883
|
+
if (isJsonStreamContentType(record.contentType)) return publicStreamJsonValues(message).map((value) => JSON.stringify(value));
|
|
1884
|
+
return [new TextDecoder().decode(streamMessageBytes(message))];
|
|
1885
|
+
}
|
|
1886
|
+
function isSseTextCompatibleStreamContentType(contentType) {
|
|
1887
|
+
const normalized = normalizeStreamContentType(contentType);
|
|
1888
|
+
return normalized === "application/json" || normalized.startsWith("text/");
|
|
1889
|
+
}
|
|
1890
|
+
function bytesToBase64(bytes) {
|
|
1891
|
+
let binary = "";
|
|
1892
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
1893
|
+
binary += String.fromCharCode(...bytes.slice(i, i + 32768));
|
|
1894
|
+
}
|
|
1895
|
+
return btoa(binary);
|
|
1896
|
+
}
|
|
1897
|
+
function publicStreamHeaders(input) {
|
|
1898
|
+
const headers = {
|
|
1899
|
+
"content-type": input.contentType,
|
|
1900
|
+
"cache-control": "no-store",
|
|
1901
|
+
"access-control-expose-headers": STREAM_EXPOSE_HEADERS,
|
|
1902
|
+
[STREAM_OFFSET_HEADER]: input.offset,
|
|
1903
|
+
etag: publicStreamEtag(input.path, input.startOffset, input.offset, input.closed)
|
|
1904
|
+
};
|
|
1905
|
+
if (input.upToDate) headers[STREAM_UP_TO_DATE_HEADER] = "true";
|
|
1906
|
+
if (input.closed) headers[STREAM_CLOSED_HEADER] = "true";
|
|
1907
|
+
if (input.cursor && !input.closed) headers[STREAM_CURSOR_HEADER] = input.cursor;
|
|
1908
|
+
if (input.ttlSeconds !== void 0) headers[STREAM_TTL_HEADER] = String(input.ttlSeconds);
|
|
1909
|
+
if (input.expiresAt !== void 0) headers[STREAM_EXPIRES_AT_HEADER] = input.expiresAt;
|
|
1910
|
+
if (input.head) headers["content-length"] = "0";
|
|
1911
|
+
return headers;
|
|
1912
|
+
}
|
|
1913
|
+
function streamProducerWriteResponse(input) {
|
|
1914
|
+
const record = input.result.record;
|
|
1915
|
+
const producerResult = input.result.producerResult;
|
|
1916
|
+
const headers = {
|
|
1917
|
+
...publicStreamHeaders({
|
|
1918
|
+
path: input.path,
|
|
1919
|
+
contentType: record.contentType,
|
|
1920
|
+
startOffset: "-1",
|
|
1921
|
+
offset: input.offset ?? record.currentOffset,
|
|
1922
|
+
upToDate: true,
|
|
1923
|
+
closed: record.closed,
|
|
1924
|
+
ttlSeconds: record.ttlSeconds,
|
|
1925
|
+
expiresAt: record.expiresAt
|
|
1926
|
+
}),
|
|
1927
|
+
...streamProducerResultHeaders(input.producer, producerResult)
|
|
1928
|
+
};
|
|
1929
|
+
switch (producerResult?.status) {
|
|
1930
|
+
case "accepted":
|
|
1931
|
+
return new Response(null, { status: input.acceptedStatus, headers });
|
|
1932
|
+
case "duplicate":
|
|
1933
|
+
return new Response(null, { status: 204, headers });
|
|
1934
|
+
case "stale_epoch":
|
|
1935
|
+
return streamText("Stale producer epoch", 403, headers);
|
|
1936
|
+
case "invalid_epoch_seq":
|
|
1937
|
+
return streamText("New producer epoch must start with sequence 0", 400, headers);
|
|
1938
|
+
case "sequence_gap":
|
|
1939
|
+
return streamText("Producer sequence gap", 409, headers);
|
|
1940
|
+
case "stream_closed":
|
|
1941
|
+
return streamText("Stream is closed", 409, { ...headers, [STREAM_CLOSED_HEADER]: "true" });
|
|
1942
|
+
default:
|
|
1943
|
+
return streamText("Producer state error", 500, headers);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
function streamProducerResultHeaders(producer, result) {
|
|
1947
|
+
if (!result) return {};
|
|
1948
|
+
switch (result.status) {
|
|
1949
|
+
case "accepted":
|
|
1950
|
+
return {
|
|
1951
|
+
[PRODUCER_EPOCH_HEADER]: String(result.proposedState.epoch),
|
|
1952
|
+
[PRODUCER_SEQ_HEADER]: String(result.proposedState.lastSeq)
|
|
1953
|
+
};
|
|
1954
|
+
case "duplicate":
|
|
1955
|
+
return {
|
|
1956
|
+
[PRODUCER_EPOCH_HEADER]: String(producer.epoch),
|
|
1957
|
+
[PRODUCER_SEQ_HEADER]: String(producer.seq)
|
|
1958
|
+
};
|
|
1959
|
+
case "stale_epoch":
|
|
1960
|
+
return { [PRODUCER_EPOCH_HEADER]: String(result.currentEpoch) };
|
|
1961
|
+
case "sequence_gap":
|
|
1962
|
+
return {
|
|
1963
|
+
[PRODUCER_EXPECTED_SEQ_HEADER]: String(result.expectedSeq),
|
|
1964
|
+
[PRODUCER_RECEIVED_SEQ_HEADER]: String(result.receivedSeq)
|
|
1965
|
+
};
|
|
1966
|
+
case "invalid_epoch_seq":
|
|
1967
|
+
case "stream_closed":
|
|
1968
|
+
return {};
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function publicStreamCursorToken(path, offset, closed) {
|
|
1972
|
+
return `stream:${encodePublicStreamPath(path)}:${offset}${closed ? ":closed" : ""}`;
|
|
1973
|
+
}
|
|
1974
|
+
function publicStreamEtag(path, startOffset, offset, closed) {
|
|
1975
|
+
return `"stream:${encodePublicStreamPath(path)}:${startOffset}:${offset}${closed ? ":closed" : ""}"`;
|
|
1976
|
+
}
|
|
1977
|
+
function streamText(text, status, headers = {}) {
|
|
1978
|
+
return new Response(text, { status, headers: { ...headers, "content-type": "text/plain" } });
|
|
1979
|
+
}
|
|
1980
|
+
function streamJsonError(code, message, status, headers = {}) {
|
|
1981
|
+
return new Response(JSON.stringify({ ok: false, error: { code, message } }), {
|
|
1982
|
+
status,
|
|
1983
|
+
headers: { ...headers, "content-type": "application/json" }
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
function streamError(err, headers) {
|
|
1987
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1988
|
+
if (message.includes("fork source not found")) return streamText("Fork source stream not found", 404, headers);
|
|
1989
|
+
if (message.includes("Fork offset is beyond source tail")) return streamText("Fork offset is beyond source tail", 409, headers);
|
|
1990
|
+
if (message.includes("already exists")) return streamJsonError("CONFLICT_EXISTS", "Stream already exists", 409, headers);
|
|
1991
|
+
if (message.includes("Content-type mismatch")) return streamText("Content-type mismatch", 409, headers);
|
|
1992
|
+
if (message.includes("stream is closed")) return streamText("Stream is closed", 409, { ...headers ?? {}, [STREAM_CLOSED_HEADER]: "true" });
|
|
1993
|
+
if (message.includes("Stream sequence conflict")) return streamText("Stream sequence conflict", 409, headers);
|
|
1994
|
+
if (message.includes("Invalid JSON")) return streamText("Invalid JSON", 400, headers);
|
|
1995
|
+
if (message.includes("Invalid Stream-Seq")) return streamText("Invalid Stream-Seq", 400, headers);
|
|
1996
|
+
if (message.includes("Empty arrays")) return streamText("Empty arrays are not allowed", 400, headers);
|
|
1997
|
+
if (message.includes("limit exceeded")) return streamText("Stream message limit exceeded", 413, headers);
|
|
1998
|
+
return streamText(message, 400, headers);
|
|
1999
|
+
}
|
|
2000
|
+
function runListLimit(c) {
|
|
2001
|
+
const raw = Number(c.req.query("limit") ?? "50");
|
|
2002
|
+
if (!Number.isFinite(raw) || raw <= 0) return 50;
|
|
2003
|
+
return Math.min(Math.floor(raw), 1e3);
|
|
2004
|
+
}
|
|
2005
|
+
function runStatus(value) {
|
|
2006
|
+
if (value == null || value === "") return void 0;
|
|
2007
|
+
if (value === "queued" || value === "running" || value === "waiting_approval" || value === "success" || value === "failed" || value === "interrupted") return value;
|
|
2008
|
+
return false;
|
|
2009
|
+
}
|
|
2010
|
+
function nonEmpty(value) {
|
|
2011
|
+
const trimmed = value?.trim();
|
|
2012
|
+
return trimmed ? trimmed : void 0;
|
|
2013
|
+
}
|
|
2014
|
+
function selectEventsAfter(events, after, opts) {
|
|
2015
|
+
const source = after === -1 && opts.tail != null ? events.slice(-opts.tail) : events.filter((event) => event.index > after);
|
|
2016
|
+
const items = source.slice(0, opts.limit);
|
|
2017
|
+
const lastDelivered = items.at(-1)?.index ?? after;
|
|
2018
|
+
return {
|
|
2019
|
+
items,
|
|
2020
|
+
nextOffset: formatEventOffset(lastDelivered),
|
|
2021
|
+
upToDate: items.length >= source.length
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
function withOffset(event) {
|
|
2025
|
+
return { ...event, offset: formatEventOffset(event.index) };
|
|
2026
|
+
}
|
|
2027
|
+
function tailOffset(events, fallback = -1) {
|
|
2028
|
+
return formatEventOffset(events.at(-1)?.index ?? fallback);
|
|
2029
|
+
}
|
|
2030
|
+
function isCaughtUp(after, events) {
|
|
2031
|
+
return (events.at(-1)?.index ?? -1) <= after;
|
|
2032
|
+
}
|
|
2033
|
+
function isRunStreamClosed(status) {
|
|
2034
|
+
return status === "success" || status === "failed" || status === "interrupted";
|
|
2035
|
+
}
|
|
2036
|
+
function isEventStreamClosed(run, events) {
|
|
2037
|
+
return isRunStreamClosed(run.status) && events.some((event) => event.type === "run_end" || event.type === "run_error");
|
|
2038
|
+
}
|
|
2039
|
+
function routeSleep(ms) {
|
|
2040
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2041
|
+
}
|
|
2042
|
+
function eventStreamHeaders(input) {
|
|
2043
|
+
const headers = {
|
|
2044
|
+
"content-type": "application/json",
|
|
2045
|
+
"cache-control": "no-store",
|
|
2046
|
+
"access-control-expose-headers": STREAM_EXPOSE_HEADERS,
|
|
2047
|
+
[STREAM_OFFSET_HEADER]: input.offset,
|
|
2048
|
+
etag: eventStreamEtag(input.runId, input.startOffset, input.offset, input.closed)
|
|
2049
|
+
};
|
|
2050
|
+
if (input.upToDate) headers[STREAM_UP_TO_DATE_HEADER] = "true";
|
|
2051
|
+
if (input.closed) headers[STREAM_CLOSED_HEADER] = "true";
|
|
2052
|
+
if (input.cursor && !input.closed) headers[STREAM_CURSOR_HEADER] = input.cursor;
|
|
2053
|
+
if (input.head) headers["content-length"] = "0";
|
|
2054
|
+
return headers;
|
|
2055
|
+
}
|
|
2056
|
+
function streamCursor(runId, offset, closed) {
|
|
2057
|
+
return `run:${runId}:${offset}${closed ? ":closed" : ""}`;
|
|
2058
|
+
}
|
|
2059
|
+
function eventStreamEtag(runId, startOffset, offset, closed) {
|
|
2060
|
+
return `"${runId}:${startOffset}:${offset}${closed ? ":closed" : ""}"`;
|
|
2061
|
+
}
|
|
2062
|
+
function streamCoordinates(c, runId) {
|
|
2063
|
+
return {
|
|
2064
|
+
streamUrl: new URL(`/runs/${encodeURIComponent(runId)}/events`, c.req.url).toString(),
|
|
2065
|
+
offset: formatEventOffset(-1)
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
function buildWorkflowCtx(options, persistence, env, mount) {
|
|
2069
|
+
return {
|
|
2070
|
+
agents: options.agents,
|
|
2071
|
+
profiles: [...mount.profiles ?? [], ...options.personas ?? options.subagents ?? []],
|
|
2072
|
+
defaultModel: mount.defaultModel,
|
|
2073
|
+
defaultPersona: mount.defaultPersona,
|
|
2074
|
+
skills: mount.skills,
|
|
2075
|
+
bindings: mount.bindings,
|
|
2076
|
+
persistence,
|
|
2077
|
+
engine: options.engine,
|
|
2078
|
+
env,
|
|
2079
|
+
baseInstructions: options.baseInstructions
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
async function recoverAppRuns(options, env) {
|
|
2083
|
+
const persistence = options.persistence ?? getDefaultPersistence();
|
|
2084
|
+
const recovered = await recoverRuns(options.agents, { persistence, engine: options.engine, env });
|
|
2085
|
+
const workflows = normalizeWorkflows(options.workflows);
|
|
2086
|
+
const running = await persistence.runs.list({ status: "running" });
|
|
2087
|
+
for (const record of running) {
|
|
2088
|
+
if (record.kind !== "workflow") continue;
|
|
2089
|
+
const mount = workflows[record.agent];
|
|
2090
|
+
if (!mount) {
|
|
2091
|
+
await persistence.runs.update(record.runId, { status: "interrupted", endedAt: Date.now() });
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
void advanceWorkflow(mount.def, buildWorkflowCtx(options, persistence, env, mount), record.runId).catch(() => {
|
|
2095
|
+
});
|
|
2096
|
+
recovered.push(record.runId);
|
|
2097
|
+
}
|
|
2098
|
+
return recovered;
|
|
2099
|
+
}
|
|
2100
|
+
function normalizeWorkflows(input) {
|
|
2101
|
+
const out = {};
|
|
2102
|
+
for (const [name, value] of Object.entries(input ?? {})) {
|
|
2103
|
+
out[name] = "def" in value ? value : { def: value };
|
|
2104
|
+
}
|
|
2105
|
+
return out;
|
|
2106
|
+
}
|
|
2107
|
+
function normalizeChannels(input) {
|
|
2108
|
+
const out = {};
|
|
2109
|
+
for (const [name, channel] of Object.entries(input ?? {})) {
|
|
2110
|
+
if (!isChannelDefinition(channel)) throw new Error(`Invalid channel "${name}": expected defineChannel({ handle })`);
|
|
2111
|
+
out[name] = channel;
|
|
2112
|
+
}
|
|
2113
|
+
return out;
|
|
2114
|
+
}
|
|
2115
|
+
function channelPath(name, channel) {
|
|
2116
|
+
if (channel.path) return channel.path.startsWith("/") ? channel.path : `/channels/${channel.path}`;
|
|
2117
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error(`Invalid channel name "${name}": use letters, numbers, underscores, or dashes`);
|
|
2118
|
+
return `/channels/${name}`;
|
|
2119
|
+
}
|
|
2120
|
+
function channelMethods(channel) {
|
|
2121
|
+
const methods = channel.methods?.length ? channel.methods : ["POST"];
|
|
2122
|
+
return [...new Set(methods.map((method) => method.toUpperCase()))];
|
|
2123
|
+
}
|
|
2124
|
+
function singleAgentName(agents) {
|
|
2125
|
+
const names = Object.keys(agents);
|
|
2126
|
+
return names.length === 1 ? names[0] : void 0;
|
|
2127
|
+
}
|
|
2128
|
+
function createScheduledHandler(options) {
|
|
2129
|
+
return async (event, env, ctx) => {
|
|
2130
|
+
const persistence = options.persistenceFor?.(env ?? {}) ?? getDefaultPersistence();
|
|
2131
|
+
const atlas = routeContext(
|
|
2132
|
+
options.agents,
|
|
2133
|
+
persistence,
|
|
2134
|
+
{ baseInstructions: options.baseInstructions, sharedPersonas: options.personas ?? options.subagents, engine: options.engine, durability: options.durability },
|
|
2135
|
+
env
|
|
2136
|
+
);
|
|
2137
|
+
const when = new Date(event.scheduledTime ?? Date.now());
|
|
2138
|
+
const dispatched = [];
|
|
2139
|
+
for (const [name, trig] of Object.entries(options.triggers ?? {})) {
|
|
2140
|
+
const cron = matchingTriggerCron(trig, event, when);
|
|
2141
|
+
if (!cron || !options.agents[name]) continue;
|
|
2142
|
+
const { runId, done } = await atlas.dispatch(name, {
|
|
2143
|
+
payload: { trigger: "cron", cron, scheduledTime: when.getTime() }
|
|
2144
|
+
});
|
|
2145
|
+
ctx?.waitUntil?.(done);
|
|
2146
|
+
dispatched.push({ agent: name, runId });
|
|
2147
|
+
}
|
|
2148
|
+
return { dispatched };
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function matchingTriggerCron(trig, event, when) {
|
|
2152
|
+
const crons = triggerCronList(trig);
|
|
2153
|
+
if (!crons.length) return void 0;
|
|
2154
|
+
if (event.cron && crons.includes(event.cron)) return event.cron;
|
|
2155
|
+
for (const cron of crons) {
|
|
2156
|
+
try {
|
|
2157
|
+
if (cronMatches(cron, when)) return cron;
|
|
2158
|
+
} catch {
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return void 0;
|
|
2162
|
+
}
|
|
2163
|
+
function triggerCronList(trig) {
|
|
2164
|
+
const out = [];
|
|
2165
|
+
if (typeof trig?.cron === "string" && trig.cron.trim()) out.push(trig.cron);
|
|
2166
|
+
if (Array.isArray(trig?.crons)) {
|
|
2167
|
+
for (const cron of trig.crons) {
|
|
2168
|
+
if (typeof cron === "string" && cron.trim()) out.push(cron);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return [...new Set(out)];
|
|
2172
|
+
}
|
|
2173
|
+
export {
|
|
2174
|
+
corsFromEnv,
|
|
2175
|
+
createApp,
|
|
2176
|
+
createScheduledHandler,
|
|
2177
|
+
keepAlive,
|
|
2178
|
+
mintScopedToken,
|
|
2179
|
+
rateLimit,
|
|
2180
|
+
rateLimitFromEnv,
|
|
2181
|
+
recoverAppRuns,
|
|
2182
|
+
requireApiKey,
|
|
2183
|
+
scopedTokensFromEnv
|
|
2184
|
+
};
|