@zooid/transport-matrix 0.13.0 → 0.14.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 +152 -4
- package/dist/index.js +898 -94
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/context-provider.test.ts +61 -4
- package/src/context-provider.ts +40 -2
- package/src/index.ts +23 -3
- package/src/invocation-registry.test.ts +22 -0
- package/src/invocation-registry.ts +32 -0
- package/src/matrix-client.ts +58 -34
- package/src/router.test.ts +129 -10
- package/src/router.ts +78 -2
- package/src/task-completion.test.ts +23 -0
- package/src/task-completion.ts +37 -0
- package/src/task-dispatch.test.ts +53 -0
- package/src/task-dispatch.ts +68 -0
- package/src/task-envelope.test.ts +27 -0
- package/src/task-registry.test.ts +50 -0
- package/src/task-registry.ts +152 -0
- package/src/transport.test.ts +403 -114
- package/src/transport.ts +719 -110
package/dist/index.js
CHANGED
|
@@ -13,7 +13,10 @@ var MatrixClient = class {
|
|
|
13
13
|
const r = await this.fetch(`${this.homeserver}/_matrix/client/v3/register`, {
|
|
14
14
|
method: "POST",
|
|
15
15
|
headers: { Authorization: `Bearer ${this.asToken}` },
|
|
16
|
-
body: JSON.stringify({
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
type: "m.login.application_service",
|
|
18
|
+
username: localpart2
|
|
19
|
+
})
|
|
17
20
|
});
|
|
18
21
|
if (r.status === 200) return await r.json();
|
|
19
22
|
if (r.status === 400) {
|
|
@@ -151,12 +154,15 @@ var MatrixClient = class {
|
|
|
151
154
|
async sendMessage(input) {
|
|
152
155
|
const content = { ...input.content };
|
|
153
156
|
if (input.threadRoot) {
|
|
154
|
-
content["m.relates_to"] = {
|
|
157
|
+
content["m.relates_to"] = {
|
|
158
|
+
rel_type: "m.thread",
|
|
159
|
+
event_id: input.threadRoot
|
|
160
|
+
};
|
|
155
161
|
}
|
|
156
|
-
return this.sendEvent(input.roomId, input.asUserId, "m.room.message", content);
|
|
162
|
+
return this.sendEvent(input.roomId, input.asUserId, "m.room.message", content, input.txnId);
|
|
157
163
|
}
|
|
158
164
|
async sendCustomEvent(input) {
|
|
159
|
-
return this.sendEvent(input.roomId, input.asUserId, input.eventType, input.content);
|
|
165
|
+
return this.sendEvent(input.roomId, input.asUserId, input.eventType, input.content, input.txnId);
|
|
160
166
|
}
|
|
161
167
|
async setTyping(input) {
|
|
162
168
|
const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(input.roomId)}/typing/${encodeURIComponent(input.asUserId)}?user_id=${encodeURIComponent(input.asUserId)}`;
|
|
@@ -218,7 +224,7 @@ var MatrixClient = class {
|
|
|
218
224
|
user_id: opts.asUserId
|
|
219
225
|
});
|
|
220
226
|
if (opts.from) params.set("from", opts.from);
|
|
221
|
-
const url = `${this.homeserver}/_matrix/client/v1/rooms/${encodeURIComponent(opts.roomId)}/relations/${encodeURIComponent(opts.rootEventId)}/m.thread?${params.toString()}`;
|
|
227
|
+
const url = `${this.homeserver}/_matrix/client/v1/rooms/${encodeURIComponent(opts.roomId)}/relations/${encodeURIComponent(opts.rootEventId)}/m.thread/m.room.message?${params.toString()}`;
|
|
222
228
|
const r = await this.fetch(url, {
|
|
223
229
|
method: "GET",
|
|
224
230
|
headers: { Authorization: `Bearer ${this.asToken}` }
|
|
@@ -281,15 +287,19 @@ var MatrixClient = class {
|
|
|
281
287
|
const body = await r.json();
|
|
282
288
|
return body.name ?? null;
|
|
283
289
|
}
|
|
284
|
-
async sendEvent(roomId, asUserId, eventType, content) {
|
|
285
|
-
const txn = randomUUID();
|
|
290
|
+
async sendEvent(roomId, asUserId, eventType, content, txnId) {
|
|
291
|
+
const txn = txnId ?? randomUUID();
|
|
286
292
|
const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/${eventType}/${txn}?user_id=${encodeURIComponent(asUserId)}`;
|
|
287
293
|
const r = await this.fetch(url, {
|
|
288
294
|
method: "PUT",
|
|
289
295
|
headers: { Authorization: `Bearer ${this.asToken}` },
|
|
290
296
|
body: JSON.stringify(content)
|
|
291
297
|
});
|
|
292
|
-
if (!r.ok)
|
|
298
|
+
if (!r.ok) {
|
|
299
|
+
const err = new Error(`sendEvent(${eventType}) failed: ${r.status}`);
|
|
300
|
+
err.status = r.status;
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
293
303
|
return await r.json();
|
|
294
304
|
}
|
|
295
305
|
};
|
|
@@ -429,7 +439,7 @@ var MatrixContextProvider = class {
|
|
|
429
439
|
};
|
|
430
440
|
});
|
|
431
441
|
}
|
|
432
|
-
async
|
|
442
|
+
async getRoomInfo(channelId) {
|
|
433
443
|
const name = await this.opts.client.fetchRoomName(channelId, this.opts.asUserId);
|
|
434
444
|
return {
|
|
435
445
|
id: channelId,
|
|
@@ -437,6 +447,30 @@ var MatrixContextProvider = class {
|
|
|
437
447
|
transport: "matrix"
|
|
438
448
|
};
|
|
439
449
|
}
|
|
450
|
+
async getRooms() {
|
|
451
|
+
const rooms = this.opts.rooms ?? [];
|
|
452
|
+
return Promise.all(
|
|
453
|
+
rooms.map(async (r) => {
|
|
454
|
+
const name = await this.opts.client.fetchRoomName(r.alias, this.opts.asUserId);
|
|
455
|
+
return { id: r.alias, name: name ?? r.alias, transport: "matrix" };
|
|
456
|
+
})
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
async sendMessage(input) {
|
|
460
|
+
const rooms = this.opts.rooms ?? [];
|
|
461
|
+
if (!rooms.some((r) => r.alias === input.room)) {
|
|
462
|
+
throw new Error(`not_in_room: this agent is not a member of ${input.room}`);
|
|
463
|
+
}
|
|
464
|
+
const { event_id } = await this.opts.client.sendMessage({
|
|
465
|
+
roomId: input.room,
|
|
466
|
+
asUserId: this.opts.asUserId,
|
|
467
|
+
// m.notice, not m.text: agent prose sends as m.notice so
|
|
468
|
+
// .m.rule.suppress_notices silences it server-side (ZNC025 §10).
|
|
469
|
+
content: { msgtype: "m.notice", body: input.text },
|
|
470
|
+
...input.thread_id ? { threadRoot: input.thread_id } : {}
|
|
471
|
+
});
|
|
472
|
+
return { event_id, ...input.thread_id ? { thread_id: input.thread_id } : {} };
|
|
473
|
+
}
|
|
440
474
|
};
|
|
441
475
|
|
|
442
476
|
// src/registration.ts
|
|
@@ -515,7 +549,7 @@ function inboundThreadRoot(event) {
|
|
|
515
549
|
const r = event.content?.["m.relates_to"];
|
|
516
550
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
517
551
|
}
|
|
518
|
-
function route(event, agents, threadStates) {
|
|
552
|
+
function route(event, agents, threadStates, task) {
|
|
519
553
|
if (event.type !== "m.room.message") return [];
|
|
520
554
|
if (!event.content?.msgtype) return [];
|
|
521
555
|
if (isMediaMsgtype(event.content.msgtype)) return [];
|
|
@@ -524,8 +558,25 @@ function route(event, agents, threadStates) {
|
|
|
524
558
|
const threadRoot = inboundThreadRoot(event);
|
|
525
559
|
const threadState = threadRoot ? threadStates?.get(threadRoot) : void 0;
|
|
526
560
|
for (const a of agents) {
|
|
527
|
-
if (event.sender === a.userId) continue;
|
|
528
561
|
if (!a.rooms.some((r) => r.alias === event.room_id)) continue;
|
|
562
|
+
if (task?.isRoot) {
|
|
563
|
+
if (a.name === task.assignee) matches.push(a);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (event.sender === a.userId) continue;
|
|
567
|
+
if (task) {
|
|
568
|
+
if (mentions.has(a.userId)) {
|
|
569
|
+
matches.push(a);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const senderAgent = agents.find((x) => x.userId === event.sender);
|
|
573
|
+
if (senderAgent) {
|
|
574
|
+
continue;
|
|
575
|
+
} else if (a.name === task.assignee) {
|
|
576
|
+
matches.push(a);
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
529
580
|
if (a.trigger === "any") {
|
|
530
581
|
matches.push(a);
|
|
531
582
|
continue;
|
|
@@ -537,7 +588,7 @@ function route(event, agents, threadStates) {
|
|
|
537
588
|
if (threadState) {
|
|
538
589
|
const senderAgent = agents.find((x) => x.userId === event.sender);
|
|
539
590
|
if (senderAgent) {
|
|
540
|
-
if (
|
|
591
|
+
if (isReturnRoute(event, a, agents, threadState)) matches.push(a);
|
|
541
592
|
} else {
|
|
542
593
|
const lastPoster = threadState.participants.at(-1);
|
|
543
594
|
if (lastPoster) {
|
|
@@ -550,6 +601,22 @@ function route(event, agents, threadStates) {
|
|
|
550
601
|
}
|
|
551
602
|
return matches;
|
|
552
603
|
}
|
|
604
|
+
function isReturnRoute(event, agent, agents, threadState) {
|
|
605
|
+
if (!threadState || agent.trigger !== "mention") return false;
|
|
606
|
+
const sender = agents.find((x) => x.userId === event.sender);
|
|
607
|
+
if (!sender || sender.name === agent.name) return false;
|
|
608
|
+
return threadState.callers[sender.name] === agent.name;
|
|
609
|
+
}
|
|
610
|
+
function wouldCycleCallers(callers, callee, caller) {
|
|
611
|
+
const seen = /* @__PURE__ */ new Set();
|
|
612
|
+
let cursor = caller;
|
|
613
|
+
while (cursor !== void 0) {
|
|
614
|
+
if (cursor === callee || seen.has(cursor)) return true;
|
|
615
|
+
seen.add(cursor);
|
|
616
|
+
cursor = callers[cursor];
|
|
617
|
+
}
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
553
620
|
|
|
554
621
|
// src/space-provisioner.ts
|
|
555
622
|
async function ensureWorkforceSpace(opts) {
|
|
@@ -745,6 +812,7 @@ function buildUserPowerLevels(asUserId, admins, agents, roomAlias) {
|
|
|
745
812
|
// src/transport.ts
|
|
746
813
|
import { Hono } from "hono";
|
|
747
814
|
import { timingSafeEqual } from "crypto";
|
|
815
|
+
import { THREAD_RESULT_FIELD, THREAD_START_FIELD as THREAD_START_FIELD2 } from "@zooid/core";
|
|
748
816
|
|
|
749
817
|
// src/session-keys.ts
|
|
750
818
|
var HANDOFF_KEY_SEP = "|";
|
|
@@ -1061,8 +1129,282 @@ var SyncLoop = class {
|
|
|
1061
1129
|
}
|
|
1062
1130
|
};
|
|
1063
1131
|
|
|
1132
|
+
// src/transport.ts
|
|
1133
|
+
import { NO_PENDING_INPUT } from "@zooid/core";
|
|
1134
|
+
|
|
1135
|
+
// src/task-registry.ts
|
|
1136
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1137
|
+
var MAX_OPEN_TASKS_PER_ROOM = 5;
|
|
1138
|
+
var TaskRegistry = class {
|
|
1139
|
+
constructor(opts = {}) {
|
|
1140
|
+
this.opts = opts;
|
|
1141
|
+
this.runIdValue = this.opts.runId ?? randomUUID2();
|
|
1142
|
+
}
|
|
1143
|
+
opts;
|
|
1144
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1145
|
+
byRoot = /* @__PURE__ */ new Map();
|
|
1146
|
+
generations = /* @__PURE__ */ new Map();
|
|
1147
|
+
runIdValue;
|
|
1148
|
+
get max() {
|
|
1149
|
+
return this.opts.maxOpenPerRoom ?? MAX_OPEN_TASKS_PER_ROOM;
|
|
1150
|
+
}
|
|
1151
|
+
get runId() {
|
|
1152
|
+
return this.runIdValue;
|
|
1153
|
+
}
|
|
1154
|
+
save() {
|
|
1155
|
+
if (!this.opts.journal) return;
|
|
1156
|
+
const rows = [...this.tasks.values()].map((r) => ({ ...r, runId: r.runId ?? this.runId }));
|
|
1157
|
+
const max = this.opts.maxClosedRecords ?? 500;
|
|
1158
|
+
const open = rows.filter((r) => r.phase !== "closed");
|
|
1159
|
+
const closed = rows.filter((r) => r.phase === "closed").sort((a, b) => (b.closedAt ?? "").localeCompare(a.closedAt ?? "")).slice(0, max);
|
|
1160
|
+
this.opts.journal.save([...open, ...closed]);
|
|
1161
|
+
}
|
|
1162
|
+
openCount(roomId) {
|
|
1163
|
+
return [...this.tasks.values()].filter((t) => t.roomId === roomId && t.phase !== "closed").length;
|
|
1164
|
+
}
|
|
1165
|
+
reserve(input) {
|
|
1166
|
+
if (this.openCount(input.roomId) >= this.max) return;
|
|
1167
|
+
const id = this.opts.newId?.() ?? randomUUID2();
|
|
1168
|
+
const rec = {
|
|
1169
|
+
taskId: id,
|
|
1170
|
+
attemptId: id,
|
|
1171
|
+
phase: "reserved",
|
|
1172
|
+
runId: this.runId,
|
|
1173
|
+
...input
|
|
1174
|
+
};
|
|
1175
|
+
this.tasks.set(id, rec);
|
|
1176
|
+
this.save();
|
|
1177
|
+
return rec;
|
|
1178
|
+
}
|
|
1179
|
+
activate(taskId, threadRoot) {
|
|
1180
|
+
const r = this.tasks.get(taskId);
|
|
1181
|
+
if (!r) return;
|
|
1182
|
+
r.phase = "open";
|
|
1183
|
+
r.threadRoot = threadRoot;
|
|
1184
|
+
this.byRoot.set(threadRoot, taskId);
|
|
1185
|
+
this.save();
|
|
1186
|
+
}
|
|
1187
|
+
abandon(taskId) {
|
|
1188
|
+
this.tasks.delete(taskId);
|
|
1189
|
+
this.save();
|
|
1190
|
+
}
|
|
1191
|
+
markUncertain(taskId) {
|
|
1192
|
+
const r = this.tasks.get(taskId);
|
|
1193
|
+
if (r?.phase === "reserved") r.phase = "uncertain";
|
|
1194
|
+
this.save();
|
|
1195
|
+
}
|
|
1196
|
+
adopt(attemptId, threadRoot) {
|
|
1197
|
+
const r = this.tasks.get(attemptId);
|
|
1198
|
+
if (!r) return;
|
|
1199
|
+
if (r.phase === "closed" || r.threadRoot && r.threadRoot !== threadRoot) return r;
|
|
1200
|
+
this.activate(r.taskId, threadRoot);
|
|
1201
|
+
return r;
|
|
1202
|
+
}
|
|
1203
|
+
taskForRoot(threadRoot) {
|
|
1204
|
+
const id = this.byRoot.get(threadRoot);
|
|
1205
|
+
return id ? this.tasks.get(id) : void 0;
|
|
1206
|
+
}
|
|
1207
|
+
openTaskFor(agent, root) {
|
|
1208
|
+
const r = this.taskForRoot(root);
|
|
1209
|
+
return r?.phase === "open" && r.assignee === agent ? r : void 0;
|
|
1210
|
+
}
|
|
1211
|
+
recordSummary(id, summary) {
|
|
1212
|
+
const r = this.tasks.get(id);
|
|
1213
|
+
if (!r || r.summary !== void 0) return "already_recorded";
|
|
1214
|
+
r.summary = summary;
|
|
1215
|
+
this.save();
|
|
1216
|
+
return "recorded";
|
|
1217
|
+
}
|
|
1218
|
+
clearSummary(id) {
|
|
1219
|
+
const r = this.tasks.get(id);
|
|
1220
|
+
if (r) r.summary = void 0;
|
|
1221
|
+
this.save();
|
|
1222
|
+
}
|
|
1223
|
+
close(id) {
|
|
1224
|
+
const r = this.tasks.get(id);
|
|
1225
|
+
if (!r || r.phase === "closed") return false;
|
|
1226
|
+
r.phase = "closed";
|
|
1227
|
+
r.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1228
|
+
this.save();
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
/** Reconcile records from a prior daemon run and retain closed roots for trust checks. */
|
|
1232
|
+
restore() {
|
|
1233
|
+
const rows = this.opts.journal?.load() ?? [];
|
|
1234
|
+
const interrupted = [];
|
|
1235
|
+
for (const row of rows) {
|
|
1236
|
+
const rec = { ...row };
|
|
1237
|
+
if (rec.phase !== "closed" && rec.runId !== this.runId) {
|
|
1238
|
+
rec.phase = "closed";
|
|
1239
|
+
rec.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1240
|
+
interrupted.push(rec);
|
|
1241
|
+
}
|
|
1242
|
+
this.tasks.set(rec.taskId, rec);
|
|
1243
|
+
if (rec.threadRoot) this.byRoot.set(rec.threadRoot, rec.taskId);
|
|
1244
|
+
}
|
|
1245
|
+
this.save();
|
|
1246
|
+
return interrupted;
|
|
1247
|
+
}
|
|
1248
|
+
generationOf(agent, session) {
|
|
1249
|
+
return this.generations.get(`${agent}::${session}`) ?? 0;
|
|
1250
|
+
}
|
|
1251
|
+
bumpGeneration(agent, session) {
|
|
1252
|
+
const k = `${agent}::${session}`;
|
|
1253
|
+
this.generations.set(k, this.generationOf(agent, session) + 1);
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// src/invocation-registry.ts
|
|
1258
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1259
|
+
var InvocationRegistry = class {
|
|
1260
|
+
constructor(opts = {}) {
|
|
1261
|
+
this.opts = opts;
|
|
1262
|
+
}
|
|
1263
|
+
opts;
|
|
1264
|
+
records = /* @__PURE__ */ new Map();
|
|
1265
|
+
byCallee = /* @__PURE__ */ new Map();
|
|
1266
|
+
byEvent = /* @__PURE__ */ new Map();
|
|
1267
|
+
open(input) {
|
|
1268
|
+
const record = { invocationId: this.opts.newId?.() ?? randomUUID3(), state: "outstanding", ...input };
|
|
1269
|
+
this.records.set(record.invocationId, record);
|
|
1270
|
+
return record;
|
|
1271
|
+
}
|
|
1272
|
+
attachCallEvent(id, eventId, sessionKey) {
|
|
1273
|
+
const r = this.records.get(id);
|
|
1274
|
+
if (!r) return;
|
|
1275
|
+
r.callEventId = eventId;
|
|
1276
|
+
r.calleeSessionKey = sessionKey;
|
|
1277
|
+
this.byEvent.set(eventId, id);
|
|
1278
|
+
this.byCallee.set(sessionKey, id);
|
|
1279
|
+
}
|
|
1280
|
+
get(id) {
|
|
1281
|
+
return this.records.get(id);
|
|
1282
|
+
}
|
|
1283
|
+
byCallEvent(eventId) {
|
|
1284
|
+
const id = this.byEvent.get(eventId);
|
|
1285
|
+
return id ? this.records.get(id) : void 0;
|
|
1286
|
+
}
|
|
1287
|
+
forCalleeSession(session) {
|
|
1288
|
+
const id = this.byCallee.get(session);
|
|
1289
|
+
return id ? this.records.get(id) : void 0;
|
|
1290
|
+
}
|
|
1291
|
+
outstandingFor(session) {
|
|
1292
|
+
return [...this.records.values()].filter((x) => x.state === "outstanding" && x.callerSessionKey === session);
|
|
1293
|
+
}
|
|
1294
|
+
outstandingForTask(taskId) {
|
|
1295
|
+
return [...this.records.values()].filter((x) => x.state === "outstanding" && x.taskId === taskId);
|
|
1296
|
+
}
|
|
1297
|
+
resolve(id) {
|
|
1298
|
+
const r = this.records.get(id);
|
|
1299
|
+
if (!r || r.state !== "outstanding") return;
|
|
1300
|
+
r.state = "returned";
|
|
1301
|
+
return r;
|
|
1302
|
+
}
|
|
1303
|
+
cancelForTask(taskId) {
|
|
1304
|
+
const records = this.outstandingForTask(taskId);
|
|
1305
|
+
for (const r of records) r.state = "cancelled";
|
|
1306
|
+
return records;
|
|
1307
|
+
}
|
|
1308
|
+
ancestorAgents(session) {
|
|
1309
|
+
const agents = [], seen = /* @__PURE__ */ new Set([session]);
|
|
1310
|
+
let cursor = session;
|
|
1311
|
+
for (; ; ) {
|
|
1312
|
+
const r = this.forCalleeSession(cursor);
|
|
1313
|
+
if (!r || r.state !== "outstanding") break;
|
|
1314
|
+
agents.push(r.callerAgent);
|
|
1315
|
+
if (seen.has(r.callerSessionKey)) break;
|
|
1316
|
+
seen.add(r.callerSessionKey);
|
|
1317
|
+
cursor = r.callerSessionKey;
|
|
1318
|
+
}
|
|
1319
|
+
return agents;
|
|
1320
|
+
}
|
|
1321
|
+
isOutstandingAncestor(session, agent) {
|
|
1322
|
+
return this.ancestorAgents(session).includes(agent);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
// src/task-completion.ts
|
|
1327
|
+
function evaluateCompletion(input) {
|
|
1328
|
+
const prose = input.prose?.trim();
|
|
1329
|
+
const output = prose ? { type: "message", text: prose } : void 0;
|
|
1330
|
+
const finish = (completion) => ({
|
|
1331
|
+
decision: "finish",
|
|
1332
|
+
completion: { agent: input.agent, thread_id: input.threadId, ...completion }
|
|
1333
|
+
});
|
|
1334
|
+
if (input.error !== void 0)
|
|
1335
|
+
return finish({ status: "failed", error: input.error instanceof Error ? input.error.message : String(input.error), ...output ? { output } : {} });
|
|
1336
|
+
if (input.stopReason === "cancelled") return finish({ status: "cancelled", ...output ? { output } : {} });
|
|
1337
|
+
if (input.stopReason === "max_tokens" || input.stopReason === "max_turn_requests")
|
|
1338
|
+
return finish({ status: "partial", reason: input.stopReason, ...output ? { output } : {} });
|
|
1339
|
+
if (input.stopReason === "refusal") return finish({ status: "failed", reason: "refusal", ...output ? { output } : {} });
|
|
1340
|
+
if (input.awaitingHuman > 0) return { decision: "stay_open", reason: "awaiting_human" };
|
|
1341
|
+
if (input.outstanding > 0) return { decision: "stay_open", reason: "outstanding_handoff" };
|
|
1342
|
+
if (input.summary) return finish({ status: "complete", output: { type: "message", text: input.summary } });
|
|
1343
|
+
if (output) return finish({ status: "complete", output });
|
|
1344
|
+
return finish({ status: "failed", reason: "no_result", error: "No result produced" });
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/task-dispatch.ts
|
|
1348
|
+
import { THREAD_START_FIELD } from "@zooid/core";
|
|
1349
|
+
function checkDelegable(agentName, roomId, bindings) {
|
|
1350
|
+
const target = bindings.find((b) => b.name === agentName);
|
|
1351
|
+
if (!target)
|
|
1352
|
+
return {
|
|
1353
|
+
ok: false,
|
|
1354
|
+
reason: `unknown_agent: no agent named "${agentName}" is configured here`
|
|
1355
|
+
};
|
|
1356
|
+
if (!target.rooms.some((r) => r.alias === roomId))
|
|
1357
|
+
return {
|
|
1358
|
+
ok: false,
|
|
1359
|
+
reason: `not_in_room: "${agentName}" is not a member of this room`
|
|
1360
|
+
};
|
|
1361
|
+
return { ok: true };
|
|
1362
|
+
}
|
|
1363
|
+
function buildAssignmentContent(input) {
|
|
1364
|
+
const escaped = input.prompt.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1365
|
+
const body = `${input.assigneeUserId} ${input.prompt}`.trim();
|
|
1366
|
+
return {
|
|
1367
|
+
msgtype: "m.notice",
|
|
1368
|
+
body,
|
|
1369
|
+
format: "org.matrix.custom.html",
|
|
1370
|
+
formatted_body: `<a href="https://matrix.to/#/${encodeURIComponent(input.assigneeUserId)}">${input.assigneeUserId}</a> ${escaped}`,
|
|
1371
|
+
"m.mentions": { user_ids: [input.assigneeUserId] },
|
|
1372
|
+
[THREAD_START_FIELD]: input.start
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function renderCompletionPrompt(c) {
|
|
1376
|
+
return [
|
|
1377
|
+
`[task result] ${c.agent} \u2014 status: ${c.status} (thread ${c.thread_id})`,
|
|
1378
|
+
...c.reason ? [`reason: ${c.reason}`] : [],
|
|
1379
|
+
...c.error ? [`error: ${c.error}`] : [],
|
|
1380
|
+
...c.output?.text ? ["", c.output.text] : []
|
|
1381
|
+
].join("\n");
|
|
1382
|
+
}
|
|
1383
|
+
function renderInvocationReturn(c) {
|
|
1384
|
+
return [
|
|
1385
|
+
`[handoff result] ${c.agent} \u2014 status: ${c.status}`,
|
|
1386
|
+
...c.reason ? [`reason: ${c.reason}`] : [],
|
|
1387
|
+
...c.error ? [`error: ${c.error}`] : [],
|
|
1388
|
+
...c.output?.text ? ["", c.output.text] : []
|
|
1389
|
+
].join("\n");
|
|
1390
|
+
}
|
|
1391
|
+
function renderDelivery(notify) {
|
|
1392
|
+
return notify === "caller" ? "Each result returns to you as a new turn when that task completes. End your turn now \u2014 do not read the task thread to wait for it." : "No result returns to you. The task thread is the result surface; thread_id is for later reference, not something to wait on.";
|
|
1393
|
+
}
|
|
1394
|
+
function renderAssigneeEnvelope(input) {
|
|
1395
|
+
return [
|
|
1396
|
+
`[task] from ${input.parentAgent} \u2014 you are the assignee of this thread.`,
|
|
1397
|
+
"Call zooid_complete_task with a self-contained summary when you are done;",
|
|
1398
|
+
"ending your turn without one publishes your last message as the result.",
|
|
1399
|
+
"Sibling task threads are refused here \u2014 @mention an agent in this thread to hand off.",
|
|
1400
|
+
"",
|
|
1401
|
+
input.prompt
|
|
1402
|
+
].join("\n");
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1064
1405
|
// src/transport.ts
|
|
1065
1406
|
var STARTUP_GRACE_MS = 5e3;
|
|
1407
|
+
var RETURN_GRACE_MS = 9e4;
|
|
1066
1408
|
async function buildMediaBlocks(items, opts) {
|
|
1067
1409
|
const blocks = [];
|
|
1068
1410
|
const pathLines = [];
|
|
@@ -1154,9 +1496,19 @@ function inboundThreadRoot2(evt) {
|
|
|
1154
1496
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
1155
1497
|
}
|
|
1156
1498
|
function createMatrixTransport(opts) {
|
|
1157
|
-
const {
|
|
1499
|
+
const {
|
|
1500
|
+
agents,
|
|
1501
|
+
approvals,
|
|
1502
|
+
client,
|
|
1503
|
+
bindings,
|
|
1504
|
+
hsToken,
|
|
1505
|
+
adminUserId,
|
|
1506
|
+
botUserId,
|
|
1507
|
+
mode = "appservice"
|
|
1508
|
+
} = opts;
|
|
1158
1509
|
const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
|
|
1159
1510
|
const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
|
|
1511
|
+
const returnGraceMs = opts.returnGraceMs ?? RETURN_GRACE_MS;
|
|
1160
1512
|
const mediaClient = opts.media;
|
|
1161
1513
|
const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
|
|
1162
1514
|
const pendingMedia = new PendingMediaStore();
|
|
@@ -1171,6 +1523,70 @@ function createMatrixTransport(opts) {
|
|
|
1171
1523
|
const bufferMessageIds = /* @__PURE__ */ new Map();
|
|
1172
1524
|
const sendQueue = /* @__PURE__ */ new Map();
|
|
1173
1525
|
const threadStates = /* @__PURE__ */ new Map();
|
|
1526
|
+
const taskRegistry = new TaskRegistry({ journal: opts.taskJournal, runId: opts.taskRunId });
|
|
1527
|
+
const interruptedTasks = taskRegistry.restore();
|
|
1528
|
+
const invocations = new InvocationRegistry();
|
|
1529
|
+
const pendingInput = opts.pendingInput ?? NO_PENDING_INPUT;
|
|
1530
|
+
const bindingFor = (name) => bindings.find((b) => b.name === name);
|
|
1531
|
+
const turnQueues = /* @__PURE__ */ new Map();
|
|
1532
|
+
const pendingReturns = /* @__PURE__ */ new Map();
|
|
1533
|
+
const returnKey = (agentName, threadRoot) => `${agentName}::${threadRoot}`;
|
|
1534
|
+
function stashReturn(sender, threadRoot, roomId, evt, targets) {
|
|
1535
|
+
const key = returnKey(sender.name, threadRoot);
|
|
1536
|
+
let pending = pendingReturns.get(key);
|
|
1537
|
+
if (!pending) {
|
|
1538
|
+
pending = { roomId, threadRoot, event: evt, texts: [], targets: /* @__PURE__ */ new Map() };
|
|
1539
|
+
pendingReturns.set(key, pending);
|
|
1540
|
+
}
|
|
1541
|
+
pending.event = evt;
|
|
1542
|
+
const body = evt.content?.body?.trim();
|
|
1543
|
+
if (body) pending.texts.push(body);
|
|
1544
|
+
for (const t of targets) pending.targets.set(t.name, t);
|
|
1545
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1546
|
+
pending.timer = setTimeout(() => releaseReturn(key), returnGraceMs);
|
|
1547
|
+
pending.timer.unref?.();
|
|
1548
|
+
console.log(
|
|
1549
|
+
`[matrix] holding return ${sender.name} \u2192 ${[...pending.targets.keys()].join(",")} until turn end (thread=${threadRoot})`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
function releaseReturn(key) {
|
|
1553
|
+
const pending = pendingReturns.get(key);
|
|
1554
|
+
if (!pending) return;
|
|
1555
|
+
pendingReturns.delete(key);
|
|
1556
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1557
|
+
const promptText = pending.texts.join("\n\n");
|
|
1558
|
+
for (const target of pending.targets.values()) {
|
|
1559
|
+
console.log(`[matrix] \u2192 ${target.name} (${target.userId}) [return]`);
|
|
1560
|
+
void enqueueTurn(target, {
|
|
1561
|
+
roomId: pending.roomId,
|
|
1562
|
+
threadRoot: pending.threadRoot,
|
|
1563
|
+
sessionKey: sessionKeyFor(
|
|
1564
|
+
target.name,
|
|
1565
|
+
pending.threadRoot,
|
|
1566
|
+
threadStates.get(pending.threadRoot)
|
|
1567
|
+
),
|
|
1568
|
+
event: pending.event,
|
|
1569
|
+
...promptText ? { promptText } : {}
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function dropPendingReturn(threadRoot, agentName) {
|
|
1574
|
+
for (const [key, pending] of pendingReturns) {
|
|
1575
|
+
if (pending.threadRoot !== threadRoot) continue;
|
|
1576
|
+
if (!pending.targets.delete(agentName)) continue;
|
|
1577
|
+
if (pending.targets.size === 0) {
|
|
1578
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1579
|
+
pendingReturns.delete(key);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function dropThreadReturns(threadRoot) {
|
|
1584
|
+
for (const [key, pending] of pendingReturns) {
|
|
1585
|
+
if (pending.threadRoot !== threadRoot) continue;
|
|
1586
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1587
|
+
pendingReturns.delete(key);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1174
1590
|
const cutoffTs = mode === "client" ? Number.NEGATIVE_INFINITY : Date.now() - STARTUP_GRACE_MS;
|
|
1175
1591
|
const seenEventIds = /* @__PURE__ */ new Set();
|
|
1176
1592
|
const flushedCounts = /* @__PURE__ */ new Map();
|
|
@@ -1204,14 +1620,21 @@ function createMatrixTransport(opts) {
|
|
|
1204
1620
|
lastFlushed.set(sessionId, text);
|
|
1205
1621
|
flushedCounts.set(sessionId, (flushedCounts.get(sessionId) ?? 0) + 1);
|
|
1206
1622
|
const content = buildTextContent(text);
|
|
1623
|
+
const pendingInvocations = registerOutgoingHandoffs(sessionId, text);
|
|
1207
1624
|
const tail = (sendQueue.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
1208
1625
|
try {
|
|
1209
|
-
await client.sendMessage({
|
|
1626
|
+
const { event_id } = await client.sendMessage({
|
|
1210
1627
|
roomId: ctx.roomId,
|
|
1211
1628
|
asUserId: ctx.agent.userId,
|
|
1212
1629
|
content,
|
|
1213
1630
|
threadRoot: ctx.threadRoot
|
|
1214
1631
|
});
|
|
1632
|
+
for (const invocation of pendingInvocations)
|
|
1633
|
+
invocations.attachCallEvent(
|
|
1634
|
+
invocation.invocationId,
|
|
1635
|
+
event_id,
|
|
1636
|
+
composeHandoffKey(ctx.threadRoot, event_id)
|
|
1637
|
+
);
|
|
1215
1638
|
} catch (err) {
|
|
1216
1639
|
console.warn(`[matrix:${ctx.agent.name}] sendMessage flush failed:`, err);
|
|
1217
1640
|
}
|
|
@@ -1219,6 +1642,30 @@ function createMatrixTransport(opts) {
|
|
|
1219
1642
|
sendQueue.set(sessionId, tail);
|
|
1220
1643
|
return true;
|
|
1221
1644
|
};
|
|
1645
|
+
function registerOutgoingHandoffs(sessionId, text) {
|
|
1646
|
+
const ctx = sessions.get(sessionId);
|
|
1647
|
+
if (!ctx) return [];
|
|
1648
|
+
const task = taskRegistry.taskForRoot(ctx.threadRoot);
|
|
1649
|
+
if (!task || task.phase !== "open") return [];
|
|
1650
|
+
const sessionKey = sessionKeyFor(ctx.agent.name, ctx.threadRoot, threadStates.get(ctx.threadRoot));
|
|
1651
|
+
const opened = [];
|
|
1652
|
+
for (const userId of extractMentions({ content: { body: text } })) {
|
|
1653
|
+
const callee = bindings.find((binding) => binding.userId === userId);
|
|
1654
|
+
if (!callee || callee.name === ctx.agent.name) continue;
|
|
1655
|
+
if (invocations.isOutstandingAncestor(sessionKey, callee.name)) {
|
|
1656
|
+
void client.sendCustomEvent({
|
|
1657
|
+
roomId: ctx.roomId,
|
|
1658
|
+
asUserId: ctx.agent.userId,
|
|
1659
|
+
eventType: "dev.zooid.error",
|
|
1660
|
+
content: { body: `\u26A0 [handoff_circular] Cannot hand off to ${callee.name}: it is waiting on ${ctx.agent.name}`, code: "handoff_circular", message: `Cannot hand off to ${callee.name}: it is waiting on ${ctx.agent.name}`, transient: false, "m.relates_to": { rel_type: "m.thread", event_id: ctx.threadRoot } }
|
|
1661
|
+
});
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
opened.push(invocations.open({ taskId: task.taskId, callerAgent: ctx.agent.name, callerSessionKey: sessionKey, calleeAgent: callee.name }));
|
|
1665
|
+
taskRegistry.clearSummary(task.taskId);
|
|
1666
|
+
}
|
|
1667
|
+
return opened;
|
|
1668
|
+
}
|
|
1222
1669
|
agents.onEvent = async (name, event) => {
|
|
1223
1670
|
const ctx = sessions.get(event.sessionId);
|
|
1224
1671
|
if (!ctx) {
|
|
@@ -1234,8 +1681,7 @@ function createMatrixTransport(opts) {
|
|
|
1234
1681
|
if (block.type === "text" && typeof block.text === "string") {
|
|
1235
1682
|
const prevMessageId = bufferMessageIds.get(event.sessionId);
|
|
1236
1683
|
const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
|
|
1237
|
-
if (event.messageId !== void 0)
|
|
1238
|
-
bufferMessageIds.set(event.sessionId, event.messageId);
|
|
1684
|
+
if (event.messageId !== void 0) bufferMessageIds.set(event.sessionId, event.messageId);
|
|
1239
1685
|
if (messageChanged) flushBuffer(event.sessionId);
|
|
1240
1686
|
const current = buffers.get(event.sessionId) ?? "";
|
|
1241
1687
|
const needsBreak = current.length > 0 && block.text === "";
|
|
@@ -1247,7 +1693,12 @@ function createMatrixTransport(opts) {
|
|
|
1247
1693
|
const bytes = Buffer.from(block.data, "base64");
|
|
1248
1694
|
const ext = (block.mimeType.split("/")[1] ?? "png").replace(/[^a-z0-9]/gi, "");
|
|
1249
1695
|
const filename = `image.${ext}`;
|
|
1250
|
-
void mediaClient.upload({
|
|
1696
|
+
void mediaClient.upload({
|
|
1697
|
+
data: bytes,
|
|
1698
|
+
contentType: block.mimeType,
|
|
1699
|
+
filename,
|
|
1700
|
+
asUserId: ctx2.agent.userId
|
|
1701
|
+
}).then(
|
|
1251
1702
|
({ content_uri }) => client.sendMessage({
|
|
1252
1703
|
roomId: ctx2.roomId,
|
|
1253
1704
|
asUserId: ctx2.agent.userId,
|
|
@@ -1303,7 +1754,10 @@ function createMatrixTransport(opts) {
|
|
|
1303
1754
|
tool_call_id: handle.toolCallId,
|
|
1304
1755
|
options: handle.options
|
|
1305
1756
|
};
|
|
1306
|
-
content["m.relates_to"] = {
|
|
1757
|
+
content["m.relates_to"] = {
|
|
1758
|
+
rel_type: "m.thread",
|
|
1759
|
+
event_id: ctx.threadRoot
|
|
1760
|
+
};
|
|
1307
1761
|
if (handle.toolKind !== void 0) content.tool_kind = handle.toolKind;
|
|
1308
1762
|
if (handle.toolTitle !== void 0) content.tool_title = handle.toolTitle;
|
|
1309
1763
|
if (handle.toolInput !== void 0) content.tool_input = handle.toolInput;
|
|
@@ -1314,6 +1768,51 @@ function createMatrixTransport(opts) {
|
|
|
1314
1768
|
content
|
|
1315
1769
|
});
|
|
1316
1770
|
});
|
|
1771
|
+
function reportTurnFailure(agent, input, err) {
|
|
1772
|
+
console.error(`[matrix] runTurn failed for ${agent.name}:`, err);
|
|
1773
|
+
const c = classify(err);
|
|
1774
|
+
const body = toErrorBody(
|
|
1775
|
+
{
|
|
1776
|
+
kind: "error",
|
|
1777
|
+
agentId: agent.name,
|
|
1778
|
+
sessionId: null,
|
|
1779
|
+
turnId: null,
|
|
1780
|
+
code: c.code,
|
|
1781
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1782
|
+
detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
|
|
1783
|
+
transient: c.transient,
|
|
1784
|
+
acp_error: c.acp_error
|
|
1785
|
+
},
|
|
1786
|
+
input.threadRoot
|
|
1787
|
+
);
|
|
1788
|
+
void client.sendCustomEvent({
|
|
1789
|
+
roomId: input.roomId,
|
|
1790
|
+
asUserId: agent.userId,
|
|
1791
|
+
eventType: "dev.zooid.error",
|
|
1792
|
+
content: body
|
|
1793
|
+
}).catch((e) => console.warn(`[matrix:${agent.name}] dev.zooid.error send failed:`, e));
|
|
1794
|
+
}
|
|
1795
|
+
function enqueueTurn(agent, input) {
|
|
1796
|
+
const key = `${agent.name}::${input.sessionKey}`;
|
|
1797
|
+
const chained = (turnQueues.get(key) ?? Promise.resolve()).then(() => runTurn(agent, input)).then(() => {
|
|
1798
|
+
let st = threadStates.get(input.threadRoot);
|
|
1799
|
+
if (!st) {
|
|
1800
|
+
st = {
|
|
1801
|
+
participants: [],
|
|
1802
|
+
rootMentions: [],
|
|
1803
|
+
callers: {},
|
|
1804
|
+
handoffs: {}
|
|
1805
|
+
};
|
|
1806
|
+
threadStates.set(input.threadRoot, st);
|
|
1807
|
+
}
|
|
1808
|
+
if (st.participants.at(-1) !== agent.name) st.participants.push(agent.name);
|
|
1809
|
+
}).catch((err) => reportTurnFailure(agent, input, err));
|
|
1810
|
+
turnQueues.set(key, chained);
|
|
1811
|
+
void chained.finally(() => {
|
|
1812
|
+
if (turnQueues.get(key) === chained) turnQueues.delete(key);
|
|
1813
|
+
});
|
|
1814
|
+
return chained;
|
|
1815
|
+
}
|
|
1317
1816
|
async function handleInboundEvent(evt) {
|
|
1318
1817
|
if (evt.event_id) {
|
|
1319
1818
|
if (seenEventIds.has(evt.event_id)) {
|
|
@@ -1352,6 +1851,7 @@ function createMatrixTransport(opts) {
|
|
|
1352
1851
|
return;
|
|
1353
1852
|
}
|
|
1354
1853
|
console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
|
|
1854
|
+
dropThreadReturns(threadRoot);
|
|
1355
1855
|
if (!threadStates.has(threadRoot) && evt.room_id) {
|
|
1356
1856
|
try {
|
|
1357
1857
|
threadStates.set(
|
|
@@ -1365,8 +1865,11 @@ function createMatrixTransport(opts) {
|
|
|
1365
1865
|
const st = threadStates.get(threadRoot);
|
|
1366
1866
|
for (const a of bindings) {
|
|
1367
1867
|
agents.endSession(a.name, threadRoot);
|
|
1868
|
+
taskRegistry.bumpGeneration(a.name, threadRoot);
|
|
1368
1869
|
for (const arc of st?.handoffs[a.name] ?? []) {
|
|
1369
|
-
|
|
1870
|
+
const key = composeHandoffKey(threadRoot, arc);
|
|
1871
|
+
agents.endSession(a.name, key);
|
|
1872
|
+
taskRegistry.bumpGeneration(a.name, key);
|
|
1370
1873
|
}
|
|
1371
1874
|
}
|
|
1372
1875
|
return;
|
|
@@ -1390,6 +1893,15 @@ function createMatrixTransport(opts) {
|
|
|
1390
1893
|
console.error(`[matrix] cancelSession(${t.agent}, ${t.sessionId}) failed:`, err);
|
|
1391
1894
|
});
|
|
1392
1895
|
}
|
|
1896
|
+
const task = taskRegistry.taskForRoot(threadRoot);
|
|
1897
|
+
if (task?.phase === "open" && !targets.some((t) => t.agent === task.assignee)) {
|
|
1898
|
+
const assignee = bindingFor(task.assignee);
|
|
1899
|
+
if (assignee)
|
|
1900
|
+
await finishTask(task, {
|
|
1901
|
+
agent: assignee,
|
|
1902
|
+
completion: { agent: assignee.name, thread_id: threadRoot, status: "cancelled" }
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1393
1905
|
return;
|
|
1394
1906
|
}
|
|
1395
1907
|
if (!content.session_id) {
|
|
@@ -1404,7 +1916,10 @@ function createMatrixTransport(opts) {
|
|
|
1404
1916
|
`[matrix] interrupt session=${content.session_id} agent=${ctx.agent.name}` + (content.reason ? ` reason=${content.reason}` : "")
|
|
1405
1917
|
);
|
|
1406
1918
|
await agents.cancelSession(ctx.agent.name, content.session_id).catch((err) => {
|
|
1407
|
-
console.error(
|
|
1919
|
+
console.error(
|
|
1920
|
+
`[matrix] cancelSession(${ctx.agent.name}, ${content.session_id}) failed:`,
|
|
1921
|
+
err
|
|
1922
|
+
);
|
|
1408
1923
|
});
|
|
1409
1924
|
return;
|
|
1410
1925
|
}
|
|
@@ -1412,15 +1927,20 @@ function createMatrixTransport(opts) {
|
|
|
1412
1927
|
const content = evt.content ?? {};
|
|
1413
1928
|
if (!content.session_id || !content.approval_id || !content.decision) return;
|
|
1414
1929
|
const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
|
|
1415
|
-
const ok = approvals.resolve(
|
|
1416
|
-
content.session_id,
|
|
1417
|
-
content.approval_id,
|
|
1418
|
-
decision
|
|
1419
|
-
);
|
|
1930
|
+
const ok = approvals.resolve(content.session_id, content.approval_id, decision);
|
|
1420
1931
|
if (!ok) console.warn(`[matrix] unknown approval ${content.approval_id}`);
|
|
1421
1932
|
return;
|
|
1422
1933
|
}
|
|
1423
1934
|
logInbound(evt);
|
|
1935
|
+
if (evt.type === "dev.zooid.turn.end") {
|
|
1936
|
+
const agentId = evt.content?.agent_id;
|
|
1937
|
+
const endedRoot = inboundThreadRoot2(evt);
|
|
1938
|
+
const senderAgent = bindings.find((binding) => binding.userId === evt.sender);
|
|
1939
|
+
if (agentId && endedRoot && senderAgent?.name === agentId) {
|
|
1940
|
+
releaseReturn(returnKey(agentId, endedRoot));
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1424
1944
|
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)) {
|
|
1425
1945
|
pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
|
|
1426
1946
|
eventId: evt.event_id,
|
|
@@ -1446,7 +1966,32 @@ function createMatrixTransport(opts) {
|
|
|
1446
1966
|
console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
|
|
1447
1967
|
}
|
|
1448
1968
|
}
|
|
1449
|
-
const
|
|
1969
|
+
const startField = evt.content?.[THREAD_START_FIELD2];
|
|
1970
|
+
if (startField?.attempt_id && !inboundRel && evt.event_id)
|
|
1971
|
+
taskRegistry.adopt(startField.attempt_id, evt.event_id);
|
|
1972
|
+
if (evt.content?.[THREAD_RESULT_FIELD] !== void 0) return;
|
|
1973
|
+
const taskRec = promotedRoot ? taskRegistry.taskForRoot(promotedRoot) : void 0;
|
|
1974
|
+
const taskCtx = taskRec && taskRec.phase !== "reserved" ? {
|
|
1975
|
+
assignee: taskRec.assignee,
|
|
1976
|
+
isRoot: !inboundRel && evt.event_id === taskRec.threadRoot
|
|
1977
|
+
} : void 0;
|
|
1978
|
+
let matches = route(evt, bindings, threadStates, taskCtx);
|
|
1979
|
+
if (taskCtx && !taskCtx.isRoot && evt.event_id && bindings.some((b) => b.userId === evt.sender)) {
|
|
1980
|
+
const invocation = invocations.byCallEvent(evt.event_id);
|
|
1981
|
+
matches = invocation ? matches.filter((match) => match.name === invocation.calleeAgent) : [];
|
|
1982
|
+
}
|
|
1983
|
+
if (evt.type === "m.room.message" && promotedRoot && evt.room_id) {
|
|
1984
|
+
const senderBinding = bindings.find((b) => b.userId === evt.sender);
|
|
1985
|
+
if (senderBinding) {
|
|
1986
|
+
const st = threadStates.get(promotedRoot);
|
|
1987
|
+
const held = matches.filter((m) => isReturnRoute(evt, m, bindings, st));
|
|
1988
|
+
if (held.length > 0) {
|
|
1989
|
+
matches = matches.filter((m) => !held.includes(m));
|
|
1990
|
+
stashReturn(senderBinding, promotedRoot, evt.room_id, evt, held);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
for (const m of matches) dropPendingReturn(promotedRoot, m.name);
|
|
1994
|
+
}
|
|
1450
1995
|
const senderIsBot = bindings.some((b) => b.userId === evt.sender);
|
|
1451
1996
|
if (evt.type === "m.room.message" && matches.length === 0 && !senderIsBot) {
|
|
1452
1997
|
console.warn(
|
|
@@ -1459,55 +2004,35 @@ function createMatrixTransport(opts) {
|
|
|
1459
2004
|
st = { participants: [], rootMentions: [], callers: {}, handoffs: {} };
|
|
1460
2005
|
threadStates.set(promotedRoot, st);
|
|
1461
2006
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
if (
|
|
1470
|
-
|
|
1471
|
-
|
|
2007
|
+
if (taskCtx?.isRoot) {
|
|
2008
|
+
if (!st.rootMentions.includes(taskRec.assignee)) st.rootMentions.push(taskRec.assignee);
|
|
2009
|
+
} else {
|
|
2010
|
+
const msgMentions = new Set(extractMentions(evt));
|
|
2011
|
+
const senderAgent = bindings.find((b) => b.userId === evt.sender);
|
|
2012
|
+
for (const a of bindings) {
|
|
2013
|
+
if (!msgMentions.has(a.userId)) continue;
|
|
2014
|
+
if (!st.rootMentions.includes(a.name)) st.rootMentions.push(a.name);
|
|
2015
|
+
if (senderAgent && a.name !== senderAgent.name && !wouldCycleCallers(st.callers, a.name, senderAgent.name)) {
|
|
2016
|
+
st.callers[a.name] = senderAgent.name;
|
|
2017
|
+
if (evt.event_id) {
|
|
2018
|
+
const arcs = st.handoffs[a.name] ??= [];
|
|
2019
|
+
if (!arcs.includes(evt.event_id)) arcs.push(evt.event_id);
|
|
2020
|
+
}
|
|
1472
2021
|
}
|
|
1473
2022
|
}
|
|
1474
2023
|
}
|
|
1475
2024
|
}
|
|
1476
2025
|
for (const a of matches) {
|
|
1477
2026
|
console.log(`[matrix] \u2192 ${a.name} (${a.userId})`);
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
console.error(`[matrix] runTurn failed for ${a.name}:`, err);
|
|
1488
|
-
const c = classify(err);
|
|
1489
|
-
const threadRoot = inboundThreadRoot2(evt) ?? evt.event_id;
|
|
1490
|
-
if (!threadRoot || !evt.room_id) return;
|
|
1491
|
-
const body = toErrorBody(
|
|
1492
|
-
{
|
|
1493
|
-
kind: "error",
|
|
1494
|
-
agentId: a.name,
|
|
1495
|
-
sessionId: null,
|
|
1496
|
-
turnId: null,
|
|
1497
|
-
code: c.code,
|
|
1498
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1499
|
-
detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
|
|
1500
|
-
transient: c.transient,
|
|
1501
|
-
acp_error: c.acp_error
|
|
1502
|
-
},
|
|
1503
|
-
threadRoot
|
|
1504
|
-
);
|
|
1505
|
-
void client.sendCustomEvent({
|
|
1506
|
-
roomId: evt.room_id,
|
|
1507
|
-
asUserId: a.userId,
|
|
1508
|
-
eventType: "dev.zooid.error",
|
|
1509
|
-
content: body
|
|
1510
|
-
}).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
|
|
2027
|
+
if (!promotedRoot || !evt.room_id) continue;
|
|
2028
|
+
const sessionKey = sessionKeyFor(a.name, promotedRoot, threadStates.get(promotedRoot));
|
|
2029
|
+
const taskEnvelope = taskCtx?.isRoot && a.name === taskRec.assignee ? { parentAgent: taskRec.parent.agent } : void 0;
|
|
2030
|
+
void enqueueTurn(a, {
|
|
2031
|
+
roomId: evt.room_id,
|
|
2032
|
+
threadRoot: promotedRoot,
|
|
2033
|
+
sessionKey,
|
|
2034
|
+
event: evt,
|
|
2035
|
+
...taskEnvelope ? { taskEnvelope } : {}
|
|
1511
2036
|
});
|
|
1512
2037
|
}
|
|
1513
2038
|
}
|
|
@@ -1548,13 +2073,10 @@ function createMatrixTransport(opts) {
|
|
|
1548
2073
|
return c.json({});
|
|
1549
2074
|
});
|
|
1550
2075
|
app.get("/healthz", (c) => c.text("ok"));
|
|
1551
|
-
async function runTurn(agent,
|
|
1552
|
-
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
const sessionKey = sessionKeyFor(agent.name, threadRoot, threadStates.get(threadRoot));
|
|
1556
|
-
const sessionId = await agents.ensureSession(agent.name, sessionKey, evt.room_id, threadRoot);
|
|
1557
|
-
sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
|
|
2076
|
+
async function runTurn(agent, input) {
|
|
2077
|
+
const { roomId, threadRoot, sessionKey } = input;
|
|
2078
|
+
const sessionId = await agents.ensureSession(agent.name, sessionKey, roomId, threadRoot);
|
|
2079
|
+
sessions.set(sessionId, { agent, roomId, threadRoot });
|
|
1558
2080
|
buffers.set(sessionId, "");
|
|
1559
2081
|
bufferMessageIds.delete(sessionId);
|
|
1560
2082
|
flushedCounts.set(sessionId, 0);
|
|
@@ -1563,10 +2085,14 @@ function createMatrixTransport(opts) {
|
|
|
1563
2085
|
pendingCommands.delete(sessionId);
|
|
1564
2086
|
void agents.onEvent?.(agent.name, stashedCommands);
|
|
1565
2087
|
}
|
|
1566
|
-
const roomId = evt.room_id;
|
|
1567
2088
|
const TYPING_TTL_MS = 3e4;
|
|
1568
2089
|
const TYPING_REFRESH_MS = 25e3;
|
|
1569
|
-
const safeTyping = (typing) => client.setTyping({
|
|
2090
|
+
const safeTyping = (typing) => client.setTyping({
|
|
2091
|
+
roomId,
|
|
2092
|
+
asUserId: agent.userId,
|
|
2093
|
+
typing,
|
|
2094
|
+
timeoutMs: TYPING_TTL_MS
|
|
2095
|
+
}).catch((err) => console.warn(`[matrix:${agent.name}] setTyping(${typing}) failed:`, err));
|
|
1570
2096
|
const safePresence = (presence) => client.setPresence({ asUserId: agent.userId, presence }).catch(
|
|
1571
2097
|
(err) => console.warn(`[matrix:${agent.name}] setPresence(${presence}) failed:`, err)
|
|
1572
2098
|
);
|
|
@@ -1575,13 +2101,19 @@ function createMatrixTransport(opts) {
|
|
|
1575
2101
|
const refresh = setInterval(() => {
|
|
1576
2102
|
void safeTyping(true);
|
|
1577
2103
|
}, TYPING_REFRESH_MS);
|
|
2104
|
+
let turnError;
|
|
2105
|
+
let stopReason;
|
|
1578
2106
|
try {
|
|
1579
|
-
const rawBody =
|
|
1580
|
-
const
|
|
2107
|
+
const rawBody = input.event?.content?.body ?? "";
|
|
2108
|
+
const strippedPromptText = input.promptText ?? stripMention(rawBody, agent.userId);
|
|
2109
|
+
const promptText = input.taskEnvelope ? renderAssigneeEnvelope({
|
|
2110
|
+
parentAgent: input.taskEnvelope.parentAgent,
|
|
2111
|
+
prompt: strippedPromptText
|
|
2112
|
+
}) : strippedPromptText;
|
|
1581
2113
|
const pendingItems = pendingMedia.drain(
|
|
1582
|
-
|
|
1583
|
-
inboundThreadRoot2(
|
|
1584
|
-
|
|
2114
|
+
roomId,
|
|
2115
|
+
input.event ? inboundThreadRoot2(input.event) : void 0,
|
|
2116
|
+
input.event?.sender ?? ""
|
|
1585
2117
|
);
|
|
1586
2118
|
const { blocks, pathLines } = await buildMediaBlocks(pendingItems, {
|
|
1587
2119
|
agent,
|
|
@@ -1590,7 +2122,7 @@ function createMatrixTransport(opts) {
|
|
|
1590
2122
|
onError: (item, err) => {
|
|
1591
2123
|
console.warn(`[matrix:${agent.name}] media_failed for ${item.body}:`, err);
|
|
1592
2124
|
void sendMediaError(
|
|
1593
|
-
{ agent, roomId
|
|
2125
|
+
{ agent, roomId, threadRoot },
|
|
1594
2126
|
err,
|
|
1595
2127
|
`Could not process attachment: ${item.body}`,
|
|
1596
2128
|
client
|
|
@@ -1598,22 +2130,25 @@ function createMatrixTransport(opts) {
|
|
|
1598
2130
|
}
|
|
1599
2131
|
});
|
|
1600
2132
|
const fullPromptText = [promptText, ...pathLines].filter(Boolean).join("\n");
|
|
1601
|
-
await agents.prompt(agent.name, {
|
|
2133
|
+
const promptResult = await agents.prompt(agent.name, {
|
|
1602
2134
|
threadId: sessionKey,
|
|
1603
|
-
channelId:
|
|
2135
|
+
channelId: roomId,
|
|
1604
2136
|
contextThreadId: threadRoot,
|
|
1605
2137
|
content: [...blocks, { type: "text", text: fullPromptText }]
|
|
1606
2138
|
});
|
|
2139
|
+
stopReason = promptResult.stopReason;
|
|
1607
2140
|
const drainStart = Date.now();
|
|
1608
2141
|
let drained = buffers.get(sessionId) ?? "";
|
|
1609
2142
|
while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
|
|
1610
2143
|
await delay(drainQuietMs);
|
|
1611
2144
|
const next = buffers.get(sessionId) ?? "";
|
|
1612
|
-
if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0))
|
|
1613
|
-
break;
|
|
2145
|
+
if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0)) break;
|
|
1614
2146
|
drained = next;
|
|
1615
2147
|
}
|
|
1616
2148
|
flushBuffer(sessionId);
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
turnError = err;
|
|
2151
|
+
throw err;
|
|
1617
2152
|
} finally {
|
|
1618
2153
|
clearInterval(refresh);
|
|
1619
2154
|
await safeTyping(false);
|
|
@@ -1622,18 +2157,42 @@ function createMatrixTransport(opts) {
|
|
|
1622
2157
|
const producedOutput = (flushedCounts.get(sessionId) ?? 0) > 0;
|
|
1623
2158
|
if (!producedOutput) {
|
|
1624
2159
|
console.warn(
|
|
1625
|
-
`[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${
|
|
2160
|
+
`[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${roomId}`
|
|
1626
2161
|
);
|
|
1627
2162
|
}
|
|
1628
2163
|
await client.sendCustomEvent({
|
|
1629
|
-
roomId
|
|
2164
|
+
roomId,
|
|
1630
2165
|
asUserId: agent.userId,
|
|
1631
2166
|
eventType: "dev.zooid.turn.end",
|
|
1632
2167
|
content: toTurnEndBody(
|
|
1633
|
-
{
|
|
2168
|
+
{
|
|
2169
|
+
agentId: agent.name,
|
|
2170
|
+
sessionId,
|
|
2171
|
+
producedOutput,
|
|
2172
|
+
lastMessage: lastFlushed.get(sessionId)
|
|
2173
|
+
},
|
|
1634
2174
|
threadRoot
|
|
1635
2175
|
)
|
|
1636
2176
|
}).catch((e) => console.warn(`[matrix:${agent.name}] turn.end send failed:`, e));
|
|
2177
|
+
const task = taskRegistry.taskForRoot(threadRoot);
|
|
2178
|
+
const invocation = invocations.forCalleeSession(sessionKey);
|
|
2179
|
+
const isAssignee = task?.phase === "open" && task.assignee === agent.name && task.threadRoot === sessionKey;
|
|
2180
|
+
if (task?.phase === "open" && (isAssignee || invocation?.state === "outstanding")) {
|
|
2181
|
+
const decision = evaluateCompletion({
|
|
2182
|
+
agent: agent.name,
|
|
2183
|
+
threadId: isAssignee ? threadRoot : invocation?.calleeSessionKey ?? sessionKey,
|
|
2184
|
+
stopReason,
|
|
2185
|
+
error: turnError,
|
|
2186
|
+
summary: isAssignee ? task.summary : void 0,
|
|
2187
|
+
prose: lastFlushed.get(sessionId),
|
|
2188
|
+
outstanding: invocations.outstandingFor(sessionKey).length,
|
|
2189
|
+
awaitingHuman: pendingInput.countFor(sessionKey)
|
|
2190
|
+
});
|
|
2191
|
+
if (decision.decision === "finish") {
|
|
2192
|
+
if (isAssignee) await finishTask(task, { agent, completion: decision.completion });
|
|
2193
|
+
else if (invocation) returnInvocation(invocation, decision.completion, task);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
1637
2196
|
buffers.delete(sessionId);
|
|
1638
2197
|
bufferMessageIds.delete(sessionId);
|
|
1639
2198
|
flushedCounts.delete(sessionId);
|
|
@@ -1641,6 +2200,237 @@ function createMatrixTransport(opts) {
|
|
|
1641
2200
|
sendQueue.delete(sessionId);
|
|
1642
2201
|
}
|
|
1643
2202
|
}
|
|
2203
|
+
async function finishTask(task, ctx) {
|
|
2204
|
+
const threadId = task.threadRoot;
|
|
2205
|
+
const completion = ctx.completion;
|
|
2206
|
+
if (!taskRegistry.close(task.taskId)) return;
|
|
2207
|
+
const cancelled = invocations.cancelForTask(task.taskId);
|
|
2208
|
+
pendingInput.cancelFor([threadId, ...cancelled.map((i) => i.calleeSessionKey).filter((x) => Boolean(x))]);
|
|
2209
|
+
await client.sendCustomEvent({
|
|
2210
|
+
roomId: task.roomId,
|
|
2211
|
+
asUserId: ctx.agent.userId,
|
|
2212
|
+
eventType: THREAD_RESULT_FIELD,
|
|
2213
|
+
content: {
|
|
2214
|
+
...completion,
|
|
2215
|
+
"m.relates_to": { rel_type: "m.thread", event_id: threadId }
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
2218
|
+
if (task.summary && task.summary !== completion.output?.text)
|
|
2219
|
+
await client.sendMessage({
|
|
2220
|
+
roomId: task.roomId,
|
|
2221
|
+
asUserId: ctx.agent.userId,
|
|
2222
|
+
threadRoot: threadId,
|
|
2223
|
+
content: buildTextContent(task.summary)
|
|
2224
|
+
});
|
|
2225
|
+
if (task.notify === "none") return;
|
|
2226
|
+
const parent = bindingFor(task.parent.agent);
|
|
2227
|
+
await client.sendMessage({
|
|
2228
|
+
roomId: task.roomId,
|
|
2229
|
+
asUserId: ctx.agent.userId,
|
|
2230
|
+
threadRoot: task.parent.threadRoot,
|
|
2231
|
+
content: {
|
|
2232
|
+
msgtype: "m.notice",
|
|
2233
|
+
body: renderCompletionPrompt(completion),
|
|
2234
|
+
[THREAD_RESULT_FIELD]: completion
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
if (!parent || taskRegistry.generationOf(task.parent.agent, task.parent.sessionKey) !== task.parent.generation)
|
|
2238
|
+
return;
|
|
2239
|
+
void enqueueTurn(parent, {
|
|
2240
|
+
roomId: task.roomId,
|
|
2241
|
+
threadRoot: task.parent.threadRoot,
|
|
2242
|
+
sessionKey: task.parent.sessionKey,
|
|
2243
|
+
promptText: renderCompletionPrompt(completion)
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
function returnInvocation(invocation, completion, task) {
|
|
2247
|
+
const resolved = invocations.resolve(invocation.invocationId);
|
|
2248
|
+
if (!resolved || task.phase !== "open") return;
|
|
2249
|
+
const caller = bindingFor(resolved.callerAgent);
|
|
2250
|
+
if (!caller || !task.threadRoot) return;
|
|
2251
|
+
void enqueueTurn(caller, {
|
|
2252
|
+
roomId: task.roomId,
|
|
2253
|
+
threadRoot: task.threadRoot,
|
|
2254
|
+
sessionKey: resolved.callerSessionKey,
|
|
2255
|
+
promptText: renderInvocationReturn(completion)
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
const taskActions = {
|
|
2259
|
+
async startTasks(caller, input) {
|
|
2260
|
+
const notify = input.notify ?? "caller";
|
|
2261
|
+
const results = new Array(input.tasks.length);
|
|
2262
|
+
const callerBinding = bindingFor(caller.agentName);
|
|
2263
|
+
const enclosing = taskRegistry.taskForRoot(caller.threadRoot);
|
|
2264
|
+
const admitted = [];
|
|
2265
|
+
for (const [index, spec] of input.tasks.entries()) {
|
|
2266
|
+
if (!callerBinding) {
|
|
2267
|
+
results[index] = {
|
|
2268
|
+
agent: spec.agent,
|
|
2269
|
+
status: "refused",
|
|
2270
|
+
reason: "unknown_caller"
|
|
2271
|
+
};
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
if (enclosing) {
|
|
2275
|
+
results[index] = {
|
|
2276
|
+
agent: spec.agent,
|
|
2277
|
+
status: "refused",
|
|
2278
|
+
reason: "depth_limit: this thread is itself a delegated task. Do the work here, or @mention another agent in this thread to hand off."
|
|
2279
|
+
};
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
const admission = checkDelegable(spec.agent, caller.channelId, bindings);
|
|
2283
|
+
if (!admission.ok) {
|
|
2284
|
+
results[index] = {
|
|
2285
|
+
agent: spec.agent,
|
|
2286
|
+
status: "refused",
|
|
2287
|
+
reason: admission.reason
|
|
2288
|
+
};
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
const rec = taskRegistry.reserve({
|
|
2292
|
+
roomId: caller.channelId,
|
|
2293
|
+
assignee: spec.agent,
|
|
2294
|
+
notify,
|
|
2295
|
+
parent: {
|
|
2296
|
+
agent: caller.agentName,
|
|
2297
|
+
threadRoot: caller.threadRoot,
|
|
2298
|
+
sessionKey: caller.sessionKey,
|
|
2299
|
+
generation: taskRegistry.generationOf(caller.agentName, caller.sessionKey)
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
if (!rec) {
|
|
2303
|
+
results[index] = {
|
|
2304
|
+
agent: spec.agent,
|
|
2305
|
+
status: "refused",
|
|
2306
|
+
reason: `at_capacity: ${MAX_OPEN_TASKS_PER_ROOM} tasks are already open in this room. Wait for one to finish.`
|
|
2307
|
+
};
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
admitted.push({ index, spec, rec });
|
|
2311
|
+
}
|
|
2312
|
+
await Promise.all(
|
|
2313
|
+
admitted.map(async ({ index, spec, rec }) => {
|
|
2314
|
+
const assignee = bindingFor(spec.agent);
|
|
2315
|
+
const content = buildAssignmentContent({
|
|
2316
|
+
assigneeUserId: assignee.userId,
|
|
2317
|
+
prompt: spec.prompt,
|
|
2318
|
+
start: {
|
|
2319
|
+
version: 1,
|
|
2320
|
+
assignee: spec.agent,
|
|
2321
|
+
attempt_id: rec.attemptId,
|
|
2322
|
+
parent: {
|
|
2323
|
+
agent: rec.parent.agent,
|
|
2324
|
+
thread_root: rec.parent.threadRoot,
|
|
2325
|
+
session_key: rec.parent.sessionKey
|
|
2326
|
+
},
|
|
2327
|
+
notify
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
const post = () => client.sendMessage({
|
|
2331
|
+
roomId: caller.channelId,
|
|
2332
|
+
asUserId: callerBinding.userId,
|
|
2333
|
+
content,
|
|
2334
|
+
txnId: rec.attemptId
|
|
2335
|
+
});
|
|
2336
|
+
try {
|
|
2337
|
+
const { event_id } = await post();
|
|
2338
|
+
taskRegistry.activate(rec.taskId, event_id);
|
|
2339
|
+
results[index] = {
|
|
2340
|
+
agent: spec.agent,
|
|
2341
|
+
status: "started",
|
|
2342
|
+
thread_id: event_id
|
|
2343
|
+
};
|
|
2344
|
+
} catch {
|
|
2345
|
+
try {
|
|
2346
|
+
const { event_id } = await post();
|
|
2347
|
+
taskRegistry.activate(rec.taskId, event_id);
|
|
2348
|
+
results[index] = {
|
|
2349
|
+
agent: spec.agent,
|
|
2350
|
+
status: "started",
|
|
2351
|
+
thread_id: event_id
|
|
2352
|
+
};
|
|
2353
|
+
} catch (second) {
|
|
2354
|
+
const status = second.status;
|
|
2355
|
+
if (status !== void 0 && status >= 400 && status < 500 && status !== 429) {
|
|
2356
|
+
taskRegistry.abandon(rec.taskId);
|
|
2357
|
+
results[index] = {
|
|
2358
|
+
agent: spec.agent,
|
|
2359
|
+
status: "failed",
|
|
2360
|
+
reason: `post_failed: ${String(second.message)}`
|
|
2361
|
+
};
|
|
2362
|
+
} else {
|
|
2363
|
+
taskRegistry.markUncertain(rec.taskId);
|
|
2364
|
+
results[index] = {
|
|
2365
|
+
agent: spec.agent,
|
|
2366
|
+
status: "failed",
|
|
2367
|
+
reason: `post_uncertain: ${String(second.message)}`,
|
|
2368
|
+
attempt_id: rec.attemptId
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
})
|
|
2374
|
+
);
|
|
2375
|
+
return { results, notify, delivery: renderDelivery(notify) };
|
|
2376
|
+
},
|
|
2377
|
+
async completeTask(caller, input) {
|
|
2378
|
+
const summary = input.summary.trim();
|
|
2379
|
+
if (!summary) return { status: "refused", reason: "summary must be non-empty" };
|
|
2380
|
+
const rec = taskRegistry.openTaskFor(caller.agentName, caller.threadRoot);
|
|
2381
|
+
if (!rec || rec.threadRoot !== caller.sessionKey)
|
|
2382
|
+
return {
|
|
2383
|
+
status: "refused",
|
|
2384
|
+
reason: "no_open_task: this session is not the assignee of an open task"
|
|
2385
|
+
};
|
|
2386
|
+
if (invocations.outstandingFor(caller.sessionKey).length)
|
|
2387
|
+
return { status: "refused", reason: "outstanding_handoff: wait for delegated work to return" };
|
|
2388
|
+
return { status: taskRegistry.recordSummary(rec.taskId, summary) };
|
|
2389
|
+
},
|
|
2390
|
+
async describeRole(caller) {
|
|
2391
|
+
const enclosing = taskRegistry.taskForRoot(caller.threadRoot);
|
|
2392
|
+
const openTask = taskRegistry.openTaskFor(caller.agentName, caller.threadRoot);
|
|
2393
|
+
return {
|
|
2394
|
+
is_task_assignee: openTask !== void 0 && openTask.threadRoot === caller.sessionKey,
|
|
2395
|
+
can_start_task_threads: enclosing === void 0
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
queueMicrotask(() => {
|
|
2400
|
+
for (const task of interruptedTasks) {
|
|
2401
|
+
if (!task.threadRoot) continue;
|
|
2402
|
+
const assignee = bindingFor(task.assignee);
|
|
2403
|
+
if (!assignee) continue;
|
|
2404
|
+
const completion = {
|
|
2405
|
+
agent: task.assignee,
|
|
2406
|
+
thread_id: task.threadRoot,
|
|
2407
|
+
status: "cancelled",
|
|
2408
|
+
reason: "interrupted_by_restart"
|
|
2409
|
+
};
|
|
2410
|
+
void client.sendCustomEvent({
|
|
2411
|
+
roomId: task.roomId,
|
|
2412
|
+
asUserId: assignee.userId,
|
|
2413
|
+
eventType: THREAD_RESULT_FIELD,
|
|
2414
|
+
content: { ...completion, "m.relates_to": { rel_type: "m.thread", event_id: task.threadRoot } }
|
|
2415
|
+
});
|
|
2416
|
+
if (task.notify !== "none") {
|
|
2417
|
+
const parent = bindingFor(task.parent.agent);
|
|
2418
|
+
if (parent && taskRegistry.generationOf(task.parent.agent, task.parent.sessionKey) === task.parent.generation) {
|
|
2419
|
+
void client.sendMessage({
|
|
2420
|
+
roomId: task.roomId,
|
|
2421
|
+
asUserId: assignee.userId,
|
|
2422
|
+
threadRoot: task.parent.threadRoot,
|
|
2423
|
+
content: {
|
|
2424
|
+
msgtype: "m.notice",
|
|
2425
|
+
body: renderCompletionPrompt(completion),
|
|
2426
|
+
[THREAD_RESULT_FIELD]: completion
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
void enqueueTurn(parent, { roomId: task.roomId, threadRoot: task.parent.threadRoot, sessionKey: task.parent.sessionKey, promptText: renderCompletionPrompt(completion) });
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
1644
2434
|
const syncLoops = mode === "client" ? bindings.map(
|
|
1645
2435
|
(b) => new SyncLoop({
|
|
1646
2436
|
client,
|
|
@@ -1652,6 +2442,7 @@ function createMatrixTransport(opts) {
|
|
|
1652
2442
|
) : void 0;
|
|
1653
2443
|
return {
|
|
1654
2444
|
app,
|
|
2445
|
+
taskActions,
|
|
1655
2446
|
syncLoops,
|
|
1656
2447
|
bootstrap: async (bootstrapOpts = {}) => {
|
|
1657
2448
|
await pool.bootstrap({ adminUserId, ...bootstrapOpts });
|
|
@@ -1667,7 +2458,12 @@ function createMatrixTransport(opts) {
|
|
|
1667
2458
|
};
|
|
1668
2459
|
}
|
|
1669
2460
|
async function rebuildThreadState(client, roomId, rootEventId, bindings) {
|
|
1670
|
-
const state = {
|
|
2461
|
+
const state = {
|
|
2462
|
+
participants: [],
|
|
2463
|
+
rootMentions: [],
|
|
2464
|
+
callers: {},
|
|
2465
|
+
handoffs: {}
|
|
2466
|
+
};
|
|
1671
2467
|
const asUser = (bindings.find((b) => b.rooms.some((r) => r.alias === roomId)) ?? bindings[0])?.userId;
|
|
1672
2468
|
if (!asUser) return state;
|
|
1673
2469
|
const root = await client.fetchEvent(roomId, rootEventId, asUser);
|
|
@@ -1678,7 +2474,7 @@ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
|
|
|
1678
2474
|
for (const a of bindings) {
|
|
1679
2475
|
if (!rootMentions.has(a.userId)) continue;
|
|
1680
2476
|
if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
|
|
1681
|
-
if (rootSenderAgent && a.name !== rootSenderAgent.name) {
|
|
2477
|
+
if (rootSenderAgent && a.name !== rootSenderAgent.name && !wouldCycleCallers(state.callers, a.name, rootSenderAgent.name)) {
|
|
1682
2478
|
state.callers[a.name] = rootSenderAgent.name;
|
|
1683
2479
|
const arcs = state.handoffs[a.name] ??= [];
|
|
1684
2480
|
if (!arcs.includes(rootEventId)) arcs.push(rootEventId);
|
|
@@ -1698,7 +2494,7 @@ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
|
|
|
1698
2494
|
for (const a of bindings) {
|
|
1699
2495
|
if (!mentions.has(a.userId)) continue;
|
|
1700
2496
|
if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
|
|
1701
|
-
if (evSenderAgent && a.name !== evSenderAgent.name) {
|
|
2497
|
+
if (evSenderAgent && a.name !== evSenderAgent.name && !wouldCycleCallers(state.callers, a.name, evSenderAgent.name)) {
|
|
1702
2498
|
state.callers[a.name] = evSenderAgent.name;
|
|
1703
2499
|
if (evId) {
|
|
1704
2500
|
const arcs = state.handoffs[a.name] ??= [];
|
|
@@ -1767,9 +2563,11 @@ export {
|
|
|
1767
2563
|
AGENT_KEY_RE,
|
|
1768
2564
|
BotPool,
|
|
1769
2565
|
INLINE_IMAGE_MIMES,
|
|
2566
|
+
InvocationRegistry,
|
|
1770
2567
|
MAX_DOWNLOAD_BYTES,
|
|
1771
2568
|
MAX_INLINE_IMAGE_BYTES,
|
|
1772
2569
|
MAX_MEDIA_PER_TURN,
|
|
2570
|
+
MAX_OPEN_TASKS_PER_ROOM,
|
|
1773
2571
|
MEDIA_MSGTYPES,
|
|
1774
2572
|
MatrixClient,
|
|
1775
2573
|
MatrixContextProvider,
|
|
@@ -1777,17 +2575,23 @@ export {
|
|
|
1777
2575
|
PendingMediaStore,
|
|
1778
2576
|
SLUG_RE,
|
|
1779
2577
|
SyncLoop,
|
|
2578
|
+
TaskRegistry,
|
|
1780
2579
|
agentMxid,
|
|
2580
|
+
buildAssignmentContent,
|
|
1781
2581
|
buildWorkforceRoster,
|
|
2582
|
+
checkDelegable,
|
|
1782
2583
|
createMatrixTransport,
|
|
1783
2584
|
ensureDefaultChannel,
|
|
1784
2585
|
ensureWorkforceSpace,
|
|
2586
|
+
evaluateCompletion,
|
|
1785
2587
|
extractMentions,
|
|
1786
2588
|
isMediaMsgtype,
|
|
1787
2589
|
isValidAgentKey,
|
|
1788
2590
|
isValidWorkstation,
|
|
1789
2591
|
parseMxcUri,
|
|
1790
2592
|
publishWorkforce,
|
|
2593
|
+
renderCompletionPrompt,
|
|
2594
|
+
renderInvocationReturn,
|
|
1791
2595
|
renderRegistration,
|
|
1792
2596
|
route,
|
|
1793
2597
|
serverNameFromMxid,
|