@cohortapp/agent-sdk 2.3.2 → 2.4.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/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 +84 -16
- package/lib/setup/enroll-from-cohort.test.mjs +43 -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,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/execution/effects.mjs — the real machinery behind each disposition.
|
|
3
|
+
*
|
|
4
|
+
* `drive.mjs` sequences; this module supplies. It is the only file in
|
|
5
|
+
* `lib/execution/` that touches the network or the filesystem, which is what
|
|
6
|
+
* keeps the decision ladder and the driver testable with plain fakes.
|
|
7
|
+
*
|
|
8
|
+
* Every effect here is a THIN adapter onto machinery that already exists. This
|
|
9
|
+
* repo has repeatedly grown a second mechanism next to a working one, so the
|
|
10
|
+
* rule for this file is: if there is an existing module that does the thing,
|
|
11
|
+
* call it, and if there isn't, write to the on-disk format an existing consumer
|
|
12
|
+
* already reads.
|
|
13
|
+
*
|
|
14
|
+
* react → handed to the caller's responder/session spawner. NOT
|
|
15
|
+
* reimplemented here: the dispatcher owns session spawning
|
|
16
|
+
* and prompt construction, and a second spawner would be
|
|
17
|
+
* exactly the duplication this codebase keeps suffering.
|
|
18
|
+
* schedule → appends a queue item to `state/queues/inbound.yaml` in the
|
|
19
|
+
* format `lib/backlog.mjs parseQueueItems` already parses and
|
|
20
|
+
* the daemon's 2-minute backlog sweep already dispatches. No
|
|
21
|
+
* dispatcher edit, no new transport.
|
|
22
|
+
* delegate → `lib/org/handoff.mjs sendHandoff` (the A2A envelope path).
|
|
23
|
+
* escalate → `escalation.create` via `lib/org/client.mjs call`, which is
|
|
24
|
+
* the first real writer for the escalation lane.
|
|
25
|
+
* requestApproval → `lib/org/approvals.mjs requestApproval` + `awaitDecision`,
|
|
26
|
+
* including the `payloadHash` single-use binding.
|
|
27
|
+
* arbitrate → `lib/org/leases.mjs preSendArbitration` (3s fail-open).
|
|
28
|
+
* reportDrift → append to `state/plan/drift.jsonl`, mirrored best-effort.
|
|
29
|
+
* propose → append to `state/plan/proposed.jsonl` (the observe-only sink).
|
|
30
|
+
*
|
|
31
|
+
* Every function returns `{ok, ref, error}` rather than throwing, matching the
|
|
32
|
+
* shape `drive.runEffect` normalises to.
|
|
33
|
+
*
|
|
34
|
+
* @module lib/execution/effects
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
"use strict";
|
|
38
|
+
|
|
39
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
40
|
+
import { dirname, join } from "node:path";
|
|
41
|
+
|
|
42
|
+
import { resolveAgentRoot } from "../agent-root.mjs";
|
|
43
|
+
import { appendJsonl } from "../fs-atomic.mjs";
|
|
44
|
+
import { yamlScalar } from "../channels/inbox-item.mjs";
|
|
45
|
+
|
|
46
|
+
/** Where scheduled work lands — a queue file the daemon's sweep already reads. */
|
|
47
|
+
export const INBOUND_QUEUE_REL = "state/queues/inbound.yaml";
|
|
48
|
+
/** Where drift records land, mirrored by the reconciler. */
|
|
49
|
+
export const DRIFT_REL = "state/plan/drift.jsonl";
|
|
50
|
+
/** Where observe-only proposals land instead of becoming work (§5.10). */
|
|
51
|
+
export const PROPOSED_REL = "state/plan/proposed.jsonl";
|
|
52
|
+
|
|
53
|
+
/** Map a directedness tier + surface latency onto the queue's priority vocabulary. */
|
|
54
|
+
function priorityFor(decision) {
|
|
55
|
+
if (decision.reason === "budget_exhausted" || decision.reason === "actor_flood") return "normal";
|
|
56
|
+
if (decision.tier === "T0") return "high";
|
|
57
|
+
return "normal";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A stable, human-legible queue item id. Derived from the dedupe key so a
|
|
62
|
+
* redelivery that somehow slips past the journal guard collides on the id rather
|
|
63
|
+
* than creating a second item.
|
|
64
|
+
*/
|
|
65
|
+
function queueItemId(decision) {
|
|
66
|
+
const raw = String(decision.key || `${decision.family}.${decision.kind}`);
|
|
67
|
+
return `inb-${raw.replace(/[^A-Za-z0-9._#:@-]/g, "-").slice(0, 80)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Append one item to `state/queues/inbound.yaml`, creating the file with its
|
|
72
|
+
* header when absent. Idempotent on the item id: an id already present in the
|
|
73
|
+
* file is a no-op, so a retry of the same decision cannot double-queue.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} decision
|
|
76
|
+
* @param {object} [o] { agentRoot, nowMs }
|
|
77
|
+
* @returns {{ok:boolean, ref:object|null, error:string|null}}
|
|
78
|
+
*/
|
|
79
|
+
export function scheduleToQueue(decision, o = {}) {
|
|
80
|
+
const root = resolveAgentRoot(o.agentRoot);
|
|
81
|
+
const path = join(root, INBOUND_QUEUE_REL);
|
|
82
|
+
const id = queueItemId(decision);
|
|
83
|
+
const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
87
|
+
if (!existsSync(path)) {
|
|
88
|
+
writeFileSync(
|
|
89
|
+
path,
|
|
90
|
+
[
|
|
91
|
+
"queue: inbound",
|
|
92
|
+
"description: >-",
|
|
93
|
+
" Directed inbound events the execution ladder queued rather than answered",
|
|
94
|
+
" immediately. Written by lib/execution/effects.mjs; consumed by the daemon's",
|
|
95
|
+
" backlog sweep. Each item carries the decision that produced it.",
|
|
96
|
+
"items:",
|
|
97
|
+
"",
|
|
98
|
+
].join("\n"),
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
const body = readFileSync(path, "utf-8");
|
|
102
|
+
// Anchor on a whole `id:` key at line start so a `correlation_id:` can
|
|
103
|
+
// never be mistaken for a match — the same trap lib/backlog.mjs documents.
|
|
104
|
+
const re = new RegExp(`^\\s*-?\\s*"?id"?:\\s*["']?${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']?\\s*$`, "m");
|
|
105
|
+
if (re.test(body)) {
|
|
106
|
+
return { ok: true, ref: { id, path, deduped: true }, error: null };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const title = `${decision.surface || "inbound"}: ${decision.reason || "queued"}`;
|
|
111
|
+
const nextAction =
|
|
112
|
+
decision.disposition === "schedule" && decision.surface
|
|
113
|
+
? `Handle the ${decision.surface} event ${decision.key} at rung ${decision.rung ?? "?"}`
|
|
114
|
+
: `Handle ${decision.key}`;
|
|
115
|
+
|
|
116
|
+
const lines = [
|
|
117
|
+
` - id: ${yamlScalar(id)}`,
|
|
118
|
+
` title: ${yamlScalar(title)}`,
|
|
119
|
+
" status: open",
|
|
120
|
+
` priority: ${priorityFor(decision)}`,
|
|
121
|
+
` next_action: ${yamlScalar(nextAction)}`,
|
|
122
|
+
` created: ${yamlScalar(new Date(nowMs).toISOString())}`,
|
|
123
|
+
` surface: ${yamlScalar(decision.surface || "")}`,
|
|
124
|
+
` event_key: ${yamlScalar(decision.key || "")}`,
|
|
125
|
+
` thread: ${yamlScalar(decision.thread || "")}`,
|
|
126
|
+
` rung: ${Number.isFinite(decision.rung) ? decision.rung : "null"}`,
|
|
127
|
+
` obligation_key: ${yamlScalar(decision.obligationKey || "")}`,
|
|
128
|
+
// The reasoning travels WITH the work. A queue item whose provenance is a
|
|
129
|
+
// mystery is a queue item nobody trusts enough to action.
|
|
130
|
+
` why: ${yamlScalar((decision.why || []).join(" | "))}`,
|
|
131
|
+
"",
|
|
132
|
+
];
|
|
133
|
+
appendFileSync(path, lines.join("\n"));
|
|
134
|
+
return { ok: true, ref: { id, path, deduped: false }, error: null };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return { ok: false, ref: null, error: err && err.message ? err.message : String(err) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Append a drift record locally. Mirroring to hq is the reconciler's job. */
|
|
141
|
+
export function writeDrift(drift, o = {}) {
|
|
142
|
+
const root = resolveAgentRoot(o.agentRoot);
|
|
143
|
+
const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
|
|
144
|
+
const ok = appendJsonl(join(root, DRIFT_REL), {
|
|
145
|
+
at: new Date(nowMs).toISOString(),
|
|
146
|
+
kind: drift && drift.kind ? String(drift.kind) : "unknown",
|
|
147
|
+
key: (drift && drift.key) || (o.decision && o.decision.obligationKey) || null,
|
|
148
|
+
detail: (drift && drift.detail) || null,
|
|
149
|
+
source: "execution-ladder",
|
|
150
|
+
});
|
|
151
|
+
return { ok, ref: { kind: drift && drift.kind }, error: ok ? null : "drift append failed" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Append an observe-only proposal instead of doing the work. */
|
|
155
|
+
export function writeProposal(decision, o = {}) {
|
|
156
|
+
const root = resolveAgentRoot(o.agentRoot);
|
|
157
|
+
const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
|
|
158
|
+
const ok = appendJsonl(join(root, PROPOSED_REL), {
|
|
159
|
+
at: new Date(nowMs).toISOString(),
|
|
160
|
+
source: "execution-ladder",
|
|
161
|
+
key: decision.key || null,
|
|
162
|
+
surface: decision.surface || null,
|
|
163
|
+
disposition: decision.disposition,
|
|
164
|
+
reason: decision.reason,
|
|
165
|
+
rung: decision.rung ?? null,
|
|
166
|
+
obligationKey: decision.obligationKey || null,
|
|
167
|
+
why: decision.why || [],
|
|
168
|
+
});
|
|
169
|
+
return { ok, ref: { proposed: true }, error: ok ? null : "proposal append failed" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Append a PROPOSED REACT obligation for an event the compiled plan did not
|
|
174
|
+
* cover (§6.1: "emit `PlanDrift{uncovered_event}` **and** a proposed REACT
|
|
175
|
+
* obligation — this is how the plan learns instead of ossifying").
|
|
176
|
+
*
|
|
177
|
+
* Lands in the same file as observe-only proposals, tagged `type:"obligation"`,
|
|
178
|
+
* so a human reviewing the window sees one list: what the agent would have done,
|
|
179
|
+
* and what the plan is missing.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} obligation a schema-valid proposed obligation
|
|
182
|
+
* @param {object} [o] { agentRoot, nowMs, decision }
|
|
183
|
+
*/
|
|
184
|
+
export function writeObligationProposal(obligation, o = {}) {
|
|
185
|
+
const root = resolveAgentRoot(o.agentRoot);
|
|
186
|
+
const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
|
|
187
|
+
if (!obligation || typeof obligation !== "object" || !obligation.key) {
|
|
188
|
+
return { ok: false, ref: null, error: "obligation proposal missing a key" };
|
|
189
|
+
}
|
|
190
|
+
const decision = o.decision || {};
|
|
191
|
+
const ok = appendJsonl(join(root, PROPOSED_REL), {
|
|
192
|
+
at: new Date(nowMs).toISOString(),
|
|
193
|
+
source: "execution-ladder",
|
|
194
|
+
type: "obligation",
|
|
195
|
+
key: obligation.key,
|
|
196
|
+
obligation,
|
|
197
|
+
from_event: {
|
|
198
|
+
key: decision.key || null,
|
|
199
|
+
surface: decision.surface || null,
|
|
200
|
+
tier: decision.tier || null,
|
|
201
|
+
reason: decision.reason || null,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return { ok, ref: { key: obligation.key }, error: ok ? null : "obligation proposal append failed" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build the default effect set.
|
|
209
|
+
*
|
|
210
|
+
* `react` is deliberately NOT defaulted to a session spawn: the dispatcher owns
|
|
211
|
+
* spawning, and this module must not grow a second spawner. The caller wires it
|
|
212
|
+
* (see the wiring notes) and gets a loud `react_effect_missing` degradation if
|
|
213
|
+
* it forgets — which is the correct failure, because a missing responder is a
|
|
214
|
+
* real outage and must not look like a successful no-op.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} o {
|
|
217
|
+
* agentRoot, nowMs, client, base, token, me, principal,
|
|
218
|
+
* react // (args) => Promise<{ok, ref}> — REQUIRED for react_now
|
|
219
|
+
* escalationTarget // member id / "principal"
|
|
220
|
+
* }
|
|
221
|
+
* @returns {object} the effects object `drive` consumes
|
|
222
|
+
*/
|
|
223
|
+
export function defaultEffects(o = {}) {
|
|
224
|
+
const agentRoot = resolveAgentRoot(o.agentRoot);
|
|
225
|
+
const common = { agentRoot, nowMs: o.nowMs };
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
react: typeof o.react === "function" ? o.react : undefined,
|
|
229
|
+
|
|
230
|
+
schedule: async ({ decision }) => scheduleToQueue(decision, common),
|
|
231
|
+
|
|
232
|
+
delegate: async ({ decision, candidate }) => {
|
|
233
|
+
const { sendHandoff } = await import("../org/handoff.mjs");
|
|
234
|
+
const r = await sendHandoff({
|
|
235
|
+
agentRoot,
|
|
236
|
+
from: o.me || "",
|
|
237
|
+
to: decision.delegateTo,
|
|
238
|
+
intent: `${decision.surface || "inbound"} event ${decision.key}: ${decision.reason}`,
|
|
239
|
+
payload: {
|
|
240
|
+
event_key: decision.key,
|
|
241
|
+
surface: decision.surface,
|
|
242
|
+
thread: decision.thread,
|
|
243
|
+
why: decision.why,
|
|
244
|
+
entityId: candidate ? candidate.entityId : null,
|
|
245
|
+
},
|
|
246
|
+
...(o.transport ? { transport: o.transport } : {}),
|
|
247
|
+
});
|
|
248
|
+
return { ok: r && r.ok !== false, ref: r, error: r && r.error ? String(r.error) : null };
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
escalate: async ({ decision }) => {
|
|
252
|
+
const { call } = await import("../org/client.mjs");
|
|
253
|
+
const r = await call(
|
|
254
|
+
"escalation.create",
|
|
255
|
+
{
|
|
256
|
+
// No message bodies, ever — ids and reasons only. `assertRedactionSafe`
|
|
257
|
+
// bans addresses and content, not member ids.
|
|
258
|
+
summary: `${decision.surface || "inbound"}: ${decision.reason}`,
|
|
259
|
+
waitingOnMemberId: decision.escalateTo || o.escalationTarget || undefined,
|
|
260
|
+
options: (decision.options || []).map((label) => ({ label: String(label) })),
|
|
261
|
+
context: {
|
|
262
|
+
event_key: decision.key,
|
|
263
|
+
surface: decision.surface,
|
|
264
|
+
thread: decision.thread,
|
|
265
|
+
obligation_key: decision.obligationKey || null,
|
|
266
|
+
why: decision.why,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{ agentRoot, client: o.client, base: o.base, token: o.token },
|
|
270
|
+
);
|
|
271
|
+
return { ok: !!r && r.ok !== false, ref: r, error: r && r.error ? String(r.error) : null };
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
requestApproval: async ({ decision, classes }) => {
|
|
275
|
+
const { requestApproval, awaitDecision } = await import("../org/approvals.mjs");
|
|
276
|
+
const asked = await requestApproval({
|
|
277
|
+
agentRoot,
|
|
278
|
+
action: decision.action && decision.action.method ? decision.action.method : decision.surface,
|
|
279
|
+
classification: classes,
|
|
280
|
+
payload: {
|
|
281
|
+
event_key: decision.key,
|
|
282
|
+
surface: decision.surface,
|
|
283
|
+
thread: decision.thread,
|
|
284
|
+
rung: decision.rung,
|
|
285
|
+
},
|
|
286
|
+
summary: `${decision.surface}: ${decision.reason}`,
|
|
287
|
+
client: o.client,
|
|
288
|
+
base: o.base,
|
|
289
|
+
token: o.token,
|
|
290
|
+
});
|
|
291
|
+
if (!asked || asked.ok === false) {
|
|
292
|
+
return { ok: false, ref: asked, error: (asked && asked.error) || "approval request failed" };
|
|
293
|
+
}
|
|
294
|
+
const id = asked.id || (asked.approval && asked.approval.id);
|
|
295
|
+
if (!id) return { ok: true, ref: { granted: undefined, asked }, error: null };
|
|
296
|
+
const decided = await awaitDecision(id, o.approvalTimeoutMs, {
|
|
297
|
+
agentRoot,
|
|
298
|
+
client: o.client,
|
|
299
|
+
base: o.base,
|
|
300
|
+
token: o.token,
|
|
301
|
+
});
|
|
302
|
+
const status = String((decided && (decided.status || decided.state)) || "").toLowerCase();
|
|
303
|
+
const granted = status === "approved" ? true : status === "rejected" || status === "denied" ? false : undefined;
|
|
304
|
+
return { ok: true, ref: { granted, id, status, decided }, error: null };
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
arbitrate: async ({ decision, candidate, resource }) => {
|
|
308
|
+
const { preSendArbitration } = await import("../org/leases.mjs");
|
|
309
|
+
const ids = (candidate && candidate.ids) || {};
|
|
310
|
+
const r = await preSendArbitration({
|
|
311
|
+
platform: "cohort",
|
|
312
|
+
channel: ids.channelId || resource || decision.thread || "",
|
|
313
|
+
thread: ids.threadRootId || ids.messageId || "",
|
|
314
|
+
agentRoot,
|
|
315
|
+
client: o.client,
|
|
316
|
+
base: o.base,
|
|
317
|
+
token: o.token,
|
|
318
|
+
});
|
|
319
|
+
return { ok: true, ref: r, error: null };
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
reportDrift: async ({ drift, decision }) => writeDrift(drift, { ...common, decision }),
|
|
323
|
+
|
|
324
|
+
propose: async ({ decision }) => writeProposal(decision, common),
|
|
325
|
+
|
|
326
|
+
proposeObligation: async ({ obligation, decision }) =>
|
|
327
|
+
writeObligationProposal(obligation, { ...common, decision }),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export default {
|
|
332
|
+
defaultEffects,
|
|
333
|
+
scheduleToQueue,
|
|
334
|
+
writeDrift,
|
|
335
|
+
writeProposal,
|
|
336
|
+
writeObligationProposal,
|
|
337
|
+
INBOUND_QUEUE_REL,
|
|
338
|
+
DRIFT_REL,
|
|
339
|
+
PROPOSED_REL,
|
|
340
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effects.test.mjs — the on-disk side effects, and the format contract with the
|
|
3
|
+
* machinery that already consumes them.
|
|
4
|
+
* Run: node --test lib/execution/effects.test.mjs
|
|
5
|
+
*/
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { parseQueueItems } from "../backlog.mjs";
|
|
15
|
+
import {
|
|
16
|
+
scheduleToQueue,
|
|
17
|
+
writeDrift,
|
|
18
|
+
writeProposal,
|
|
19
|
+
defaultEffects,
|
|
20
|
+
INBOUND_QUEUE_REL,
|
|
21
|
+
DRIFT_REL,
|
|
22
|
+
PROPOSED_REL,
|
|
23
|
+
} from "./effects.mjs";
|
|
24
|
+
|
|
25
|
+
const NOW = Date.parse("2026-08-11T12:00:00Z");
|
|
26
|
+
function tmp() { return mkdtempSync(join(tmpdir(), "exec-effects-")); }
|
|
27
|
+
|
|
28
|
+
const DECISION = {
|
|
29
|
+
key: "board.item.assigned#42",
|
|
30
|
+
thread: "board:-:t1",
|
|
31
|
+
surface: "task_assigned",
|
|
32
|
+
disposition: "schedule",
|
|
33
|
+
reason: "not_respondable",
|
|
34
|
+
tier: "T0",
|
|
35
|
+
rung: 3,
|
|
36
|
+
obligationKey: "react.task_assigned",
|
|
37
|
+
why: ["directed: assignee on task_assigned (tier T0)", "task_assigned is not a conversational surface"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
test("a scheduled item is parseable by the EXISTING backlog parser", () => {
|
|
41
|
+
// The whole reuse claim rests on this: the daemon's 2-minute sweep already
|
|
42
|
+
// reads this format, so scheduling needs no dispatcher change.
|
|
43
|
+
const root = tmp();
|
|
44
|
+
try {
|
|
45
|
+
const r = scheduleToQueue(DECISION, { agentRoot: root, nowMs: NOW });
|
|
46
|
+
assert.equal(r.ok, true);
|
|
47
|
+
const body = readFileSync(join(root, INBOUND_QUEUE_REL), "utf-8");
|
|
48
|
+
const items = parseQueueItems(body, "inbound.yaml");
|
|
49
|
+
assert.equal(items.length, 1);
|
|
50
|
+
assert.equal(items[0].status, "open");
|
|
51
|
+
assert.equal(items[0].priority, "high", "a T0 event is high priority");
|
|
52
|
+
assert.ok(items[0].id, "the id must survive — the claim subsystem is gated on it");
|
|
53
|
+
assert.ok(items[0].title.includes("task_assigned"));
|
|
54
|
+
assert.ok(items[0].next_action);
|
|
55
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("the reasoning travels WITH the queued work", () => {
|
|
59
|
+
const root = tmp();
|
|
60
|
+
try {
|
|
61
|
+
scheduleToQueue(DECISION, { agentRoot: root, nowMs: NOW });
|
|
62
|
+
const body = readFileSync(join(root, INBOUND_QUEUE_REL), "utf-8");
|
|
63
|
+
assert.match(body, /why:/);
|
|
64
|
+
assert.match(body, /tier T0/, "a queue item whose provenance is a mystery is one nobody actions");
|
|
65
|
+
assert.match(body, /event_key: "board.item.assigned#42"/);
|
|
66
|
+
assert.match(body, /obligation_key: "react.task_assigned"/);
|
|
67
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("scheduling the same decision twice does NOT double-queue", () => {
|
|
71
|
+
const root = tmp();
|
|
72
|
+
try {
|
|
73
|
+
const a = scheduleToQueue(DECISION, { agentRoot: root, nowMs: NOW });
|
|
74
|
+
const b = scheduleToQueue(DECISION, { agentRoot: root, nowMs: NOW });
|
|
75
|
+
assert.equal(a.ref.deduped, false);
|
|
76
|
+
assert.equal(b.ref.deduped, true);
|
|
77
|
+
const items = parseQueueItems(readFileSync(join(root, INBOUND_QUEUE_REL), "utf-8"), "q");
|
|
78
|
+
assert.equal(items.length, 1);
|
|
79
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("distinct events append distinct items", () => {
|
|
83
|
+
const root = tmp();
|
|
84
|
+
try {
|
|
85
|
+
scheduleToQueue(DECISION, { agentRoot: root, nowMs: NOW });
|
|
86
|
+
scheduleToQueue({ ...DECISION, key: "board.item.assigned#43" }, { agentRoot: root, nowMs: NOW });
|
|
87
|
+
const items = parseQueueItems(readFileSync(join(root, INBOUND_QUEUE_REL), "utf-8"), "q");
|
|
88
|
+
assert.equal(items.length, 2);
|
|
89
|
+
assert.notEqual(items[0].id, items[1].id);
|
|
90
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("the dedupe check anchors on a whole `id` key, not a substring", () => {
|
|
94
|
+
const root = tmp();
|
|
95
|
+
try {
|
|
96
|
+
// `#4` must not be seen as already present because `#42` is.
|
|
97
|
+
scheduleToQueue({ ...DECISION, key: "board.item.assigned#42" }, { agentRoot: root, nowMs: NOW });
|
|
98
|
+
const r = scheduleToQueue({ ...DECISION, key: "board.item.assigned#4" }, { agentRoot: root, nowMs: NOW });
|
|
99
|
+
assert.equal(r.ref.deduped, false, "a prefix collision must not swallow a distinct event");
|
|
100
|
+
assert.equal(parseQueueItems(readFileSync(join(root, INBOUND_QUEUE_REL), "utf-8"), "q").length, 2);
|
|
101
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a hostile surface/reason cannot inject YAML lines", () => {
|
|
105
|
+
const root = tmp();
|
|
106
|
+
try {
|
|
107
|
+
scheduleToQueue(
|
|
108
|
+
{ ...DECISION, key: "k1", surface: 'x"\nstatus: blocked\npriority: "critical', why: ["a\nb"] },
|
|
109
|
+
{ agentRoot: root, nowMs: NOW },
|
|
110
|
+
);
|
|
111
|
+
const items = parseQueueItems(readFileSync(join(root, INBOUND_QUEUE_REL), "utf-8"), "q");
|
|
112
|
+
assert.equal(items.length, 1, "the injected `status: blocked` must not become a real key");
|
|
113
|
+
assert.equal(items[0].status, "open");
|
|
114
|
+
assert.equal(items[0].priority, "high");
|
|
115
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a lower-tier or downgraded event queues at normal priority", () => {
|
|
119
|
+
const root = tmp();
|
|
120
|
+
try {
|
|
121
|
+
scheduleToQueue({ ...DECISION, key: "k1", tier: "T1" }, { agentRoot: root, nowMs: NOW });
|
|
122
|
+
scheduleToQueue({ ...DECISION, key: "k2", reason: "actor_flood" }, { agentRoot: root, nowMs: NOW });
|
|
123
|
+
const items = parseQueueItems(readFileSync(join(root, INBOUND_QUEUE_REL), "utf-8"), "q");
|
|
124
|
+
assert.deepEqual(items.map((i) => i.priority), ["normal", "normal"]);
|
|
125
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("an unwritable root is reported, not thrown", () => {
|
|
129
|
+
const r = scheduleToQueue(DECISION, { agentRoot: "/proc/nonexistent-cannot-create", nowMs: NOW });
|
|
130
|
+
assert.equal(r.ok, false);
|
|
131
|
+
assert.ok(r.error, "the caller must be told the work was not queued");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("drift and proposals are appended as JSONL with their provenance", () => {
|
|
135
|
+
const root = tmp();
|
|
136
|
+
try {
|
|
137
|
+
writeDrift({ kind: "uncovered_event", detail: { surface: "dm" } }, { agentRoot: root, nowMs: NOW, decision: DECISION });
|
|
138
|
+
const drift = JSON.parse(readFileSync(join(root, DRIFT_REL), "utf-8").trim());
|
|
139
|
+
assert.equal(drift.kind, "uncovered_event");
|
|
140
|
+
assert.equal(drift.key, "react.task_assigned");
|
|
141
|
+
assert.equal(drift.source, "execution-ladder");
|
|
142
|
+
|
|
143
|
+
writeProposal({ ...DECISION, disposition: "react_now" }, { agentRoot: root, nowMs: NOW });
|
|
144
|
+
const prop = JSON.parse(readFileSync(join(root, PROPOSED_REL), "utf-8").trim());
|
|
145
|
+
assert.equal(prop.disposition, "react_now");
|
|
146
|
+
assert.deepEqual(prop.why, DECISION.why, "the observe-only window is only readable if the reasoning is kept");
|
|
147
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("writeDrift tolerates a malformed drift record", () => {
|
|
151
|
+
const root = tmp();
|
|
152
|
+
try {
|
|
153
|
+
assert.equal(writeDrift(null, { agentRoot: root, nowMs: NOW }).ok, true);
|
|
154
|
+
const row = JSON.parse(readFileSync(join(root, DRIFT_REL), "utf-8").trim());
|
|
155
|
+
assert.equal(row.kind, "unknown");
|
|
156
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("defaultEffects wires every effect EXCEPT react, which the caller must supply", () => {
|
|
160
|
+
const root = tmp();
|
|
161
|
+
try {
|
|
162
|
+
const e = defaultEffects({ agentRoot: root });
|
|
163
|
+
for (const name of ["schedule", "delegate", "escalate", "requestApproval", "arbitrate", "reportDrift", "propose"]) {
|
|
164
|
+
assert.equal(typeof e[name], "function", `${name} should be wired`);
|
|
165
|
+
}
|
|
166
|
+
// `react` is deliberately absent: the dispatcher owns session spawning and
|
|
167
|
+
// this module must not grow a second spawner. A caller that forgets gets a
|
|
168
|
+
// loud `react_effect_missing`, which is the correct failure.
|
|
169
|
+
assert.equal(e.react, undefined);
|
|
170
|
+
assert.equal(typeof defaultEffects({ agentRoot: root, react: async () => ({ ok: true }) }).react, "function");
|
|
171
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("the default schedule effect writes through to the queue", async () => {
|
|
175
|
+
const root = tmp();
|
|
176
|
+
try {
|
|
177
|
+
const e = defaultEffects({ agentRoot: root, nowMs: NOW });
|
|
178
|
+
const r = await e.schedule({ decision: DECISION });
|
|
179
|
+
assert.equal(r.ok, true);
|
|
180
|
+
assert.ok(existsSync(join(root, INBOUND_QUEUE_REL)));
|
|
181
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("the default propose + reportDrift effects write through", async () => {
|
|
185
|
+
const root = tmp();
|
|
186
|
+
try {
|
|
187
|
+
const e = defaultEffects({ agentRoot: root, nowMs: NOW });
|
|
188
|
+
assert.equal((await e.propose({ decision: DECISION })).ok, true);
|
|
189
|
+
assert.equal((await e.reportDrift({ drift: { kind: "budget_breach" }, decision: DECISION })).ok, true);
|
|
190
|
+
assert.ok(existsSync(join(root, PROPOSED_REL)));
|
|
191
|
+
assert.ok(existsSync(join(root, DRIFT_REL)));
|
|
192
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
193
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/execution/index.mjs — the one function the inbound pipeline calls.
|
|
3
|
+
*
|
|
4
|
+
* The inbound layer (`lib/org/inbound/`) produces "here is an event, and here is
|
|
5
|
+
* whether it is yours". Everything after that — should we answer, queue, hand
|
|
6
|
+
* off, ask, or drop; how much machinery it deserves; what gates it must clear;
|
|
7
|
+
* and writing down why — is this package, behind one call:
|
|
8
|
+
*
|
|
9
|
+
* import { handleInboundEvent } from "../execution/index.mjs";
|
|
10
|
+
*
|
|
11
|
+
* const result = await handleInboundEvent({
|
|
12
|
+
* candidate, // directedness.classifyEvent(ev)
|
|
13
|
+
* verdict, // directedness.resolveDirected(candidate, me, facts)
|
|
14
|
+
* me, // my member id
|
|
15
|
+
* runtime, // emergencyStop / directive / staleness / budget
|
|
16
|
+
* effects, // defaultEffects({ agentRoot, react, ... })
|
|
17
|
+
* agentRoot,
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* `result.decision.why` is the ordered, plain-English reasoning; `result.effect`
|
|
21
|
+
* is what actually happened. Both are already on disk in
|
|
22
|
+
* `state/execution/journal.jsonl` by the time this returns.
|
|
23
|
+
*
|
|
24
|
+
* The history view is loaded HERE rather than in `decide` so that the decision
|
|
25
|
+
* function stays pure and the (bounded, tail-only) journal read happens once per
|
|
26
|
+
* event. A caller batching a burst of events can load the history itself and
|
|
27
|
+
* pass it in, which is what the sweep does.
|
|
28
|
+
*
|
|
29
|
+
* @module lib/execution
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
"use strict";
|
|
33
|
+
|
|
34
|
+
import { decide, willAct } from "./disposition.mjs";
|
|
35
|
+
import { drive } from "./drive.mjs";
|
|
36
|
+
import { loadHistory, recordHumanTurn, threadKey } from "./journal.mjs";
|
|
37
|
+
|
|
38
|
+
export { decide, willAct } from "./disposition.mjs";
|
|
39
|
+
export { drive, EFFECT_FOR } from "./drive.mjs";
|
|
40
|
+
export { routeRung, baseRung, RUNGS } from "./route.mjs";
|
|
41
|
+
export {
|
|
42
|
+
policyFor,
|
|
43
|
+
SURFACE_POLICY,
|
|
44
|
+
ORG_SURFACE_POLICY,
|
|
45
|
+
EXTENDED_SURFACE_POLICY,
|
|
46
|
+
EXTENDED_SURFACE_NAMES,
|
|
47
|
+
DISPOSITIONS,
|
|
48
|
+
uncoveredSurfaces,
|
|
49
|
+
orphanPolicies,
|
|
50
|
+
adoptedExtendedSurfaces,
|
|
51
|
+
} from "./surface-policy.mjs";
|
|
52
|
+
export {
|
|
53
|
+
intake,
|
|
54
|
+
fromPair,
|
|
55
|
+
fromMessageEvent,
|
|
56
|
+
fromInboxItem,
|
|
57
|
+
fromChannelEvent,
|
|
58
|
+
fromAlert,
|
|
59
|
+
fromRawEvent,
|
|
60
|
+
parseRawRef,
|
|
61
|
+
} from "./intake.mjs";
|
|
62
|
+
export {
|
|
63
|
+
matchObligation,
|
|
64
|
+
proposeReact,
|
|
65
|
+
reactObligations,
|
|
66
|
+
loadReactObligations,
|
|
67
|
+
} from "./match.mjs";
|
|
68
|
+
export { processInbound, processOne, batchHistory } from "./pipeline.mjs";
|
|
69
|
+
export {
|
|
70
|
+
loadHistory,
|
|
71
|
+
recordDecision,
|
|
72
|
+
recordOutcome,
|
|
73
|
+
recordHumanTurn,
|
|
74
|
+
dedupeKey,
|
|
75
|
+
threadKey,
|
|
76
|
+
journalPath,
|
|
77
|
+
} from "./journal.mjs";
|
|
78
|
+
export { defaultEffects, scheduleToQueue, writeDrift, writeProposal } from "./effects.mjs";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decide and act on one inbound event.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} o
|
|
84
|
+
* @param {object} o.candidate
|
|
85
|
+
* @param {object} o.verdict
|
|
86
|
+
* @param {string} [o.me]
|
|
87
|
+
* @param {object} [o.history] pre-loaded (a batch caller); loaded here if absent
|
|
88
|
+
* @param {object} [o.runtime]
|
|
89
|
+
* @param {object|null} [o.obligation]
|
|
90
|
+
* @param {object} [o.need]
|
|
91
|
+
* @param {Array} [o.manifest]
|
|
92
|
+
* @param {object} [o.facts]
|
|
93
|
+
* @param {object} [o.effects]
|
|
94
|
+
* @param {string} [o.agentRoot]
|
|
95
|
+
* @param {number} [o.nowMs]
|
|
96
|
+
* @param {string} [o.traceId]
|
|
97
|
+
* @param {Function} [o.log]
|
|
98
|
+
* @param {boolean} [o.humanTurn] the actor is a human — resets the ping-pong
|
|
99
|
+
* counter for this thread BEFORE the decision is taken, so a human
|
|
100
|
+
* re-entering a conversation immediately unblocks the agent.
|
|
101
|
+
* @returns {Promise<{decision:object, ok:boolean, effect:string|null, ref:any, spoke:boolean, degraded:string[], error:string|null}>}
|
|
102
|
+
*/
|
|
103
|
+
export async function handleInboundEvent(o = {}) {
|
|
104
|
+
const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
|
|
105
|
+
|
|
106
|
+
// A human turn resets the chain counter first. Doing this before the history
|
|
107
|
+
// load (rather than after the decision) is what makes "human replies, agent
|
|
108
|
+
// may speak again" work on the very next event instead of the one after.
|
|
109
|
+
if (o.humanTurn === true && o.candidate) {
|
|
110
|
+
recordHumanTurn(
|
|
111
|
+
{
|
|
112
|
+
thread: threadKey(o.candidate),
|
|
113
|
+
actor: String(o.candidate.actor || ""),
|
|
114
|
+
surface: o.verdict ? o.verdict.surface : null,
|
|
115
|
+
},
|
|
116
|
+
{ agentRoot: o.agentRoot, nowMs, traceId: o.traceId },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const history =
|
|
121
|
+
o.history || loadHistory({ agentRoot: o.agentRoot, nowMs, path: o.journalPath, rows: o.rows });
|
|
122
|
+
|
|
123
|
+
const decision = decide({
|
|
124
|
+
candidate: o.candidate,
|
|
125
|
+
verdict: o.verdict,
|
|
126
|
+
me: o.me,
|
|
127
|
+
history,
|
|
128
|
+
runtime: o.runtime,
|
|
129
|
+
obligation: o.obligation,
|
|
130
|
+
need: o.need,
|
|
131
|
+
manifest: o.manifest,
|
|
132
|
+
facts: o.facts,
|
|
133
|
+
policy: o.policy,
|
|
134
|
+
actorFloodLimit: o.actorFloodLimit,
|
|
135
|
+
nowMs,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const res = await drive(decision, {
|
|
139
|
+
candidate: o.candidate,
|
|
140
|
+
effects: o.effects,
|
|
141
|
+
agentRoot: o.agentRoot,
|
|
142
|
+
nowMs,
|
|
143
|
+
traceId: o.traceId,
|
|
144
|
+
log: o.log,
|
|
145
|
+
append: o.append,
|
|
146
|
+
journalPath: o.journalPath,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return { ...res, decision };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default { handleInboundEvent, decide, drive, willAct };
|