@electric-ax/agents-server 0.4.11 → 0.4.13

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
@@ -1989,7 +1989,8 @@ var StreamClient = class {
1989
1989
  url: this.streamUrl(path$2),
1990
1990
  headers: this.streamHeaders(),
1991
1991
  contentType: opts.contentType,
1992
- body: opts.body
1992
+ body: opts.body,
1993
+ closed: opts.closed
1993
1994
  });
1994
1995
  });
1995
1996
  }
@@ -2089,30 +2090,11 @@ var StreamClient = class {
2089
2090
  offset: fromOffset ?? `-1`,
2090
2091
  live: false
2091
2092
  });
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
- });
2093
+ const body = await response.body();
2094
+ return { messages: body.length === 0 ? [] : [{
2095
+ data: body,
2096
+ offset: response.offset
2097
+ }] };
2116
2098
  });
2117
2099
  }
2118
2100
  async readJson(path$2, fromOffset) {
@@ -2719,9 +2701,45 @@ function createInitialQueuePosition(date) {
2719
2701
  const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
2720
2702
  const DEFAULT_FORK_WAIT_POLL_MS = 250;
2721
2703
  const SERVER_SIGNAL_SENDER = `/_electric/server`;
2704
+ const DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
2722
2705
  function sleep(ms) {
2723
2706
  return new Promise((resolve$1) => setTimeout(resolve$1, ms));
2724
2707
  }
2708
+ function maxAttachmentBytes() {
2709
+ const configured = Number(process.env.ELECTRIC_AGENTS_MAX_ATTACHMENT_BYTES);
2710
+ return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : DEFAULT_MAX_ATTACHMENT_BYTES;
2711
+ }
2712
+ function manifestAttachmentKey(id) {
2713
+ return `attachment:${id}`;
2714
+ }
2715
+ function getEntityAttachmentStreamPath(entityUrl, attachmentId) {
2716
+ return `${entityUrl.replace(/\/+$/, ``)}/attachments/${attachmentId}`;
2717
+ }
2718
+ function isStreamCreateConflict(error) {
2719
+ return !!error && typeof error === `object` && (`status` in error && error.status === 409 || `code` in error && error.code === `CONFLICT_SEQ`);
2720
+ }
2721
+ function assertCanonicalAttachmentStreamPath(entityUrl, attachment) {
2722
+ const expected = getEntityAttachmentStreamPath(entityUrl, attachment.id);
2723
+ if (attachment.streamPath === expected) return;
2724
+ throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment stream path does not match its entity and id`, 409);
2725
+ }
2726
+ function validateAttachmentId(id) {
2727
+ if (!id || id.includes(`/`) || id.startsWith(`.`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment id must not be empty, start with ".", or contain forward slashes`, 400);
2728
+ }
2729
+ function validateAttachmentSubject(subject) {
2730
+ if (!subject.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `attachment subject key is required`, 400);
2731
+ 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);
2732
+ }
2733
+ function concatByteMessages(messages) {
2734
+ const total = messages.reduce((sum, message) => sum + message.data.length, 0);
2735
+ const bytes = new Uint8Array(total);
2736
+ let offset = 0;
2737
+ for (const message of messages) {
2738
+ bytes.set(message.data, offset);
2739
+ offset += message.data.length;
2740
+ }
2741
+ return bytes;
2742
+ }
2725
2743
  function omitUndefined$1(value) {
2726
2744
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
2727
2745
  }
@@ -3087,6 +3105,15 @@ var EntityManager = class {
3087
3105
  await this.streamClient.fork(forkPath, sourcePath);
3088
3106
  createdStreams.push(forkPath);
3089
3107
  }
3108
+ for (const plan of entityPlans) {
3109
+ const manifests = snapshot.manifestsByEntity.get(plan.source.url) ?? new Map();
3110
+ for (const manifest of manifests.values()) {
3111
+ if (manifest.kind !== `attachment` || typeof manifest.streamPath !== `string` || typeof manifest.id !== `string`) continue;
3112
+ const forkPath = getEntityAttachmentStreamPath(plan.fork.url, manifest.id);
3113
+ await this.streamClient.fork(forkPath, manifest.streamPath);
3114
+ createdStreams.push(forkPath);
3115
+ }
3116
+ }
3090
3117
  for (const plan of entityPlans) {
3091
3118
  const reconciliation = this.buildForkReconciliation(plan, snapshot, entityUrlMap, sharedStateIdMap, stringMap);
3092
3119
  activeManifestsByEntity.set(plan.fork.url, reconciliation.manifests);
@@ -3496,6 +3523,16 @@ var EntityManager = class {
3496
3523
  changed: true
3497
3524
  };
3498
3525
  }
