@zooid/transport-matrix 0.8.0 → 0.9.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.
- package/dist/index.d.ts +7 -0
- package/dist/index.js +118 -44
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bot-pool.ts +1 -1
- package/src/context-provider.ts +1 -1
- package/src/event-encoders.test.ts +22 -0
- package/src/event-encoders.ts +13 -0
- package/src/matrix-client.test.ts +35 -2
- package/src/matrix-client.ts +19 -0
- package/src/transport.test.ts +161 -32
- package/src/transport.ts +177 -68
- package/src/workforce-publisher.test.ts +2 -2
- package/src/workforce-publisher.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -92,6 +92,9 @@ declare class MatrixClient {
|
|
|
92
92
|
asUserId: string;
|
|
93
93
|
targetUserId: string;
|
|
94
94
|
}): Promise<void>;
|
|
95
|
+
leaveRoom(roomId: string, asUserId: string, opts?: {
|
|
96
|
+
reason?: string;
|
|
97
|
+
}): Promise<void>;
|
|
95
98
|
joinRoom(roomIdOrAlias: string, asUserId: string): Promise<void>;
|
|
96
99
|
sendMessage(input: SendMessageInput): Promise<{
|
|
97
100
|
event_id: string;
|
|
@@ -320,6 +323,10 @@ interface CreateMatrixTransportOptions {
|
|
|
320
323
|
media?: MediaClientLike;
|
|
321
324
|
/** Injected attachment writer (defaults to the real writeAttachment). */
|
|
322
325
|
writeAttachmentFn?: typeof writeAttachment;
|
|
326
|
+
/** AS sender-bot MXID (@<sender_localpart>:<server>). Together with the agent
|
|
327
|
+
* bindings this forms the set of "our bot users" whose ad-hoc invites are
|
|
328
|
+
* declined. */
|
|
329
|
+
botUserId?: string;
|
|
323
330
|
}
|
|
324
331
|
declare function createMatrixTransport(opts: CreateMatrixTransportOptions): {
|
|
325
332
|
app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
package/dist/index.js
CHANGED
|
@@ -125,6 +125,18 @@ var MatrixClient = class {
|
|
|
125
125
|
}
|
|
126
126
|
throw new Error(`invite(${opts.targetUserId}) failed: ${r.status}`);
|
|
127
127
|
}
|
|
128
|
+
async leaveRoom(roomId, asUserId, opts) {
|
|
129
|
+
const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/leave?user_id=${encodeURIComponent(asUserId)}`;
|
|
130
|
+
const r = await this.fetch(url, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${this.asToken}`,
|
|
134
|
+
"content-type": "application/json"
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(opts?.reason ? { reason: opts.reason } : {})
|
|
137
|
+
});
|
|
138
|
+
if (!r.ok) throw new Error(`leaveRoom(${roomId}, ${asUserId}) failed: ${r.status}`);
|
|
139
|
+
}
|
|
128
140
|
async joinRoom(roomIdOrAlias, asUserId) {
|
|
129
141
|
const url = `${this.homeserver}/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}?user_id=${encodeURIComponent(asUserId)}`;
|
|
130
142
|
const r = await this.fetch(url, {
|
|
@@ -729,6 +741,15 @@ function toPlanBody(evt) {
|
|
|
729
741
|
entries: evt.entries
|
|
730
742
|
};
|
|
731
743
|
}
|
|
744
|
+
function toAvailableCommandsBody(evt) {
|
|
745
|
+
return {
|
|
746
|
+
session_id: evt.sessionId,
|
|
747
|
+
available_commands: evt.commands.map((c) => ({
|
|
748
|
+
name: c.name,
|
|
749
|
+
description: c.description
|
|
750
|
+
}))
|
|
751
|
+
};
|
|
752
|
+
}
|
|
732
753
|
var RECOVERY_URLS = {
|
|
733
754
|
auth_missing: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
734
755
|
auth_invalid: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
@@ -985,7 +1006,7 @@ async function sendMediaError(ctx, _err, message, client) {
|
|
|
985
1006
|
await client.sendCustomEvent({
|
|
986
1007
|
roomId: ctx.roomId,
|
|
987
1008
|
asUserId: ctx.agent.userId,
|
|
988
|
-
eventType: "
|
|
1009
|
+
eventType: "dev.zooid.error",
|
|
989
1010
|
content: toErrorBody(
|
|
990
1011
|
{
|
|
991
1012
|
kind: "error",
|
|
@@ -998,7 +1019,7 @@ async function sendMediaError(ctx, _err, message, client) {
|
|
|
998
1019
|
},
|
|
999
1020
|
ctx.threadRoot
|
|
1000
1021
|
)
|
|
1001
|
-
}).catch((e) => console.warn(`[matrix:${ctx.agent.name}]
|
|
1022
|
+
}).catch((e) => console.warn(`[matrix:${ctx.agent.name}] dev.zooid.error send failed:`, e));
|
|
1002
1023
|
}
|
|
1003
1024
|
var SEEN_EVENT_CAP = 5e3;
|
|
1004
1025
|
var DRAIN_QUIET_MS = 300;
|
|
@@ -1009,13 +1030,18 @@ function inboundThreadRoot2(evt) {
|
|
|
1009
1030
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
1010
1031
|
}
|
|
1011
1032
|
function createMatrixTransport(opts) {
|
|
1012
|
-
const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
|
|
1033
|
+
const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId } = opts;
|
|
1013
1034
|
const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
|
|
1014
1035
|
const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
|
|
1015
1036
|
const mediaClient = opts.media;
|
|
1016
1037
|
const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
|
|
1017
1038
|
const pendingMedia = new PendingMediaStore();
|
|
1018
1039
|
const pool = new BotPool(client, bindings);
|
|
1040
|
+
const ourBotUserIds = /* @__PURE__ */ new Set([
|
|
1041
|
+
...botUserId ? [botUserId] : [],
|
|
1042
|
+
...bindings.map((b) => b.userId)
|
|
1043
|
+
]);
|
|
1044
|
+
const DECLINE_REASON = "Bots are placed in rooms only by the zooid daemon (workforce-as-code). Ad-hoc invites are declined \u2014 add the bot to the room in zooid.yaml.";
|
|
1019
1045
|
const sessions = /* @__PURE__ */ new Map();
|
|
1020
1046
|
const buffers = /* @__PURE__ */ new Map();
|
|
1021
1047
|
const bufferMessageIds = /* @__PURE__ */ new Map();
|
|
@@ -1023,23 +1049,68 @@ function createMatrixTransport(opts) {
|
|
|
1023
1049
|
const threadStates = /* @__PURE__ */ new Map();
|
|
1024
1050
|
const cutoffTs = Date.now() - STARTUP_GRACE_MS;
|
|
1025
1051
|
const seenEventIds = /* @__PURE__ */ new Set();
|
|
1052
|
+
const flushedCounts = /* @__PURE__ */ new Map();
|
|
1053
|
+
const pendingCommands = /* @__PURE__ */ new Map();
|
|
1054
|
+
const buildTextContent = (text) => {
|
|
1055
|
+
const content = {
|
|
1056
|
+
msgtype: "m.text",
|
|
1057
|
+
body: text
|
|
1058
|
+
};
|
|
1059
|
+
const html = toMatrixHtml(text);
|
|
1060
|
+
if (html) {
|
|
1061
|
+
const escapedPlain = "<p>" + text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "</p>";
|
|
1062
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
1063
|
+
if (norm(html) !== norm(escapedPlain)) {
|
|
1064
|
+
content.format = "org.matrix.custom.html";
|
|
1065
|
+
content.formatted_body = html;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return content;
|
|
1069
|
+
};
|
|
1070
|
+
const flushBuffer = (sessionId) => {
|
|
1071
|
+
const ctx = sessions.get(sessionId);
|
|
1072
|
+
const text = buffers.get(sessionId) ?? "";
|
|
1073
|
+
if (!ctx || text.length === 0) return false;
|
|
1074
|
+
buffers.set(sessionId, "");
|
|
1075
|
+
flushedCounts.set(sessionId, (flushedCounts.get(sessionId) ?? 0) + 1);
|
|
1076
|
+
const content = buildTextContent(text);
|
|
1077
|
+
const tail = (sendQueue.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
1078
|
+
try {
|
|
1079
|
+
await client.sendMessage({
|
|
1080
|
+
roomId: ctx.roomId,
|
|
1081
|
+
asUserId: ctx.agent.userId,
|
|
1082
|
+
content,
|
|
1083
|
+
threadRoot: ctx.threadRoot
|
|
1084
|
+
});
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
console.warn(`[matrix:${ctx.agent.name}] sendMessage flush failed:`, err);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
sendQueue.set(sessionId, tail);
|
|
1090
|
+
return true;
|
|
1091
|
+
};
|
|
1026
1092
|
agents.onEvent = async (name, event) => {
|
|
1027
1093
|
const ctx = sessions.get(event.sessionId);
|
|
1028
1094
|
if (!ctx) {
|
|
1029
|
-
|
|
1095
|
+
if (event.type === "available_commands") {
|
|
1096
|
+
pendingCommands.set(event.sessionId, event);
|
|
1097
|
+
} else {
|
|
1098
|
+
console.warn(`[matrix:${name}] no session ctx for ${event.sessionId}`);
|
|
1099
|
+
}
|
|
1030
1100
|
return;
|
|
1031
1101
|
}
|
|
1032
1102
|
if (event.type === "agent_message_chunk") {
|
|
1033
1103
|
const block = event.content;
|
|
1034
1104
|
if (block.type === "text" && typeof block.text === "string") {
|
|
1035
|
-
const current = buffers.get(event.sessionId) ?? "";
|
|
1036
1105
|
const prevMessageId = bufferMessageIds.get(event.sessionId);
|
|
1037
1106
|
const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
|
|
1038
|
-
const needsBreak = current.length > 0 && (block.text === "" || messageChanged);
|
|
1039
|
-
const prefix = needsBreak ? "\n\n" : "";
|
|
1040
|
-
buffers.set(event.sessionId, current + prefix + block.text);
|
|
1041
1107
|
if (event.messageId !== void 0)
|
|
1042
1108
|
bufferMessageIds.set(event.sessionId, event.messageId);
|
|
1109
|
+
if (messageChanged) flushBuffer(event.sessionId);
|
|
1110
|
+
const current = buffers.get(event.sessionId) ?? "";
|
|
1111
|
+
const needsBreak = current.length > 0 && block.text === "";
|
|
1112
|
+
const prefix = needsBreak ? "\n\n" : "";
|
|
1113
|
+
buffers.set(event.sessionId, current + prefix + block.text);
|
|
1043
1114
|
} else if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string" && mediaClient) {
|
|
1044
1115
|
const ctx2 = sessions.get(event.sessionId);
|
|
1045
1116
|
if (ctx2) {
|
|
@@ -1068,8 +1139,9 @@ function createMatrixTransport(opts) {
|
|
|
1068
1139
|
}
|
|
1069
1140
|
return;
|
|
1070
1141
|
}
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1142
|
+
flushBuffer(event.sessionId);
|
|
1143
|
+
const eventType = event.type === "tool_call" ? "dev.zooid.tool_call" : event.type === "tool_call_update" ? "dev.zooid.tool_call_update" : event.type === "available_commands" ? "dev.zooid.available_commands_update" : "dev.zooid.plan";
|
|
1144
|
+
const body = event.type === "tool_call" ? toToolCallBody(event) : event.type === "tool_call_update" ? toUpdateBody(event) : event.type === "available_commands" ? toAvailableCommandsBody(event) : toPlanBody(event);
|
|
1073
1145
|
body["m.relates_to"] = { rel_type: "m.thread", event_id: ctx.threadRoot };
|
|
1074
1146
|
const tail = (sendQueue.get(event.sessionId) ?? Promise.resolve()).then(async () => {
|
|
1075
1147
|
try {
|
|
@@ -1108,7 +1180,7 @@ function createMatrixTransport(opts) {
|
|
|
1108
1180
|
void client.sendCustomEvent({
|
|
1109
1181
|
roomId: ctx.roomId,
|
|
1110
1182
|
asUserId: ctx.agent.userId,
|
|
1111
|
-
eventType: "
|
|
1183
|
+
eventType: "dev.zooid.approval_request",
|
|
1112
1184
|
content
|
|
1113
1185
|
});
|
|
1114
1186
|
});
|
|
@@ -1142,20 +1214,33 @@ function createMatrixTransport(opts) {
|
|
|
1142
1214
|
);
|
|
1143
1215
|
continue;
|
|
1144
1216
|
}
|
|
1145
|
-
if (evt.type === "
|
|
1217
|
+
if (evt.type === "m.room.member" && evt.content?.membership === "invite") {
|
|
1218
|
+
const target = evt.state_key;
|
|
1219
|
+
const inviter = evt.sender;
|
|
1220
|
+
if (target && evt.room_id && ourBotUserIds.has(target) && (!inviter || !ourBotUserIds.has(inviter))) {
|
|
1221
|
+
console.log(
|
|
1222
|
+
`[matrix] declining ad-hoc invite for ${target} in ${evt.room_id} from ${inviter ?? "unknown"}`
|
|
1223
|
+
);
|
|
1224
|
+
await client.leaveRoom(evt.room_id, target, { reason: DECLINE_REASON }).catch(
|
|
1225
|
+
(err) => console.warn(`[matrix] leaveRoom(${evt.room_id}, ${target}) failed:`, err)
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (evt.type === "dev.zooid.session_reset") {
|
|
1146
1231
|
const relates = evt.content?.["m.relates_to"];
|
|
1147
1232
|
const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
|
|
1148
1233
|
if (!threadRoot) {
|
|
1149
|
-
console.log("[matrix] dropping
|
|
1234
|
+
console.log("[matrix] dropping dev.zooid.session_reset without thread relation");
|
|
1150
1235
|
continue;
|
|
1151
1236
|
}
|
|
1152
|
-
console.log(`[matrix] inbound
|
|
1237
|
+
console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
|
|
1153
1238
|
for (const a of bindings) {
|
|
1154
1239
|
agents.endSession(a.name, threadRoot);
|
|
1155
1240
|
}
|
|
1156
1241
|
continue;
|
|
1157
1242
|
}
|
|
1158
|
-
if (evt.type === "
|
|
1243
|
+
if (evt.type === "dev.zooid.interrupt") {
|
|
1159
1244
|
const content = evt.content ?? {};
|
|
1160
1245
|
const relates = evt.content?.["m.relates_to"];
|
|
1161
1246
|
const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
|
|
@@ -1177,7 +1262,7 @@ function createMatrixTransport(opts) {
|
|
|
1177
1262
|
continue;
|
|
1178
1263
|
}
|
|
1179
1264
|
if (!content.session_id) {
|
|
1180
|
-
console.warn(`[matrix]
|
|
1265
|
+
console.warn(`[matrix] dev.zooid.interrupt missing session_id (event_id=${evt.event_id})`);
|
|
1181
1266
|
continue;
|
|
1182
1267
|
}
|
|
1183
1268
|
const ctx = sessions.get(content.session_id);
|
|
@@ -1192,7 +1277,7 @@ function createMatrixTransport(opts) {
|
|
|
1192
1277
|
});
|
|
1193
1278
|
continue;
|
|
1194
1279
|
}
|
|
1195
|
-
if (evt.type === "
|
|
1280
|
+
if (evt.type === "dev.zooid.approval_response") {
|
|
1196
1281
|
const content = evt.content ?? {};
|
|
1197
1282
|
if (!content.session_id || !content.approval_id || !content.decision) continue;
|
|
1198
1283
|
const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
|
|
@@ -1282,9 +1367,9 @@ function createMatrixTransport(opts) {
|
|
|
1282
1367
|
void client.sendCustomEvent({
|
|
1283
1368
|
roomId: evt.room_id,
|
|
1284
1369
|
asUserId: a.userId,
|
|
1285
|
-
eventType: "
|
|
1370
|
+
eventType: "dev.zooid.error",
|
|
1286
1371
|
content: body2
|
|
1287
|
-
}).catch((e) => console.warn(`[matrix:${a.name}]
|
|
1372
|
+
}).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
|
|
1288
1373
|
});
|
|
1289
1374
|
}
|
|
1290
1375
|
}
|
|
@@ -1318,6 +1403,12 @@ function createMatrixTransport(opts) {
|
|
|
1318
1403
|
sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
|
|
1319
1404
|
buffers.set(sessionId, "");
|
|
1320
1405
|
bufferMessageIds.delete(sessionId);
|
|
1406
|
+
flushedCounts.set(sessionId, 0);
|
|
1407
|
+
const stashedCommands = pendingCommands.get(sessionId);
|
|
1408
|
+
if (stashedCommands) {
|
|
1409
|
+
pendingCommands.delete(sessionId);
|
|
1410
|
+
void agents.onEvent?.(agent.name, stashedCommands);
|
|
1411
|
+
}
|
|
1321
1412
|
const roomId = evt.room_id;
|
|
1322
1413
|
const TYPING_TTL_MS = 3e4;
|
|
1323
1414
|
const TYPING_REFRESH_MS = 25e3;
|
|
@@ -1363,32 +1454,13 @@ function createMatrixTransport(opts) {
|
|
|
1363
1454
|
while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
|
|
1364
1455
|
await delay(drainQuietMs);
|
|
1365
1456
|
const next = buffers.get(sessionId) ?? "";
|
|
1366
|
-
if (next === drained && next.length > 0)
|
|
1457
|
+
if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0))
|
|
1458
|
+
break;
|
|
1367
1459
|
drained = next;
|
|
1368
1460
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
const content = {
|
|
1373
|
-
msgtype: "m.text",
|
|
1374
|
-
body: text
|
|
1375
|
-
};
|
|
1376
|
-
if (html) {
|
|
1377
|
-
const escapedPlain = "<p>" + text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "</p>";
|
|
1378
|
-
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
1379
|
-
if (norm(html) !== norm(escapedPlain)) {
|
|
1380
|
-
content.format = "org.matrix.custom.html";
|
|
1381
|
-
content.formatted_body = html;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
await client.sendMessage({
|
|
1385
|
-
roomId: evt.room_id,
|
|
1386
|
-
asUserId: agent.userId,
|
|
1387
|
-
content,
|
|
1388
|
-
threadRoot
|
|
1389
|
-
// every reply threads, full stop
|
|
1390
|
-
});
|
|
1391
|
-
} else {
|
|
1461
|
+
flushBuffer(sessionId);
|
|
1462
|
+
await (sendQueue.get(sessionId) ?? Promise.resolve());
|
|
1463
|
+
if ((flushedCounts.get(sessionId) ?? 0) === 0) {
|
|
1392
1464
|
console.warn(
|
|
1393
1465
|
`[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
|
|
1394
1466
|
);
|
|
@@ -1399,6 +1471,8 @@ function createMatrixTransport(opts) {
|
|
|
1399
1471
|
await safePresence("online");
|
|
1400
1472
|
buffers.delete(sessionId);
|
|
1401
1473
|
bufferMessageIds.delete(sessionId);
|
|
1474
|
+
flushedCounts.delete(sessionId);
|
|
1475
|
+
sendQueue.delete(sessionId);
|
|
1402
1476
|
}
|
|
1403
1477
|
}
|
|
1404
1478
|
return {
|
|
@@ -1484,7 +1558,7 @@ async function publishWorkforce(opts) {
|
|
|
1484
1558
|
await opts.client.sendStateEvent({
|
|
1485
1559
|
roomId: opts.spaceRoomId,
|
|
1486
1560
|
asUserId: opts.asUserId,
|
|
1487
|
-
eventType: "
|
|
1561
|
+
eventType: "dev.zooid.workforce",
|
|
1488
1562
|
stateKey: "",
|
|
1489
1563
|
content: buildWorkforceRoster(opts.agents)
|
|
1490
1564
|
});
|