@zooid/transport-matrix 0.12.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 +164 -5
- package/dist/index.js +946 -101
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/context-provider.test.ts +178 -3
- package/src/context-provider.ts +62 -5
- package/src/event-encoders.test.ts +69 -1
- package/src/event-encoders.ts +41 -1
- 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 +520 -111
- package/src/transport.ts +760 -116
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
|
};
|
|
@@ -331,7 +341,8 @@ var MatrixContextProvider = class {
|
|
|
331
341
|
const threads = [];
|
|
332
342
|
for (const ev of chunk) {
|
|
333
343
|
if (ev.type !== "m.room.message") continue;
|
|
334
|
-
if (ev.content?.msgtype !== "m.text" || typeof ev.content.body !== "string")
|
|
344
|
+
if (ev.content?.msgtype !== "m.text" && ev.content?.msgtype !== "m.notice" || typeof ev.content.body !== "string")
|
|
345
|
+
continue;
|
|
335
346
|
const relatesTo = ev.content["m.relates_to"];
|
|
336
347
|
if (relatesTo?.rel_type === "m.thread") continue;
|
|
337
348
|
const agent = this.opts.agentBots.get(ev.sender);
|
|
@@ -405,7 +416,7 @@ var MatrixContextProvider = class {
|
|
|
405
416
|
...threadId !== void 0 ? { thread_id: threadId } : {}
|
|
406
417
|
};
|
|
407
418
|
}
|
|
408
|
-
if (msgtype !== "m.text" || typeof body !== "string") return null;
|
|
419
|
+
if (msgtype !== "m.text" && msgtype !== "m.notice" || typeof body !== "string") return null;
|
|
409
420
|
return {
|
|
410
421
|
id: ev.event_id,
|
|
411
422
|
sender: ev.sender,
|
|
@@ -428,7 +439,7 @@ var MatrixContextProvider = class {
|
|
|
428
439
|
};
|
|
429
440
|
});
|
|
430
441
|
}
|
|
431
|
-
async
|
|
442
|
+
async getRoomInfo(channelId) {
|
|
432
443
|
const name = await this.opts.client.fetchRoomName(channelId, this.opts.asUserId);
|
|
433
444
|
return {
|
|
434
445
|
id: channelId,
|
|
@@ -436,6 +447,30 @@ var MatrixContextProvider = class {
|
|
|
436
447
|
transport: "matrix"
|
|
437
448
|
};
|
|
438
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
|
+
}
|
|
439
474
|
};
|
|
440
475
|
|
|
441
476
|
// src/registration.ts
|
|
@@ -514,7 +549,7 @@ function inboundThreadRoot(event) {
|
|
|
514
549
|
const r = event.content?.["m.relates_to"];
|
|
515
550
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
516
551
|
}
|
|
517
|
-
function route(event, agents, threadStates) {
|
|
552
|
+
function route(event, agents, threadStates, task) {
|
|
518
553
|
if (event.type !== "m.room.message") return [];
|
|
519
554
|
if (!event.content?.msgtype) return [];
|
|
520
555
|
if (isMediaMsgtype(event.content.msgtype)) return [];
|
|
@@ -523,8 +558,25 @@ function route(event, agents, threadStates) {
|
|
|
523
558
|
const threadRoot = inboundThreadRoot(event);
|
|
524
559
|
const threadState = threadRoot ? threadStates?.get(threadRoot) : void 0;
|
|
525
560
|
for (const a of agents) {
|
|
526
|
-
if (event.sender === a.userId) continue;
|
|
527
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
|
+
}
|
|
528
580
|
if (a.trigger === "any") {
|
|
529
581
|
matches.push(a);
|
|
530
582
|
continue;
|
|
@@ -536,7 +588,7 @@ function route(event, agents, threadStates) {
|
|
|
536
588
|
if (threadState) {
|
|
537
589
|
const senderAgent = agents.find((x) => x.userId === event.sender);
|
|
538
590
|
if (senderAgent) {
|
|
539
|
-
if (
|
|
591
|
+
if (isReturnRoute(event, a, agents, threadState)) matches.push(a);
|
|
540
592
|
} else {
|
|
541
593
|
const lastPoster = threadState.participants.at(-1);
|
|
542
594
|
if (lastPoster) {
|
|
@@ -549,6 +601,22 @@ function route(event, agents, threadStates) {
|
|
|
549
601
|
}
|
|
550
602
|
return matches;
|
|
551
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
|
+
}
|
|
552
620
|
|
|
553
621
|
// src/space-provisioner.ts
|
|
554
622
|
async function ensureWorkforceSpace(opts) {
|
|
@@ -744,6 +812,7 @@ function buildUserPowerLevels(asUserId, admins, agents, roomAlias) {
|
|
|
744
812
|
// src/transport.ts
|
|
745
813
|
import { Hono } from "hono";
|
|
746
814
|
import { timingSafeEqual } from "crypto";
|
|
815
|
+
import { THREAD_RESULT_FIELD, THREAD_START_FIELD as THREAD_START_FIELD2 } from "@zooid/core";
|
|
747
816
|
|
|
748
817
|
// src/session-keys.ts
|
|
749
818
|
var HANDOFF_KEY_SEP = "|";
|
|
@@ -821,7 +890,11 @@ var RECOVERY_URLS = {
|
|
|
821
890
|
function toErrorBody(evt, threadRoot) {
|
|
822
891
|
const msg = evt.message.slice(0, 250);
|
|
823
892
|
const out = {
|
|
824
|
-
msgtype:
|
|
893
|
+
// No msgtype: dev.zooid.error is not m.room.message, so the field is
|
|
894
|
+
// meaningless here — it was a vestige of copying the message-body shape.
|
|
895
|
+
// Its presence used to force careful push-rule `before` positioning
|
|
896
|
+
// (ZNC025 §10); that positioning is kept regardless, since it also
|
|
897
|
+
// protects rules for event types that never carried the field.
|
|
825
898
|
body: `\u26A0 [${evt.code}] ${msg}`,
|
|
826
899
|
code: evt.code,
|
|
827
900
|
message: msg,
|
|
@@ -836,6 +909,25 @@ function toErrorBody(evt, threadRoot) {
|
|
|
836
909
|
if (recovery) out.recovery = recovery;
|
|
837
910
|
return out;
|
|
838
911
|
}
|
|
912
|
+
var PREVIEW_MAX = 140;
|
|
913
|
+
function toTurnEndBody(evt, threadRoot) {
|
|
914
|
+
const preview = evt.lastMessage?.trim().replace(/\s+/g, " ");
|
|
915
|
+
return {
|
|
916
|
+
// `body` stays the turn-boundary summary: it is what a generic Matrix
|
|
917
|
+
// client renders for this event, and the prose is already its own message
|
|
918
|
+
// in the timeline. The preview below exists only for the push, which
|
|
919
|
+
// cannot see that message — agent prose is `m.notice`, deliberately
|
|
920
|
+
// silenced by `.m.rule.suppress_notices` so a chatty turn doesn't fire one
|
|
921
|
+
// push per chunk ([[ZNC025]] §10). Without it the only notification the
|
|
922
|
+
// user gets says an agent finished and nothing about what it said.
|
|
923
|
+
body: evt.producedOutput ? `${evt.agentId} finished` : `${evt.agentId} finished without output`,
|
|
924
|
+
...preview ? { last_message: preview.slice(0, PREVIEW_MAX) } : {},
|
|
925
|
+
agent_id: evt.agentId,
|
|
926
|
+
session_id: evt.sessionId,
|
|
927
|
+
produced_output: evt.producedOutput,
|
|
928
|
+
"m.relates_to": { rel_type: "m.thread", event_id: threadRoot }
|
|
929
|
+
};
|
|
930
|
+
}
|
|
839
931
|
|
|
840
932
|
// src/transport.ts
|
|
841
933
|
import { classify } from "@zooid/acp-client";
|
|
@@ -1037,8 +1129,282 @@ var SyncLoop = class {
|
|
|
1037
1129
|
}
|
|
1038
1130
|
};
|
|
1039
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
|
+
|
|
1040
1405
|
// src/transport.ts
|
|
1041
1406
|
var STARTUP_GRACE_MS = 5e3;
|
|
1407
|
+
var RETURN_GRACE_MS = 9e4;
|
|
1042
1408
|
async function buildMediaBlocks(items, opts) {
|
|
1043
1409
|
const blocks = [];
|
|
1044
1410
|
const pathLines = [];
|
|
@@ -1130,9 +1496,19 @@ function inboundThreadRoot2(evt) {
|
|
|
1130
1496
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
1131
1497
|
}
|
|
1132
1498
|
function createMatrixTransport(opts) {
|
|
1133
|
-
const {
|
|
1499
|
+
const {
|
|
1500
|
+
agents,
|
|
1501
|
+
approvals,
|
|
1502
|
+
client,
|
|
1503
|
+
bindings,
|
|
1504
|
+
hsToken,
|
|
1505
|
+
adminUserId,
|
|
1506
|
+
botUserId,
|
|
1507
|
+
mode = "appservice"
|
|
1508
|
+
} = opts;
|
|
1134
1509
|
const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
|
|
1135
1510
|
const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
|
|
1511
|
+
const returnGraceMs = opts.returnGraceMs ?? RETURN_GRACE_MS;
|
|
1136
1512
|
const mediaClient = opts.media;
|
|
1137
1513
|
const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
|
|
1138
1514
|
const pendingMedia = new PendingMediaStore();
|
|
@@ -1147,13 +1523,81 @@ function createMatrixTransport(opts) {
|
|
|
1147
1523
|
const bufferMessageIds = /* @__PURE__ */ new Map();
|
|
1148
1524
|
const sendQueue = /* @__PURE__ */ new Map();
|
|
1149
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
|
+
}
|
|
1150
1590
|
const cutoffTs = mode === "client" ? Number.NEGATIVE_INFINITY : Date.now() - STARTUP_GRACE_MS;
|
|
1151
1591
|
const seenEventIds = /* @__PURE__ */ new Set();
|
|
1152
1592
|
const flushedCounts = /* @__PURE__ */ new Map();
|
|
1153
1593
|
const pendingCommands = /* @__PURE__ */ new Map();
|
|
1154
1594
|
const buildTextContent = (text) => {
|
|
1155
1595
|
const content = {
|
|
1156
|
-
|
|
1596
|
+
// m.notice, not m.text: .m.rule.suppress_notices silences the
|
|
1597
|
+
// chunk-storm of agent prose server-side (ZNC025 §10) instead of every
|
|
1598
|
+
// client having to filter it. dev.zooid.error carries the same tweak
|
|
1599
|
+
// for the same reason.
|
|
1600
|
+
msgtype: "m.notice",
|
|
1157
1601
|
body: text
|
|
1158
1602
|
};
|
|
1159
1603
|
const html = toMatrixHtml(text);
|
|
@@ -1167,21 +1611,30 @@ function createMatrixTransport(opts) {
|
|
|
1167
1611
|
}
|
|
1168
1612
|
return content;
|
|
1169
1613
|
};
|
|
1614
|
+
const lastFlushed = /* @__PURE__ */ new Map();
|
|
1170
1615
|
const flushBuffer = (sessionId) => {
|
|
1171
1616
|
const ctx = sessions.get(sessionId);
|
|
1172
1617
|
const text = buffers.get(sessionId) ?? "";
|
|
1173
1618
|
if (!ctx || text.length === 0) return false;
|
|
1174
1619
|
buffers.set(sessionId, "");
|
|
1620
|
+
lastFlushed.set(sessionId, text);
|
|
1175
1621
|
flushedCounts.set(sessionId, (flushedCounts.get(sessionId) ?? 0) + 1);
|
|
1176
1622
|
const content = buildTextContent(text);
|
|
1623
|
+
const pendingInvocations = registerOutgoingHandoffs(sessionId, text);
|
|
1177
1624
|
const tail = (sendQueue.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
1178
1625
|
try {
|
|
1179
|
-
await client.sendMessage({
|
|
1626
|
+
const { event_id } = await client.sendMessage({
|
|
1180
1627
|
roomId: ctx.roomId,
|
|
1181
1628
|
asUserId: ctx.agent.userId,
|
|
1182
1629
|
content,
|
|
1183
1630
|
threadRoot: ctx.threadRoot
|
|
1184
1631
|
});
|
|
1632
|
+
for (const invocation of pendingInvocations)
|
|
1633
|
+
invocations.attachCallEvent(
|
|
1634
|
+
invocation.invocationId,
|
|
1635
|
+
event_id,
|
|
1636
|
+
composeHandoffKey(ctx.threadRoot, event_id)
|
|
1637
|
+
);
|
|
1185
1638
|
} catch (err) {
|
|
1186
1639
|
console.warn(`[matrix:${ctx.agent.name}] sendMessage flush failed:`, err);
|
|
1187
1640
|
}
|
|
@@ -1189,6 +1642,30 @@ function createMatrixTransport(opts) {
|
|
|
1189
1642
|
sendQueue.set(sessionId, tail);
|
|
1190
1643
|
return true;
|
|
1191
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
|
+
}
|
|
1192
1669
|
agents.onEvent = async (name, event) => {
|
|
1193
1670
|
const ctx = sessions.get(event.sessionId);
|
|
1194
1671
|
if (!ctx) {
|
|
@@ -1204,8 +1681,7 @@ function createMatrixTransport(opts) {
|
|
|
1204
1681
|
if (block.type === "text" && typeof block.text === "string") {
|
|
1205
1682
|
const prevMessageId = bufferMessageIds.get(event.sessionId);
|
|
1206
1683
|
const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
|
|
1207
|
-
if (event.messageId !== void 0)
|
|
1208
|
-
bufferMessageIds.set(event.sessionId, event.messageId);
|
|
1684
|
+
if (event.messageId !== void 0) bufferMessageIds.set(event.sessionId, event.messageId);
|
|
1209
1685
|
if (messageChanged) flushBuffer(event.sessionId);
|
|
1210
1686
|
const current = buffers.get(event.sessionId) ?? "";
|
|
1211
1687
|
const needsBreak = current.length > 0 && block.text === "";
|
|
@@ -1217,7 +1693,12 @@ function createMatrixTransport(opts) {
|
|
|
1217
1693
|
const bytes = Buffer.from(block.data, "base64");
|
|
1218
1694
|
const ext = (block.mimeType.split("/")[1] ?? "png").replace(/[^a-z0-9]/gi, "");
|
|
1219
1695
|
const filename = `image.${ext}`;
|
|
1220
|
-
void mediaClient.upload({
|
|
1696
|
+
void mediaClient.upload({
|
|
1697
|
+
data: bytes,
|
|
1698
|
+
contentType: block.mimeType,
|
|
1699
|
+
filename,
|
|
1700
|
+
asUserId: ctx2.agent.userId
|
|
1701
|
+
}).then(
|
|
1221
1702
|
({ content_uri }) => client.sendMessage({
|
|
1222
1703
|
roomId: ctx2.roomId,
|
|
1223
1704
|
asUserId: ctx2.agent.userId,
|
|
@@ -1273,7 +1754,10 @@ function createMatrixTransport(opts) {
|
|
|
1273
1754
|
tool_call_id: handle.toolCallId,
|
|
1274
1755
|
options: handle.options
|
|
1275
1756
|
};
|
|
1276
|
-
content["m.relates_to"] = {
|
|
1757
|
+
content["m.relates_to"] = {
|
|
1758
|
+
rel_type: "m.thread",
|
|
1759
|
+
event_id: ctx.threadRoot
|
|
1760
|
+
};
|
|
1277
1761
|
if (handle.toolKind !== void 0) content.tool_kind = handle.toolKind;
|
|
1278
1762
|
if (handle.toolTitle !== void 0) content.tool_title = handle.toolTitle;
|
|
1279
1763
|
if (handle.toolInput !== void 0) content.tool_input = handle.toolInput;
|
|
@@ -1284,6 +1768,51 @@ function createMatrixTransport(opts) {
|
|
|
1284
1768
|
content
|
|
1285
1769
|
});
|
|
1286
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
|
+
}
|
|
1287
1816
|
async function handleInboundEvent(evt) {
|
|
1288
1817
|
if (evt.event_id) {
|
|
1289
1818
|
if (seenEventIds.has(evt.event_id)) {
|
|
@@ -1322,6 +1851,7 @@ function createMatrixTransport(opts) {
|
|
|
1322
1851
|
return;
|
|
1323
1852
|
}
|
|
1324
1853
|
console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
|
|
1854
|
+
dropThreadReturns(threadRoot);
|
|
1325
1855
|
if (!threadStates.has(threadRoot) && evt.room_id) {
|
|
1326
1856
|
try {
|
|
1327
1857
|
threadStates.set(
|
|
@@ -1335,8 +1865,11 @@ function createMatrixTransport(opts) {
|
|
|
1335
1865
|
const st = threadStates.get(threadRoot);
|
|
1336
1866
|
for (const a of bindings) {
|
|
1337
1867
|
agents.endSession(a.name, threadRoot);
|
|
1868
|
+
taskRegistry.bumpGeneration(a.name, threadRoot);
|
|
1338
1869
|
for (const arc of st?.handoffs[a.name] ?? []) {
|
|
1339
|
-
|
|
1870
|
+
const key = composeHandoffKey(threadRoot, arc);
|
|
1871
|
+
agents.endSession(a.name, key);
|
|
1872
|
+
taskRegistry.bumpGeneration(a.name, key);
|
|
1340
1873
|
}
|
|
1341
1874
|
}
|
|
1342
1875
|
return;
|
|
@@ -1360,6 +1893,15 @@ function createMatrixTransport(opts) {
|
|
|
1360
1893
|
console.error(`[matrix] cancelSession(${t.agent}, ${t.sessionId}) failed:`, err);
|
|
1361
1894
|
});
|
|
1362
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
|
+
}
|
|
1363
1905
|
return;
|
|
1364
1906
|
}
|
|
1365
1907
|
if (!content.session_id) {
|
|
@@ -1374,7 +1916,10 @@ function createMatrixTransport(opts) {
|
|
|
1374
1916
|
`[matrix] interrupt session=${content.session_id} agent=${ctx.agent.name}` + (content.reason ? ` reason=${content.reason}` : "")
|
|
1375
1917
|
);
|
|
1376
1918
|
await agents.cancelSession(ctx.agent.name, content.session_id).catch((err) => {
|
|
1377
|
-
console.error(
|
|
1919
|
+
console.error(
|
|
1920
|
+
`[matrix] cancelSession(${ctx.agent.name}, ${content.session_id}) failed:`,
|
|
1921
|
+
err
|
|
1922
|
+
);
|
|
1378
1923
|
});
|
|
1379
1924
|
return;
|
|
1380
1925
|
}
|
|
@@ -1382,15 +1927,20 @@ function createMatrixTransport(opts) {
|
|
|
1382
1927
|
const content = evt.content ?? {};
|
|
1383
1928
|
if (!content.session_id || !content.approval_id || !content.decision) return;
|
|
1384
1929
|
const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
|
|
1385
|
-
const ok = approvals.resolve(
|
|
1386
|
-
content.session_id,
|
|
1387
|
-
content.approval_id,
|
|
1388
|
-
decision
|
|
1389
|
-
);
|
|
1930
|
+
const ok = approvals.resolve(content.session_id, content.approval_id, decision);
|
|
1390
1931
|
if (!ok) console.warn(`[matrix] unknown approval ${content.approval_id}`);
|
|
1391
1932
|
return;
|
|
1392
1933
|
}
|
|
1393
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
|
+
}
|
|
1394
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)) {
|
|
1395
1945
|
pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
|
|
1396
1946
|
eventId: evt.event_id,
|
|
@@ -1416,7 +1966,32 @@ function createMatrixTransport(opts) {
|
|
|
1416
1966
|
console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
|
|
1417
1967
|
}
|
|
1418
1968
|
}
|
|
1419
|
-
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
|
+
}
|
|
1420
1995
|
const senderIsBot = bindings.some((b) => b.userId === evt.sender);
|
|
1421
1996
|
if (evt.type === "m.room.message" && matches.length === 0 && !senderIsBot) {
|
|
1422
1997
|
console.warn(
|
|
@@ -1429,55 +2004,35 @@ function createMatrixTransport(opts) {
|
|
|
1429
2004
|
st = { participants: [], rootMentions: [], callers: {}, handoffs: {} };
|
|
1430
2005
|
threadStates.set(promotedRoot, st);
|
|
1431
2006
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
if (
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
+
}
|
|
1442
2021
|
}
|
|
1443
2022
|
}
|
|
1444
2023
|
}
|
|
1445
2024
|
}
|
|
1446
2025
|
for (const a of matches) {
|
|
1447
2026
|
console.log(`[matrix] \u2192 ${a.name} (${a.userId})`);
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
console.error(`[matrix] runTurn failed for ${a.name}:`, err);
|
|
1458
|
-
const c = classify(err);
|
|
1459
|
-
const threadRoot = inboundThreadRoot2(evt) ?? evt.event_id;
|
|
1460
|
-
if (!threadRoot || !evt.room_id) return;
|
|
1461
|
-
const body = toErrorBody(
|
|
1462
|
-
{
|
|
1463
|
-
kind: "error",
|
|
1464
|
-
agentId: a.name,
|
|
1465
|
-
sessionId: null,
|
|
1466
|
-
turnId: null,
|
|
1467
|
-
code: c.code,
|
|
1468
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1469
|
-
detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
|
|
1470
|
-
transient: c.transient,
|
|
1471
|
-
acp_error: c.acp_error
|
|
1472
|
-
},
|
|
1473
|
-
threadRoot
|
|
1474
|
-
);
|
|
1475
|
-
void client.sendCustomEvent({
|
|
1476
|
-
roomId: evt.room_id,
|
|
1477
|
-
asUserId: a.userId,
|
|
1478
|
-
eventType: "dev.zooid.error",
|
|
1479
|
-
content: body
|
|
1480
|
-
}).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 } : {}
|
|
1481
2036
|
});
|
|
1482
2037
|
}
|
|
1483
2038
|
}
|
|
@@ -1518,13 +2073,10 @@ function createMatrixTransport(opts) {
|
|
|
1518
2073
|
return c.json({});
|
|
1519
2074
|
});
|
|
1520
2075
|
app.get("/healthz", (c) => c.text("ok"));
|
|
1521
|
-
async function runTurn(agent,
|
|
1522
|
-
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
const sessionKey = sessionKeyFor(agent.name, threadRoot, threadStates.get(threadRoot));
|
|
1526
|
-
const sessionId = await agents.ensureSession(agent.name, sessionKey, evt.room_id, threadRoot);
|
|
1527
|
-
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 });
|
|
1528
2080
|
buffers.set(sessionId, "");
|
|
1529
2081
|
bufferMessageIds.delete(sessionId);
|
|
1530
2082
|
flushedCounts.set(sessionId, 0);
|
|
@@ -1533,10 +2085,14 @@ function createMatrixTransport(opts) {
|
|
|
1533
2085
|
pendingCommands.delete(sessionId);
|
|
1534
2086
|
void agents.onEvent?.(agent.name, stashedCommands);
|
|
1535
2087
|
}
|
|
1536
|
-
const roomId = evt.room_id;
|
|
1537
2088
|
const TYPING_TTL_MS = 3e4;
|
|
1538
2089
|
const TYPING_REFRESH_MS = 25e3;
|
|
1539
|
-
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));
|
|
1540
2096
|
const safePresence = (presence) => client.setPresence({ asUserId: agent.userId, presence }).catch(
|
|
1541
2097
|
(err) => console.warn(`[matrix:${agent.name}] setPresence(${presence}) failed:`, err)
|
|
1542
2098
|
);
|
|
@@ -1545,13 +2101,19 @@ function createMatrixTransport(opts) {
|
|
|
1545
2101
|
const refresh = setInterval(() => {
|
|
1546
2102
|
void safeTyping(true);
|
|
1547
2103
|
}, TYPING_REFRESH_MS);
|
|
2104
|
+
let turnError;
|
|
2105
|
+
let stopReason;
|
|
1548
2106
|
try {
|
|
1549
|
-
const rawBody =
|
|
1550
|
-
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;
|
|
1551
2113
|
const pendingItems = pendingMedia.drain(
|
|
1552
|
-
|
|
1553
|
-
inboundThreadRoot2(
|
|
1554
|
-
|
|
2114
|
+
roomId,
|
|
2115
|
+
input.event ? inboundThreadRoot2(input.event) : void 0,
|
|
2116
|
+
input.event?.sender ?? ""
|
|
1555
2117
|
);
|
|
1556
2118
|
const { blocks, pathLines } = await buildMediaBlocks(pendingItems, {
|
|
1557
2119
|
agent,
|
|
@@ -1560,7 +2122,7 @@ function createMatrixTransport(opts) {
|
|
|
1560
2122
|
onError: (item, err) => {
|
|
1561
2123
|
console.warn(`[matrix:${agent.name}] media_failed for ${item.body}:`, err);
|
|
1562
2124
|
void sendMediaError(
|
|
1563
|
-
{ agent, roomId
|
|
2125
|
+
{ agent, roomId, threadRoot },
|
|
1564
2126
|
err,
|
|
1565
2127
|
`Could not process attachment: ${item.body}`,
|
|
1566
2128
|
client
|
|
@@ -1568,38 +2130,307 @@ function createMatrixTransport(opts) {
|
|
|
1568
2130
|
}
|
|
1569
2131
|
});
|
|
1570
2132
|
const fullPromptText = [promptText, ...pathLines].filter(Boolean).join("\n");
|
|
1571
|
-
await agents.prompt(agent.name, {
|
|
2133
|
+
const promptResult = await agents.prompt(agent.name, {
|
|
1572
2134
|
threadId: sessionKey,
|
|
1573
|
-
channelId:
|
|
2135
|
+
channelId: roomId,
|
|
1574
2136
|
contextThreadId: threadRoot,
|
|
1575
2137
|
content: [...blocks, { type: "text", text: fullPromptText }]
|
|
1576
2138
|
});
|
|
2139
|
+
stopReason = promptResult.stopReason;
|
|
1577
2140
|
const drainStart = Date.now();
|
|
1578
2141
|
let drained = buffers.get(sessionId) ?? "";
|
|
1579
2142
|
while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
|
|
1580
2143
|
await delay(drainQuietMs);
|
|
1581
2144
|
const next = buffers.get(sessionId) ?? "";
|
|
1582
|
-
if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0))
|
|
1583
|
-
break;
|
|
2145
|
+
if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0)) break;
|
|
1584
2146
|
drained = next;
|
|
1585
2147
|
}
|
|
1586
2148
|
flushBuffer(sessionId);
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
`[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
|
|
1591
|
-
);
|
|
1592
|
-
}
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
turnError = err;
|
|
2151
|
+
throw err;
|
|
1593
2152
|
} finally {
|
|
1594
2153
|
clearInterval(refresh);
|
|
1595
2154
|
await safeTyping(false);
|
|
1596
2155
|
await safePresence("online");
|
|
2156
|
+
await (sendQueue.get(sessionId) ?? Promise.resolve());
|
|
2157
|
+
const producedOutput = (flushedCounts.get(sessionId) ?? 0) > 0;
|
|
2158
|
+
if (!producedOutput) {
|
|
2159
|
+
console.warn(
|
|
2160
|
+
`[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${roomId}`
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
await client.sendCustomEvent({
|
|
2164
|
+
roomId,
|
|
2165
|
+
asUserId: agent.userId,
|
|
2166
|
+
eventType: "dev.zooid.turn.end",
|
|
2167
|
+
content: toTurnEndBody(
|
|
2168
|
+
{
|
|
2169
|
+
agentId: agent.name,
|
|
2170
|
+
sessionId,
|
|
2171
|
+
producedOutput,
|
|
2172
|
+
lastMessage: lastFlushed.get(sessionId)
|
|
2173
|
+
},
|
|
2174
|
+
threadRoot
|
|
2175
|
+
)
|
|
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
|
+
}
|
|
1597
2196
|
buffers.delete(sessionId);
|
|
1598
2197
|
bufferMessageIds.delete(sessionId);
|
|
1599
2198
|
flushedCounts.delete(sessionId);
|
|
2199
|
+
lastFlushed.delete(sessionId);
|
|
1600
2200
|
sendQueue.delete(sessionId);
|
|
1601
2201
|
}
|
|
1602
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
|
+
});
|
|
1603
2434
|
const syncLoops = mode === "client" ? bindings.map(
|
|
1604
2435
|
(b) => new SyncLoop({
|
|
1605
2436
|
client,
|
|
@@ -1611,6 +2442,7 @@ function createMatrixTransport(opts) {
|
|
|
1611
2442
|
) : void 0;
|
|
1612
2443
|
return {
|
|
1613
2444
|
app,
|
|
2445
|
+
taskActions,
|
|
1614
2446
|
syncLoops,
|
|
1615
2447
|
bootstrap: async (bootstrapOpts = {}) => {
|
|
1616
2448
|
await pool.bootstrap({ adminUserId, ...bootstrapOpts });
|
|
@@ -1626,7 +2458,12 @@ function createMatrixTransport(opts) {
|
|
|
1626
2458
|
};
|
|
1627
2459
|
}
|
|
1628
2460
|
async function rebuildThreadState(client, roomId, rootEventId, bindings) {
|
|
1629
|
-
const state = {
|
|
2461
|
+
const state = {
|
|
2462
|
+
participants: [],
|
|
2463
|
+
rootMentions: [],
|
|
2464
|
+
callers: {},
|
|
2465
|
+
handoffs: {}
|
|
2466
|
+
};
|
|
1630
2467
|
const asUser = (bindings.find((b) => b.rooms.some((r) => r.alias === roomId)) ?? bindings[0])?.userId;
|
|
1631
2468
|
if (!asUser) return state;
|
|
1632
2469
|
const root = await client.fetchEvent(roomId, rootEventId, asUser);
|
|
@@ -1637,7 +2474,7 @@ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
|
|
|
1637
2474
|
for (const a of bindings) {
|
|
1638
2475
|
if (!rootMentions.has(a.userId)) continue;
|
|
1639
2476
|
if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
|
|
1640
|
-
if (rootSenderAgent && a.name !== rootSenderAgent.name) {
|
|
2477
|
+
if (rootSenderAgent && a.name !== rootSenderAgent.name && !wouldCycleCallers(state.callers, a.name, rootSenderAgent.name)) {
|
|
1641
2478
|
state.callers[a.name] = rootSenderAgent.name;
|
|
1642
2479
|
const arcs = state.handoffs[a.name] ??= [];
|
|
1643
2480
|
if (!arcs.includes(rootEventId)) arcs.push(rootEventId);
|
|
@@ -1657,7 +2494,7 @@ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
|
|
|
1657
2494
|
for (const a of bindings) {
|
|
1658
2495
|
if (!mentions.has(a.userId)) continue;
|
|
1659
2496
|
if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
|
|
1660
|
-
if (evSenderAgent && a.name !== evSenderAgent.name) {
|
|
2497
|
+
if (evSenderAgent && a.name !== evSenderAgent.name && !wouldCycleCallers(state.callers, a.name, evSenderAgent.name)) {
|
|
1661
2498
|
state.callers[a.name] = evSenderAgent.name;
|
|
1662
2499
|
if (evId) {
|
|
1663
2500
|
const arcs = state.handoffs[a.name] ??= [];
|
|
@@ -1726,9 +2563,11 @@ export {
|
|
|
1726
2563
|
AGENT_KEY_RE,
|
|
1727
2564
|
BotPool,
|
|
1728
2565
|
INLINE_IMAGE_MIMES,
|
|
2566
|
+
InvocationRegistry,
|
|
1729
2567
|
MAX_DOWNLOAD_BYTES,
|
|
1730
2568
|
MAX_INLINE_IMAGE_BYTES,
|
|
1731
2569
|
MAX_MEDIA_PER_TURN,
|
|
2570
|
+
MAX_OPEN_TASKS_PER_ROOM,
|
|
1732
2571
|
MEDIA_MSGTYPES,
|
|
1733
2572
|
MatrixClient,
|
|
1734
2573
|
MatrixContextProvider,
|
|
@@ -1736,17 +2575,23 @@ export {
|
|
|
1736
2575
|
PendingMediaStore,
|
|
1737
2576
|
SLUG_RE,
|
|
1738
2577
|
SyncLoop,
|
|
2578
|
+
TaskRegistry,
|
|
1739
2579
|
agentMxid,
|
|
2580
|
+
buildAssignmentContent,
|
|
1740
2581
|
buildWorkforceRoster,
|
|
2582
|
+
checkDelegable,
|
|
1741
2583
|
createMatrixTransport,
|
|
1742
2584
|
ensureDefaultChannel,
|
|
1743
2585
|
ensureWorkforceSpace,
|
|
2586
|
+
evaluateCompletion,
|
|
1744
2587
|
extractMentions,
|
|
1745
2588
|
isMediaMsgtype,
|
|
1746
2589
|
isValidAgentKey,
|
|
1747
2590
|
isValidWorkstation,
|
|
1748
2591
|
parseMxcUri,
|
|
1749
2592
|
publishWorkforce,
|
|
2593
|
+
renderCompletionPrompt,
|
|
2594
|
+
renderInvocationReturn,
|
|
1750
2595
|
renderRegistration,
|
|
1751
2596
|
route,
|
|
1752
2597
|
serverNameFromMxid,
|