zooid 0.14.1 → 0.16.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/README.md +1 -1
- package/dist/bin.js +93 -25
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-R5S26T7B.js → chunk-HE7DVDJA.js} +1545 -816
- package/dist/chunk-HE7DVDJA.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +10 -10
- package/src/build-registry.context.test.ts +1 -0
- package/src/build-registry.test.ts +1 -0
- package/src/build-registry.ts +3 -0
- package/src/build-registry.zod043.test.ts +1 -0
- package/src/build-registry.zod044.test.ts +1 -0
- package/src/commands/dev.ts +3 -0
- package/src/commands/start.ts +2 -0
- package/src/daemon/start-daemon.ts +6 -0
- package/src/daemon/trigger-runner.mxid.test.ts +145 -0
- package/src/daemon/trigger-runner.test.ts +3 -4
- package/src/daemon/trigger-runner.ts +45 -7
- package/src/daemon/trigger-scheduler.ts +12 -7
- package/src/daemon/webhook-routes.ts +3 -7
- package/src/observability/capture-agent.ts +35 -1
- package/src/observability/lifecycle-log.test.ts +48 -0
- package/src/observability/lifecycle-log.ts +29 -0
- package/src/observability/observability.integration.test.ts +22 -0
- package/src/web/static.test.ts +14 -0
- package/src/web/static.ts +3 -0
- package/dist/chunk-R5S26T7B.js.map +0 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ zooid dev
|
|
|
42
42
|
|
|
43
43
|
> **Prerequisite:** `zooid dev` runs Tuwunel inside a container, so you need a container engine installed and running first — either **Docker** (with the daemon started) or **Podman** aliased as `docker`.
|
|
44
44
|
|
|
45
|
-
`zooid dev` starts a Tuwunel Matrix homeserver in a container, generates the Application Service registration, registers an `admin:admin` user, runs the daemon, and serves the [Zooid web client](https://github.com/zooid-ai/clients).
|
|
45
|
+
`zooid dev` starts a Tuwunel Matrix homeserver in a container, generates the Application Service registration, registers an `admin:admin` user, runs the daemon, and serves the [Zooid web client](https://github.com/zooid-ai/clients). The client's runtime settings come from a generated `/config.json`; its `workforce_space` field is the `matrix.space` alias localpart from `zooid.yaml` (default `dev`). A static deployment sets `workforce_space` in the `config.json` it serves beside the bundle — there is no build-time variable.
|
|
46
46
|
|
|
47
47
|
Open `http://localhost:5173`, log in as `admin` / `admin`, join `#welcome`, and `@`-mention your agent.
|
|
48
48
|
|
package/dist/bin.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
MediaClient,
|
|
6
6
|
SpawnRegistry,
|
|
7
7
|
buildAcpRegistry,
|
|
8
|
+
buildMentionContent,
|
|
8
9
|
contextEligibleAgents,
|
|
9
10
|
createMatrixTransport,
|
|
10
11
|
ensureDefaultChannel,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
renderTemplate,
|
|
20
21
|
startAgentSocketServers,
|
|
21
22
|
startWorkforcePublisher
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-HE7DVDJA.js";
|
|
23
24
|
|
|
24
25
|
// src/bin.ts
|
|
25
26
|
import { resolve as resolve8 } from "path";
|
|
@@ -697,9 +698,15 @@ function shouldBindHttpListener(mode) {
|
|
|
697
698
|
import { Cron } from "croner";
|
|
698
699
|
|
|
699
700
|
// src/daemon/trigger-runner.ts
|
|
701
|
+
var warnedTargets = /* @__PURE__ */ new Set();
|
|
700
702
|
async function fireTrigger(deps) {
|
|
701
|
-
const { name, as, message,
|
|
703
|
+
const { name, as, message, agentUserIds, resolveRoom, ensureBot, sendMessage, getJoinedMembers } = deps;
|
|
702
704
|
try {
|
|
705
|
+
const target = message.mention.startsWith("@") ? message.mention : agentUserIds[message.mention];
|
|
706
|
+
if (!target) {
|
|
707
|
+
console.warn(`[trigger:${name}] unknown agent "${message.mention}" \u2014 skipping`);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
703
710
|
const roomId = await resolveRoom(message.room);
|
|
704
711
|
if (!roomId) {
|
|
705
712
|
console.warn(`[trigger:${name}] cannot resolve room ${message.room} \u2014 skipping`);
|
|
@@ -710,13 +717,24 @@ async function fireTrigger(deps) {
|
|
|
710
717
|
roomId,
|
|
711
718
|
asUserId: as,
|
|
712
719
|
content: {
|
|
713
|
-
msgtype: "m.text",
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
720
|
+
...buildMentionContent({ userId: target, text: message.text, msgtype: "m.text" }),
|
|
721
|
+
"dev.zooid.trigger": {
|
|
722
|
+
name,
|
|
723
|
+
fired_at: Date.now(),
|
|
724
|
+
...message.ttlMs !== void 0 ? { ttl_ms: message.ttlMs } : {}
|
|
725
|
+
}
|
|
718
726
|
}
|
|
719
727
|
});
|
|
728
|
+
if (getJoinedMembers) {
|
|
729
|
+
const { joined } = await getJoinedMembers(roomId, as);
|
|
730
|
+
const key = `${name}|${target}`;
|
|
731
|
+
if (!(target in joined) && !warnedTargets.has(key)) {
|
|
732
|
+
warnedTargets.add(key);
|
|
733
|
+
console.warn(
|
|
734
|
+
`[trigger:${name}] ${target} is not in ${message.room} \u2014 nothing will handle this unless that agent joins before the message expires`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
720
738
|
} catch (err) {
|
|
721
739
|
console.warn(`[trigger:${name}] failed:`, err.message);
|
|
722
740
|
}
|
|
@@ -731,18 +749,22 @@ function validateCron(name, expr) {
|
|
|
731
749
|
}
|
|
732
750
|
}
|
|
733
751
|
function startTriggerScheduler(deps) {
|
|
734
|
-
const { triggers, agentUserIds, resolveRoom, ensureBot, sendMessage } = deps;
|
|
752
|
+
const { triggers, agentUserIds, resolveRoom, ensureBot, sendMessage, getJoinedMembers } = deps;
|
|
735
753
|
const jobs = [];
|
|
736
754
|
for (const [name, trigger] of Object.entries(triggers)) {
|
|
737
755
|
if (!trigger.schedule) continue;
|
|
738
756
|
const job = new Cron(trigger.schedule, () => {
|
|
739
757
|
for (const message of trigger.messages) {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
758
|
+
void fireTrigger({
|
|
759
|
+
name,
|
|
760
|
+
as: trigger.as,
|
|
761
|
+
message,
|
|
762
|
+
agentUserIds,
|
|
763
|
+
resolveRoom,
|
|
764
|
+
ensureBot,
|
|
765
|
+
sendMessage,
|
|
766
|
+
getJoinedMembers
|
|
767
|
+
});
|
|
746
768
|
}
|
|
747
769
|
});
|
|
748
770
|
jobs.push(job);
|
|
@@ -955,19 +977,15 @@ async function handleDelivery(deps, name, trigger, raw, headers, cache, customDe
|
|
|
955
977
|
};
|
|
956
978
|
for (const message of trigger.messages) {
|
|
957
979
|
if (message.match !== void 0 && !evaluateMatch(message.match, ctx)) continue;
|
|
958
|
-
const agentUserId = deps.agentUserIds[message.mention];
|
|
959
|
-
if (!agentUserId) {
|
|
960
|
-
console.warn(`[webhook:${name}] unknown agent "${message.mention}" \u2014 skipping`);
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
980
|
await fireTrigger({
|
|
964
981
|
name,
|
|
965
982
|
as: trigger.as,
|
|
966
983
|
message: { ...message, text: renderTemplate(message.text, ctx) },
|
|
967
|
-
|
|
984
|
+
agentUserIds: deps.agentUserIds,
|
|
968
985
|
resolveRoom: deps.resolveRoom,
|
|
969
986
|
ensureBot: deps.ensureBot,
|
|
970
|
-
sendMessage: deps.sendMessage
|
|
987
|
+
sendMessage: deps.sendMessage,
|
|
988
|
+
getJoinedMembers: deps.getJoinedMembers
|
|
971
989
|
});
|
|
972
990
|
}
|
|
973
991
|
} catch (err) {
|
|
@@ -1095,6 +1113,7 @@ async function startDaemon(opts = {}) {
|
|
|
1095
1113
|
const registry = buildAcpRegistry(config, {
|
|
1096
1114
|
approvals,
|
|
1097
1115
|
onTap: opts.onTap,
|
|
1116
|
+
onLifecycle: opts.onLifecycle,
|
|
1098
1117
|
agentsDir: opts.agentsDir,
|
|
1099
1118
|
contextSpawnRegistry,
|
|
1100
1119
|
daemonSockPaths: contextSockets.paths,
|
|
@@ -1237,7 +1256,8 @@ async function startDaemon(opts = {}) {
|
|
|
1237
1256
|
agentUserIds,
|
|
1238
1257
|
resolveRoom,
|
|
1239
1258
|
ensureBot,
|
|
1240
|
-
sendMessage: (m) => client.sendMessage(m)
|
|
1259
|
+
sendMessage: (m) => client.sendMessage(m),
|
|
1260
|
+
getJoinedMembers: (roomId, asUserId2) => client.getJoinedMembers(roomId, asUserId2)
|
|
1241
1261
|
});
|
|
1242
1262
|
for (const [name] of webhookTriggers) {
|
|
1243
1263
|
console.log(`[webhook] POST ${WEBHOOK_ROUTE_PREFIX}/${name}`);
|
|
@@ -1281,7 +1301,8 @@ async function startDaemon(opts = {}) {
|
|
|
1281
1301
|
agentUserIds,
|
|
1282
1302
|
resolveRoom,
|
|
1283
1303
|
ensureBot,
|
|
1284
|
-
sendMessage: (m) => client.sendMessage(m)
|
|
1304
|
+
sendMessage: (m) => client.sendMessage(m),
|
|
1305
|
+
getJoinedMembers: (roomId, asUserId2) => client.getJoinedMembers(roomId, asUserId2)
|
|
1285
1306
|
});
|
|
1286
1307
|
const scheduled = Object.values(config.triggers).filter((t) => t.schedule).length;
|
|
1287
1308
|
if (scheduled > 0) console.log(`[trigger] scheduled ${scheduled} trigger(s)`);
|
|
@@ -1317,7 +1338,8 @@ async function startDaemon(opts = {}) {
|
|
|
1317
1338
|
client,
|
|
1318
1339
|
spaceRoomId,
|
|
1319
1340
|
asUserId,
|
|
1320
|
-
getAgents: () => bindings
|
|
1341
|
+
getAgents: () => bindings,
|
|
1342
|
+
stateKey: matrix.transport.workstation ?? ""
|
|
1321
1343
|
});
|
|
1322
1344
|
console.log(`[matrix] published dev.zooid.workforce (${bindings.length} agents)`);
|
|
1323
1345
|
void publisher;
|
|
@@ -1637,6 +1659,7 @@ function webStatic(opts) {
|
|
|
1637
1659
|
"/config.json",
|
|
1638
1660
|
(c) => c.json({
|
|
1639
1661
|
homeserver_url: opts.homeserverUrl,
|
|
1662
|
+
...opts.workforceSpace ? { workforce_space: opts.workforceSpace } : {},
|
|
1640
1663
|
...opts.pushGatewayUrl ? { push_gateway_url: opts.pushGatewayUrl } : {},
|
|
1641
1664
|
...opts.vapidPublicKey ? { vapid_public_key: opts.vapidPublicKey } : {}
|
|
1642
1665
|
})
|
|
@@ -1873,6 +1896,12 @@ function wireAgentCapture(opts) {
|
|
|
1873
1896
|
const sink = new JsonlSink(opts.paths.agentTap(opts.agent));
|
|
1874
1897
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1875
1898
|
const pending = [];
|
|
1899
|
+
const counters = {
|
|
1900
|
+
closeAttempts: 0,
|
|
1901
|
+
closeSuccesses: 0,
|
|
1902
|
+
sessionsReclaimed: 0,
|
|
1903
|
+
recoveries: { cached: 0, resume: 0, load: 0, new: 0 }
|
|
1904
|
+
};
|
|
1876
1905
|
const onTap = (event) => {
|
|
1877
1906
|
if (event.kind === "session_update") {
|
|
1878
1907
|
const variant = event.update.sessionUpdate ?? "unknown";
|
|
@@ -1894,6 +1923,25 @@ function wireAgentCapture(opts) {
|
|
|
1894
1923
|
};
|
|
1895
1924
|
return {
|
|
1896
1925
|
onTap,
|
|
1926
|
+
onLifecycle: (event) => {
|
|
1927
|
+
if (event.reason && event.outcome !== "unsupported") counters.closeAttempts++;
|
|
1928
|
+
if (event.reason && event.outcome === "closed") {
|
|
1929
|
+
counters.closeSuccesses++;
|
|
1930
|
+
counters.sessionsReclaimed++;
|
|
1931
|
+
}
|
|
1932
|
+
if (event.outcome === "recovered" && event.recoveryMethod) counters.recoveries[event.recoveryMethod]++;
|
|
1933
|
+
pending.push(sink.write({
|
|
1934
|
+
ts: now().toISOString(),
|
|
1935
|
+
agent: opts.agent,
|
|
1936
|
+
kind: "session_lifecycle",
|
|
1937
|
+
session_key: event.sessionKey,
|
|
1938
|
+
session_id: event.sessionId,
|
|
1939
|
+
reason: event.reason,
|
|
1940
|
+
outcome: event.outcome,
|
|
1941
|
+
recovery_method: event.recoveryMethod
|
|
1942
|
+
}));
|
|
1943
|
+
},
|
|
1944
|
+
readLifecycleCounters: () => ({ ...counters, recoveries: { ...counters.recoveries } }),
|
|
1897
1945
|
close: async () => {
|
|
1898
1946
|
await Promise.all(pending);
|
|
1899
1947
|
await sink.close();
|
|
@@ -2059,6 +2107,7 @@ async function runDev(flags) {
|
|
|
2059
2107
|
// the web client without needing an invite.
|
|
2060
2108
|
publicWorkforceSpace: true,
|
|
2061
2109
|
onTap: (agentName, event) => captures[agentName]?.onTap(event),
|
|
2110
|
+
onLifecycle: (agentName, event) => captures[agentName]?.onLifecycle(event),
|
|
2062
2111
|
prepullLog: (line) => {
|
|
2063
2112
|
t.output = line.replace(/^\[zooid\]\s+/, "").trim();
|
|
2064
2113
|
}
|
|
@@ -2095,6 +2144,8 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
|
|
|
2095
2144
|
const app = webStatic({
|
|
2096
2145
|
webRoot,
|
|
2097
2146
|
homeserverUrl: homeserver,
|
|
2147
|
+
// Same default as the daemon's space provisioning (start-daemon.ts).
|
|
2148
|
+
workforceSpace: matrix.transport.space ?? "dev",
|
|
2098
2149
|
...ctx.daemon?.vapidPublicKey ? {
|
|
2099
2150
|
// Tuwunel runs in a container; `localhost` here would
|
|
2100
2151
|
// resolve to the container, not the daemon. Same shorthand
|
|
@@ -2757,6 +2808,22 @@ async function dumpByTurn(dayDir, turnId, writer) {
|
|
|
2757
2808
|
// src/commands/start.ts
|
|
2758
2809
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2759
2810
|
import { resolve as resolve6 } from "path";
|
|
2811
|
+
|
|
2812
|
+
// src/observability/lifecycle-log.ts
|
|
2813
|
+
function formatLifecycleLine(agentName, event) {
|
|
2814
|
+
if (event.outcome === "unsupported") return null;
|
|
2815
|
+
if (event.recoveryMethod === "cached") return null;
|
|
2816
|
+
const what = event.outcome === "recovered" ? `recovered (${event.recoveryMethod ?? "unknown"})` : `${event.reason ?? "close"} \u2192 ${event.outcome}`;
|
|
2817
|
+
return `[lifecycle] ${agentName} key=${event.sessionKey} session=${event.sessionId} ${what}`;
|
|
2818
|
+
}
|
|
2819
|
+
function logLifecycle(agentName, event, out = console) {
|
|
2820
|
+
const line = formatLifecycleLine(agentName, event);
|
|
2821
|
+
if (!line) return;
|
|
2822
|
+
if (event.outcome === "failed") out.error(line);
|
|
2823
|
+
else out.log(line);
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// src/commands/start.ts
|
|
2760
2827
|
async function runStart(flags) {
|
|
2761
2828
|
if (flags.printToken) {
|
|
2762
2829
|
process.stdout.write(`${randomBytes2(32).toString("hex")}
|
|
@@ -2768,7 +2835,8 @@ async function runStart(flags) {
|
|
|
2768
2835
|
const handle = await startDaemon({
|
|
2769
2836
|
cliFlags: flags,
|
|
2770
2837
|
installSignalHandlers: true,
|
|
2771
|
-
agentsDir: layout.agentsDir
|
|
2838
|
+
agentsDir: layout.agentsDir,
|
|
2839
|
+
onLifecycle: (agentName, event) => logLifecycle(agentName, event)
|
|
2772
2840
|
});
|
|
2773
2841
|
console.log(`zooid listening on http://localhost:${handle.port}`);
|
|
2774
2842
|
for (const name of handle.agentNames) console.log(` agent: ${name}`);
|