@openparachute/agent 0.2.0 → 0.2.3-rc.2
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/.parachute/module.json +3 -3
- package/package.json +5 -1
- package/src/agent-defs.test.ts +11 -11
- package/src/agents.test.ts +2 -2
- package/src/backends/programmatic.test.ts +5 -5
- package/src/daemon-agent-defs-api.test.ts +6 -6
- package/src/daemon-agent-env-api.test.ts +2 -2
- package/src/daemon-config-api.test.ts +2 -2
- package/src/daemon-jobs-api.test.ts +2 -2
- package/src/daemon.test.ts +5 -5
- package/src/jobs.test.ts +2 -2
- package/src/mint-token.test.ts +2 -2
- package/src/module-manifest.test.ts +2 -2
- package/src/programmatic-wiring.test.ts +2 -2
- package/src/spawn-agent-cli.test.ts +2 -2
- package/src/spawn-agent.test.ts +4 -4
- package/src/transports/vault.test.ts +65 -64
- package/src/transports/vault.ts +21 -21
- package/web/ui/dist/assets/index-5KEwEhfi.js +60 -0
- package/web/ui/dist/assets/index-C-iWdFFV.css +1 -0
- package/web/ui/dist/index.html +15 -0
|
@@ -117,10 +117,10 @@ describe("VaultTransport — reply (outbound note write)", () => {
|
|
|
117
117
|
// a child-only-tagged note is invisible to a `tag:#agent/message` query), and
|
|
118
118
|
// the directional child `#agent/message/outbound` is the trigger discriminator.
|
|
119
119
|
// We WRITE only the `#agent/message*` tags.
|
|
120
|
-
expect(sent.tags).toEqual(["
|
|
120
|
+
expect(sent.tags).toEqual(["agent/message", "agent/message/outbound"]);
|
|
121
121
|
// Regression guard: the queryable parent tag MUST be present literally.
|
|
122
|
-
expect(sent.tags).toContain("
|
|
123
|
-
expect(sent.tags).toContain("
|
|
122
|
+
expect(sent.tags).toContain("agent/message");
|
|
123
|
+
expect(sent.tags).toContain("agent/message/outbound");
|
|
124
124
|
// Write-discipline: the interim/legacy tags are gone (CONTRACT dropped them).
|
|
125
125
|
expect(sent.tags).not.toContain("#agent-message");
|
|
126
126
|
expect(sent.tags).not.toContain("#agent-message/outbound");
|
|
@@ -246,8 +246,8 @@ describe("VaultTransport — writeThread (#agent/thread note, the unified model)
|
|
|
246
246
|
// LOOP SAFETY (HARD CONSTRAINT 4): the thread note carries the thread tag EXACTLY —
|
|
247
247
|
// NOT a message tag + NOT the inbound child — so it can never wake a session.
|
|
248
248
|
expect(sent.tags).toEqual([AGENT_THREAD_TAG]);
|
|
249
|
-
expect(sent.tags).not.toContain("
|
|
250
|
-
expect(sent.tags).not.toContain("
|
|
249
|
+
expect(sent.tags).not.toContain("agent/message");
|
|
250
|
+
expect(sent.tags).not.toContain("agent/message/inbound");
|
|
251
251
|
// Indexed/queryable fields.
|
|
252
252
|
expect(sent.metadata.status).toBe("ok");
|
|
253
253
|
expect(sent.metadata.definition).toBe("Agents/digest");
|
|
@@ -1007,7 +1007,7 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
|
|
|
1007
1007
|
getUrls.push(u);
|
|
1008
1008
|
capturedAuth = (init?.headers as Record<string, string> | undefined)?.authorization ?? "";
|
|
1009
1009
|
// CONTRACT: a SINGLE `#agent/message` query (no interim/legacy union).
|
|
1010
|
-
if (u.includes("tag
|
|
1010
|
+
if (u.includes("tag=agent%2Fmessage")) {
|
|
1011
1011
|
// Return notes OUT of ts order (prove the ascending sort) + a note from a
|
|
1012
1012
|
// DIFFERENT channel (prove the client-side channel filter excludes it).
|
|
1013
1013
|
return new Response(
|
|
@@ -1015,19 +1015,19 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
|
|
|
1015
1015
|
{
|
|
1016
1016
|
id: "n-out",
|
|
1017
1017
|
content: "session reply",
|
|
1018
|
-
tags: ["
|
|
1018
|
+
tags: ["agent/message", "agent/message/outbound"],
|
|
1019
1019
|
metadata: { agent: "eng", direction: "outbound", sender: "session", ts: "2026-06-08T00:00:02Z", in_reply_to: "n-in" },
|
|
1020
1020
|
},
|
|
1021
1021
|
{
|
|
1022
1022
|
id: "n-other",
|
|
1023
1023
|
content: "different channel — must be excluded",
|
|
1024
|
-
tags: ["
|
|
1024
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1025
1025
|
metadata: { agent: "other", direction: "inbound", sender: "x", ts: "2026-06-08T00:00:03Z" },
|
|
1026
1026
|
},
|
|
1027
1027
|
{
|
|
1028
1028
|
id: "n-in",
|
|
1029
1029
|
content: "hi session",
|
|
1030
|
-
tags: ["
|
|
1030
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1031
1031
|
metadata: { agent: "eng", direction: "inbound", sender: "aaron", ts: "2026-06-08T00:00:01Z" },
|
|
1032
1032
|
},
|
|
1033
1033
|
]),
|
|
@@ -1049,7 +1049,7 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
|
|
|
1049
1049
|
// `metadata=` operator filter (the routing-key field isn't indexed on a bare
|
|
1050
1050
|
// vault; we filter client-side). Overfetches the tag so other channels don't
|
|
1051
1051
|
// crowd us out.
|
|
1052
|
-
const agentGets = getUrls.filter((u) => u.includes("tag
|
|
1052
|
+
const agentGets = getUrls.filter((u) => u.includes("tag=agent%2Fmessage"));
|
|
1053
1053
|
expect(agentGets).toHaveLength(1);
|
|
1054
1054
|
// No interim/legacy queries are issued.
|
|
1055
1055
|
expect(getUrls.some((u) => u.includes("tag=%23agent-message"))).toBe(false);
|
|
@@ -1076,12 +1076,12 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
|
|
|
1076
1076
|
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
1077
1077
|
const u = String(url);
|
|
1078
1078
|
if (u.includes("/api/notes") && (init?.method ?? "GET") === "GET") {
|
|
1079
|
-
if (u.includes("tag
|
|
1079
|
+
if (u.includes("tag=agent%2Fmessage")) {
|
|
1080
1080
|
return new Response(
|
|
1081
1081
|
JSON.stringify([1, 2, 3, 4].map((i) => ({
|
|
1082
1082
|
id: "n" + i,
|
|
1083
1083
|
content: "m" + i,
|
|
1084
|
-
tags: ["
|
|
1084
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1085
1085
|
metadata: { agent: "eng", direction: "inbound", sender: "aaron", ts: "2026-06-08T00:00:0" + i + "Z" },
|
|
1086
1086
|
}))),
|
|
1087
1087
|
{ status: 200 },
|
|
@@ -1102,13 +1102,13 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
|
|
|
1102
1102
|
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
1103
1103
|
const u = String(url);
|
|
1104
1104
|
if (u.includes("/api/notes") && (init?.method ?? "GET") === "GET") {
|
|
1105
|
-
if (u.includes("tag
|
|
1105
|
+
if (u.includes("tag=agent%2Fmessage")) {
|
|
1106
1106
|
return new Response(
|
|
1107
1107
|
JSON.stringify([
|
|
1108
1108
|
// Outbound child → direction inferred "outbound".
|
|
1109
|
-
{ id: "a", content: "x", tags: ["
|
|
1109
|
+
{ id: "a", content: "x", tags: ["agent/message", "agent/message/outbound"], metadata: { agent: "eng", ts: "2026-06-08T00:00:01Z" } },
|
|
1110
1110
|
// No direction signal at all → defaults to "inbound".
|
|
1111
|
-
{ id: "b", content: "y", tags: ["
|
|
1111
|
+
{ id: "b", content: "y", tags: ["agent/message", "agent/message/inbound"], metadata: { agent: "eng", ts: "2026-06-08T00:00:02Z" } },
|
|
1112
1112
|
]),
|
|
1113
1113
|
{ status: 200 },
|
|
1114
1114
|
);
|
|
@@ -1168,11 +1168,11 @@ describe("VaultTransport — writeInbound (the chat's send → wakes the session
|
|
|
1168
1168
|
};
|
|
1169
1169
|
expect(sent.content).toBe("wake up");
|
|
1170
1170
|
// The INBOUND tag pair — the child is the trigger discriminator that wakes the session.
|
|
1171
|
-
expect(sent.tags).toEqual(["
|
|
1172
|
-
expect(sent.tags).toContain("
|
|
1173
|
-
expect(sent.tags).toContain("
|
|
1171
|
+
expect(sent.tags).toEqual(["agent/message", "agent/message/inbound"]);
|
|
1172
|
+
expect(sent.tags).toContain("agent/message");
|
|
1173
|
+
expect(sent.tags).toContain("agent/message/inbound");
|
|
1174
1174
|
// It must NOT carry the outbound tag (that would be a reply, never wake).
|
|
1175
|
-
expect(sent.tags).not.toContain("
|
|
1175
|
+
expect(sent.tags).not.toContain("agent/message/outbound");
|
|
1176
1176
|
// Write-discipline: the legacy tag family is gone (CONTRACT dropped it).
|
|
1177
1177
|
expect(sent.tags).not.toContain("#channel-message");
|
|
1178
1178
|
// CONTRACT: the routing key under `metadata.agent` ONLY — no `channel`. The vault
|
|
@@ -1244,8 +1244,8 @@ describe("VaultTransport — writeCallback (agent-to-agent reply_to substrate)",
|
|
|
1244
1244
|
|
|
1245
1245
|
expect(result.sent).toEqual(["callback-note-1"]);
|
|
1246
1246
|
// The callback is an INBOUND note (so it wakes the sender via the normal vault trigger).
|
|
1247
|
-
expect(sent!.tags).toEqual(["
|
|
1248
|
-
expect(sent!.tags).not.toContain("
|
|
1247
|
+
expect(sent!.tags).toEqual(["agent/message", "agent/message/inbound"]);
|
|
1248
|
+
expect(sent!.tags).not.toContain("agent/message/outbound");
|
|
1249
1249
|
// The metadata contract — all present fields stamped.
|
|
1250
1250
|
expect(sent!.metadata.callback).toBe("true");
|
|
1251
1251
|
expect(sent!.metadata.status).toBe("ok");
|
|
@@ -1315,7 +1315,7 @@ describe("VaultTransport — ingestInbound", () => {
|
|
|
1315
1315
|
void t.ingestInbound({
|
|
1316
1316
|
id: "note-in-1",
|
|
1317
1317
|
content: "hello session",
|
|
1318
|
-
tags: ["
|
|
1318
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1319
1319
|
metadata: { agent: "eng", direction: "inbound", sender: "aaron", ts: "2026-06-08T00:00:00Z" },
|
|
1320
1320
|
});
|
|
1321
1321
|
expect(ctx.emitted).toHaveLength(1);
|
|
@@ -1341,7 +1341,7 @@ describe("VaultTransport — ingestInbound", () => {
|
|
|
1341
1341
|
void t.ingestInbound({
|
|
1342
1342
|
id: "our-own-reply",
|
|
1343
1343
|
content: "I am awake",
|
|
1344
|
-
tags: ["
|
|
1344
|
+
tags: ["agent/message", "agent/message/outbound"],
|
|
1345
1345
|
metadata: { channel: "eng", direction: "outbound", sender: "session" },
|
|
1346
1346
|
});
|
|
1347
1347
|
expect(ctx.emitted).toHaveLength(0);
|
|
@@ -1382,7 +1382,7 @@ describe("VaultTransport — ingestInbound", () => {
|
|
|
1382
1382
|
await t.ingestInbound({
|
|
1383
1383
|
id: "note-att-1",
|
|
1384
1384
|
content: "look at these",
|
|
1385
|
-
tags: ["
|
|
1385
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1386
1386
|
metadata: { agent: "eng", direction: "inbound", sender: "aaron" },
|
|
1387
1387
|
// inline list from the trigger payload — the has-attachments SIGNAL.
|
|
1388
1388
|
attachments: [{ id: "a1", path: "2026-06-24/pic.png", mimeType: "image/png" }],
|
|
@@ -1408,7 +1408,7 @@ describe("VaultTransport — ingestInbound", () => {
|
|
|
1408
1408
|
await t.ingestInbound({
|
|
1409
1409
|
id: "note-att-fail",
|
|
1410
1410
|
content: "still delivered",
|
|
1411
|
-
tags: ["
|
|
1411
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1412
1412
|
metadata: { agent: "eng", direction: "inbound" },
|
|
1413
1413
|
attachments: [{ id: "a1", path: "2026-06-24/pic.png", mimeType: "image/png" }],
|
|
1414
1414
|
});
|
|
@@ -1429,7 +1429,7 @@ describe("VaultTransport — ingestInbound", () => {
|
|
|
1429
1429
|
void t.ingestInbound({
|
|
1430
1430
|
id: "note-plain",
|
|
1431
1431
|
content: "no files",
|
|
1432
|
-
tags: ["
|
|
1432
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1433
1433
|
metadata: { agent: "eng", direction: "inbound" },
|
|
1434
1434
|
});
|
|
1435
1435
|
expect(ctx.emitted).toHaveLength(1);
|
|
@@ -1447,7 +1447,7 @@ describe("VaultTransport — ingestInbound", () => {
|
|
|
1447
1447
|
void t.ingestInbound({
|
|
1448
1448
|
id: "note-deleg-1",
|
|
1449
1449
|
content: "do the sub-task",
|
|
1450
|
-
tags: ["
|
|
1450
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1451
1451
|
metadata: {
|
|
1452
1452
|
channel: "worker",
|
|
1453
1453
|
direction: "inbound",
|
|
@@ -1482,10 +1482,11 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
|
|
|
1482
1482
|
|
|
1483
1483
|
expect(calls).toHaveLength(AGENT_VAULT_TAG_SCHEMA.length);
|
|
1484
1484
|
|
|
1485
|
-
// Namespace ROOT
|
|
1485
|
+
// Namespace ROOT `agent` — no parent_names, just a description. A single bare
|
|
1486
|
+
// segment needs no percent-encoding.
|
|
1486
1487
|
const root = calls[0]!;
|
|
1487
1488
|
expect(root.url).toBe(
|
|
1488
|
-
"http://127.0.0.1:1940/vault/default/api/tags
|
|
1489
|
+
"http://127.0.0.1:1940/vault/default/api/tags/agent",
|
|
1489
1490
|
);
|
|
1490
1491
|
expect(root.init.method).toBe("PUT");
|
|
1491
1492
|
expect((root.init.headers as Record<string, string>).authorization).toBe(
|
|
@@ -1497,19 +1498,19 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
|
|
|
1497
1498
|
};
|
|
1498
1499
|
expect("parent_names" in rootBody).toBe(false);
|
|
1499
1500
|
|
|
1500
|
-
// Definition (NEW) — name carries
|
|
1501
|
+
// Definition (NEW) — name carries `/`; rolls up to the namespace root.
|
|
1501
1502
|
const def = calls[1]!;
|
|
1502
1503
|
expect(def.url).toBe(
|
|
1503
|
-
"http://127.0.0.1:1940/vault/default/api/tags
|
|
1504
|
+
"http://127.0.0.1:1940/vault/default/api/tags/agent%2Fdefinition",
|
|
1504
1505
|
);
|
|
1505
|
-
expect(decodeURIComponent(def.url.split("/api/tags/")[1]!)).toBe("
|
|
1506
|
+
expect(decodeURIComponent(def.url.split("/api/tags/")[1]!)).toBe("agent/definition");
|
|
1506
1507
|
const defBody = JSON.parse(String(def.init.body)) as { parent_names?: string[] };
|
|
1507
|
-
expect(defBody.parent_names).toEqual(["
|
|
1508
|
+
expect(defBody.parent_names).toEqual(["agent"]);
|
|
1508
1509
|
|
|
1509
1510
|
// Message parent (NEW) — rolls up to the namespace root.
|
|
1510
1511
|
const parent = calls[2]!;
|
|
1511
1512
|
expect(parent.url).toBe(
|
|
1512
|
-
"http://127.0.0.1:1940/vault/default/api/tags
|
|
1513
|
+
"http://127.0.0.1:1940/vault/default/api/tags/agent%2Fmessage",
|
|
1513
1514
|
);
|
|
1514
1515
|
const parentBody = JSON.parse(String(parent.init.body)) as {
|
|
1515
1516
|
description?: string;
|
|
@@ -1518,23 +1519,23 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
|
|
|
1518
1519
|
expect(parentBody.description).toBe(
|
|
1519
1520
|
"A message in a Parachute channel (parent of /inbound + /outbound).",
|
|
1520
1521
|
);
|
|
1521
|
-
expect(parentBody.parent_names).toEqual(["
|
|
1522
|
+
expect(parentBody.parent_names).toEqual(["agent"]);
|
|
1522
1523
|
|
|
1523
|
-
// Inbound child (NEW) — name carries
|
|
1524
|
+
// Inbound child (NEW) — name carries `/`. The vault route matches a
|
|
1524
1525
|
// single path segment (`[^/]+`) then decodeURIComponent's it, so the `/` MUST
|
|
1525
1526
|
// be encoded as `%2F` (a bare slash would fail the single-segment match → 404).
|
|
1526
1527
|
const inbound = calls[3]!;
|
|
1527
1528
|
expect(inbound.url).toBe(
|
|
1528
|
-
"http://127.0.0.1:1940/vault/default/api/tags
|
|
1529
|
+
"http://127.0.0.1:1940/vault/default/api/tags/agent%2Fmessage%2Finbound",
|
|
1529
1530
|
);
|
|
1530
1531
|
// Confirm the encoding decodes back to the literal tag name the vault stores.
|
|
1531
1532
|
const encodedSegment = inbound.url.split("/api/tags/")[1]!;
|
|
1532
|
-
expect(decodeURIComponent(encodedSegment)).toBe("
|
|
1533
|
+
expect(decodeURIComponent(encodedSegment)).toBe("agent/message/inbound");
|
|
1533
1534
|
const inboundBody = JSON.parse(String(inbound.init.body)) as {
|
|
1534
1535
|
description?: string;
|
|
1535
1536
|
parent_names?: string[];
|
|
1536
1537
|
};
|
|
1537
|
-
expect(inboundBody.parent_names).toEqual(["
|
|
1538
|
+
expect(inboundBody.parent_names).toEqual(["agent/message"]);
|
|
1538
1539
|
expect(inboundBody.description).toBe(
|
|
1539
1540
|
"Human→session message; the vault trigger fires on this.",
|
|
1540
1541
|
);
|
|
@@ -1542,19 +1543,19 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
|
|
|
1542
1543
|
// Outbound child (NEW) — same encoding, parent declared.
|
|
1543
1544
|
const outbound = calls[4]!;
|
|
1544
1545
|
expect(outbound.url).toBe(
|
|
1545
|
-
"http://127.0.0.1:1940/vault/default/api/tags
|
|
1546
|
+
"http://127.0.0.1:1940/vault/default/api/tags/agent%2Fmessage%2Foutbound",
|
|
1546
1547
|
);
|
|
1547
1548
|
expect(decodeURIComponent(outbound.url.split("/api/tags/")[1]!)).toBe(
|
|
1548
|
-
"
|
|
1549
|
+
"agent/message/outbound",
|
|
1549
1550
|
);
|
|
1550
1551
|
const outboundBody = JSON.parse(String(outbound.init.body)) as { parent_names?: string[] };
|
|
1551
|
-
expect(outboundBody.parent_names).toEqual(["
|
|
1552
|
+
expect(outboundBody.parent_names).toEqual(["agent/message"]);
|
|
1552
1553
|
|
|
1553
1554
|
// Job (NEW) — rolls up to the namespace root.
|
|
1554
1555
|
const job = calls[5]!;
|
|
1555
|
-
expect(decodeURIComponent(job.url.split("/api/tags/")[1]!)).toBe("
|
|
1556
|
+
expect(decodeURIComponent(job.url.split("/api/tags/")[1]!)).toBe("agent/job");
|
|
1556
1557
|
const jobBody = JSON.parse(String(job.init.body)) as { parent_names?: string[] };
|
|
1557
|
-
expect(jobBody.parent_names).toEqual(["
|
|
1558
|
+
expect(jobBody.parent_names).toEqual(["agent"]);
|
|
1558
1559
|
});
|
|
1559
1560
|
|
|
1560
1561
|
test("schema declares ONLY the #agent/* namespace rollup (CONTRACT dropped interim + legacy, 7 entries)", async () => {
|
|
@@ -1564,29 +1565,29 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
|
|
|
1564
1565
|
// schema entries — exactly 7 entries, all under `#agent/*`.
|
|
1565
1566
|
const names = AGENT_VAULT_TAG_SCHEMA.map((e) => e.name);
|
|
1566
1567
|
expect(names).toEqual([
|
|
1567
|
-
"
|
|
1568
|
-
"
|
|
1569
|
-
"
|
|
1570
|
-
"
|
|
1571
|
-
"
|
|
1572
|
-
"
|
|
1573
|
-
"
|
|
1568
|
+
"agent",
|
|
1569
|
+
"agent/definition",
|
|
1570
|
+
"agent/message",
|
|
1571
|
+
"agent/message/inbound",
|
|
1572
|
+
"agent/message/outbound",
|
|
1573
|
+
"agent/job",
|
|
1574
|
+
"agent/thread",
|
|
1574
1575
|
]);
|
|
1575
1576
|
// The interim/legacy families are gone entirely.
|
|
1576
1577
|
expect(names).not.toContain("#agent-message");
|
|
1577
1578
|
expect(names).not.toContain("#channel-message");
|
|
1578
1579
|
// The namespace children all roll up to the `#agent` root (the human rollup).
|
|
1579
1580
|
const byName = (n: string) => AGENT_VAULT_TAG_SCHEMA.find((e) => e.name === n)!;
|
|
1580
|
-
expect(byName("
|
|
1581
|
-
expect(byName("
|
|
1582
|
-
expect(byName("
|
|
1583
|
-
expect(byName("
|
|
1584
|
-
expect(byName("
|
|
1585
|
-
expect(byName("
|
|
1581
|
+
expect(byName("agent/definition").parent_names).toEqual(["agent"]);
|
|
1582
|
+
expect(byName("agent/message").parent_names).toEqual(["agent"]);
|
|
1583
|
+
expect(byName("agent/job").parent_names).toEqual(["agent"]);
|
|
1584
|
+
expect(byName("agent/thread").parent_names).toEqual(["agent"]);
|
|
1585
|
+
expect(byName("agent/message/inbound").parent_names).toEqual(["agent/message"]);
|
|
1586
|
+
expect(byName("agent/message/outbound").parent_names).toEqual(["agent/message"]);
|
|
1586
1587
|
// `#agent/thread` declares INDEXED string fields so threads are operator-queryable —
|
|
1587
1588
|
// "all failed threads" (status), "all threads of agent X" (definition), "all
|
|
1588
1589
|
// multi-threaded threads" (mode). The three axes carry over from the run record VERBATIM.
|
|
1589
|
-
expect(byName("
|
|
1590
|
+
expect(byName("agent/thread").fields).toEqual({
|
|
1590
1591
|
// The canonical `agent` routing-key alias is declared indexed.
|
|
1591
1592
|
agent: { type: "string", indexed: true },
|
|
1592
1593
|
status: { type: "string", indexed: true },
|
|
@@ -1594,11 +1595,11 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
|
|
|
1594
1595
|
mode: { type: "string", indexed: true },
|
|
1595
1596
|
});
|
|
1596
1597
|
// `#agent/message` declares the indexed `agent` routing key.
|
|
1597
|
-
expect(byName("
|
|
1598
|
+
expect(byName("agent/message").fields).toEqual({
|
|
1598
1599
|
agent: { type: "string", indexed: true },
|
|
1599
1600
|
});
|
|
1600
1601
|
// CONTRACT: `#agent/job` indexes the routing key under `agent` ONLY — no `channel`.
|
|
1601
|
-
expect(byName("
|
|
1602
|
+
expect(byName("agent/job").fields).toEqual({
|
|
1602
1603
|
agent: { type: "string", indexed: true },
|
|
1603
1604
|
enabled: { type: "string", indexed: true },
|
|
1604
1605
|
lastStatus: { type: "string", indexed: true },
|
|
@@ -1691,7 +1692,7 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
|
|
|
1691
1692
|
void t.ingestInbound({
|
|
1692
1693
|
id: "n1",
|
|
1693
1694
|
content: "still works",
|
|
1694
|
-
tags: ["
|
|
1695
|
+
tags: ["agent/message", "agent/message/inbound"],
|
|
1695
1696
|
metadata: { channel: "eng", direction: "inbound", sender: "aaron" },
|
|
1696
1697
|
});
|
|
1697
1698
|
expect(ctx.emitted).toHaveLength(1);
|
|
@@ -1741,7 +1742,7 @@ describe("VaultTransport — injectInbound (runner seam)", () => {
|
|
|
1741
1742
|
expect(noteCalls).toHaveLength(1);
|
|
1742
1743
|
const body = JSON.parse(String(noteCalls[0]!.init.body));
|
|
1743
1744
|
// Inbound: BOTH the parent + the inbound child (the trigger discriminator).
|
|
1744
|
-
expect(body.tags).toEqual(["
|
|
1745
|
+
expect(body.tags).toEqual(["agent/message", "agent/message/inbound"]);
|
|
1745
1746
|
expect(body.content).toBe("Run the morning weave");
|
|
1746
1747
|
expect(body.metadata.direction).toBe("inbound");
|
|
1747
1748
|
expect(body.metadata.sender).toBe("runner:morning");
|
|
@@ -1789,7 +1790,7 @@ describe("VaultTransport — scheduled-job notes (vault-native store)", () => {
|
|
|
1789
1790
|
|
|
1790
1791
|
const t = new VaultTransport(baseConfig());
|
|
1791
1792
|
const jobs = await t.listJobNotes();
|
|
1792
|
-
expect(urls[0]).toContain("tag
|
|
1793
|
+
expect(urls[0]).toContain("tag=agent%2Fjob");
|
|
1793
1794
|
expect(urls[0]).toContain("include_content=true");
|
|
1794
1795
|
expect(jobs).toHaveLength(2);
|
|
1795
1796
|
// id = the slug from metadata.jobId; noteId = the vault note id.
|
|
@@ -1825,7 +1826,7 @@ describe("VaultTransport — scheduled-job notes (vault-native store)", () => {
|
|
|
1825
1826
|
expect(r.id).toBe("Channels/eng/jobs/m");
|
|
1826
1827
|
const body = JSON.parse(String(calls[0]!.init.body));
|
|
1827
1828
|
expect(body.path).toBe("Channels/eng/jobs/m");
|
|
1828
|
-
expect(body.tags).toEqual(["
|
|
1829
|
+
expect(body.tags).toEqual(["agent/job"]);
|
|
1829
1830
|
expect(body.metadata.enabled).toBe("true");
|
|
1830
1831
|
expect(body.metadata.jobId).toBe("m"); // slug persisted for stable display
|
|
1831
1832
|
// CONTRACT: routing key under `metadata.agent` ONLY — no `channel`.
|
package/src/transports/vault.ts
CHANGED
|
@@ -261,11 +261,11 @@ export class InboundClaimConflictError extends Error {
|
|
|
261
261
|
/** Parent tag (NEW, namespaced) — carried LITERALLY on every note WE write; query
|
|
262
262
|
* this + metadata.channel to see BOTH directions of a channel (the slash children
|
|
263
263
|
* are namespace, not inheritance). */
|
|
264
|
-
const AGENT_MESSAGE_TAG = "
|
|
264
|
+
const AGENT_MESSAGE_TAG = "agent/message";
|
|
265
265
|
/** Inbound child (NEW) — the vault trigger fires on this exact tag (never matches outbound → no loop). */
|
|
266
|
-
const AGENT_MESSAGE_INBOUND_TAG = "
|
|
266
|
+
const AGENT_MESSAGE_INBOUND_TAG = "agent/message/inbound";
|
|
267
267
|
/** Outbound child (NEW) — replies carry this; the trigger's exact-match predicate excludes it. */
|
|
268
|
-
const AGENT_MESSAGE_OUTBOUND_TAG = "
|
|
268
|
+
const AGENT_MESSAGE_OUTBOUND_TAG = "agent/message/outbound";
|
|
269
269
|
|
|
270
270
|
/** Metadata key carrying the channel-queue claim status (design 2026-06-18). */
|
|
271
271
|
const STATUS_META_KEY = "status";
|
|
@@ -369,7 +369,7 @@ function buildThreadSummaryBody(t: {
|
|
|
369
369
|
* (it always queries the exact leaf tag); it exists for the nice human rollup, per
|
|
370
370
|
* the design's namespacing decision.
|
|
371
371
|
*/
|
|
372
|
-
export const AGENT_ROOT_TAG = "
|
|
372
|
+
export const AGENT_ROOT_TAG = "agent";
|
|
373
373
|
|
|
374
374
|
/**
|
|
375
375
|
* Agent-definition tag — a vault-native agent IS a `#agent/definition` note (design
|
|
@@ -377,7 +377,7 @@ export const AGENT_ROOT_TAG = "#agent";
|
|
|
377
377
|
* METADATA is the config (name, backend, workspace, isolation, the def-vault binding).
|
|
378
378
|
* The module reads these notes from a def-vault and instantiates each as a live agent.
|
|
379
379
|
*/
|
|
380
|
-
export const AGENT_DEFINITION_TAG = "
|
|
380
|
+
export const AGENT_DEFINITION_TAG = "agent/definition";
|
|
381
381
|
|
|
382
382
|
/**
|
|
383
383
|
* Scheduled-job tag — the runner's vault-native job store (design
|
|
@@ -386,7 +386,7 @@ export const AGENT_DEFINITION_TAG = "#agent/definition";
|
|
|
386
386
|
* `#agent/message`. Introduced in Phase 2 as the flat `#agent-job`; moved into the
|
|
387
387
|
* `#agent/*` namespace (`#agent/job`) by the vault-native-agents work (Phase 4a).
|
|
388
388
|
*/
|
|
389
|
-
export const AGENT_JOB_TAG = "
|
|
389
|
+
export const AGENT_JOB_TAG = "agent/job";
|
|
390
390
|
/** Default path prefix under which job notes are written: `Channels/<ch>/jobs/<id>`. */
|
|
391
391
|
const JOB_PATH_PREFIX = "Channels";
|
|
392
392
|
|
|
@@ -413,7 +413,7 @@ const JOB_PATH_PREFIX = "Channels";
|
|
|
413
413
|
* The note carries `['#agent/thread']` EXACTLY — NOT a message tag, NOT the inbound
|
|
414
414
|
* child — so it can never wake a session (no loop).
|
|
415
415
|
*/
|
|
416
|
-
export const AGENT_THREAD_TAG = "
|
|
416
|
+
export const AGENT_THREAD_TAG = "agent/thread";
|
|
417
417
|
/** Default path prefix under which thread notes are written: `Threads/<ch>/<leaf>`. */
|
|
418
418
|
const THREAD_PATH_PREFIX = "Threads";
|
|
419
419
|
|
|
@@ -557,7 +557,7 @@ export const AGENT_VAULT_TRIGGER_TEMPLATE = {
|
|
|
557
557
|
name: "channel_inbound_<channel>", // hub substitutes the channel name
|
|
558
558
|
events: ["created"],
|
|
559
559
|
when: {
|
|
560
|
-
tags: ["
|
|
560
|
+
tags: ["agent/message/inbound"],
|
|
561
561
|
has_metadata: ["agent"],
|
|
562
562
|
missing_metadata: ["channel_inbound_rendered_at"],
|
|
563
563
|
},
|
|
@@ -581,7 +581,7 @@ export const AGENT_DEF_VAULT_TRIGGER_TEMPLATE = {
|
|
|
581
581
|
name: "agent_def_reload",
|
|
582
582
|
events: ["created", "updated", "deleted"],
|
|
583
583
|
when: {
|
|
584
|
-
tags: ["
|
|
584
|
+
tags: ["agent/definition"],
|
|
585
585
|
},
|
|
586
586
|
action: {
|
|
587
587
|
webhook: "<hub-origin>/agent/api/vault/agent-def", // hub fills origin + the auth.bearer
|
|
@@ -657,11 +657,11 @@ export class VaultTransport implements Transport {
|
|
|
657
657
|
* `decodeURIComponent`'d (parachute-vault `src/routes.ts` handleTags, the
|
|
658
658
|
* "Routes with tag name" block + `routing.ts` `apiPath.startsWith("/tags")`).
|
|
659
659
|
* Because the route matches a SINGLE path segment (`[^/]+`, no literal slash)
|
|
660
|
-
* and decodes it, the tag name — which contains
|
|
661
|
-
* (
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
660
|
+
* and decodes it, the tag name — which contains a `/`
|
|
661
|
+
* (`agent/message/inbound`) — must be `encodeURIComponent`'d so the `/` becomes
|
|
662
|
+
* `%2F`; the route then decodes that back to the literal name. A bare `/` in the
|
|
663
|
+
* URL would fail the `[^/]+` match → 404, silently dropping the declaration. The
|
|
664
|
+
* PUT body is `{ description?, parent_names? }`.
|
|
665
665
|
*
|
|
666
666
|
* Best-effort + non-fatal by contract: every failure is caught and `console.warn`'d,
|
|
667
667
|
* never thrown — the tag-both write floor is the fallback.
|
|
@@ -669,8 +669,8 @@ export class VaultTransport implements Transport {
|
|
|
669
669
|
async ensureSchema(): Promise<void> {
|
|
670
670
|
for (const entry of AGENT_VAULT_TAG_SCHEMA) {
|
|
671
671
|
try {
|
|
672
|
-
// Single-segment, percent-encoded name:
|
|
673
|
-
//
|
|
672
|
+
// Single-segment, percent-encoded name: `agent/message/inbound` →
|
|
673
|
+
// `agent%2Fmessage%2Finbound`. The vault decodes it back to the literal.
|
|
674
674
|
const url = `${this.vaultUrl}/vault/${this.vault}/api/tags/${encodeURIComponent(entry.name)}`;
|
|
675
675
|
const body: {
|
|
676
676
|
description?: string;
|
|
@@ -1099,7 +1099,7 @@ export class VaultTransport implements Transport {
|
|
|
1099
1099
|
* namespace, not query inheritance, so we never key off them here.
|
|
1100
1100
|
*
|
|
1101
1101
|
* GET <vaultUrl>/vault/<vault>/api/notes
|
|
1102
|
-
* ?tag
|
|
1102
|
+
* ?tag=agent%2Fmessage (the `/` MUST be percent-encoded)
|
|
1103
1103
|
* &include_content=true (we need the bodies)
|
|
1104
1104
|
* &limit=<n> (default 200)
|
|
1105
1105
|
*
|
|
@@ -1138,7 +1138,7 @@ export class VaultTransport implements Transport {
|
|
|
1138
1138
|
// empty transcript).
|
|
1139
1139
|
const fetchByTag = async (tag: string): Promise<RawNote[]> => {
|
|
1140
1140
|
const params = new URLSearchParams();
|
|
1141
|
-
params.set("tag", tag); // URLSearchParams encodes
|
|
1141
|
+
params.set("tag", tag); // URLSearchParams encodes `/` → `%2F`
|
|
1142
1142
|
params.set("include_content", "true");
|
|
1143
1143
|
params.set("limit", String(fetchLimit));
|
|
1144
1144
|
const url = `${this.vaultUrl}/vault/${this.vault}/api/notes?${params.toString()}`;
|
|
@@ -1380,7 +1380,7 @@ export class VaultTransport implements Transport {
|
|
|
1380
1380
|
// Overfetch (the tag query spans all channels) then keep this channel's items.
|
|
1381
1381
|
const fetchLimit = Math.min(Math.max(limit * 4, 500), 2000);
|
|
1382
1382
|
const params = new URLSearchParams();
|
|
1383
|
-
params.set("tag", AGENT_MESSAGE_INBOUND_TAG); // → %
|
|
1383
|
+
params.set("tag", AGENT_MESSAGE_INBOUND_TAG); // → agent%2Fmessage%2Finbound
|
|
1384
1384
|
params.set("include_content", "true");
|
|
1385
1385
|
params.set("limit", String(fetchLimit));
|
|
1386
1386
|
// NEWEST-first at the vault (default order_by is `updated_at`) so a hard cap drops
|
|
@@ -1508,7 +1508,7 @@ export class VaultTransport implements Transport {
|
|
|
1508
1508
|
|
|
1509
1509
|
/**
|
|
1510
1510
|
* List the scheduled-job notes in THIS channel's vault. Queries by the parent
|
|
1511
|
-
* `#agent/job` tag (URLSearchParams encodes
|
|
1511
|
+
* `#agent/job` tag (URLSearchParams encodes `/`→`%2F`) and returns ALL job
|
|
1512
1512
|
* notes in the vault — the CALLER filters by `metadata.channel` (same index-free
|
|
1513
1513
|
* pattern as loadTranscript; we don't assume a `channel` index exists). Throws
|
|
1514
1514
|
* on a non-ok vault response so the API surfaces a clear error rather than a
|
|
@@ -1517,7 +1517,7 @@ export class VaultTransport implements Transport {
|
|
|
1517
1517
|
async listJobNotes(opts?: { limit?: number }): Promise<JobNote[]> {
|
|
1518
1518
|
const limit = opts?.limit ?? 500;
|
|
1519
1519
|
const params = new URLSearchParams();
|
|
1520
|
-
params.set("tag", AGENT_JOB_TAG); // → %
|
|
1520
|
+
params.set("tag", AGENT_JOB_TAG); // → agent%2Fjob
|
|
1521
1521
|
params.set("include_content", "true");
|
|
1522
1522
|
params.set("limit", String(limit));
|
|
1523
1523
|
const url = `${this.vaultUrl}/vault/${this.vault}/api/notes?${params.toString()}`;
|