@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/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
- const LOG_DIR = USE_FILE_LOGS ? process.env.ELECTRIC_AGENTS_LOG_DIR ?? node_path.default.resolve(process.cwd(), `logs`) : void 0;
1222
- const LOG_FILE = LOG_DIR ? node_path.default.join(LOG_DIR, `agent-server-${Date.now()}.jsonl`) : void 0;
1223
- if (LOG_DIR) node_fs.default.mkdirSync(LOG_DIR, { recursive: true });
1224
- const streams = [];
1225
- if (LOG_FILE) streams.push({ stream: pino.default.destination({
1226
- dest: LOG_FILE,
1227
- sync: IS_ELECTRON_MAIN
1228
- }) });
1229
- if (USE_PRETTY_LOGS) streams.push({ stream: pino.default.transport({
1230
- target: `pino-pretty`,
1231
- options: {
1232
- colorize: true,
1233
- ignore: `pid,hostname,name`,
1234
- translateTime: `SYS:HH:MM:ss`
1235
- }
1236
- }) });
1237
- const logger = streams.length > 0 ? (0, pino.default)({
1238
- base: void 0,
1239
- level: LOG_LEVEL
1240
- }, pino.default.multistream(streams)) : (0, pino.default)({
1241
- base: void 0,
1242
- enabled: false,
1243
- level: LOG_LEVEL
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 a of args) if (a instanceof Error) errors.push(a);
1249
- else parts.push(typeof a === `string` ? a : JSON.stringify(a));
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
- logger.info(msg);
1271
+ getLogger().info(msg);
1259
1272
  },
1260
1273
  warn(...args) {
1261
1274
  const { err, msg } = formatArgs(args);
1262
- if (err) logger.warn({ err }, msg);
1263
- else logger.warn(msg);
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) logger.error({ err }, msg);
1268
- else logger.error(msg);
1280
+ if (err) getLogger().error({ err }, msg);
1281
+ else getLogger().error(msg);
1269
1282
  },
1270
1283
  event(obj, msg) {
1271
- logger.info(obj, msg);
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 messages = [];
2093
- return await new Promise((resolve$1, reject) => {
2094
- let settled = false;
2095
- let unsub = () => {};
2096
- const finish = (r) => {
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$1) {
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$1.map((stream) => this.backendSubscriptionPath(normalizeSubscriptionStreamPath(stream))) })
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$1 = input.claim.streams.map((stream) => ({
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: streams$1,
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<{