zooid 0.7.3 → 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();
@@ -23352,7 +23355,11 @@ function acpUpdateToAgentEvent(notif) {
23352
23355
  return {
23353
23356
  type: "agent_message_chunk",
23354
23357
  sessionId,
23355
- content: update.content
23358
+ content: update.content,
23359
+ // `messageId` is not part of the ACP `agent_message_chunk` schema, but
23360
+ // opencode includes it. Forward it so transports can split on message
23361
+ // boundaries; harmlessly undefined for agents that omit it.
23362
+ messageId: update.messageId
23356
23363
  };
23357
23364
  case "tool_call":
23358
23365
  return {
@@ -23494,6 +23501,7 @@ var AcpClient = class {
23494
23501
  this.options = options;
23495
23502
  this.turns = options.onTap ? new TurnTracker({ agentId: options.agent.id, onTap: options.onTap }) : null;
23496
23503
  }
23504
+ options;
23497
23505
  process = null;
23498
23506
  runtimeChild = null;
23499
23507
  connection = null;
@@ -25043,6 +25051,7 @@ var MatrixContextProvider = class {
25043
25051
  constructor(opts) {
25044
25052
  this.opts = opts;
25045
25053
  }
25054
+ opts;
25046
25055
  async getRoomHistory(channelId, hopts) {
25047
25056
  const { chunk, end } = await this.opts.client.fetchRoomMessages({
25048
25057
  roomId: channelId,
@@ -25130,14 +25139,29 @@ var MatrixContextProvider = class {
25130
25139
  }
25131
25140
  toMessage(ev) {
25132
25141
  if (ev.type !== "m.room.message") return null;
25133
- 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;
25134
25144
  const agent = this.opts.agentBots.get(ev.sender);
25135
- const relatesTo = ev.content["m.relates_to"];
25145
+ const relatesTo = ev.content?.["m.relates_to"];
25136
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;
25137
25161
  return {
25138
25162
  id: ev.event_id,
25139
25163
  sender: ev.sender,
25140
- text: ev.content.body,
25164
+ text: body,
25141
25165
  timestamp: new Date(ev.origin_server_ts).toISOString(),
25142
25166
  is_agent: agent !== void 0,
25143
25167
  ...agent !== void 0 ? { agent_name: agent } : {},
@@ -25211,6 +25235,10 @@ function stripMention(body, userId) {
25211
25235
  }
25212
25236
 
25213
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
+ }
25214
25242
  function inboundThreadRoot(event) {
25215
25243
  const r = event.content?.["m.relates_to"];
25216
25244
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
@@ -25218,6 +25246,7 @@ function inboundThreadRoot(event) {
25218
25246
  function route(event, agents, threadStates) {
25219
25247
  if (event.type !== "m.room.message") return [];
25220
25248
  if (!event.content?.msgtype) return [];
25249
+ if (isMediaMsgtype(event.content.msgtype)) return [];
25221
25250
  const mentions = new Set(extractMentions(event));
25222
25251
  const matches = [];
25223
25252
  const threadRoot = inboundThreadRoot(event);
@@ -25256,10 +25285,12 @@ async function ensureWorkforceSpace(opts) {
25256
25285
  name: display,
25257
25286
  preset: opts.preset,
25258
25287
  creation_content: { type: "m.space" },
25259
- // A workspace is joined by invitation, not self-service. Pin the space's
25260
- // join rule to invite regardless of preset so it can't be walked into
25261
- // (which would otherwise satisfy every restricted child room's allow).
25262
- 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
+ ]
25263
25294
  };
25264
25295
  if (opts.admins && opts.admins.length > 0) {
25265
25296
  body.invite = opts.admins;
@@ -25310,6 +25341,8 @@ var BotPool = class {
25310
25341
  this.client = client;
25311
25342
  this.agents = agents;
25312
25343
  }
25344
+ client;
25345
+ agents;
25313
25346
  async bootstrap(opts = {}) {
25314
25347
  const aliasToId = /* @__PURE__ */ new Map();
25315
25348
  const attachedToSpace = /* @__PURE__ */ new Set();
@@ -25572,8 +25605,180 @@ function toMatrixHtml(markdown) {
25572
25605
  });
25573
25606
  }
25574
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
+
25575
25698
  // ../transport-matrix/src/transport.ts
25576
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
+ }
25577
25782
  var SEEN_EVENT_CAP = 5e3;
25578
25783
  var DRAIN_QUIET_MS = 300;
25579
25784
  var DRAIN_MAX_MS = 3e4;
@@ -25586,9 +25791,13 @@ function createMatrixTransport(opts) {
25586
25791
  const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
25587
25792
  const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
25588
25793
  const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
25794
+ const mediaClient = opts.media;
25795
+ const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
25796
+ const pendingMedia = new PendingMediaStore();
25589
25797
  const pool = new BotPool(client, bindings);
25590
25798
  const sessions = /* @__PURE__ */ new Map();
25591
25799
  const buffers = /* @__PURE__ */ new Map();
25800
+ const bufferMessageIds = /* @__PURE__ */ new Map();
25592
25801
  const sendQueue = /* @__PURE__ */ new Map();
25593
25802
  const threadStates = /* @__PURE__ */ new Map();
25594
25803
  const cutoffTs = Date.now() - STARTUP_GRACE_MS;
@@ -25603,8 +25812,36 @@ function createMatrixTransport(opts) {
25603
25812
  const block = event.content;
25604
25813
  if (block.type === "text" && typeof block.text === "string") {
25605
25814
  const current = buffers.get(event.sessionId) ?? "";
25606
- const prefix = block.text === "" && current.length > 0 ? "\n\n" : "";
25815
+ const prevMessageId = bufferMessageIds.get(event.sessionId);
25816
+ const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
25817
+ const needsBreak = current.length > 0 && (block.text === "" || messageChanged);
25818
+ const prefix = needsBreak ? "\n\n" : "";
25607
25819
  buffers.set(event.sessionId, current + prefix + block.text);
25820
+ if (event.messageId !== void 0)
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
+ }
25608
25845
  } else {
25609
25846
  console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
25610
25847
  }
@@ -25747,6 +25984,18 @@ function createMatrixTransport(opts) {
25747
25984
  continue;
25748
25985
  }
25749
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
+ }
25750
25999
  const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
25751
26000
  const inboundRel = inboundThreadRoot2(evt);
25752
26001
  if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
@@ -25847,6 +26096,7 @@ function createMatrixTransport(opts) {
25847
26096
  const sessionId = await agents.ensureSession(agent.name, sessionKey, evt.room_id);
25848
26097
  sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
25849
26098
  buffers.set(sessionId, "");
26099
+ bufferMessageIds.delete(sessionId);
25850
26100
  const roomId = evt.room_id;
25851
26101
  const TYPING_TTL_MS = 3e4;
25852
26102
  const TYPING_REFRESH_MS = 25e3;
@@ -25862,10 +26112,30 @@ function createMatrixTransport(opts) {
25862
26112
  try {
25863
26113
  const rawBody = evt.content?.body ?? "";
25864
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");
25865
26135
  await agents.prompt(agent.name, {
25866
26136
  threadId: sessionKey,
25867
26137
  channelId: evt.room_id,
25868
- content: [{ type: "text", text: promptText }]
26138
+ content: [...blocks, { type: "text", text: fullPromptText }]
25869
26139
  });
25870
26140
  const drainStart = Date.now();
25871
26141
  let drained = buffers.get(sessionId) ?? "";
@@ -25907,6 +26177,7 @@ function createMatrixTransport(opts) {
25907
26177
  await safeTyping(false);
25908
26178
  await safePresence("online");
25909
26179
  buffers.delete(sessionId);
26180
+ bufferMessageIds.delete(sessionId);
25910
26181
  }
25911
26182
  }
25912
26183
  return {
@@ -31579,10 +31850,11 @@ export {
31579
31850
  renderRegistration,
31580
31851
  ensureWorkforceSpace,
31581
31852
  ensureDefaultChannel,
31853
+ MediaClient,
31582
31854
  createMatrixTransport,
31583
31855
  startWorkforcePublisher,
31584
31856
  SpawnRegistry,
31585
31857
  startDaemonSocketServer,
31586
31858
  buildAcpRegistry
31587
31859
  };
31588
- //# sourceMappingURL=chunk-3IKBBKGI.js.map
31860
+ //# sourceMappingURL=chunk-R6EDLH23.js.map