@electric-ax/agents-server 0.4.12 → 0.4.14
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/dist/entrypoint.js +325 -62
- package/dist/index.cjs +325 -62
- package/dist/index.d.cts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +325 -62
- package/package.json +9 -9
- package/src/entity-manager.ts +351 -1
- package/src/routing/durable-streams-router.ts +4 -1
- package/src/routing/entities-router.ts +226 -0
- package/src/routing/stream-append.ts +3 -0
- package/src/stream-client.ts +14 -33
- package/src/utils/log.ts +63 -52
package/dist/index.cjs
CHANGED
|
@@ -1218,35 +1218,48 @@ const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
|
1218
1218
|
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
1219
1219
|
const USE_FILE_LOGS = process.env.ELECTRIC_AGENTS_LOG_FILE !== `false`;
|
|
1220
1220
|
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (
|
|
1224
|
-
const streams = [];
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1221
|
+
let _logger;
|
|
1222
|
+
function getLogger() {
|
|
1223
|
+
if (_logger) return _logger;
|
|
1224
|
+
const streams = [];
|
|
1225
|
+
try {
|
|
1226
|
+
if (USE_FILE_LOGS) {
|
|
1227
|
+
const logDir = process.env.ELECTRIC_AGENTS_LOG_DIR ?? node_path.default.resolve(process.cwd(), `logs`);
|
|
1228
|
+
node_fs.default.mkdirSync(logDir, { recursive: true });
|
|
1229
|
+
const logFile = node_path.default.join(logDir, `agent-server-${Date.now()}.jsonl`);
|
|
1230
|
+
streams.push({ stream: pino.default.destination({
|
|
1231
|
+
dest: logFile,
|
|
1232
|
+
sync: IS_ELECTRON_MAIN
|
|
1233
|
+
}) });
|
|
1234
|
+
}
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
process.stderr.write(`[agents-server] Failed to initialize file logging: ${err instanceof Error ? err.message : err}\n`);
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
if (USE_PRETTY_LOGS) streams.push({ stream: pino.default.transport({
|
|
1240
|
+
target: `pino-pretty`,
|
|
1241
|
+
options: {
|
|
1242
|
+
colorize: true,
|
|
1243
|
+
ignore: `pid,hostname,name`,
|
|
1244
|
+
translateTime: `SYS:HH:MM:ss`
|
|
1245
|
+
}
|
|
1246
|
+
}) });
|
|
1247
|
+
} catch {}
|
|
1248
|
+
_logger = streams.length > 0 ? (0, pino.default)({
|
|
1249
|
+
base: void 0,
|
|
1250
|
+
level: LOG_LEVEL
|
|
1251
|
+
}, pino.default.multistream(streams)) : (0, pino.default)({
|
|
1252
|
+
base: void 0,
|
|
1253
|
+
enabled: false,
|
|
1254
|
+
level: LOG_LEVEL
|
|
1255
|
+
});
|
|
1256
|
+
return _logger;
|
|
1257
|
+
}
|
|
1245
1258
|
function formatArgs(args) {
|
|
1246
1259
|
const errors = [];
|
|
1247
1260
|
const parts = [];
|
|
1248
|
-
for (const
|
|
1249
|
-
else parts.push(typeof
|
|
1261
|
+
for (const value of args) if (value instanceof Error) errors.push(value);
|
|
1262
|
+
else parts.push(typeof value === `string` ? value : JSON.stringify(value));
|
|
1250
1263
|
return {
|
|
1251
1264
|
err: errors[0],
|
|
1252
1265
|
msg: parts.join(` `)
|
|
@@ -1255,20 +1268,20 @@ function formatArgs(args) {
|
|
|
1255
1268
|
const serverLog = {
|
|
1256
1269
|
info(...args) {
|
|
1257
1270
|
const { msg } = formatArgs(args);
|
|
1258
|
-
|
|
1271
|
+
getLogger().info(msg);
|
|
1259
1272
|
},
|
|
1260
1273
|
warn(...args) {
|
|
1261
1274
|
const { err, msg } = formatArgs(args);
|
|
1262
|
-
if (err)
|
|
1263
|
-
else
|
|
1275
|
+
if (err) getLogger().warn({ err }, msg);
|
|
1276
|
+
else getLogger().warn(msg);
|
|
1264
1277
|
},
|
|
1265
1278
|
error(...args) {
|
|
1266
1279
|
const { err, msg } = formatArgs(args);
|
|
1267
|
-
if (err)
|
|
1268
|
-
else
|
|
1280
|
+
if (err) getLogger().error({ err }, msg);
|
|
1281
|
+
else getLogger().error(msg);
|
|
1269
1282
|
},
|
|
1270
1283
|
event(obj, msg) {
|
|
1271
|
-
|
|
1284
|
+
getLogger().info(obj, msg);
|
|
1272
1285
|
}
|
|
1273
1286
|
};
|
|
1274
1287
|
|
|
@@ -1989,7 +2002,8 @@ var StreamClient = class {
|
|
|
1989
2002
|
url: this.streamUrl(path$2),
|
|
1990
2003
|
headers: this.streamHeaders(),
|
|
1991
2004
|
contentType: opts.contentType,
|
|
1992
|
-
body: opts.body
|
|
2005
|
+
body: opts.body,
|
|
2006
|
+
closed: opts.closed
|
|
1993
2007
|
});
|
|
1994
2008
|
});
|
|
1995
2009
|
}
|
|
@@ -2089,30 +2103,11 @@ var StreamClient = class {
|
|
|
2089
2103
|
offset: fromOffset ?? `-1`,
|
|
2090
2104
|
live: false
|
|
2091
2105
|
});
|
|
2092
|
-
const
|
|
2093
|
-
return
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
if (settled) return;
|
|
2098
|
-
settled = true;
|
|
2099
|
-
unsub();
|
|
2100
|
-
resolve$1(r);
|
|
2101
|
-
};
|
|
2102
|
-
unsub = response.subscribeBytes((chunk) => {
|
|
2103
|
-
messages.push({
|
|
2104
|
-
data: chunk.data,
|
|
2105
|
-
offset: chunk.offset
|
|
2106
|
-
});
|
|
2107
|
-
if (chunk.upToDate || chunk.streamClosed) finish({ messages });
|
|
2108
|
-
});
|
|
2109
|
-
response.closed.then(() => finish({ messages })).catch((err) => {
|
|
2110
|
-
if (settled) return;
|
|
2111
|
-
settled = true;
|
|
2112
|
-
unsub();
|
|
2113
|
-
reject(err);
|
|
2114
|
-
});
|
|
2115
|
-
});
|
|
2106
|
+
const body = await response.body();
|
|
2107
|
+
return { messages: body.length === 0 ? [] : [{
|
|
2108
|
+
data: body,
|
|
2109
|
+
offset: response.offset
|
|
2110
|
+
}] };
|
|
2116
2111
|
});
|
|
2117
2112
|
}
|
|
2118
2113
|
async readJson(path$2, fromOffset) {
|
|
@@ -2266,11 +2261,11 @@ var StreamClient = class {
|
|
|
2266
2261
|
if (res.status === 404 || res.status === 204) return;
|
|
2267
2262
|
if (!res.ok) throw new Error(`Subscription delete failed: ${res.status} ${await res.text()}`);
|
|
2268
2263
|
}
|
|
2269
|
-
async addSubscriptionStreams(subscriptionId, streams
|
|
2264
|
+
async addSubscriptionStreams(subscriptionId, streams) {
|
|
2270
2265
|
const res = await fetch(this.subscriptionChildUrl(subscriptionId, `streams`), {
|
|
2271
2266
|
method: `POST`,
|
|
2272
2267
|
headers: await this.requestHeaders({ "content-type": `application/json` }),
|
|
2273
|
-
body: JSON.stringify({ streams: streams
|
|
2268
|
+
body: JSON.stringify({ streams: streams.map((stream) => this.backendSubscriptionPath(normalizeSubscriptionStreamPath(stream))) })
|
|
2274
2269
|
});
|
|
2275
2270
|
return await this.subscriptionJson(res, `Subscription stream add failed`);
|
|
2276
2271
|
}
|
|
@@ -2719,9 +2714,45 @@ function createInitialQueuePosition(date) {
|
|
|
2719
2714
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
2720
2715
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
2721
2716
|
const SERVER_SIGNAL_SENDER = `/_electric/server`;
|
|
2717
|
+
const DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
2722
2718
|
function sleep(ms) {
|
|
2723
2719
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
2724
2720
|
}
|
|
2721
|
+
function maxAttachmentBytes() {
|
|
2722
|
+
const configured = Number(process.env.ELECTRIC_AGENTS_MAX_ATTACHMENT_BYTES);
|
|
2723
|
+
return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : DEFAULT_MAX_ATTACHMENT_BYTES;
|
|
2724
|
+
}
|
|
2725
|
+
function manifestAttachmentKey(id) {
|
|
2726
|
+
return `attachment:${id}`;
|
|
2727
|
+
}
|
|
2728
|
+
function getEntityAttachmentStreamPath(entityUrl, attachmentId) {
|
|
2729
|
+
return `${entityUrl.replace(/\/+$/, ``)}/attachments/${attachmentId}`;
|
|
2730
|
+
}
|
|
2731
|
+
function isStreamCreateConflict(error) {
|
|
2732
|
+
return !!error && typeof error === `object` && (`status` in error && error.status === 409 || `code` in error && error.code === `CONFLICT_SEQ`);
|
|
2733
|
+
}
|
|
2734
|
+
function assertCanonicalAttachmentStreamPath(entityUrl, attachment) {
|
|
2735
|
+
const expected = getEntityAttachmentStreamPath(entityUrl, attachment.id);
|
|
2736
|
+
if (attachment.streamPath === expected) return;
|
|
2737
|
+
throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment stream path does not match its entity and id`, 409);
|
|
2738
|
+
}
|
|
2739
|
+
function validateAttachmentId(id) {
|
|
2740
|
+
if (!id || id.includes(`/`) || id.startsWith(`.`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment id must not be empty, start with ".", or contain forward slashes`, 400);
|
|
2741
|
+
}
|
|
2742
|
+
function validateAttachmentSubject(subject) {
|
|
2743
|
+
if (!subject.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment subject key is required`, 400);
|
|
2744
|
+
if (subject.type !== `inbox` && subject.type !== `run` && subject.type !== `text` && subject.type !== `tool_call` && subject.type !== `context`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `invalid attachment subject type`, 400);
|
|
2745
|
+
}
|
|
2746
|
+
function concatByteMessages(messages) {
|
|
2747
|
+
const total = messages.reduce((sum, message) => sum + message.data.length, 0);
|
|
2748
|
+
const bytes = new Uint8Array(total);
|
|
2749
|
+
let offset = 0;
|
|
2750
|
+
for (const message of messages) {
|
|
2751
|
+
bytes.set(message.data, offset);
|
|
2752
|
+
offset += message.data.length;
|
|
2753
|
+
}
|
|
2754
|
+
return bytes;
|
|
2755
|
+
}
|
|
2725
2756
|
function omitUndefined$1(value) {
|
|
2726
2757
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
2727
2758
|
}
|
|
@@ -3087,6 +3118,15 @@ var EntityManager = class {
|
|
|
3087
3118
|
await this.streamClient.fork(forkPath, sourcePath);
|
|
3088
3119
|
createdStreams.push(forkPath);
|
|
3089
3120
|
}
|
|
3121
|
+
for (const plan of entityPlans) {
|
|
3122
|
+
const manifests = snapshot.manifestsByEntity.get(plan.source.url) ?? new Map();
|
|
3123
|
+
for (const manifest of manifests.values()) {
|
|
3124
|
+
if (manifest.kind !== `attachment` || typeof manifest.streamPath !== `string` || typeof manifest.id !== `string`) continue;
|
|
3125
|
+
const forkPath = getEntityAttachmentStreamPath(plan.fork.url, manifest.id);
|
|
3126
|
+
await this.streamClient.fork(forkPath, manifest.streamPath);
|
|
3127
|
+
createdStreams.push(forkPath);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3090
3130
|
for (const plan of entityPlans) {
|
|
3091
3131
|
const reconciliation = this.buildForkReconciliation(plan, snapshot, entityUrlMap, sharedStateIdMap, stringMap);
|
|
3092
3132
|
activeManifestsByEntity.set(plan.fork.url, reconciliation.manifests);
|
|
@@ -3496,6 +3536,16 @@ var EntityManager = class {
|
|
|
3496
3536
|
changed: true
|
|
3497
3537
|
};
|
|
3498
3538
|
}
|
|
3539
|
+
if (next.kind === `attachment` && typeof next.streamPath === `string` && typeof next.id === `string`) for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3540
|
+
const prefix = `${sourceUrl}/attachments/`;
|
|
3541
|
+
if (!next.streamPath.startsWith(prefix)) continue;
|
|
3542
|
+
next.streamPath = getEntityAttachmentStreamPath(forkUrl, next.id);
|
|
3543
|
+
return {
|
|
3544
|
+
key,
|
|
3545
|
+
value: next,
|
|
3546
|
+
changed: true
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3499
3549
|
if (next.kind === `schedule` && next.scheduleType === `future_send`) {
|
|
3500
3550
|
let changed = false;
|
|
3501
3551
|
if (typeof next.targetUrl === `string`) {
|
|
@@ -3693,6 +3743,93 @@ var EntityManager = class {
|
|
|
3693
3743
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
|
|
3694
3744
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3695
3745
|
}
|
|
3746
|
+
isAttachmentStreamPath(path$2) {
|
|
3747
|
+
return /^\/[^/]+\/[^/]+\/attachments\/[^/]+$/.test(path$2);
|
|
3748
|
+
}
|
|
3749
|
+
async createAttachment(entityUrl, req) {
|
|
3750
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3751
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3752
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3753
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
3754
|
+
const id = req.id ?? (0, node_crypto.randomUUID)();
|
|
3755
|
+
validateAttachmentId(id);
|
|
3756
|
+
validateAttachmentSubject(req.subject);
|
|
3757
|
+
const limit = maxAttachmentBytes();
|
|
3758
|
+
if (req.bytes.length > limit) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment exceeds maximum size of ${limit} bytes`, 413);
|
|
3759
|
+
const mimeType = req.mimeType.trim() || `application/octet-stream`;
|
|
3760
|
+
const streamPath = getEntityAttachmentStreamPath(entityUrl, id);
|
|
3761
|
+
const manifestKey = manifestAttachmentKey(id);
|
|
3762
|
+
const txid = (0, node_crypto.randomUUID)();
|
|
3763
|
+
const now = new Date().toISOString();
|
|
3764
|
+
const sha256 = (0, node_crypto.createHash)(`sha256`).update(req.bytes).digest(`hex`);
|
|
3765
|
+
const attachment = {
|
|
3766
|
+
key: manifestKey,
|
|
3767
|
+
kind: `attachment`,
|
|
3768
|
+
id,
|
|
3769
|
+
streamPath,
|
|
3770
|
+
status: `complete`,
|
|
3771
|
+
subject: req.subject,
|
|
3772
|
+
role: req.role ?? `input`,
|
|
3773
|
+
mimeType,
|
|
3774
|
+
...req.filename ? { filename: req.filename } : {},
|
|
3775
|
+
byteLength: req.bytes.length,
|
|
3776
|
+
sha256,
|
|
3777
|
+
createdAt: now,
|
|
3778
|
+
...req.createdBy ? { createdBy: req.createdBy } : {},
|
|
3779
|
+
...req.meta ? { meta: req.meta } : {}
|
|
3780
|
+
};
|
|
3781
|
+
let streamCreated = false;
|
|
3782
|
+
try {
|
|
3783
|
+
await this.streamClient.create(streamPath, {
|
|
3784
|
+
contentType: mimeType,
|
|
3785
|
+
body: req.bytes,
|
|
3786
|
+
closed: true
|
|
3787
|
+
});
|
|
3788
|
+
streamCreated = true;
|
|
3789
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, attachment, { txid });
|
|
3790
|
+
} catch (error) {
|
|
3791
|
+
if (streamCreated) await this.streamClient.delete(streamPath).catch(() => void 0);
|
|
3792
|
+
if (!streamCreated && isStreamCreateConflict(error)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment already exists at id "${id}"`, 409);
|
|
3793
|
+
throw error;
|
|
3794
|
+
}
|
|
3795
|
+
return {
|
|
3796
|
+
txid,
|
|
3797
|
+
attachment
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
async getAttachment(entityUrl, id) {
|
|
3801
|
+
validateAttachmentId(id);
|
|
3802
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3803
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3804
|
+
const events = await this.streamClient.readJson(entity.streams.main);
|
|
3805
|
+
const manifest = this.reduceStateRows(events, `manifest`).get(manifestAttachmentKey(id));
|
|
3806
|
+
if (!manifest || manifest.kind !== `attachment`) return null;
|
|
3807
|
+
return manifest;
|
|
3808
|
+
}
|
|
3809
|
+
async readAttachment(entityUrl, id) {
|
|
3810
|
+
const attachment = await this.getAttachment(entityUrl, id);
|
|
3811
|
+
if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
|
|
3812
|
+
if (attachment.status !== `complete`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment is not complete`, 409);
|
|
3813
|
+
assertCanonicalAttachmentStreamPath(entityUrl, attachment);
|
|
3814
|
+
const result = await this.streamClient.read(attachment.streamPath);
|
|
3815
|
+
return {
|
|
3816
|
+
attachment,
|
|
3817
|
+
bytes: concatByteMessages(result.messages)
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
async deleteAttachment(entityUrl, id) {
|
|
3821
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3822
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3823
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3824
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
3825
|
+
const attachment = await this.getAttachment(entityUrl, id);
|
|
3826
|
+
if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
|
|
3827
|
+
assertCanonicalAttachmentStreamPath(entityUrl, attachment);
|
|
3828
|
+
const txid = (0, node_crypto.randomUUID)();
|
|
3829
|
+
await this.writeManifestEntry(entityUrl, manifestAttachmentKey(id), `delete`, void 0, { txid });
|
|
3830
|
+
await this.streamClient.delete(attachment.streamPath).catch(() => void 0);
|
|
3831
|
+
return { txid };
|
|
3832
|
+
}
|
|
3696
3833
|
async setTag(entityUrl, key, req, token) {
|
|
3697
3834
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3698
3835
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -6094,6 +6231,7 @@ async function handleStreamAppend(request, runtime, forward) {
|
|
|
6094
6231
|
const { manager } = runtime;
|
|
6095
6232
|
const entity = await manager.registry.getEntityByStream(path$2);
|
|
6096
6233
|
const isSharedState = path$2.startsWith(`/_electric/shared-state/`);
|
|
6234
|
+
if (!entity && manager.isAttachmentStreamPath(path$2)) return apiError(401, ErrCodeUnauthorized, `Invalid write token`);
|
|
6097
6235
|
if (!entity && !isSharedState) return void 0;
|
|
6098
6236
|
const body = await request.readBody();
|
|
6099
6237
|
const event = decodeStreamAppendEvent(body);
|
|
@@ -6662,8 +6800,9 @@ async function streamAppend(request, ctx) {
|
|
|
6662
6800
|
}));
|
|
6663
6801
|
}
|
|
6664
6802
|
async function proxyPassThrough(request, ctx) {
|
|
6665
|
-
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6666
6803
|
const streamPath = new URL(request.url).pathname;
|
|
6804
|
+
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
6805
|
+
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6667
6806
|
const method = request.method.toUpperCase();
|
|
6668
6807
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
6669
6808
|
try {
|
|
@@ -6816,6 +6955,13 @@ const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6816
6955
|
lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
|
|
6817
6956
|
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6818
6957
|
});
|
|
6958
|
+
const attachmentSubjectTypes = new Set([
|
|
6959
|
+
`inbox`,
|
|
6960
|
+
`run`,
|
|
6961
|
+
`text`,
|
|
6962
|
+
`tool_call`,
|
|
6963
|
+
`context`
|
|
6964
|
+
]);
|
|
6819
6965
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
6820
6966
|
entitiesRouter.get(`/`, listEntities);
|
|
6821
6967
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
@@ -6824,6 +6970,9 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
|
6824
6970
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6825
6971
|
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
6826
6972
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6973
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
|
|
6974
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
|
|
6975
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
|
|
6827
6976
|
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6828
6977
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6829
6978
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
@@ -6850,6 +6999,82 @@ function requireExistingEntityRoute(request) {
|
|
|
6850
6999
|
if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
|
|
6851
7000
|
return request.entityRoute;
|
|
6852
7001
|
}
|
|
7002
|
+
function invalidAttachmentRequest(message) {
|
|
7003
|
+
throw new ElectricAgentsError(ErrCodeInvalidRequest, message, 400);
|
|
7004
|
+
}
|
|
7005
|
+
function formString(form, key) {
|
|
7006
|
+
const value = form.get(key);
|
|
7007
|
+
if (typeof value !== `string`) return void 0;
|
|
7008
|
+
const trimmed = value.trim();
|
|
7009
|
+
return trimmed || void 0;
|
|
7010
|
+
}
|
|
7011
|
+
function parseJsonFormField(form, key) {
|
|
7012
|
+
const raw = formString(form, key);
|
|
7013
|
+
if (!raw) return void 0;
|
|
7014
|
+
try {
|
|
7015
|
+
return JSON.parse(raw);
|
|
7016
|
+
} catch {
|
|
7017
|
+
invalidAttachmentRequest(`Invalid JSON field: ${key}`);
|
|
7018
|
+
}
|
|
7019
|
+
}
|
|
7020
|
+
function parseAttachmentSubject(form) {
|
|
7021
|
+
const explicit = parseJsonFormField(form, `subject`);
|
|
7022
|
+
if (explicit !== void 0) {
|
|
7023
|
+
if (!explicit || typeof explicit !== `object` || Array.isArray(explicit)) invalidAttachmentRequest(`attachment subject must be an object`);
|
|
7024
|
+
const subject = explicit;
|
|
7025
|
+
const type$1 = subject.type;
|
|
7026
|
+
const key$1 = subject.key;
|
|
7027
|
+
if (typeof type$1 !== `string` || typeof key$1 !== `string`) invalidAttachmentRequest(`attachment subject requires type and key`);
|
|
7028
|
+
if (!attachmentSubjectTypes.has(type$1)) invalidAttachmentRequest(`invalid attachment subject type`);
|
|
7029
|
+
return {
|
|
7030
|
+
type: type$1,
|
|
7031
|
+
key: key$1
|
|
7032
|
+
};
|
|
7033
|
+
}
|
|
7034
|
+
const type = formString(form, `subjectType`);
|
|
7035
|
+
const key = formString(form, `subjectKey`);
|
|
7036
|
+
if (!type || !key) invalidAttachmentRequest(`attachment subject is required`);
|
|
7037
|
+
if (!attachmentSubjectTypes.has(type)) invalidAttachmentRequest(`invalid attachment subject type`);
|
|
7038
|
+
return {
|
|
7039
|
+
type,
|
|
7040
|
+
key
|
|
7041
|
+
};
|
|
7042
|
+
}
|
|
7043
|
+
function getUploadedFormFile(value) {
|
|
7044
|
+
if (value !== null && typeof value === `object` && `arrayBuffer` in value && typeof value.arrayBuffer === `function`) return value;
|
|
7045
|
+
return null;
|
|
7046
|
+
}
|
|
7047
|
+
async function parseAttachmentForm(request) {
|
|
7048
|
+
const contentType = request.headers.get(`content-type`)?.toLowerCase() ?? ``;
|
|
7049
|
+
if (!contentType.includes(`multipart/form-data`)) invalidAttachmentRequest(`Attachment uploads must use multipart/form-data`);
|
|
7050
|
+
let form;
|
|
7051
|
+
try {
|
|
7052
|
+
form = await request.formData();
|
|
7053
|
+
} catch {
|
|
7054
|
+
invalidAttachmentRequest(`Invalid multipart form data`);
|
|
7055
|
+
}
|
|
7056
|
+
const file = getUploadedFormFile(form.get(`file`));
|
|
7057
|
+
if (!file) invalidAttachmentRequest(`Missing file field`);
|
|
7058
|
+
const role = formString(form, `role`);
|
|
7059
|
+
if (role !== void 0 && role !== `input` && role !== `output`) invalidAttachmentRequest(`invalid attachment role`);
|
|
7060
|
+
const fileName = formString(form, `filename`) ?? (typeof file.name === `string` ? file.name : void 0);
|
|
7061
|
+
const mimeType = formString(form, `mimeType`) || (typeof file.type === `string` ? file.type : void 0) || `application/octet-stream`;
|
|
7062
|
+
const meta = parseJsonFormField(form, `meta`);
|
|
7063
|
+
if (meta !== void 0 && (typeof meta !== `object` || Array.isArray(meta))) invalidAttachmentRequest(`attachment meta must be an object`);
|
|
7064
|
+
return {
|
|
7065
|
+
id: formString(form, `id`),
|
|
7066
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
7067
|
+
mimeType,
|
|
7068
|
+
filename: fileName,
|
|
7069
|
+
subject: parseAttachmentSubject(form),
|
|
7070
|
+
role,
|
|
7071
|
+
meta
|
|
7072
|
+
};
|
|
7073
|
+
}
|
|
7074
|
+
function contentDisposition(filename) {
|
|
7075
|
+
const fallback = filename.replace(/["\\\r\n]/g, `_`);
|
|
7076
|
+
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
7077
|
+
}
|
|
6853
7078
|
function rejectPrincipalEntityMutation(request, action) {
|
|
6854
7079
|
const { entity } = requireExistingEntityRoute(request);
|
|
6855
7080
|
if (entity.type !== `principal`) return void 0;
|
|
@@ -7034,6 +7259,44 @@ async function sendEntity(request, ctx) {
|
|
|
7034
7259
|
});
|
|
7035
7260
|
return (0, itty_router.status)(204);
|
|
7036
7261
|
}
|
|
7262
|
+
async function createAttachment(request, ctx) {
|
|
7263
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
|
|
7264
|
+
if (principalMutationError) return principalMutationError;
|
|
7265
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7266
|
+
const form = await parseAttachmentForm(request);
|
|
7267
|
+
const result = await ctx.entityManager.createAttachment(entityUrl, {
|
|
7268
|
+
id: form.id,
|
|
7269
|
+
bytes: form.bytes,
|
|
7270
|
+
mimeType: form.mimeType,
|
|
7271
|
+
filename: form.filename,
|
|
7272
|
+
subject: form.subject,
|
|
7273
|
+
role: form.role,
|
|
7274
|
+
createdBy: ctx.principal.url,
|
|
7275
|
+
meta: form.meta
|
|
7276
|
+
});
|
|
7277
|
+
return (0, itty_router.json)(result, { status: 201 });
|
|
7278
|
+
}
|
|
7279
|
+
async function readAttachment(request, ctx) {
|
|
7280
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7281
|
+
const result = await ctx.entityManager.readAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
|
|
7282
|
+
const headers = new Headers({
|
|
7283
|
+
"content-type": result.attachment.mimeType,
|
|
7284
|
+
"content-length": String(result.bytes.length),
|
|
7285
|
+
"cache-control": `private, max-age=31536000, immutable`
|
|
7286
|
+
});
|
|
7287
|
+
if (result.attachment.filename) headers.set(`content-disposition`, contentDisposition(result.attachment.filename));
|
|
7288
|
+
return new Response(result.bytes, {
|
|
7289
|
+
status: 200,
|
|
7290
|
+
headers
|
|
7291
|
+
});
|
|
7292
|
+
}
|
|
7293
|
+
async function deleteAttachment(request, ctx) {
|
|
7294
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `stripped of attachments`);
|
|
7295
|
+
if (principalMutationError) return principalMutationError;
|
|
7296
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7297
|
+
const result = await ctx.entityManager.deleteAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
|
|
7298
|
+
return (0, itty_router.json)(result);
|
|
7299
|
+
}
|
|
7037
7300
|
async function updateInboxMessage(request, ctx) {
|
|
7038
7301
|
const parsed = routeBody(request);
|
|
7039
7302
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
@@ -7634,7 +7897,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7634
7897
|
leaseExpiresAt: input.claim.lease_ttl_ms ? new Date(Date.now() + input.claim.lease_ttl_ms) : void 0
|
|
7635
7898
|
});
|
|
7636
7899
|
await ctx.entityManager.registry.updateStatus(entity.url, `running`);
|
|
7637
|
-
const streams
|
|
7900
|
+
const streams = input.claim.streams.map((stream) => ({
|
|
7638
7901
|
path: withLeadingSlash(stream.path),
|
|
7639
7902
|
offset: stream.tail_offset ?? ``
|
|
7640
7903
|
}));
|
|
@@ -7643,7 +7906,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7643
7906
|
epoch: input.claim.generation,
|
|
7644
7907
|
wakeId: input.claim.wake_id,
|
|
7645
7908
|
streamPath: primaryStream,
|
|
7646
|
-
streams
|
|
7909
|
+
streams,
|
|
7647
7910
|
callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
7648
7911
|
claimToken: input.claim.token,
|
|
7649
7912
|
triggerEvent: `message_received`,
|
package/dist/index.d.cts
CHANGED
|
@@ -3723,6 +3723,7 @@ declare class StreamClient {
|
|
|
3723
3723
|
create(path: string, opts: {
|
|
3724
3724
|
contentType: string;
|
|
3725
3725
|
body?: Uint8Array | string;
|
|
3726
|
+
closed?: boolean;
|
|
3726
3727
|
}): Promise<void>;
|
|
3727
3728
|
fork(path: string, sourcePath: string): Promise<void>;
|
|
3728
3729
|
append(path: string, data: Uint8Array | string, opts?: {
|
|
@@ -4075,6 +4076,45 @@ declare class SchemaValidator {
|
|
|
4075
4076
|
//#endregion
|
|
4076
4077
|
//#region src/entity-manager.d.ts
|
|
4077
4078
|
type WriteTokenValidator = (entity: ElectricAgentsEntity, token: string) => boolean;
|
|
4079
|
+
type AttachmentSubjectType = `inbox` | `run` | `text` | `tool_call` | `context`;
|
|
4080
|
+
type AttachmentRole = `input` | `output`;
|
|
4081
|
+
interface CreateAttachmentRequest {
|
|
4082
|
+
id?: string;
|
|
4083
|
+
bytes: Uint8Array;
|
|
4084
|
+
mimeType: string;
|
|
4085
|
+
filename?: string;
|
|
4086
|
+
subject: {
|
|
4087
|
+
type: AttachmentSubjectType;
|
|
4088
|
+
key: string;
|
|
4089
|
+
};
|
|
4090
|
+
role?: AttachmentRole;
|
|
4091
|
+
createdBy?: string;
|
|
4092
|
+
meta?: Record<string, unknown>;
|
|
4093
|
+
}
|
|
4094
|
+
interface ReadAttachmentResult {
|
|
4095
|
+
attachment: ManifestAttachmentEntry;
|
|
4096
|
+
bytes: Uint8Array;
|
|
4097
|
+
}
|
|
4098
|
+
type ManifestAttachmentEntry = {
|
|
4099
|
+
key: string;
|
|
4100
|
+
kind: `attachment`;
|
|
4101
|
+
id: string;
|
|
4102
|
+
streamPath: string;
|
|
4103
|
+
status: `pending` | `complete` | `failed`;
|
|
4104
|
+
subject: {
|
|
4105
|
+
type: AttachmentSubjectType;
|
|
4106
|
+
key: string;
|
|
4107
|
+
};
|
|
4108
|
+
role: AttachmentRole;
|
|
4109
|
+
mimeType: string;
|
|
4110
|
+
filename?: string;
|
|
4111
|
+
byteLength?: number;
|
|
4112
|
+
sha256?: string;
|
|
4113
|
+
createdAt: string;
|
|
4114
|
+
createdBy?: string;
|
|
4115
|
+
error?: string;
|
|
4116
|
+
meta?: Record<string, unknown>;
|
|
4117
|
+
};
|
|
4078
4118
|
type ForkSubtreeOptions = {
|
|
4079
4119
|
rootInstanceId?: string;
|
|
4080
4120
|
waitTimeoutMs?: number;
|
|
@@ -4171,6 +4211,16 @@ declare class EntityManager {
|
|
|
4171
4211
|
status?: `pending` | `processed` | `cancelled`;
|
|
4172
4212
|
}): Promise<void>;
|
|
4173
4213
|
deleteInboxMessage(entityUrl: string, key: string): Promise<void>;
|
|
4214
|
+
isAttachmentStreamPath(path: string): boolean;
|
|
4215
|
+
createAttachment(entityUrl: string, req: CreateAttachmentRequest): Promise<{
|
|
4216
|
+
txid: string;
|
|
4217
|
+
attachment: ManifestAttachmentEntry;
|
|
4218
|
+
}>;
|
|
4219
|
+
getAttachment(entityUrl: string, id: string): Promise<ManifestAttachmentEntry | null>;
|
|
4220
|
+
readAttachment(entityUrl: string, id: string): Promise<ReadAttachmentResult>;
|
|
4221
|
+
deleteAttachment(entityUrl: string, id: string): Promise<{
|
|
4222
|
+
txid: string;
|
|
4223
|
+
}>;
|
|
4174
4224
|
setTag(entityUrl: string, key: string, req: SetTagRequest, token: string): Promise<ElectricAgentsEntity>;
|
|
4175
4225
|
deleteTag(entityUrl: string, key: string, token: string): Promise<ElectricAgentsEntity>;
|
|
4176
4226
|
ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
|
package/dist/index.d.ts
CHANGED
|
@@ -3724,6 +3724,7 @@ declare class StreamClient {
|
|
|
3724
3724
|
create(path: string, opts: {
|
|
3725
3725
|
contentType: string;
|
|
3726
3726
|
body?: Uint8Array | string;
|
|
3727
|
+
closed?: boolean;
|
|
3727
3728
|
}): Promise<void>;
|
|
3728
3729
|
fork(path: string, sourcePath: string): Promise<void>;
|
|
3729
3730
|
append(path: string, data: Uint8Array | string, opts?: {
|
|
@@ -4076,6 +4077,45 @@ declare class SchemaValidator {
|
|
|
4076
4077
|
//#endregion
|
|
4077
4078
|
//#region src/entity-manager.d.ts
|
|
4078
4079
|
type WriteTokenValidator = (entity: ElectricAgentsEntity, token: string) => boolean;
|
|
4080
|
+
type AttachmentSubjectType = `inbox` | `run` | `text` | `tool_call` | `context`;
|
|
4081
|
+
type AttachmentRole = `input` | `output`;
|
|
4082
|
+
interface CreateAttachmentRequest {
|
|
4083
|
+
id?: string;
|
|
4084
|
+
bytes: Uint8Array;
|
|
4085
|
+
mimeType: string;
|
|
4086
|
+
filename?: string;
|
|
4087
|
+
subject: {
|
|
4088
|
+
type: AttachmentSubjectType;
|
|
4089
|
+
key: string;
|
|
4090
|
+
};
|
|
4091
|
+
role?: AttachmentRole;
|
|
4092
|
+
createdBy?: string;
|
|
4093
|
+
meta?: Record<string, unknown>;
|
|
4094
|
+
}
|
|
4095
|
+
interface ReadAttachmentResult {
|
|
4096
|
+
attachment: ManifestAttachmentEntry;
|
|
4097
|
+
bytes: Uint8Array;
|
|
4098
|
+
}
|
|
4099
|
+
type ManifestAttachmentEntry = {
|
|
4100
|
+
key: string;
|
|
4101
|
+
kind: `attachment`;
|
|
4102
|
+
id: string;
|
|
4103
|
+
streamPath: string;
|
|
4104
|
+
status: `pending` | `complete` | `failed`;
|
|
4105
|
+
subject: {
|
|
4106
|
+
type: AttachmentSubjectType;
|
|
4107
|
+
key: string;
|
|
4108
|
+
};
|
|
4109
|
+
role: AttachmentRole;
|
|
4110
|
+
mimeType: string;
|
|
4111
|
+
filename?: string;
|
|
4112
|
+
byteLength?: number;
|
|
4113
|
+
sha256?: string;
|
|
4114
|
+
createdAt: string;
|
|
4115
|
+
createdBy?: string;
|
|
4116
|
+
error?: string;
|
|
4117
|
+
meta?: Record<string, unknown>;
|
|
4118
|
+
};
|
|
4079
4119
|
type ForkSubtreeOptions = {
|
|
4080
4120
|
rootInstanceId?: string;
|
|
4081
4121
|
waitTimeoutMs?: number;
|
|
@@ -4172,6 +4212,16 @@ declare class EntityManager {
|
|
|
4172
4212
|
status?: `pending` | `processed` | `cancelled`;
|
|
4173
4213
|
}): Promise<void>;
|
|
4174
4214
|
deleteInboxMessage(entityUrl: string, key: string): Promise<void>;
|
|
4215
|
+
isAttachmentStreamPath(path: string): boolean;
|
|
4216
|
+
createAttachment(entityUrl: string, req: CreateAttachmentRequest): Promise<{
|
|
4217
|
+
txid: string;
|
|
4218
|
+
attachment: ManifestAttachmentEntry;
|
|
4219
|
+
}>;
|
|
4220
|
+
getAttachment(entityUrl: string, id: string): Promise<ManifestAttachmentEntry | null>;
|
|
4221
|
+
readAttachment(entityUrl: string, id: string): Promise<ReadAttachmentResult>;
|
|
4222
|
+
deleteAttachment(entityUrl: string, id: string): Promise<{
|
|
4223
|
+
txid: string;
|
|
4224
|
+
}>;
|
|
4175
4225
|
setTag(entityUrl: string, key: string, req: SetTagRequest, token: string): Promise<ElectricAgentsEntity>;
|
|
4176
4226
|
deleteTag(entityUrl: string, key: string, token: string): Promise<ElectricAgentsEntity>;
|
|
4177
4227
|
ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
|