@cohortapp/agent-sdk 2.3.2 → 2.4.1
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/framework-features.json +30 -0
- package/lib/backlog.mjs +136 -0
- package/lib/cadences.mjs +63 -2
- package/lib/cadences.test.mjs +105 -0
- package/lib/capability/inventory.mjs +542 -0
- package/lib/capability/inventory.test.mjs +232 -0
- package/lib/capability/probe.mjs +255 -0
- package/lib/channels/contract.mjs +37 -1
- package/lib/channels/contract.test.mjs +25 -1
- package/lib/claude-bin.mjs +37 -3
- package/lib/claude-bin.test.mjs +42 -8
- package/lib/execution/disposition.mjs +501 -0
- package/lib/execution/disposition.test.mjs +482 -0
- package/lib/execution/drive.mjs +352 -0
- package/lib/execution/drive.test.mjs +270 -0
- package/lib/execution/effects.mjs +340 -0
- package/lib/execution/effects.test.mjs +193 -0
- package/lib/execution/index.mjs +152 -0
- package/lib/execution/intake.mjs +581 -0
- package/lib/execution/intake.test.mjs +343 -0
- package/lib/execution/journal.mjs +374 -0
- package/lib/execution/journal.test.mjs +261 -0
- package/lib/execution/match.mjs +331 -0
- package/lib/execution/match.test.mjs +235 -0
- package/lib/execution/pipeline.mjs +341 -0
- package/lib/execution/pipeline.test.mjs +389 -0
- package/lib/execution/route.mjs +332 -0
- package/lib/execution/route.test.mjs +186 -0
- package/lib/execution/surface-policy.mjs +446 -0
- package/lib/execution/surface-policy.test.mjs +162 -0
- package/lib/goals/admission.mjs +209 -0
- package/lib/goals/admission.test.mjs +139 -0
- package/lib/goals/classify.mjs +206 -0
- package/lib/goals/classify.test.mjs +109 -0
- package/lib/goals/collaborate.mjs +415 -0
- package/lib/goals/collaborate.test.mjs +324 -0
- package/lib/goals/gaps.mjs +111 -0
- package/lib/goals/gaps.test.mjs +284 -0
- package/lib/goals/loop.mjs +537 -0
- package/lib/goals/loop.test.mjs +719 -0
- package/lib/identity/persona.mjs +247 -0
- package/lib/identity/persona.test.mjs +117 -0
- package/lib/kpi.mjs +469 -0
- package/lib/kpi.test.mjs +244 -0
- package/lib/mandate/audit.mjs +168 -0
- package/lib/mandate/audit.test.mjs +195 -0
- package/lib/mandate/cache.mjs +162 -0
- package/lib/mandate/derive.mjs +317 -0
- package/lib/mandate/derive.test.mjs +224 -0
- package/lib/mandate/model.mjs +352 -0
- package/lib/mandate/model.test.mjs +145 -0
- package/lib/mandate/refresh.mjs +187 -0
- package/lib/mandate/refresh.test.mjs +293 -0
- package/lib/mcp/server.test.mjs +4 -4
- package/lib/org/approvals.mjs +14 -2
- package/lib/org/client.mjs +58 -22
- package/lib/org/client.test.mjs +3 -1
- package/lib/org/inbound/directedness.mjs +720 -0
- package/lib/org/inbound/directedness.test.mjs +543 -0
- package/lib/org/inbound/facts.mjs +501 -0
- package/lib/org/inbound/facts.test.mjs +375 -0
- package/lib/org/inbound/hydrate.mjs +535 -0
- package/lib/org/inbound/hydrate.test.mjs +326 -0
- package/lib/org/inbound/index.mjs +233 -0
- package/lib/org/inbound/index.test.mjs +324 -0
- package/lib/org/inbound/io.mjs +141 -0
- package/lib/org/inbound/project.mjs +201 -0
- package/lib/org/inbound/project.test.mjs +287 -0
- package/lib/org/inbound/surfaces.mjs +257 -0
- package/lib/org/knowledge.mjs +10 -1
- package/lib/org/knowledge.test.mjs +8 -1
- package/lib/org/leases.mjs +5 -0
- package/lib/org/mesh.mjs +17 -2
- package/lib/org/messaging.mjs +40 -4
- package/lib/org/messaging.test.mjs +40 -0
- package/lib/org/param-contract.mjs +694 -0
- package/lib/org/param-contract.test.mjs +451 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +8 -0
- package/lib/org/protocol.test.mjs +5 -1
- package/lib/org/push.mjs +1025 -0
- package/lib/org/push.test.mjs +690 -0
- package/lib/org/tool-surface.mjs +138 -38
- package/lib/org/tool-surface.test.mjs +13 -8
- package/lib/org/typing.mjs +341 -0
- package/lib/org/typing.test.mjs +291 -0
- package/lib/plan/compile.mjs +510 -0
- package/lib/plan/compile.test.mjs +286 -0
- package/lib/plan/emit.mjs +256 -0
- package/lib/plan/emit.test.mjs +246 -0
- package/lib/plan/explain.mjs +226 -0
- package/lib/plan/explain.test.mjs +188 -0
- package/lib/plan/schema.mjs +140 -0
- package/lib/resource-governor.mjs +47 -1
- package/lib/resource-governor.test.mjs +21 -1
- package/lib/setup/enroll-from-cohort.mjs +105 -17
- package/lib/setup/enroll-from-cohort.test.mjs +68 -1
- package/lib/setup/sections/identity.mjs +15 -4
- package/lib/setup/sections/identity.test.mjs +94 -0
- package/lib/setup/sections/inventory.mjs +178 -0
- package/lib/setup/sections/inventory.test.mjs +198 -0
- package/lib/setup/sections/mandate.mjs +392 -0
- package/lib/setup/sections/mandate.test.mjs +373 -0
- package/lib/setup/sections/subagents.mjs +427 -0
- package/lib/setup/sections/subagents.test.mjs +429 -0
- package/lib/setup/sections/verify.mjs +121 -0
- package/lib/setup/sections/verify.test.mjs +175 -0
- package/lib/setup/sot.mjs +2 -0
- package/lib/subagents/cli.mjs +463 -0
- package/lib/subagents/cli.test.mjs +389 -0
- package/lib/subagents/client.mjs +373 -0
- package/lib/subagents/client.test.mjs +309 -0
- package/lib/subagents/gap.mjs +268 -0
- package/lib/subagents/gap.test.mjs +234 -0
- package/lib/subagents/lock.mjs +296 -0
- package/lib/subagents/lock.test.mjs +248 -0
- package/lib/subagents/manifest.mjs +224 -0
- package/lib/subagents/manifest.test.mjs +175 -0
- package/lib/subagents/refs.mjs +274 -0
- package/lib/subagents/refs.test.mjs +204 -0
- package/lib/subagents/resolve.mjs +455 -0
- package/lib/subagents/resolve.test.mjs +422 -0
- package/lib/subagents/schema.mjs +467 -0
- package/lib/subagents/schema.test.mjs +306 -0
- package/package.json +8 -3
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
- package/policies/ai-disclosure.yaml +42 -2
- package/scaffold/CLAUDE.md +16 -2
- package/schedules/triggers/goal-steward.md +79 -0
- package/scripts/ci/conformance-org-api.mjs +792 -0
- package/scripts/ci/conformance-org-api.test.mjs +417 -0
- package/scripts/daemon/agent-daemon.mjs +36 -4
- package/scripts/daemon/cadence-handlers.mjs +145 -1
- package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
- package/scripts/daemon/inbox-deferral.mjs +45 -2
- package/scripts/daemon/inbox-deferral.test.mjs +56 -0
- package/scripts/daemon/inbox-wake.mjs +282 -0
- package/scripts/daemon/inbox-wake.test.mjs +199 -0
- package/scripts/daemon/prompt-builder.mjs +41 -1
- package/scripts/daemon/typing-registry.mjs +55 -2
- package/scripts/daemon/typing-registry.test.mjs +25 -0
- package/scripts/local-triggers/generate-plists.test.mjs +5 -5
- package/scripts/setup/gen-subagent-manifest.mjs +95 -0
- package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
- package/scripts/setup/generate-plan.mjs +108 -0
- package/scripts/setup/init-capability-manifest.mjs +70 -0
- package/scripts/setup/init-skill-marketplace.mjs +155 -0
- package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/execution/pipeline.mjs — the ONE call the inbox pipeline makes.
|
|
3
|
+
*
|
|
4
|
+
* `index.handleInboundEvent` decides and acts on one event, but it demands a
|
|
5
|
+
* `{candidate, verdict}` pair and an already-matched obligation. Nothing that
|
|
6
|
+
* actually holds inbound events has either: the daemon has flattened inbox items
|
|
7
|
+
* on disk, the channel adapters have their own item shape, the diagnostics
|
|
8
|
+
* modules have alert records, and the compiled plan sits unread in
|
|
9
|
+
* `config/plan.yaml`. This module closes that distance so a caller can hand over
|
|
10
|
+
* whatever it happens to have:
|
|
11
|
+
*
|
|
12
|
+
* import { processInbound } from "../../lib/execution/pipeline.mjs";
|
|
13
|
+
*
|
|
14
|
+
* const { results, stats } = await processInbound({
|
|
15
|
+
* items, // inbox items | MessageEvents | raw rows | alerts
|
|
16
|
+
* me: cfg.org.cohort.memberId,
|
|
17
|
+
* agentRoot,
|
|
18
|
+
* runtime, // emergencyStop / directive / staleness / budget
|
|
19
|
+
* effects: defaultEffects({ agentRoot, react, me }),
|
|
20
|
+
* log,
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* What it adds over calling `handleInboundEvent` in a loop, all of which is
|
|
24
|
+
* wrong if a caller re-implements it:
|
|
25
|
+
*
|
|
26
|
+
* **One history load per batch.** `loadHistory` scans the journal tail; doing
|
|
27
|
+
* it per item turns a 200-event sweep into 200 file reads.
|
|
28
|
+
*
|
|
29
|
+
* **In-batch dedupe, ping-pong and flood accounting.** The on-disk history is
|
|
30
|
+
* a SNAPSHOT taken before the batch. Without an overlay, an event delivered
|
|
31
|
+
* twice inside one sweep (the SSE push and the poll backstop converge — that
|
|
32
|
+
* convergence is the design, per §6.1) passes the dedupe guard twice, and a
|
|
33
|
+
* burst from one actor never trips the flood limit. The overlay makes the
|
|
34
|
+
* guards behave as if each event had been processed alone, in order.
|
|
35
|
+
*
|
|
36
|
+
* **Obligation binding.** The plan is read ONCE and every event is matched
|
|
37
|
+
* against its REACT obligations, so `decide` gets the argument that lifts it
|
|
38
|
+
* off the rung-1 cap — and an uncovered event produces the proposed obligation
|
|
39
|
+
* §6.1 asks for, not just a complaint.
|
|
40
|
+
*
|
|
41
|
+
* **Per-item isolation.** One malformed row cannot take a sweep down. It is
|
|
42
|
+
* caught, logged as an error (never swallowed), counted, and the batch
|
|
43
|
+
* continues.
|
|
44
|
+
*
|
|
45
|
+
* @module lib/execution/pipeline
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
"use strict";
|
|
49
|
+
|
|
50
|
+
import { handleInboundEvent } from "./index.mjs";
|
|
51
|
+
import { intake } from "./intake.mjs";
|
|
52
|
+
import { loadHistory, threadKey, dedupeKey } from "./journal.mjs";
|
|
53
|
+
import { matchObligation, proposeReact, loadReactObligations } from "./match.mjs";
|
|
54
|
+
import { tierOf, willAct } from "./disposition.mjs";
|
|
55
|
+
|
|
56
|
+
/** Blank-safe string. */
|
|
57
|
+
function s(v) {
|
|
58
|
+
return v == null ? "" : String(v);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wrap a history snapshot so decisions taken EARLIER IN THIS BATCH count.
|
|
63
|
+
*
|
|
64
|
+
* The journal is appended by `drive` as it goes, but `loadHistory` already read
|
|
65
|
+
* its tail; re-reading per item would be correct and slow. This overlay is the
|
|
66
|
+
* cheap half: it tracks the same three facts the guards ask for and answers from
|
|
67
|
+
* the union of "on disk before the batch" and "decided during it".
|
|
68
|
+
*
|
|
69
|
+
* @param {object} base a `loadHistory` view
|
|
70
|
+
* @returns {object} the same interface, plus `note(decision, spoke)`
|
|
71
|
+
*/
|
|
72
|
+
export function batchHistory(base) {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const chains = new Map();
|
|
75
|
+
const actors = new Map();
|
|
76
|
+
// Threads a human spoke in DURING this batch. The on-disk chain count for them
|
|
77
|
+
// is stale by definition, so it is masked rather than added to — otherwise a
|
|
78
|
+
// human re-entering a conversation mid-sweep would not unblock the agent until
|
|
79
|
+
// the next sweep, which is the exact bug the human-turn reset exists to stop.
|
|
80
|
+
const reset = new Set();
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
seen: (key) => seen.has(s(key)) || (base ? base.seen(key) : false),
|
|
84
|
+
chainDepth: (tk) =>
|
|
85
|
+
(chains.get(s(tk)) || 0) + (reset.has(s(tk)) || !base ? 0 : base.chainDepth(tk)),
|
|
86
|
+
actorActivity: (a) => (actors.get(s(a)) || 0) + (base ? base.actorActivity(a) : 0),
|
|
87
|
+
failures: (k) => (base ? base.failures(k) : 0),
|
|
88
|
+
rows: base ? base.rows : [],
|
|
89
|
+
degraded: base ? !!base.degraded : true,
|
|
90
|
+
degradedReason: base ? base.degradedReason : "no history supplied",
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A human spoke in this thread. Called BEFORE the decision that follows it,
|
|
94
|
+
* mirroring `handleInboundEvent`'s ordering.
|
|
95
|
+
*/
|
|
96
|
+
humanTurn(tk) {
|
|
97
|
+
const k = s(tk);
|
|
98
|
+
if (!k) return;
|
|
99
|
+
reset.add(k);
|
|
100
|
+
chains.set(k, 0);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** Fold one processed event back into the overlay. */
|
|
104
|
+
note(decision, spoke) {
|
|
105
|
+
if (!decision) return;
|
|
106
|
+
if (decision.key) seen.add(s(decision.key));
|
|
107
|
+
if (decision.actor) actors.set(s(decision.actor), (actors.get(s(decision.actor)) || 0) + 1);
|
|
108
|
+
// Only a turn where the agent actually SPOKE deepens a conversation —
|
|
109
|
+
// identical to the rule `loadHistory` applies to journal rows, and for the
|
|
110
|
+
// same reason: three ignores in a row must not gag the fourth message.
|
|
111
|
+
if (spoke === true && decision.thread) {
|
|
112
|
+
chains.set(s(decision.thread), (chains.get(s(decision.thread)) || 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Decide and act on ONE inbound artefact of any shape.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} o
|
|
122
|
+
* @param {object} o.item inbox item | MessageEvent | raw row | alert | {candidate,verdict}
|
|
123
|
+
* @param {string} [o.me]
|
|
124
|
+
* @param {string} [o.agentRoot]
|
|
125
|
+
* @param {object} [o.runtime]
|
|
126
|
+
* @param {object} [o.effects]
|
|
127
|
+
* @param {object} [o.history] a `batchHistory` view (made here if absent)
|
|
128
|
+
* @param {object[]} [o.obligations] REACT obligations from the compiled plan
|
|
129
|
+
* @param {Array} [o.manifest] capability manifest
|
|
130
|
+
* @param {object} [o.facts] delegation/authority facts for this item
|
|
131
|
+
* @param {boolean} [o.humanTurn]
|
|
132
|
+
* @param {number} [o.nowMs]
|
|
133
|
+
* @param {Function} [o.log]
|
|
134
|
+
* @param {string} [o.traceId]
|
|
135
|
+
* @returns {Promise<object>} { ok, origin, decision, effect, ref, spoke, degraded, error, obligationKey, proposed }
|
|
136
|
+
*/
|
|
137
|
+
export async function processOne(o = {}) {
|
|
138
|
+
const log = typeof o.log === "function" ? o.log : () => {};
|
|
139
|
+
const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
|
|
140
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
141
|
+
|
|
142
|
+
const norm = intake(o.item, { me: o.me, now: nowIso, facts: o.facts, opts: o.intakeOpts });
|
|
143
|
+
for (const d of norm.degraded) log("warn", `[execution] intake degraded (${norm.origin}): ${d}`);
|
|
144
|
+
|
|
145
|
+
const tier = norm.verdict && norm.verdict.directed ? tierOf(norm.verdict.reason) : null;
|
|
146
|
+
|
|
147
|
+
// Bind the event to the plan. An unbindable event is not an error — it is the
|
|
148
|
+
// signal that the plan has a hole, and it is reported as one below.
|
|
149
|
+
const match = matchObligation({
|
|
150
|
+
candidate: norm.candidate,
|
|
151
|
+
verdict: norm.verdict,
|
|
152
|
+
plan: { obligations: Array.isArray(o.obligations) ? o.obligations : [] },
|
|
153
|
+
tier,
|
|
154
|
+
});
|
|
155
|
+
for (const line of match.why) log("debug", `[execution] match: ${line}`);
|
|
156
|
+
|
|
157
|
+
const history = o.history || batchHistory(loadHistory({ agentRoot: o.agentRoot, nowMs }));
|
|
158
|
+
if (o.humanTurn === true && norm.candidate && typeof history.humanTurn === "function") {
|
|
159
|
+
history.humanTurn(threadKey(norm.candidate));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const res = await handleInboundEvent({
|
|
163
|
+
candidate: norm.candidate,
|
|
164
|
+
verdict: norm.verdict,
|
|
165
|
+
me: o.me,
|
|
166
|
+
history,
|
|
167
|
+
runtime: o.runtime,
|
|
168
|
+
obligation: match.obligation,
|
|
169
|
+
need: o.need,
|
|
170
|
+
manifest: o.manifest,
|
|
171
|
+
facts: o.facts,
|
|
172
|
+
actorFloodLimit: o.actorFloodLimit,
|
|
173
|
+
effects: o.effects,
|
|
174
|
+
agentRoot: o.agentRoot,
|
|
175
|
+
nowMs,
|
|
176
|
+
traceId: o.traceId,
|
|
177
|
+
log,
|
|
178
|
+
humanTurn: o.humanTurn === true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Fold the intake's own degradations onto the decision so the journal row and
|
|
182
|
+
// the caller see one list rather than two.
|
|
183
|
+
if (res.decision && Array.isArray(res.decision.degraded) && norm.degraded.length) {
|
|
184
|
+
res.decision.degraded = res.decision.degraded.concat(norm.degraded.map((d) => `intake:${d}`));
|
|
185
|
+
}
|
|
186
|
+
history.note(res.decision, res.spoke);
|
|
187
|
+
|
|
188
|
+
// §6.1's other half: an event that was unmistakably mine and matched nothing
|
|
189
|
+
// proposes the obligation that WOULD have covered it.
|
|
190
|
+
let proposed = null;
|
|
191
|
+
if (!match.obligation && willAct(res.decision) && !match.suspended.length) {
|
|
192
|
+
const p = proposeReact({ candidate: norm.candidate, verdict: norm.verdict, tier });
|
|
193
|
+
if (p.obligation) {
|
|
194
|
+
proposed = p.obligation.key;
|
|
195
|
+
const fn = o.effects && typeof o.effects.proposeObligation === "function" ? o.effects.proposeObligation : null;
|
|
196
|
+
if (fn) {
|
|
197
|
+
try {
|
|
198
|
+
await fn({ obligation: p.obligation, decision: res.decision, candidate: norm.candidate });
|
|
199
|
+
log("info", `[execution] ${p.why}`);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
log("error", `[execution] could not record the proposed obligation ${p.obligation.key}: ${err && err.message ? err.message : String(err)}`);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
log("warn", `[execution] no proposeObligation effect wired — ${p.obligation.key} was derived but not recorded`);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
log("warn", `[execution] uncovered event produced no proposable obligation: ${p.why}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
...res,
|
|
213
|
+
origin: norm.origin,
|
|
214
|
+
intakeWhy: norm.why,
|
|
215
|
+
tier,
|
|
216
|
+
obligationKey: match.obligation ? match.obligation.key || null : null,
|
|
217
|
+
suspendedObligations: match.suspended.map((x) => x.key),
|
|
218
|
+
proposed,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Decide and act on a BATCH — the shape a sweep, an SSE drain or a cadence tick
|
|
224
|
+
* actually has.
|
|
225
|
+
*
|
|
226
|
+
* Items are processed IN ORDER. Order matters: the in-batch overlay means an
|
|
227
|
+
* earlier event's decision constrains a later one (dedupe, ping-pong, flood), so
|
|
228
|
+
* shuffling the batch would change the outcome. Callers should preserve ledger
|
|
229
|
+
* order, which every producer in the tree already does.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} o as {@link processOne}, with `items` instead of `item`, plus:
|
|
232
|
+
* @param {object[]} o.items
|
|
233
|
+
* @param {object[]} [o.obligations] pre-loaded; read from config/plan.yaml when absent
|
|
234
|
+
* @param {boolean} [o.loadPlan] set false to skip the plan read entirely
|
|
235
|
+
* @param {Function} [o.factsFor] (norm, item) => facts, per item
|
|
236
|
+
* @param {Function} [o.isHumanTurn] (item) => boolean
|
|
237
|
+
* @returns {Promise<{results:object[], stats:object}>}
|
|
238
|
+
*/
|
|
239
|
+
export async function processInbound(o = {}) {
|
|
240
|
+
const log = typeof o.log === "function" ? o.log : () => {};
|
|
241
|
+
const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
|
|
242
|
+
const items = Array.isArray(o.items) ? o.items : [];
|
|
243
|
+
|
|
244
|
+
const stats = {
|
|
245
|
+
total: items.length,
|
|
246
|
+
processed: 0,
|
|
247
|
+
acted: 0,
|
|
248
|
+
ignored: 0,
|
|
249
|
+
failed: 0,
|
|
250
|
+
byDisposition: {},
|
|
251
|
+
bySurface: {},
|
|
252
|
+
byReason: {},
|
|
253
|
+
uncovered: 0,
|
|
254
|
+
proposed: [],
|
|
255
|
+
degraded: [],
|
|
256
|
+
};
|
|
257
|
+
if (items.length === 0) return { results: [], stats };
|
|
258
|
+
|
|
259
|
+
// ── the plan, read once ──────────────────────────────────────────────────
|
|
260
|
+
let obligations = Array.isArray(o.obligations) ? o.obligations : null;
|
|
261
|
+
if (!obligations && o.loadPlan !== false) {
|
|
262
|
+
const loaded = await loadReactObligations(o.agentRoot);
|
|
263
|
+
obligations = loaded.obligations;
|
|
264
|
+
if (loaded.degraded) {
|
|
265
|
+
// NEVER SILENT: with no plan every event reads as uncovered and the whole
|
|
266
|
+
// reactive lane is pinned at rung ≤1. That is a legitimate state for an
|
|
267
|
+
// un-adopted agent, but the operator has to be able to see it.
|
|
268
|
+
stats.degraded.push(`plan_unavailable:${loaded.reason}`);
|
|
269
|
+
log("warn", `[execution] no compiled REACT obligations (${loaded.reason}) — every event will route at rung ≤1`);
|
|
270
|
+
} else if (obligations.length === 0) {
|
|
271
|
+
stats.degraded.push("plan_has_no_react_obligations");
|
|
272
|
+
log("warn", "[execution] config/plan.yaml has no REACT obligations — the reactive lane is uncovered by construction");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!obligations) obligations = [];
|
|
276
|
+
|
|
277
|
+
// ── the journal tail, read once, then kept live for the batch ────────────
|
|
278
|
+
const base = o.history || loadHistory({ agentRoot: o.agentRoot, nowMs });
|
|
279
|
+
if (base && base.degraded) {
|
|
280
|
+
stats.degraded.push(`history_degraded:${base.degradedReason || "unknown"}`);
|
|
281
|
+
log("warn", `[execution] journal history degraded (${base.degradedReason || "unknown"}) — dedupe/ping-pong guards are weakened`);
|
|
282
|
+
}
|
|
283
|
+
const history = batchHistory(base);
|
|
284
|
+
|
|
285
|
+
const results = [];
|
|
286
|
+
for (const item of items) {
|
|
287
|
+
let res;
|
|
288
|
+
try {
|
|
289
|
+
const facts = typeof o.factsFor === "function" ? o.factsFor(item) : o.facts;
|
|
290
|
+
res = await processOne({
|
|
291
|
+
...o,
|
|
292
|
+
item,
|
|
293
|
+
facts,
|
|
294
|
+
history,
|
|
295
|
+
obligations,
|
|
296
|
+
nowMs,
|
|
297
|
+
humanTurn: typeof o.isHumanTurn === "function" ? o.isHumanTurn(item) === true : o.humanTurn === true,
|
|
298
|
+
});
|
|
299
|
+
stats.processed += 1;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
// A per-item throw is a bug somewhere below; it must not cost the caller
|
|
302
|
+
// the other 199 events. Loud, counted, and the batch continues.
|
|
303
|
+
const msg = err && err.message ? err.message : String(err);
|
|
304
|
+
stats.failed += 1;
|
|
305
|
+
log("error", `[execution] item threw and was skipped: ${msg}`);
|
|
306
|
+
results.push({ ok: false, error: msg, decision: null, origin: null });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
results.push(res);
|
|
311
|
+
const d = res.decision || {};
|
|
312
|
+
const disp = s(d.disposition) || "unknown";
|
|
313
|
+
stats.byDisposition[disp] = (stats.byDisposition[disp] || 0) + 1;
|
|
314
|
+
const surf = s(d.surface) || "unknown";
|
|
315
|
+
stats.bySurface[surf] = (stats.bySurface[surf] || 0) + 1;
|
|
316
|
+
const reason = s(d.reason) || "unknown";
|
|
317
|
+
stats.byReason[reason] = (stats.byReason[reason] || 0) + 1;
|
|
318
|
+
if (willAct(d)) stats.acted += 1;
|
|
319
|
+
else stats.ignored += 1;
|
|
320
|
+
if (res.ok === false) stats.failed += 1;
|
|
321
|
+
if (!res.obligationKey && willAct(d)) stats.uncovered += 1;
|
|
322
|
+
if (res.proposed) stats.proposed.push(res.proposed);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const summary = Object.entries(stats.byDisposition)
|
|
326
|
+
.map(([k, v]) => `${k}×${v}`)
|
|
327
|
+
.sort()
|
|
328
|
+
.join(", ");
|
|
329
|
+
log(
|
|
330
|
+
"info",
|
|
331
|
+
`[execution] ${stats.processed}/${stats.total} event(s): ${summary || "none"}` +
|
|
332
|
+
(stats.uncovered ? ` · ${stats.uncovered} uncovered by the plan` : "") +
|
|
333
|
+
(stats.failed ? ` · ${stats.failed} failed` : ""),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
return { results, stats };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export { dedupeKey, threadKey };
|
|
340
|
+
|
|
341
|
+
export default { processInbound, processOne, batchHistory };
|