zooid 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6979,6 +6979,7 @@ var DockerAcpRuntime = class {
6979
6979
  this.opts = opts;
6980
6980
  this.engine = opts.engine ?? "docker";
6981
6981
  }
6982
+ opts;
6982
6983
  engine;
6983
6984
  buildArgv(spec) {
6984
6985
  const image = spec.image ?? this.opts.defaultImage;
@@ -23115,6 +23116,7 @@ var AgentProcess = class extends EventEmitter {
23115
23116
  super();
23116
23117
  this.options = options;
23117
23118
  }
23119
+ options;
23118
23120
  child = null;
23119
23121
  start() {
23120
23122
  if (this.child) return;
@@ -23182,6 +23184,7 @@ var JsonFileSessionStore = class {
23182
23184
  this.opts = opts;
23183
23185
  this.path = join(opts.dir, "sessions.json");
23184
23186
  }
23187
+ opts;
23185
23188
  mem = /* @__PURE__ */ new Map();
23186
23189
  path;
23187
23190
  writeChain = Promise.resolve();
@@ -23498,6 +23501,7 @@ var AcpClient = class {
23498
23501
  this.options = options;
23499
23502
  this.turns = options.onTap ? new TurnTracker({ agentId: options.agent.id, onTap: options.onTap }) : null;
23500
23503
  }
23504
+ options;
23501
23505
  process = null;
23502
23506
  runtimeChild = null;
23503
23507
  connection = null;
@@ -25047,6 +25051,7 @@ var MatrixContextProvider = class {
25047
25051
  constructor(opts) {
25048
25052
  this.opts = opts;
25049
25053
  }
25054
+ opts;
25050
25055
  async getRoomHistory(channelId, hopts) {
25051
25056
  const { chunk, end } = await this.opts.client.fetchRoomMessages({
25052
25057
  roomId: channelId,
@@ -25134,14 +25139,29 @@ var MatrixContextProvider = class {
25134
25139
  }
25135
25140
  toMessage(ev) {
25136
25141
  if (ev.type !== "m.room.message") return null;
25137
- if (ev.content?.msgtype !== "m.text" || typeof ev.content.body !== "string") return null;
25142
+ const msgtype = ev.content?.msgtype;
25143
+ const body = ev.content?.body;
25138
25144
  const agent = this.opts.agentBots.get(ev.sender);
25139
- const relatesTo = ev.content["m.relates_to"];
25145
+ const relatesTo = ev.content?.["m.relates_to"];
25140
25146
  const threadId = relatesTo?.rel_type === "m.thread" && relatesTo.event_id ? relatesTo.event_id : void 0;
25147
+ if (msgtype === "m.image" || msgtype === "m.file" || msgtype === "m.video" || msgtype === "m.audio") {
25148
+ const kind = msgtype.slice(2);
25149
+ const name = typeof body === "string" && body ? body : "untitled";
25150
+ return {
25151
+ id: ev.event_id,
25152
+ sender: ev.sender,
25153
+ text: `[${kind}: ${name}]`,
25154
+ timestamp: new Date(ev.origin_server_ts).toISOString(),
25155
+ is_agent: agent !== void 0,
25156
+ ...agent !== void 0 ? { agent_name: agent } : {},
25157
+ ...threadId !== void 0 ? { thread_id: threadId } : {}
25158
+ };
25159
+ }
25160
+ if (msgtype !== "m.text" || typeof body !== "string") return null;
25141
25161
  return {
25142
25162
  id: ev.event_id,
25143
25163
  sender: ev.sender,
25144
- text: ev.content.body,
25164
+ text: body,
25145
25165
  timestamp: new Date(ev.origin_server_ts).toISOString(),
25146
25166
  is_agent: agent !== void 0,
25147
25167
  ...agent !== void 0 ? { agent_name: agent } : {},
@@ -25215,6 +25235,10 @@ function stripMention(body, userId) {
25215
25235
  }
25216
25236
 
25217
25237
  // ../transport-matrix/src/router.ts
25238
+ var MEDIA_MSGTYPES = /* @__PURE__ */ new Set(["m.image", "m.file", "m.video", "m.audio"]);
25239
+ function isMediaMsgtype(t) {
25240
+ return t !== void 0 && MEDIA_MSGTYPES.has(t);
25241
+ }
25218
25242
  function inboundThreadRoot(event) {
25219
25243
  const r = event.content?.["m.relates_to"];
25220
25244
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
@@ -25222,6 +25246,7 @@ function inboundThreadRoot(event) {
25222
25246
  function route(event, agents, threadStates) {
25223
25247
  if (event.type !== "m.room.message") return [];
25224
25248
  if (!event.content?.msgtype) return [];
25249
+ if (isMediaMsgtype(event.content.msgtype)) return [];
25225
25250
  const mentions = new Set(extractMentions(event));
25226
25251
  const matches = [];
25227
25252
  const threadRoot = inboundThreadRoot(event);
@@ -25260,10 +25285,12 @@ async function ensureWorkforceSpace(opts) {
25260
25285
  name: display,
25261
25286
  preset: opts.preset,
25262
25287
  creation_content: { type: "m.space" },
25263
- // A workspace is joined by invitation, not self-service. Pin the space's
25264
- // join rule to invite regardless of preset so it can't be walked into
25265
- // (which would otherwise satisfy every restricted child room's allow).
25266
- initial_state: [{ type: "m.room.join_rules", state_key: "", content: { join_rule: "invite" } }]
25288
+ // Pin the join rule regardless of preset. Defaults to invite so the space
25289
+ // can't be walked into (which would otherwise satisfy every restricted
25290
+ // child room's allow); `zooid dev` opts into `public` for local-only use.
25291
+ initial_state: [
25292
+ { type: "m.room.join_rules", state_key: "", content: { join_rule: opts.joinRule ?? "invite" } }
25293
+ ]
25267
25294
  };
25268
25295
  if (opts.admins && opts.admins.length > 0) {
25269
25296
  body.invite = opts.admins;
@@ -25314,6 +25341,8 @@ var BotPool = class {
25314
25341
  this.client = client;
25315
25342
  this.agents = agents;
25316
25343
  }
25344
+ client;
25345
+ agents;
25317
25346
  async bootstrap(opts = {}) {
25318
25347
  const aliasToId = /* @__PURE__ */ new Map();
25319
25348
  const attachedToSpace = /* @__PURE__ */ new Set();
@@ -25576,8 +25605,180 @@ function toMatrixHtml(markdown) {
25576
25605
  });
25577
25606
  }
25578
25607
 
25608
+ // ../transport-matrix/src/pending-media.ts
25609
+ var MAX_MEDIA_PER_TURN = 8;
25610
+ var PendingMediaStore = class {
25611
+ queues = /* @__PURE__ */ new Map();
25612
+ key(roomId, threadKey) {
25613
+ return `${roomId} ${threadKey ?? "main"}`;
25614
+ }
25615
+ add(roomId, threadKey, item) {
25616
+ const k = this.key(roomId, threadKey);
25617
+ const q = this.queues.get(k) ?? [];
25618
+ q.push(item);
25619
+ while (q.length > MAX_MEDIA_PER_TURN) q.shift();
25620
+ this.queues.set(k, q);
25621
+ }
25622
+ drain(roomId, threadKey, sender) {
25623
+ const k = this.key(roomId, threadKey);
25624
+ const q = this.queues.get(k) ?? [];
25625
+ const mine = q.filter((i) => i.sender === sender);
25626
+ const rest = q.filter((i) => i.sender !== sender);
25627
+ if (rest.length) this.queues.set(k, rest);
25628
+ else this.queues.delete(k);
25629
+ return mine;
25630
+ }
25631
+ };
25632
+
25633
+ // ../transport-matrix/src/media-client.ts
25634
+ var MAX_INLINE_IMAGE_BYTES = 524288;
25635
+ var INLINE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
25636
+ var MAX_DOWNLOAD_BYTES = 33554432;
25637
+ function parseMxcUri(uri) {
25638
+ const m = /^mxc:\/\/([^/]+)\/(.+)$/.exec(uri);
25639
+ return m ? { serverName: m[1], mediaId: m[2] } : null;
25640
+ }
25641
+ var MediaClient = class {
25642
+ homeserver;
25643
+ asToken;
25644
+ fetch;
25645
+ constructor(opts) {
25646
+ this.homeserver = opts.homeserver.replace(/\/$/, "");
25647
+ this.asToken = opts.asToken;
25648
+ this.fetch = opts.fetch ?? globalThis.fetch;
25649
+ }
25650
+ async download(input) {
25651
+ const parsed = parseMxcUri(input.mxcUri);
25652
+ if (!parsed) throw new Error(`not an mxc uri: ${input.mxcUri}`);
25653
+ const url2 = `${this.homeserver}/_matrix/client/v1/media/download/${encodeURIComponent(parsed.serverName)}/${encodeURIComponent(parsed.mediaId)}?user_id=${encodeURIComponent(input.asUserId)}`;
25654
+ const r = await this.fetch(url2, {
25655
+ headers: { Authorization: `Bearer ${this.asToken}` }
25656
+ });
25657
+ if (!r.ok) throw new Error(`media download failed: ${r.status}`);
25658
+ const buf = new Uint8Array(await r.arrayBuffer());
25659
+ const max = input.maxBytes ?? MAX_DOWNLOAD_BYTES;
25660
+ if (buf.byteLength > max) {
25661
+ throw new Error(`media too large: ${buf.byteLength} > ${max}`);
25662
+ }
25663
+ return { data: buf, contentType: r.headers.get("content-type") ?? "application/octet-stream" };
25664
+ }
25665
+ async upload(input) {
25666
+ const params = new URLSearchParams();
25667
+ if (input.filename) params.set("filename", input.filename);
25668
+ params.set("user_id", input.asUserId);
25669
+ const r = await this.fetch(`${this.homeserver}/_matrix/media/v3/upload?${params}`, {
25670
+ method: "POST",
25671
+ headers: { Authorization: `Bearer ${this.asToken}`, "Content-Type": input.contentType },
25672
+ body: input.data
25673
+ });
25674
+ if (!r.ok) throw new Error(`media upload failed: ${r.status}`);
25675
+ return await r.json();
25676
+ }
25677
+ };
25678
+
25679
+ // ../transport-matrix/src/attachments.ts
25680
+ import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
25681
+ import { join as join4 } from "path";
25682
+ import { posix } from "path";
25683
+ function sanitize(s, fallback) {
25684
+ const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "").replace(/^\.+/, "");
25685
+ return cleaned || fallback;
25686
+ }
25687
+ function writeAttachment(input) {
25688
+ const dir = sanitize(input.eventId, "event");
25689
+ const name = sanitize(input.filename, "file");
25690
+ const hostDir = join4(input.workspaceDir, ".zooid", "attachments", dir);
25691
+ mkdirSync2(hostDir, { recursive: true });
25692
+ const hostPath = join4(hostDir, name);
25693
+ writeFileSync(hostPath, input.data);
25694
+ const agentPath = posix.join(input.agentWorkspacePath, ".zooid", "attachments", dir, name);
25695
+ return { hostPath, agentPath };
25696
+ }
25697
+
25579
25698
  // ../transport-matrix/src/transport.ts
25580
25699
  var STARTUP_GRACE_MS = 5e3;
25700
+ async function buildMediaBlocks(items, opts) {
25701
+ const blocks = [];
25702
+ const pathLines = [];
25703
+ if (!opts.media || items.length === 0) return { blocks, pathLines };
25704
+ for (const item of items) {
25705
+ try {
25706
+ const isInlineCandidate = item.msgtype === "m.image" && INLINE_IMAGE_MIMES.includes(item.info?.mimetype ?? "") && (item.info?.size === void 0 || item.info.size <= MAX_INLINE_IMAGE_BYTES);
25707
+ if (isInlineCandidate) {
25708
+ const { data, contentType } = await opts.media.download({
25709
+ mxcUri: item.url,
25710
+ asUserId: opts.agent.userId
25711
+ });
25712
+ if (data.byteLength <= MAX_INLINE_IMAGE_BYTES) {
25713
+ blocks.push({
25714
+ type: "image",
25715
+ data: Buffer.from(data).toString("base64"),
25716
+ mimeType: contentType
25717
+ });
25718
+ continue;
25719
+ }
25720
+ if (opts.agent.workspaceDir) {
25721
+ const paths = opts.writeAttachmentFn({
25722
+ workspaceDir: opts.agent.workspaceDir,
25723
+ agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
25724
+ eventId: item.eventId,
25725
+ filename: item.filename ?? item.body,
25726
+ data
25727
+ });
25728
+ blocks.push({
25729
+ type: "resource_link",
25730
+ uri: `file://${paths.agentPath}`,
25731
+ name: item.filename ?? item.body
25732
+ });
25733
+ pathLines.push(`Attached file: ${paths.agentPath}`);
25734
+ }
25735
+ } else {
25736
+ if (!opts.agent.workspaceDir) continue;
25737
+ const { data } = await opts.media.download({
25738
+ mxcUri: item.url,
25739
+ asUserId: opts.agent.userId
25740
+ });
25741
+ const paths = opts.writeAttachmentFn({
25742
+ workspaceDir: opts.agent.workspaceDir,
25743
+ agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
25744
+ eventId: item.eventId,
25745
+ filename: item.filename ?? item.body,
25746
+ data
25747
+ });
25748
+ blocks.push({
25749
+ type: "resource_link",
25750
+ uri: `file://${paths.agentPath}`,
25751
+ name: item.filename ?? item.body,
25752
+ mimeType: item.info?.mimetype,
25753
+ size: item.info?.size
25754
+ });
25755
+ pathLines.push(`Attached file: ${paths.agentPath}`);
25756
+ }
25757
+ } catch (err) {
25758
+ opts.onError(item, err);
25759
+ }
25760
+ }
25761
+ return { blocks, pathLines };
25762
+ }
25763
+ async function sendMediaError(ctx, _err, message, client) {
25764
+ await client.sendCustomEvent({
25765
+ roomId: ctx.roomId,
25766
+ asUserId: ctx.agent.userId,
25767
+ eventType: "eco.zoon.error",
25768
+ content: toErrorBody(
25769
+ {
25770
+ kind: "error",
25771
+ agentId: ctx.agent.name,
25772
+ sessionId: null,
25773
+ turnId: null,
25774
+ code: "media_failed",
25775
+ message: message.slice(0, 250),
25776
+ transient: false
25777
+ },
25778
+ ctx.threadRoot
25779
+ )
25780
+ }).catch((e) => console.warn(`[matrix:${ctx.agent.name}] eco.zoon.error send failed:`, e));
25781
+ }
25581
25782
  var SEEN_EVENT_CAP = 5e3;
25582
25783
  var DRAIN_QUIET_MS = 300;
25583
25784
  var DRAIN_MAX_MS = 3e4;
@@ -25590,6 +25791,9 @@ function createMatrixTransport(opts) {
25590
25791
  const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
25591
25792
  const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
25592
25793
  const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
25794
+ const mediaClient = opts.media;
25795
+ const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
25796
+ const pendingMedia = new PendingMediaStore();
25593
25797
  const pool = new BotPool(client, bindings);
25594
25798
  const sessions = /* @__PURE__ */ new Map();
25595
25799
  const buffers = /* @__PURE__ */ new Map();
@@ -25615,6 +25819,29 @@ function createMatrixTransport(opts) {
25615
25819
  buffers.set(event.sessionId, current + prefix + block.text);
25616
25820
  if (event.messageId !== void 0)
25617
25821
  bufferMessageIds.set(event.sessionId, event.messageId);
25822
+ } else if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string" && mediaClient) {
25823
+ const ctx2 = sessions.get(event.sessionId);
25824
+ if (ctx2) {
25825
+ const bytes = Buffer.from(block.data, "base64");
25826
+ const ext = (block.mimeType.split("/")[1] ?? "png").replace(/[^a-z0-9]/gi, "");
25827
+ const filename = `image.${ext}`;
25828
+ void mediaClient.upload({ data: bytes, contentType: block.mimeType, filename, asUserId: ctx2.agent.userId }).then(
25829
+ ({ content_uri }) => client.sendMessage({
25830
+ roomId: ctx2.roomId,
25831
+ asUserId: ctx2.agent.userId,
25832
+ threadRoot: ctx2.threadRoot,
25833
+ content: {
25834
+ msgtype: "m.image",
25835
+ body: filename,
25836
+ url: content_uri,
25837
+ info: { mimetype: block.mimeType, size: bytes.length }
25838
+ }
25839
+ })
25840
+ ).catch((err) => {
25841
+ console.warn(`[matrix:${name}] outbound image upload failed:`, err);
25842
+ void sendMediaError(ctx2, err, "agent image upload failed", client);
25843
+ });
25844
+ }
25618
25845
  } else {
25619
25846
  console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
25620
25847
  }
@@ -25757,6 +25984,18 @@ function createMatrixTransport(opts) {
25757
25984
  continue;
25758
25985
  }
25759
25986
  logInbound(evt);
25987
+ if (evt.type === "m.room.message" && isMediaMsgtype(evt.content?.msgtype) && evt.room_id && evt.event_id && evt.sender && evt.content?.url && !bindings.some((b) => b.userId === evt.sender)) {
25988
+ pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
25989
+ eventId: evt.event_id,
25990
+ sender: evt.sender,
25991
+ msgtype: evt.content.msgtype,
25992
+ body: evt.content.body ?? "",
25993
+ filename: evt.content.filename,
25994
+ url: evt.content.url,
25995
+ info: evt.content.info
25996
+ });
25997
+ continue;
25998
+ }
25760
25999
  const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
25761
26000
  const inboundRel = inboundThreadRoot2(evt);
25762
26001
  if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
@@ -25873,10 +26112,30 @@ function createMatrixTransport(opts) {
25873
26112
  try {
25874
26113
  const rawBody = evt.content?.body ?? "";
25875
26114
  const promptText = stripMention(rawBody, agent.userId);
26115
+ const pendingItems = pendingMedia.drain(
26116
+ evt.room_id,
26117
+ inboundThreadRoot2(evt),
26118
+ evt.sender ?? ""
26119
+ );
26120
+ const { blocks, pathLines } = await buildMediaBlocks(pendingItems, {
26121
+ agent,
26122
+ media: mediaClient,
26123
+ writeAttachmentFn,
26124
+ onError: (item, err) => {
26125
+ console.warn(`[matrix:${agent.name}] media_failed for ${item.body}:`, err);
26126
+ void sendMediaError(
26127
+ { agent, roomId: evt.room_id, threadRoot },
26128
+ err,
26129
+ `Could not process attachment: ${item.body}`,
26130
+ client
26131
+ );
26132
+ }
26133
+ });
26134
+ const fullPromptText = [promptText, ...pathLines].filter(Boolean).join("\n");
25876
26135
  await agents.prompt(agent.name, {
25877
26136
  threadId: sessionKey,
25878
26137
  channelId: evt.room_id,
25879
- content: [{ type: "text", text: promptText }]
26138
+ content: [...blocks, { type: "text", text: fullPromptText }]
25880
26139
  });
25881
26140
  const drainStart = Date.now();
25882
26141
  let drained = buffers.get(sessionId) ?? "";
@@ -31591,10 +31850,11 @@ export {
31591
31850
  renderRegistration,
31592
31851
  ensureWorkforceSpace,
31593
31852
  ensureDefaultChannel,
31853
+ MediaClient,
31594
31854
  createMatrixTransport,
31595
31855
  startWorkforcePublisher,
31596
31856
  SpawnRegistry,
31597
31857
  startDaemonSocketServer,
31598
31858
  buildAcpRegistry
31599
31859
  };
31600
- //# sourceMappingURL=chunk-OKGNOROJ.js.map
31860
+ //# sourceMappingURL=chunk-R6EDLH23.js.map