3526
+ if (next.kind === `attachment` && typeof next.streamPath === `string` && typeof next.id === `string`) for (const [sourceUrl, forkUrl] of entityUrlMap) {
3527
+ const prefix = `${sourceUrl}/attachments/`;
3528
+ if (!next.streamPath.startsWith(prefix)) continue;
3529
+ next.streamPath = getEntityAttachmentStreamPath(forkUrl, next.id);
3530
+ return {
3531
+ key,
3532
+ value: next,
3533
+ changed: true
3534
+ };
3535
+ }
3499
3536
  if (next.kind === `schedule` && next.scheduleType === `future_send`) {
3500
3537
  let changed = false;
3501
3538
  if (typeof next.targetUrl === `string`) {
@@ -3693,6 +3730,93 @@ var EntityManager = class {
3693
3730
  const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
3694
3731
  await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
3695
3732
  }
3733
+ isAttachmentStreamPath(path$2) {
3734
+ return /^\/[^/]+\/[^/]+\/attachments\/[^/]+$/.test(path$2);
3735
+ }
3736
+ async createAttachment(entityUrl, req) {
3737
+ const entity = await this.registry.getEntity(entityUrl);
3738
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3739
+ if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
3740
+ if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
3741
+ const id = req.id ?? (0, node_crypto.randomUUID)();
3742
+ validateAttachmentId(id);
3743
+ validateAttachmentSubject(req.subject);
3744
+ const limit = maxAttachmentBytes();
3745
+ if (req.bytes.length > limit) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment exceeds maximum size of ${limit} bytes`, 413);
3746
+ const mimeType = req.mimeType.trim() || `application/octet-stream`;
3747
+ const streamPath = getEntityAttachmentStreamPath(entityUrl, id);
3748
+ const manifestKey = manifestAttachmentKey(id);
3749
+ const txid = (0, node_crypto.randomUUID)();
3750
+ const now = new Date().toISOString();
3751
+ const sha256 = (0, node_crypto.createHash)(`sha256`).update(req.bytes).digest(`hex`);
3752
+ const attachment = {
3753
+ key: manifestKey,
3754
+ kind: `attachment`,
3755
+ id,
3756
+ streamPath,
3757
+ status: `complete`,
3758
+ subject: req.subject,
3759
+ role: req.role ?? `input`,
3760
+ mimeType,
3761
+ ...req.filename ? { filename: req.filename } : {},
3762
+ byteLength: req.bytes.length,
3763
+ sha256,
3764
+ createdAt: now,
3765
+ ...req.createdBy ? { createdBy: req.createdBy } : {},
3766
+ ...req.meta ? { meta: req.meta } : {}
3767
+ };
3768
+ let streamCreated = false;
3769
+ try {
3770
+ await this.streamClient.create(streamPath, {
3771
+ contentType: mimeType,
3772
+ body: req.bytes,
3773
+ closed: true
3774
+ });
3775
+ streamCreated = true;
3776
+ await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, attachment, { txid });
3777
+ } catch (error) {
3778
+ if (streamCreated) await this.streamClient.delete(streamPath).catch(() => void 0);
3779
+ if (!streamCreated && isStreamCreateConflict(error)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment already exists at id "${id}"`, 409);
3780
+ throw error;
3781
+ }
3782
+ return {
3783
+ txid,
3784
+ attachment
3785
+ };
3786
+ }
3787
+ async getAttachment(entityUrl, id) {
3788
+ validateAttachmentId(id);
3789
+ const entity = await this.registry.getEntity(entityUrl);
3790
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3791
+ const events = await this.streamClient.readJson(entity.streams.main);
3792
+ const manifest = this.reduceStateRows(events, `manifest`).get(manifestAttachmentKey(id));
3793
+ if (!manifest || manifest.kind !== `attachment`) return null;
3794
+ return manifest;
3795
+ }
3796
+ async readAttachment(entityUrl, id) {
3797
+ const attachment = await this.getAttachment(entityUrl, id);
3798
+ if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
3799
+ if (attachment.status !== `complete`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Attachment is not complete`, 409);
3800
+ assertCanonicalAttachmentStreamPath(entityUrl, attachment);
3801
+ const result = await this.streamClient.read(attachment.streamPath);
3802
+ return {
3803
+ attachment,
3804
+ bytes: concatByteMessages(result.messages)
3805
+ };
3806
+ }
3807
+ async deleteAttachment(entityUrl, id) {
3808
+ const entity = await this.registry.getEntity(entityUrl);
3809
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3810
+ if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
3811
+ if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
3812
+ const attachment = await this.getAttachment(entityUrl, id);
3813
+ if (!attachment) throw new ElectricAgentsError(ErrCodeNotFound, `Attachment not found`, 404);
3814
+ assertCanonicalAttachmentStreamPath(entityUrl, attachment);
3815
+ const txid = (0, node_crypto.randomUUID)();
3816
+ await this.writeManifestEntry(entityUrl, manifestAttachmentKey(id), `delete`, void 0, { txid });
3817
+ await this.streamClient.delete(attachment.streamPath).catch(() => void 0);
3818
+ return { txid };
3819
+ }
3696
3820
  async setTag(entityUrl, key, req, token) {
3697
3821
  const entity = await this.registry.getEntity(entityUrl);
3698
3822
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
@@ -6094,6 +6218,7 @@ async function handleStreamAppend(request, runtime, forward) {
6094
6218
  const { manager } = runtime;
6095
6219
  const entity = await manager.registry.getEntityByStream(path$2);
6096
6220
  const isSharedState = path$2.startsWith(`/_electric/shared-state/`);
6221
+ if (!entity && manager.isAttachmentStreamPath(path$2)) return apiError(401, ErrCodeUnauthorized, `Invalid write token`);
6097
6222
  if (!entity && !isSharedState) return void 0;
6098
6223
  const body = await request.readBody();
6099
6224
  const event = decodeStreamAppendEvent(body);
@@ -6662,8 +6787,9 @@ async function streamAppend(request, ctx) {
6662
6787
  }));
6663
6788
  }
6664
6789
  async function proxyPassThrough(request, ctx) {
6665
- const upstream = await forwardToDurableStreams(ctx, request);
6666
6790
  const streamPath = new URL(request.url).pathname;
6791
+ if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
6792
+ const upstream = await forwardToDurableStreams(ctx, request);
6667
6793
  const method = request.method.toUpperCase();
6668
6794
  const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
6669
6795
  try {
@@ -6816,6 +6942,13 @@ const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
6816
6942
  lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
6817
6943
  reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6818
6944
  });
6945
+ const attachmentSubjectTypes = new Set([
6946
+ `inbox`,
6947
+ `run`,
6948
+ `text`,
6949
+ `tool_call`,
6950
+ `context`
6951
+ ]);
6819
6952
  const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
6820
6953
  entitiesRouter.get(`/`, listEntities);
6821
6954
  entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
@@ -6824,6 +6957,9 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
6824
6957
  entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
6825
6958
  entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
6826
6959
  entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
6960
+ entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
6961
+ entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
6962
+ entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
6827
6963
  entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
6828
6964
  entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
6829
6965
  entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
@@ -6850,6 +6986,82 @@ function requireExistingEntityRoute(request) {
6850
6986
  if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
6851
6987
  return request.entityRoute;
6852
6988
  }
6989
+ function invalidAttachmentRequest(message) {
6990
+ throw new ElectricAgentsError(ErrCodeInvalidRequest, message, 400);
6991
+ }
6992
+ function formString(form, key) {
6993
+ const value = form.get(key);
6994
+ if (typeof value !== `string`) return void 0;
6995
+ const trimmed = value.trim();
6996
+ return trimmed || void 0;
6997
+ }
6998
+ function parseJsonFormField(form, key) {
6999
+ const raw = formString(form, key);
7000
+ if (!raw) return void 0;
7001
+ try {
7002
+ return JSON.parse(raw);
7003
+ } catch {
7004
+ invalidAttachmentRequest(`Invalid JSON field: ${key}`);
7005
+ }
7006
+ }
7007
+ function parseAttachmentSubject(form) {
7008
+ const explicit = parseJsonFormField(form, `subject`);
7009
+ if (explicit !== void 0) {
7010
+ if (!explicit || typeof explicit !== `object` || Array.isArray(explicit)) invalidAttachmentRequest(`attachment subject must be an object`);
7011
+ const subject = explicit;
7012
+ const type$1 = subject.type;
7013
+ const key$1 = subject.key;
7014
+ if (typeof type$1 !== `string` || typeof key$1 !== `string`) invalidAttachmentRequest(`attachment subject requires type and key`);
7015
+ if (!attachmentSubjectTypes.has(type$1)) invalidAttachmentRequest(`invalid attachment subject type`);
7016
+ return {
7017
+ type: type$1,
7018
+ key: key$1
7019
+ };
7020
+ }
7021
+ const type = formString(form, `subjectType`);
7022
+ const key = formString(form, `subjectKey`);
7023
+ if (!type || !key) invalidAttachmentRequest(`attachment subject is required`);
7024
+ if (!attachmentSubjectTypes.has(type)) invalidAttachmentRequest(`invalid attachment subject type`);
7025
+ return {
7026
+ type,
7027
+ key
7028
+ };
7029
+ }
7030
+ function getUploadedFormFile(value) {
7031
+ if (value !== null && typeof value === `object` && `arrayBuffer` in value && typeof value.arrayBuffer === `function`) return value;
7032
+ return null;
7033
+ }
7034
+ async function parseAttachmentForm(request) {
7035
+ const contentType = request.headers.get(`content-type`)?.toLowerCase() ?? ``;
7036
+ if (!contentType.includes(`multipart/form-data`)) invalidAttachmentRequest(`Attachment uploads must use multipart/form-data`);
7037
+ let form;
7038
+ try {
7039
+ form = await request.formData();
7040
+ } catch {
7041
+ invalidAttachmentRequest(`Invalid multipart form data`);
7042
+ }
7043
+ const file = getUploadedFormFile(form.get(`file`));
7044
+ if (!file) invalidAttachmentRequest(`Missing file field`);
7045
+ const role = formString(form, `role`);
7046
+ if (role !== void 0 && role !== `input` && role !== `output`) invalidAttachmentRequest(`invalid attachment role`);
7047
+ const fileName = formString(form, `filename`) ?? (typeof file.name === `string` ? file.name : void 0);
7048
+ const mimeType = formString(form, `mimeType`) || (typeof file.type === `string` ? file.type : void 0) || `application/octet-stream`;
7049
+ const meta = parseJsonFormField(form, `meta`);
7050
+ if (meta !== void 0 && (typeof meta !== `object` || Array.isArray(meta))) invalidAttachmentRequest(`attachment meta must be an object`);
7051
+ return {
7052
+ id: formString(form, `id`),
7053
+ bytes: new Uint8Array(await file.arrayBuffer()),
7054
+ mimeType,
7055
+ filename: fileName,
7056
+ subject: parseAttachmentSubject(form),
7057
+ role,
7058
+ meta
7059
+ };
7060
+ }
7061
+ function contentDisposition(filename) {
7062
+ const fallback = filename.replace(/["\\\r\n]/g, `_`);
7063
+ return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
7064
+ }
6853
7065
  function rejectPrincipalEntityMutation(request, action) {
6854
7066
  const { entity } = requireExistingEntityRoute(request);
6855
7067
  if (entity.type !== `principal`) return void 0;
@@ -7034,6 +7246,44 @@ async function sendEntity(request, ctx) {
7034
7246
  });
7035
7247
  return (0, itty_router.status)(204);
7036
7248
  }
7249
+ async function createAttachment(request, ctx) {
7250
+ const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
7251
+ if (principalMutationError) return principalMutationError;
7252
+ const { entityUrl } = requireExistingEntityRoute(request);
7253
+ const form = await parseAttachmentForm(request);
7254
+ const result = await ctx.entityManager.createAttachment(entityUrl, {
7255
+ id: form.id,
7256
+ bytes: form.bytes,
7257
+ mimeType: form.mimeType,
7258
+ filename: form.filename,
7259
+ subject: form.subject,
7260
+ role: form.role,
7261
+ createdBy: ctx.principal.url,
7262
+ meta: form.meta
7263
+ });
7264
+ return (0, itty_router.json)(result, { status: 201 });
7265
+ }
7266
+ async function readAttachment(request, ctx) {
7267
+ const { entityUrl } = requireExistingEntityRoute(request);
7268
+ const result = await ctx.entityManager.readAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
7269
+ const headers = new Headers({
7270
+ "content-type": result.attachment.mimeType,
7271
+ "content-length": String(result.bytes.length),
7272
+ "cache-control": `private, max-age=31536000, immutable`
7273
+ });
7274
+ if (result.attachment.filename) headers.set(`content-disposition`, contentDisposition(result.attachment.filename));
7275
+ return new Response(result.bytes, {
7276
+ status: 200,
7277
+ headers
7278
+ });
7279
+ }
7280
+ async function deleteAttachment(request, ctx) {
7281
+ const principalMutationError = rejectPrincipalEntityMutation(request, `stripped of attachments`);
7282
+ if (principalMutationError) return principalMutationError;
7283
+ const { entityUrl } = requireExistingEntityRoute(request);
7284
+ const result = await ctx.entityManager.deleteAttachment(entityUrl, decodeURIComponent(request.params.attachmentId));
7285
+ return (0, itty_router.json)(result);
7286
+ }
7037
7287
  async function updateInboxMessage(request, ctx) {
7038
7288
  const parsed = routeBody(request);
7039
7289
  const { entityUrl } = requireExistingEntityRoute(request);
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<{