@cohortapp/agent-sdk 2.9.0 → 2.10.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/lib/channels/inbox-item.mjs +4 -0
- package/lib/comms/send-gate.mjs +23 -1
- package/lib/comms/send-gate.test.mjs +24 -0
- package/lib/model-router/economics.mjs +53 -1
- package/lib/model-router/economics.test.mjs +76 -0
- package/lib/model-router/resolve.mjs +57 -4
- package/lib/model-router.mjs +95 -8
- package/lib/model-router.test.mjs +305 -5
- package/lib/org/inbound/project.mjs +9 -5
- package/lib/org/messaging.mjs +6 -1
- package/lib/org/param-contract.mjs +24 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +78 -3
- package/lib/org/protocol.test.mjs +14 -2
- package/lib/org/work-ledger.mjs +5 -0
- package/lib/org/work-ledger.test.mjs +36 -0
- package/package.json +1 -1
- package/scripts/ci/conformance-org-api.mjs +18 -0
- package/scripts/ci/conformance-org-api.test.mjs +9 -1
- package/scripts/daemon/agent-daemon.mjs +20 -1
- package/scripts/daemon/assurance.mjs +20 -19
- package/scripts/daemon/assurance.test.mjs +40 -0
- package/scripts/daemon/board-mirror.mjs +92 -93
- package/scripts/daemon/board-mirror.test.mjs +124 -66
- package/scripts/daemon/deliver.mjs +457 -33
- package/scripts/daemon/deliver.test.mjs +564 -0
- package/scripts/daemon/responder-history.test.mjs +18 -2
- package/scripts/daemon/responder.mjs +7 -1
|
@@ -1,34 +1,56 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
boardItemId,
|
|
6
|
-
boardTitle,
|
|
7
|
-
openBoardItem,
|
|
8
|
-
closeBoardItem,
|
|
9
|
-
} from "./board-mirror.mjs";
|
|
4
|
+
import { boardPriority, boardTitle, closeStageFor, closeBoardItem } from "./board-mirror.mjs";
|
|
10
5
|
|
|
11
6
|
// ---------------------------------------------------------------------------
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
7
|
+
// This file used to open AND close a board row of its own through
|
|
8
|
+
// `board.create` + `board.claim` + `board.complete`. That was a second
|
|
9
|
+
// implementation of a lifecycle hq and this repo already share exactly one of
|
|
10
|
+
// (`lib/org/work-ledger.mjs` → `board.track` → hq `server/work/ledger.ts`), and
|
|
11
|
+
// the daemon called BOTH on the same item — two cards per ask on one board, one
|
|
12
|
+
// of them carrying no requester, no comments and no provenance. Worse,
|
|
13
|
+
// `board.create` gives a channel-less row ORG-WIDE visibility and the mirror
|
|
14
|
+
// sent no channel for Slack/email/voice, so a private ask's topic was published
|
|
15
|
+
// to the whole workspace.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
// What survives is the CLOSE hook, because `closeObligation` is the funnel every
|
|
18
|
+
// terminal path goes through and only two of them reach the daemon's onClose.
|
|
19
|
+
// These tests pin that it goes through the SHARED ledger, that it never
|
|
20
|
+
// overstates, and that it still fails open.
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
|
|
21
|
-
const CFG = { base: "https://hq.example", token: "t" };
|
|
23
|
+
const CFG = { org: { cohort: { enabled: true, base: "https://hq.example", token: "t" } } };
|
|
22
24
|
const okCfg = { loadOrgConfig: () => CFG };
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
/** A recording ledger seam. Answers the way `work-ledger.mjs` does. */
|
|
27
|
+
function ledger(over = {}) {
|
|
28
|
+
const seen = { calls: [] };
|
|
29
|
+
return {
|
|
30
|
+
seen,
|
|
31
|
+
deps: {
|
|
32
|
+
...okCfg,
|
|
33
|
+
recordWorkStep: async (a) => {
|
|
34
|
+
seen.calls.push(a);
|
|
35
|
+
if (over.impl) return over.impl(a);
|
|
36
|
+
return { tracked: true, reason: "ok", taskId: "task_1", col: a.stage === "done" ? "done" : "blocked" };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const REC = {
|
|
43
|
+
key: "cohort:dm:C1:9",
|
|
44
|
+
state: "answered",
|
|
45
|
+
summary: "compiling the regulatory filing calendar",
|
|
46
|
+
item: {
|
|
47
|
+
id: "cohort-msg_1",
|
|
48
|
+
raw_ref: "cohort:messaging:msg_1:1",
|
|
49
|
+
service: "cohort",
|
|
50
|
+
channel_id: "chan_1",
|
|
51
|
+
content: "can you compile the regulatory filing calendar",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
32
54
|
|
|
33
55
|
test("the title is the topic the acknowledgement already speaks aloud", () => {
|
|
34
56
|
assert.equal(
|
|
@@ -41,67 +63,103 @@ test("the title is the topic the acknowledgement already speaks aloud", () => {
|
|
|
41
63
|
assert.ok(boardTitle({ summary: "x".repeat(400) }).length <= 120);
|
|
42
64
|
});
|
|
43
65
|
|
|
44
|
-
test("
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
test("the classifier's priority words are mapped onto hq's P-scale, which is all it accepts", () => {
|
|
67
|
+
// hq validates priority against z.enum(["P0".."P4"]); "critical" 400s the
|
|
68
|
+
// whole call, so the obligation vocabulary has to be translated here.
|
|
69
|
+
assert.equal(boardPriority("critical"), "P0");
|
|
70
|
+
assert.equal(boardPriority("high"), "P1");
|
|
71
|
+
assert.equal(boardPriority("normal"), "P2");
|
|
72
|
+
assert.equal(boardPriority("ignore"), "P3");
|
|
73
|
+
assert.equal(boardPriority("P1"), "P1");
|
|
74
|
+
// No mapping → omit, and let hq's own default (P2) answer.
|
|
75
|
+
assert.equal(boardPriority("whatever"), undefined);
|
|
76
|
+
assert.equal(boardPriority(undefined), undefined);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("only `answered` closes the row as done — every other verdict blocks it", () => {
|
|
80
|
+
assert.equal(closeStageFor("answered"), "done");
|
|
81
|
+
assert.equal(closeStageFor(undefined), "done");
|
|
82
|
+
for (const bad of ["failed", "undeliverable", "stale", "anything-else"]) {
|
|
83
|
+
assert.equal(closeStageFor(bad), "failed", `${bad} must not read as delivered`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("the close goes through the SHARED ledger, not a second board writer", async () => {
|
|
88
|
+
const { seen, deps } = ledger();
|
|
89
|
+
const r = await closeBoardItem({ rec: REC, agentRoot: "/x", deps });
|
|
90
|
+
|
|
51
91
|
assert.equal(r.mirrored, true);
|
|
52
|
-
assert.equal(
|
|
53
|
-
assert.equal(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
assert.equal(r.taskId, "task_1");
|
|
93
|
+
assert.equal(seen.calls.length, 1);
|
|
94
|
+
const call = seen.calls[0];
|
|
95
|
+
// The ledger identifies the ask from the ITEM (service + channel + message),
|
|
96
|
+
// and derives the dedupe key server-side — which is what makes this land on
|
|
97
|
+
// the SAME row the daemon's `accepted`/`working` steps opened, rather than a
|
|
98
|
+
// second card beside it.
|
|
99
|
+
assert.equal(call.item, REC.item);
|
|
100
|
+
assert.equal(call.stage, "done");
|
|
101
|
+
assert.equal(call.deferred, true);
|
|
102
|
+
assert.equal(call.cfg, CFG);
|
|
103
|
+
assert.match(call.note, /Answered in the conversation/);
|
|
57
104
|
});
|
|
58
105
|
|
|
59
|
-
test("a
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
assert.
|
|
65
|
-
assert.equal(r.reason, "no-org-credential");
|
|
106
|
+
test("a failed obligation lands the row blocked, and says why", async () => {
|
|
107
|
+
const { seen, deps } = ledger();
|
|
108
|
+
await closeBoardItem({ rec: { ...REC, state: "failed", closeNote: "exit 1" }, deps });
|
|
109
|
+
assert.equal(seen.calls[0].stage, "failed");
|
|
110
|
+
assert.match(seen.calls[0].note, /exit 1/);
|
|
111
|
+
assert.match(seen.calls[0].note, /blocked/);
|
|
66
112
|
});
|
|
67
113
|
|
|
68
|
-
test("
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
assert.equal(r.mirrored, false);
|
|
74
|
-
assert.equal(r.reason, "refused");
|
|
114
|
+
test("an undeliverable obligation says so, and still does not read as delivered", async () => {
|
|
115
|
+
const { seen, deps } = ledger();
|
|
116
|
+
await closeBoardItem({ rec: { ...REC, state: "undeliverable" }, deps });
|
|
117
|
+
assert.equal(seen.calls[0].stage, "failed");
|
|
118
|
+
assert.match(seen.calls[0].note, /no way to deliver/);
|
|
75
119
|
});
|
|
76
120
|
|
|
77
|
-
test("
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
121
|
+
test("an explicit outcome overrides the record's own state", async () => {
|
|
122
|
+
const { seen, deps } = ledger();
|
|
123
|
+
await closeBoardItem({ rec: REC, outcome: "failed", deps });
|
|
124
|
+
assert.equal(seen.calls[0].stage, "failed");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a non-Cohort obligation writes NO row — a private ask is not published org-wide", async () => {
|
|
128
|
+
// THE LEAK THIS REPLACED: `board.create` with no channelId produces a row
|
|
129
|
+
// every member of the org can read, titled after the ask. The daemon polls
|
|
130
|
+
// Slack, two Gmail accounts, telegram, whatsapp and voice. The shared gate
|
|
131
|
+
// refuses all of them (`not-a-cohort-channel`) — this test pins that the
|
|
132
|
+
// refusal reaches us as a benign no-op rather than being worked around.
|
|
133
|
+
const { seen, deps } = ledger({ impl: async () => ({ tracked: false, reason: "not-a-cohort-channel" }) });
|
|
134
|
+
const r = await closeBoardItem({
|
|
135
|
+
rec: { ...REC, item: { ...REC.item, service: "slack", channel_id: "D0123" } },
|
|
136
|
+
deps,
|
|
81
137
|
});
|
|
82
138
|
assert.equal(r.mirrored, false);
|
|
83
|
-
assert.equal(r.reason, "
|
|
139
|
+
assert.equal(r.reason, "not-a-cohort-channel");
|
|
140
|
+
assert.equal(seen.calls.length, 1, "the decision is the ledger's, not a rule re-implemented here");
|
|
84
141
|
});
|
|
85
142
|
|
|
86
|
-
test("
|
|
87
|
-
let sent = null;
|
|
143
|
+
test("an obligation with no item snapshot is a no-op, not a throw", async () => {
|
|
88
144
|
const r = await closeBoardItem({
|
|
89
145
|
rec: { key: "k1" },
|
|
90
|
-
|
|
91
|
-
deps: { ...okCfg, completeItem: async (p) => { sent = p; return { ok: true }; } },
|
|
146
|
+
deps: { ...okCfg, recordWorkStep: async () => { throw new Error("must not be called"); } },
|
|
92
147
|
});
|
|
93
|
-
assert.equal(r.mirrored,
|
|
94
|
-
assert.equal(
|
|
95
|
-
assert.equal(sent.itemId, boardItemId("k1"));
|
|
148
|
+
assert.equal(r.mirrored, false);
|
|
149
|
+
assert.equal(r.reason, "no-item");
|
|
96
150
|
});
|
|
97
151
|
|
|
98
|
-
test("close is
|
|
99
|
-
for (const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
152
|
+
test("the close is fail-open: a refusal, a throw and a missing credential all resolve", async () => {
|
|
153
|
+
for (const [label, impl] of [
|
|
154
|
+
["refused", async () => ({ tracked: false, reason: "FORBIDDEN_SCOPE: no" })],
|
|
155
|
+
["threw", async () => { throw new Error("socket hang up"); }],
|
|
156
|
+
["disabled", async () => ({ tracked: false, reason: "org-disabled" })],
|
|
103
157
|
]) {
|
|
104
|
-
const r = await closeBoardItem({
|
|
105
|
-
|
|
158
|
+
const r = await closeBoardItem({
|
|
159
|
+
rec: REC,
|
|
160
|
+
deps: { ...okCfg, recordWorkStep: impl },
|
|
161
|
+
});
|
|
162
|
+
assert.equal(r.mirrored, false, label);
|
|
163
|
+
assert.ok(r.reason, `${label} must report why`);
|
|
106
164
|
}
|
|
107
165
|
});
|