@nanobpm/agentic 0.1.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.
Files changed (228) hide show
  1. package/README.md +22 -0
  2. package/dist/blackboard/family.d.ts +40 -0
  3. package/dist/blackboard/family.js +151 -0
  4. package/dist/blackboard/index.d.ts +21 -0
  5. package/dist/blackboard/index.js +19 -0
  6. package/dist/blackboard/schema.d.ts +30 -0
  7. package/dist/blackboard/schema.js +42 -0
  8. package/dist/blackboard/store.d.ts +138 -0
  9. package/dist/blackboard/store.js +216 -0
  10. package/dist/blackboard/test-db.d.ts +5 -0
  11. package/dist/blackboard/test-db.js +42 -0
  12. package/dist/channel/auth.d.ts +41 -0
  13. package/dist/channel/auth.js +67 -0
  14. package/dist/channel/clock.d.ts +11 -0
  15. package/dist/channel/clock.js +4 -0
  16. package/dist/channel/connection.d.ts +75 -0
  17. package/dist/channel/connection.js +14 -0
  18. package/dist/channel/dispatch.d.ts +46 -0
  19. package/dist/channel/dispatch.js +86 -0
  20. package/dist/channel/hub.d.ts +78 -0
  21. package/dist/channel/hub.js +157 -0
  22. package/dist/channel/index.d.ts +27 -0
  23. package/dist/channel/index.js +20 -0
  24. package/dist/channel/registry.d.ts +68 -0
  25. package/dist/channel/registry.js +84 -0
  26. package/dist/channel/ws-transport.d.ts +23 -0
  27. package/dist/channel/ws-transport.js +178 -0
  28. package/dist/cockpit/boot.d.ts +68 -0
  29. package/dist/cockpit/boot.js +202 -0
  30. package/dist/cockpit/fake-dom.d.ts +37 -0
  31. package/dist/cockpit/fake-dom.js +73 -0
  32. package/dist/cockpit/index.d.ts +27 -0
  33. package/dist/cockpit/index.js +27 -0
  34. package/dist/cockpit/relay-client.d.ts +52 -0
  35. package/dist/cockpit/relay-client.js +192 -0
  36. package/dist/cockpit/render.d.ts +58 -0
  37. package/dist/cockpit/render.js +122 -0
  38. package/dist/cockpit/terminal-session.d.ts +95 -0
  39. package/dist/cockpit/terminal-session.js +123 -0
  40. package/dist/cockpit/view.d.ts +79 -0
  41. package/dist/cockpit/view.js +58 -0
  42. package/dist/demand/c8-rest.d.ts +77 -0
  43. package/dist/demand/c8-rest.js +123 -0
  44. package/dist/demand/index.d.ts +24 -0
  45. package/dist/demand/index.js +24 -0
  46. package/dist/demand/model.d.ts +68 -0
  47. package/dist/demand/model.js +118 -0
  48. package/dist/demand/taskdef.d.ts +40 -0
  49. package/dist/demand/taskdef.js +67 -0
  50. package/dist/index.d.ts +17 -0
  51. package/dist/index.js +17 -0
  52. package/dist/presence/family.d.ts +40 -0
  53. package/dist/presence/family.js +166 -0
  54. package/dist/presence/index.d.ts +19 -0
  55. package/dist/presence/index.js +17 -0
  56. package/dist/presence/schema.d.ts +20 -0
  57. package/dist/presence/schema.js +32 -0
  58. package/dist/presence/store.d.ts +130 -0
  59. package/dist/presence/store.js +191 -0
  60. package/dist/presence/test-db.d.ts +5 -0
  61. package/dist/presence/test-db.js +42 -0
  62. package/dist/protocol/conformance/frames.d.ts +24 -0
  63. package/dist/protocol/conformance/frames.js +116 -0
  64. package/dist/protocol/conformance/index.d.ts +13 -0
  65. package/dist/protocol/conformance/index.js +13 -0
  66. package/dist/protocol/conformance/malformed.d.ts +14 -0
  67. package/dist/protocol/conformance/malformed.js +44 -0
  68. package/dist/protocol/conformance/tokens.d.ts +19 -0
  69. package/dist/protocol/conformance/tokens.js +49 -0
  70. package/dist/protocol/conformance/vocab.d.ts +23 -0
  71. package/dist/protocol/conformance/vocab.js +97 -0
  72. package/dist/protocol/families.d.ts +32 -0
  73. package/dist/protocol/families.js +45 -0
  74. package/dist/protocol/frame.d.ts +45 -0
  75. package/dist/protocol/frame.js +114 -0
  76. package/dist/protocol/hex.d.ts +7 -0
  77. package/dist/protocol/hex.js +26 -0
  78. package/dist/protocol/index.d.ts +23 -0
  79. package/dist/protocol/index.js +23 -0
  80. package/dist/protocol/lanes.d.ts +36 -0
  81. package/dist/protocol/lanes.js +40 -0
  82. package/dist/protocol/payloads.d.ts +64 -0
  83. package/dist/protocol/payloads.js +122 -0
  84. package/dist/protocol/token.d.ts +34 -0
  85. package/dist/protocol/token.js +81 -0
  86. package/dist/protocol/vocab/schema.d.ts +51 -0
  87. package/dist/protocol/vocab/schema.js +218 -0
  88. package/dist/relay/incarnation.d.ts +17 -0
  89. package/dist/relay/incarnation.js +50 -0
  90. package/dist/relay/index.d.ts +25 -0
  91. package/dist/relay/index.js +22 -0
  92. package/dist/relay/relay-family.d.ts +68 -0
  93. package/dist/relay/relay-family.js +272 -0
  94. package/dist/relay/ring.d.ts +49 -0
  95. package/dist/relay/ring.js +105 -0
  96. package/dist/relay/scheduler.d.ts +72 -0
  97. package/dist/relay/scheduler.js +180 -0
  98. package/dist/relay/validate.d.ts +29 -0
  99. package/dist/relay/validate.js +39 -0
  100. package/dist/transcript/index.d.ts +18 -0
  101. package/dist/transcript/index.js +17 -0
  102. package/dist/transcript/schema.d.ts +32 -0
  103. package/dist/transcript/schema.js +48 -0
  104. package/dist/transcript/store.d.ts +192 -0
  105. package/dist/transcript/store.js +347 -0
  106. package/dist/transcript/test-db.d.ts +5 -0
  107. package/dist/transcript/test-db.js +41 -0
  108. package/dist/vocab/core-vocab.d.ts +26 -0
  109. package/dist/vocab/core-vocab.js +67 -0
  110. package/dist/vocab/diversity.d.ts +78 -0
  111. package/dist/vocab/diversity.js +89 -0
  112. package/dist/vocab/index.d.ts +22 -0
  113. package/dist/vocab/index.js +22 -0
  114. package/dist/vocab/merge.d.ts +9 -0
  115. package/dist/vocab/merge.js +104 -0
  116. package/dist/vocab/requires.d.ts +49 -0
  117. package/dist/vocab/requires.js +107 -0
  118. package/dist/vocab/resolver.d.ts +62 -0
  119. package/dist/vocab/resolver.js +149 -0
  120. package/dist/vocab/serve.d.ts +39 -0
  121. package/dist/vocab/serve.js +36 -0
  122. package/package.json +108 -0
  123. package/page/cockpit.css +114 -0
  124. package/page/cockpit.page.json +33 -0
  125. package/page/embed.html +40 -0
  126. package/page/mount.js +78 -0
  127. package/page/standalone.html +43 -0
  128. package/src/blackboard/family.test.ts +280 -0
  129. package/src/blackboard/family.ts +208 -0
  130. package/src/blackboard/index.ts +42 -0
  131. package/src/blackboard/schema.test.ts +60 -0
  132. package/src/blackboard/schema.ts +44 -0
  133. package/src/blackboard/store.test.ts +189 -0
  134. package/src/blackboard/store.ts +331 -0
  135. package/src/blackboard/test-db.ts +47 -0
  136. package/src/channel/auth.test.ts +64 -0
  137. package/src/channel/auth.ts +101 -0
  138. package/src/channel/clock.ts +14 -0
  139. package/src/channel/connection.ts +77 -0
  140. package/src/channel/dispatch.test.ts +83 -0
  141. package/src/channel/dispatch.ts +102 -0
  142. package/src/channel/hub.test.ts +335 -0
  143. package/src/channel/hub.ts +222 -0
  144. package/src/channel/index.ts +55 -0
  145. package/src/channel/registry.test.ts +73 -0
  146. package/src/channel/registry.ts +137 -0
  147. package/src/channel/ws-transport.test.ts +234 -0
  148. package/src/channel/ws-transport.ts +212 -0
  149. package/src/cockpit/boot.test.ts +374 -0
  150. package/src/cockpit/boot.ts +280 -0
  151. package/src/cockpit/fake-dom.ts +90 -0
  152. package/src/cockpit/index.ts +63 -0
  153. package/src/cockpit/relay-client.test.ts +359 -0
  154. package/src/cockpit/relay-client.ts +234 -0
  155. package/src/cockpit/render.test.ts +149 -0
  156. package/src/cockpit/render.ts +194 -0
  157. package/src/cockpit/terminal-session.test.ts +252 -0
  158. package/src/cockpit/terminal-session.ts +194 -0
  159. package/src/cockpit/view.test.ts +117 -0
  160. package/src/cockpit/view.ts +140 -0
  161. package/src/demand/c8-rest.test.ts +140 -0
  162. package/src/demand/c8-rest.ts +167 -0
  163. package/src/demand/index.ts +42 -0
  164. package/src/demand/model.test.ts +197 -0
  165. package/src/demand/model.ts +183 -0
  166. package/src/demand/taskdef.test.ts +85 -0
  167. package/src/demand/taskdef.ts +78 -0
  168. package/src/index.ts +17 -0
  169. package/src/presence/family.test.ts +252 -0
  170. package/src/presence/family.ts +205 -0
  171. package/src/presence/index.ts +26 -0
  172. package/src/presence/schema.test.ts +53 -0
  173. package/src/presence/schema.ts +34 -0
  174. package/src/presence/store.test.ts +190 -0
  175. package/src/presence/store.ts +287 -0
  176. package/src/presence/test-db.test.ts +57 -0
  177. package/src/presence/test-db.ts +47 -0
  178. package/src/protocol/conformance/corpus.test.ts +66 -0
  179. package/src/protocol/conformance/frames.ts +142 -0
  180. package/src/protocol/conformance/index.ts +29 -0
  181. package/src/protocol/conformance/malformed.ts +59 -0
  182. package/src/protocol/conformance/tokens.ts +70 -0
  183. package/src/protocol/conformance/vocab.ts +122 -0
  184. package/src/protocol/families.ts +54 -0
  185. package/src/protocol/frame.test.ts +116 -0
  186. package/src/protocol/frame.ts +171 -0
  187. package/src/protocol/hex.ts +28 -0
  188. package/src/protocol/index.ts +84 -0
  189. package/src/protocol/lanes.test.ts +82 -0
  190. package/src/protocol/lanes.ts +54 -0
  191. package/src/protocol/payloads.test.ts +91 -0
  192. package/src/protocol/payloads.ts +201 -0
  193. package/src/protocol/token.test.ts +57 -0
  194. package/src/protocol/token.ts +123 -0
  195. package/src/protocol/vocab/schema.test.ts +67 -0
  196. package/src/protocol/vocab/schema.ts +281 -0
  197. package/src/relay/incarnation.test.ts +53 -0
  198. package/src/relay/incarnation.ts +54 -0
  199. package/src/relay/index.ts +34 -0
  200. package/src/relay/integration.test.ts +135 -0
  201. package/src/relay/relay-family.test.ts +236 -0
  202. package/src/relay/relay-family.ts +336 -0
  203. package/src/relay/ring.test.ts +138 -0
  204. package/src/relay/ring.ts +136 -0
  205. package/src/relay/scheduler.test.ts +233 -0
  206. package/src/relay/scheduler.ts +208 -0
  207. package/src/relay/validate.test.ts +43 -0
  208. package/src/relay/validate.ts +44 -0
  209. package/src/transcript/index.ts +33 -0
  210. package/src/transcript/integration.test.ts +108 -0
  211. package/src/transcript/schema.test.ts +69 -0
  212. package/src/transcript/schema.ts +51 -0
  213. package/src/transcript/store.test.ts +285 -0
  214. package/src/transcript/store.ts +530 -0
  215. package/src/transcript/test-db.ts +46 -0
  216. package/src/vocab/core-vocab.test.ts +34 -0
  217. package/src/vocab/core-vocab.ts +88 -0
  218. package/src/vocab/diversity.test.ts +153 -0
  219. package/src/vocab/diversity.ts +169 -0
  220. package/src/vocab/index.ts +55 -0
  221. package/src/vocab/merge.test.ts +73 -0
  222. package/src/vocab/merge.ts +117 -0
  223. package/src/vocab/requires.test.ts +69 -0
  224. package/src/vocab/requires.ts +155 -0
  225. package/src/vocab/resolver.test.ts +118 -0
  226. package/src/vocab/resolver.ts +187 -0
  227. package/src/vocab/serve.test.ts +64 -0
  228. package/src/vocab/serve.ts +66 -0
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # @nanobpm/agentic
2
+
3
+ The **Nano agentic protocol** (ADR 0056): one app-tier channel carrying agent presence/registry, demand×supply, a shared blackboard and a live terminal relay. One published package, subpath exports per family.
4
+
5
+ ## Subpath exports
6
+
7
+ | Import | What |
8
+ | --- | --- |
9
+ | `@nanobpm/agentic/protocol` | Wire contract, frame codec, routing-token grammar, vocab schema (S0) |
10
+ | `@nanobpm/agentic/protocol/conformance` | Shared conformance corpus |
11
+ | `@nanobpm/agentic/channel` | App-tier channel & hub, connection registry, auth (S1) |
12
+ | `@nanobpm/agentic/presence` | Presence & registry family (S2) |
13
+ | `@nanobpm/agentic/vocab` | Vocab resolver + core vocabulary (S3) |
14
+ | `@nanobpm/agentic/demand` | Demand×supply model (S4) |
15
+ | `@nanobpm/agentic/relay` | Relay ring + QoS scheduler (S5) |
16
+ | `@nanobpm/agentic/transcript` | Transcript store, retention-by-lifecycle (S6) |
17
+ | `@nanobpm/agentic/blackboard` | Blackboard channel family (S7) |
18
+ | `@nanobpm/agentic/cockpit` | Operator visibility page — the cockpit (S8) |
19
+
20
+ The barrel `@nanobpm/agentic` re-exports each family as a namespace (`protocol`, `channel`, …). The worker-side client ships separately as `@nanobpm/urban-agent-client`.
21
+
22
+ The wire contract is the single source of truth; nothing here rides the Camunda-8 engine or its transport.
@@ -0,0 +1,40 @@
1
+ import type { AgenticHub, HubConnection } from "../channel/index.ts";
2
+ import type { BlackboardStore } from "./store.ts";
3
+ export interface BlackboardFamilyOptions {
4
+ /**
5
+ * Resolve the board `scope` for a connection. Default: the capability
6
+ * credential presented at the handshake (`handshake.credential` or the
7
+ * `?capability=` query param) — so each distinct capability is its own board,
8
+ * exactly as nano-workforce's per-plan token is. Return `undefined` to reject
9
+ * the frame (the connection is not scoped to any board).
10
+ */
11
+ scopeOf?: (ctx: HubConnection) => string | undefined;
12
+ /**
13
+ * Notified of a fault this module handles while keeping the connection: a
14
+ * malformed blackboard payload ({@link BlackboardPayloadError}), a missing
15
+ * board scope, or a store error. Other exceptions propagate to {@link AgenticHub}.
16
+ */
17
+ onError?: (err: unknown, connectionId?: string) => void;
18
+ }
19
+ /** Handle to the attached blackboard family. */
20
+ export interface BlackboardFamilyHandle {
21
+ /** Detach is a no-op today; present for symmetry with the other family modules. */
22
+ stop(): void;
23
+ }
24
+ /** A malformed blackboard payload rejected before it touches the store. */
25
+ export declare class BlackboardPayloadError extends Error {
26
+ constructor(detail: string);
27
+ }
28
+ /** Raised when a connection is not scoped to any board (no capability scope). */
29
+ export declare class BlackboardScopeError extends Error {
30
+ constructor();
31
+ }
32
+ /**
33
+ * Attach the blackboard family (`blackboard`) to `hub`, backed by `store`.
34
+ * Registers one handler via the S1 seam. Returns a handle for symmetry with the
35
+ * other family modules.
36
+ *
37
+ * @throws DuplicateFamilyHandlerError if the `blackboard` family already has a
38
+ * handler on this hub (one family, one owning module).
39
+ */
40
+ export declare function attachBlackboardFamily(hub: AgenticHub, store: BlackboardStore, options?: BlackboardFamilyOptions): BlackboardFamilyHandle;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The blackboard family module — S7's self-contained attachment to the S1 hub.
3
+ *
4
+ * This module owns the `blackboard` message family. It attaches through the hub's
5
+ * canonical {@link AgenticHub.registerFamilyHandler} seam — it does NOT touch a
6
+ * shared frame→family dispatch switch — so it composes with the other family
7
+ * modules (S2 presence, S5 relay) with no shared edit.
8
+ *
9
+ * It promotes nano-workforce's per-plan blackboard HTTP hook to a first-class,
10
+ * capability-scoped channel family. Two operations ride the one `blackboard`
11
+ * family, distinguished by `payload.op`:
12
+ *
13
+ * - `append` — idempotently write an entry (kinds `file-claim`,
14
+ * `constraint-change`, `scope-change`, `learning`, `note`). The reply carries
15
+ * `{ inserted, id, conflicts }`; for a `file-claim`, `conflicts` lists prior
16
+ * claims by OTHER authors on the same files (first-writer-wins, advisory).
17
+ * - `read` — return the board's entries after `since` plus the head `cursor`
18
+ * for incremental polling.
19
+ *
20
+ * The board `scope` is CAPABILITY-DERIVED: by default it is the connection's
21
+ * capability credential (the same credential the S1 handshake gated on — the
22
+ * per-board token nano-workforce's hook already uses as the credential). A
23
+ * connection can therefore only read/write the board its capability authorises,
24
+ * with no board id trusted from the payload. Override {@link BlackboardFamilyOptions.scopeOf}
25
+ * to derive the scope differently (e.g. from an ADR 0028 grant scope).
26
+ *
27
+ * Replies ride the control/facts lane so a blackboard write is never
28
+ * head-of-line-blocked behind a bulk-output storm (invariant 5).
29
+ */
30
+ import { validatePayload } from "../protocol/index.js";
31
+ /** A malformed blackboard payload rejected before it touches the store. */
32
+ export class BlackboardPayloadError extends Error {
33
+ constructor(detail) {
34
+ super(`invalid blackboard payload: ${detail}`);
35
+ this.name = "BlackboardPayloadError";
36
+ }
37
+ }
38
+ /** Raised when a connection is not scoped to any board (no capability scope). */
39
+ export class BlackboardScopeError extends Error {
40
+ constructor() {
41
+ super("connection is not scoped to any blackboard");
42
+ this.name = "BlackboardScopeError";
43
+ }
44
+ }
45
+ function isPlainObject(value) {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+ function readString(record, key) {
49
+ const value = record[key];
50
+ return typeof value === "string" ? value : undefined;
51
+ }
52
+ function readStringArray(record, key) {
53
+ const value = record[key];
54
+ if (!Array.isArray(value))
55
+ return undefined;
56
+ return value.filter((v) => typeof v === "string");
57
+ }
58
+ /** The default scope resolver: the connection's capability credential. */
59
+ function defaultScopeOf(ctx) {
60
+ const credential = ctx.handshake.credential ?? ctx.handshake.query?.capability;
61
+ const scope = credential?.trim();
62
+ return scope !== undefined && scope !== "" ? scope : undefined;
63
+ }
64
+ /**
65
+ * Attach the blackboard family (`blackboard`) to `hub`, backed by `store`.
66
+ * Registers one handler via the S1 seam. Returns a handle for symmetry with the
67
+ * other family modules.
68
+ *
69
+ * @throws DuplicateFamilyHandlerError if the `blackboard` family already has a
70
+ * handler on this hub (one family, one owning module).
71
+ */
72
+ export function attachBlackboardFamily(hub, store, options = {}) {
73
+ store.ensureSchema();
74
+ const onError = options.onError ?? (() => { });
75
+ const scopeOf = options.scopeOf ?? defaultScopeOf;
76
+ const reject = (connectionId, detail) => {
77
+ onError(new BlackboardPayloadError(detail), connectionId);
78
+ };
79
+ hub.registerFamilyHandler("blackboard", (frame, ctx) => {
80
+ const payload = frame.payload;
81
+ const result = validatePayload("blackboard", payload);
82
+ if (!result.ok) {
83
+ reject(ctx.id, result.errors.map((e) => e.message).join("; "));
84
+ return;
85
+ }
86
+ if (!isPlainObject(payload)) {
87
+ reject(ctx.id, "not an object");
88
+ return;
89
+ }
90
+ const scope = scopeOf(ctx);
91
+ if (scope === undefined) {
92
+ onError(new BlackboardScopeError(), ctx.id);
93
+ return;
94
+ }
95
+ const op = payload.op;
96
+ try {
97
+ if (op === "read") {
98
+ handleRead(store, ctx, frame, scope, payload);
99
+ return;
100
+ }
101
+ // validatePayload already constrained op ∈ {append, read}.
102
+ handleAppend(store, ctx, frame, scope, payload, reject);
103
+ }
104
+ catch (err) {
105
+ onError(err, ctx.id);
106
+ }
107
+ });
108
+ return {
109
+ stop() { },
110
+ };
111
+ }
112
+ function handleRead(store, ctx, frame, scope, payload) {
113
+ const rawSince = payload.since;
114
+ const since = typeof rawSince === "number" ? rawSince : undefined;
115
+ const page = store.readPage(scope, { since });
116
+ // `scope` is NOT echoed: under the default resolver it is the connection's
117
+ // capability credential (a secret), and it is server-derived so the client
118
+ // never needs it back for routing. Echoing it would only widen its exposure
119
+ // to logs/recordings.
120
+ ctx.send(reply(frame, { op: "read", cursor: page.cursor, entries: page.entries }));
121
+ }
122
+ function handleAppend(store, ctx, frame, scope, payload, reject) {
123
+ const body = readString(payload, "body");
124
+ if (body === undefined || body.trim() === "") {
125
+ reject(ctx.id, "append requires a non-empty body");
126
+ return;
127
+ }
128
+ const files = readStringArray(payload, "files");
129
+ const wave = typeof payload.wave === "number" ? payload.wave : undefined;
130
+ const input = {
131
+ authorTask: readString(payload, "authorTask"),
132
+ kind: payload.kind,
133
+ files,
134
+ body,
135
+ wave,
136
+ dedupeKey: readString(payload, "dedupeKey"),
137
+ };
138
+ const { inserted, id } = store.append(scope, input);
139
+ // Conflict-of-intent is reported only for a file-claim carrying files: prior
140
+ // claims by OTHER authors on the same files, decided by insertion order
141
+ // (beforeId = this row's id), exactly as the HTTP hook did.
142
+ let conflicts = [];
143
+ if (input.kind === "file-claim" && files && files.length > 0) {
144
+ conflicts = store.detectFileClaimConflicts(scope, { authorTask: input.authorTask, files, beforeId: id });
145
+ }
146
+ ctx.send(reply(frame, { op: "append", inserted, id, conflicts }));
147
+ }
148
+ /** Build a control-lane blackboard reply that echoes the request `seq` for correlation. */
149
+ function reply(request, payload) {
150
+ return { lane: "control", family: "blackboard", seq: request.seq, payload };
151
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @nanobpm/agentic-blackboard — the blackboard family for the Nano agentic
3
+ * protocol (ADR 0056, slice S7).
4
+ *
5
+ * Promotes nano-workforce's per-plan blackboard HTTP hook to a first-class,
6
+ * capability-scoped `blackboard` channel-message family: a durable per-scope
7
+ * coordination store ({@link BlackboardStore}) with idempotent `dedupeKey`
8
+ * append, `file-claim` conflict reporting and `since`/cursor incremental reads,
9
+ * plus a self-contained family module ({@link attachBlackboardFamily}) that
10
+ * attaches to the S1 hub through its `registerFamilyHandler(family, handler)`
11
+ * seam — never a shared dispatch switch. Any Urban app gets the blackboard for
12
+ * free by attaching this module.
13
+ *
14
+ * The wire contract lives in `@nanobpm/agentic-protocol`; the hub in
15
+ * `@nanobpm/agentic-channel`. This package builds on both.
16
+ */
17
+ export { BlackboardStore, isUniqueViolation, normalizeKind, systemClock, BLACKBOARD_KINDS, } from "./store.ts";
18
+ export type { BlackboardEntry, BlackboardInput, BlackboardKind, BlackboardPage, BlackboardStoreOptions, ClaimConflict, Clock, SqliteDb, } from "./store.ts";
19
+ export { attachBlackboardFamily, BlackboardPayloadError, BlackboardScopeError, } from "./family.ts";
20
+ export type { BlackboardFamilyOptions, BlackboardFamilyHandle } from "./family.ts";
21
+ export { BLACKBOARD_TABLE, BLACKBOARD_SCHEMA_SQL } from "./schema.ts";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @nanobpm/agentic-blackboard — the blackboard family for the Nano agentic
3
+ * protocol (ADR 0056, slice S7).
4
+ *
5
+ * Promotes nano-workforce's per-plan blackboard HTTP hook to a first-class,
6
+ * capability-scoped `blackboard` channel-message family: a durable per-scope
7
+ * coordination store ({@link BlackboardStore}) with idempotent `dedupeKey`
8
+ * append, `file-claim` conflict reporting and `since`/cursor incremental reads,
9
+ * plus a self-contained family module ({@link attachBlackboardFamily}) that
10
+ * attaches to the S1 hub through its `registerFamilyHandler(family, handler)`
11
+ * seam — never a shared dispatch switch. Any Urban app gets the blackboard for
12
+ * free by attaching this module.
13
+ *
14
+ * The wire contract lives in `@nanobpm/agentic-protocol`; the hub in
15
+ * `@nanobpm/agentic-channel`. This package builds on both.
16
+ */
17
+ export { BlackboardStore, isUniqueViolation, normalizeKind, systemClock, BLACKBOARD_KINDS, } from "./store.js";
18
+ export { attachBlackboardFamily, BlackboardPayloadError, BlackboardScopeError, } from "./family.js";
19
+ export { BLACKBOARD_TABLE, BLACKBOARD_SCHEMA_SQL } from "./schema.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The canonical blackboard schema.
3
+ *
4
+ * The DDL here is the single source of truth the {@link BlackboardStore} applies
5
+ * through {@link BlackboardStore.ensureSchema}. The very same statements are
6
+ * mirrored in the app-boot migration `db/migrations/003_agentic_blackboard.sql`
7
+ * (applied by the DataLayer migration runner). To keep those two application
8
+ * paths from drifting, `schema.test.ts` normalises both and asserts they are
9
+ * statement-for-statement identical — divergence is a red test, not a silent
10
+ * production/boot mismatch.
11
+ *
12
+ * The table promotes nano-workforce's per-plan `plan_blackboard` HTTP hook to a
13
+ * first-class channel-family store: the same idempotent-append (`dedupe_key`),
14
+ * conflict-of-intent (`file-claim`) and incremental-read (`id`/cursor) semantics,
15
+ * generalised from a hard-wired `plan_key` to a capability-derived `scope` so any
16
+ * Urban app gets it for free.
17
+ */
18
+ /** The blackboard table name. */
19
+ export declare const BLACKBOARD_TABLE = "agentic_blackboard";
20
+ /**
21
+ * The canonical blackboard DDL. Forward-only and additive; every column added
22
+ * here must also be added to the boot migration (the drift guard enforces it).
23
+ *
24
+ * Idempotency: the partial UNIQUE index over `(scope, dedupe_key)` (NULLs
25
+ * excluded, so a dedupe-less note always appends) collapses a re-POST carrying a
26
+ * stable `dedupe_key` to one row — the engine may re-activate a job on retry, so
27
+ * a repeated append must be a no-op. `(scope, id)` lists a board's entries in
28
+ * write order and drives the `since`/cursor incremental read.
29
+ */
30
+ export declare const BLACKBOARD_SCHEMA_SQL = "CREATE TABLE IF NOT EXISTS agentic_blackboard (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope TEXT NOT NULL,\n author_task TEXT NOT NULL DEFAULT 'system',\n kind TEXT NOT NULL DEFAULT 'note',\n files TEXT,\n body TEXT NOT NULL,\n wave INTEGER,\n dedupe_key TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS ux_agentic_blackboard_dedupe ON agentic_blackboard (scope, dedupe_key) WHERE dedupe_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_agentic_blackboard_scope ON agentic_blackboard (scope, id);";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The canonical blackboard schema.
3
+ *
4
+ * The DDL here is the single source of truth the {@link BlackboardStore} applies
5
+ * through {@link BlackboardStore.ensureSchema}. The very same statements are
6
+ * mirrored in the app-boot migration `db/migrations/003_agentic_blackboard.sql`
7
+ * (applied by the DataLayer migration runner). To keep those two application
8
+ * paths from drifting, `schema.test.ts` normalises both and asserts they are
9
+ * statement-for-statement identical — divergence is a red test, not a silent
10
+ * production/boot mismatch.
11
+ *
12
+ * The table promotes nano-workforce's per-plan `plan_blackboard` HTTP hook to a
13
+ * first-class channel-family store: the same idempotent-append (`dedupe_key`),
14
+ * conflict-of-intent (`file-claim`) and incremental-read (`id`/cursor) semantics,
15
+ * generalised from a hard-wired `plan_key` to a capability-derived `scope` so any
16
+ * Urban app gets it for free.
17
+ */
18
+ /** The blackboard table name. */
19
+ export const BLACKBOARD_TABLE = "agentic_blackboard";
20
+ /**
21
+ * The canonical blackboard DDL. Forward-only and additive; every column added
22
+ * here must also be added to the boot migration (the drift guard enforces it).
23
+ *
24
+ * Idempotency: the partial UNIQUE index over `(scope, dedupe_key)` (NULLs
25
+ * excluded, so a dedupe-less note always appends) collapses a re-POST carrying a
26
+ * stable `dedupe_key` to one row — the engine may re-activate a job on retry, so
27
+ * a repeated append must be a no-op. `(scope, id)` lists a board's entries in
28
+ * write order and drives the `since`/cursor incremental read.
29
+ */
30
+ export const BLACKBOARD_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS ${BLACKBOARD_TABLE} (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ scope TEXT NOT NULL,
33
+ author_task TEXT NOT NULL DEFAULT 'system',
34
+ kind TEXT NOT NULL DEFAULT 'note',
35
+ files TEXT,
36
+ body TEXT NOT NULL,
37
+ wave INTEGER,
38
+ dedupe_key TEXT,
39
+ created_at TEXT NOT NULL
40
+ );
41
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_${BLACKBOARD_TABLE}_dedupe ON ${BLACKBOARD_TABLE} (scope, dedupe_key) WHERE dedupe_key IS NOT NULL;
42
+ CREATE INDEX IF NOT EXISTS idx_${BLACKBOARD_TABLE}_scope ON ${BLACKBOARD_TABLE} (scope, id);`;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The minimal synchronous SQLite handle the store needs — structurally the same
3
+ * surface the Urban runtime's DataLayer exposes (`host.openSqlite`). Kept local
4
+ * so the store depends on a shape, not on the runtime package.
5
+ */
6
+ export interface SqliteDb {
7
+ /** Execute one or more statements with no result (DDL, migrations). */
8
+ exec(sql: string): void;
9
+ /** Run a parameterised statement, returning the changed-row count. */
10
+ run(sql: string, params?: unknown[]): {
11
+ changes: number;
12
+ lastInsertRowid: number | bigint;
13
+ };
14
+ /** Run a parameterised query, returning all rows as plain objects. */
15
+ all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
16
+ }
17
+ /** A monotonic wall clock, injectable for deterministic tests. */
18
+ export interface Clock {
19
+ now(): number;
20
+ }
21
+ /** The default clock: `Date.now()`. */
22
+ export declare const systemClock: Clock;
23
+ /** The recognised blackboard entry kinds (verbatim from nano-workforce). */
24
+ export declare const BLACKBOARD_KINDS: readonly ["file-claim", "constraint-change", "scope-change", "learning", "note"];
25
+ export type BlackboardKind = (typeof BLACKBOARD_KINDS)[number];
26
+ /** Coerce an arbitrary `kind` to a known value, defaulting to "note". */
27
+ export declare function normalizeKind(kind: unknown): BlackboardKind;
28
+ /** What a writer supplies to {@link BlackboardStore.append}. */
29
+ export interface BlackboardInput {
30
+ /** The writing task's slug (or "system" for host writes). */
31
+ readonly authorTask?: string;
32
+ /** The entry kind; unrecognised values normalise to "note". */
33
+ readonly kind?: unknown;
34
+ /** Repo-relative paths this entry concerns (feeds `file-claim` conflicts). */
35
+ readonly files?: readonly string[];
36
+ /** The human/agent-readable note. Required and non-blank. */
37
+ readonly body: string;
38
+ /** Optional wave index the writer was dispatched in. */
39
+ readonly wave?: number | null;
40
+ /** Idempotency key; a repeat append under the same `scope` is a no-op. */
41
+ readonly dedupeKey?: string;
42
+ }
43
+ /** The parsed, agent-facing view of an entry (files decoded to an array). */
44
+ export interface BlackboardEntry {
45
+ readonly id: number;
46
+ readonly authorTask: string;
47
+ readonly kind: string;
48
+ readonly files: string[];
49
+ readonly body: string;
50
+ readonly wave: number | null;
51
+ readonly createdAt: string;
52
+ }
53
+ /**
54
+ * An advisory conflict-of-intent: a sibling has already claimed a file this
55
+ * writer is about to claim. Reported per (file, prior claim) so the later
56
+ * claimer can back off, coordinate, or escalate. First-writer-wins is advisory
57
+ * only — the blackboard NEVER locks; merge-time gates are the real safety net.
58
+ */
59
+ export interface ClaimConflict {
60
+ readonly file: string;
61
+ readonly authorTask: string;
62
+ readonly id: number;
63
+ readonly body: string;
64
+ readonly createdAt: string;
65
+ }
66
+ /** One incremental read: entries after `since` (write order) plus the `cursor`. */
67
+ export interface BlackboardPage {
68
+ readonly entries: BlackboardEntry[];
69
+ /**
70
+ * The board's current head id — the true head even when `since` filters every
71
+ * entry out, so a caller that is fully caught up learns it is caught up
72
+ * (cursor unchanged). `0` for an empty board.
73
+ */
74
+ readonly cursor: number;
75
+ }
76
+ export interface BlackboardStoreOptions {
77
+ /** Injectable clock for deterministic `created_at` in tests. Default {@link systemClock}. */
78
+ readonly clock?: Clock;
79
+ }
80
+ /**
81
+ * True only for a UNIQUE / PRIMARY-KEY / duplicate violation — never a
82
+ * foreign-key or other constraint failure. We match the *specific* violation
83
+ * (extended SQLite codes, or the specific words) rather than the bare word
84
+ * "constraint", so a `FOREIGN KEY constraint failed` (real corruption, not a
85
+ * benign duplicate) is always rethrown rather than silently swallowed.
86
+ */
87
+ export declare function isUniqueViolation(err: unknown): boolean;
88
+ export declare class BlackboardStore {
89
+ #private;
90
+ constructor(db: SqliteDb, options?: BlackboardStoreOptions);
91
+ /**
92
+ * Apply the canonical blackboard DDL (idempotent). Callers that let the app
93
+ * DataLayer migration runner apply `db/migrations/003_agentic_blackboard.sql`
94
+ * do not need this — the family module calls it so the store is usable against
95
+ * a bare source too. The DDL is identical to the migration (drift-guarded).
96
+ */
97
+ ensureSchema(): void;
98
+ /**
99
+ * Append an entry to a board, idempotently. A blank `body` is rejected. When a
100
+ * `dedupeKey` is supplied and an entry already exists for it under this
101
+ * `scope`, the write is a no-op and the existing id is returned
102
+ * (`inserted: false`) — so an engine job retry re-appending the same fact
103
+ * never duplicates.
104
+ */
105
+ append(scope: string, input: BlackboardInput): {
106
+ inserted: boolean;
107
+ id: number;
108
+ };
109
+ /**
110
+ * One incremental read of a board: entries with `id > since` in write order,
111
+ * plus the board's current head `cursor`. An agent polling mid-flight passes
112
+ * `cursor` back as the next `since`, so it pulls only what siblings added since
113
+ * its last read.
114
+ */
115
+ readPage(scope: string, opts?: {
116
+ since?: number;
117
+ }): BlackboardPage;
118
+ /** A board's entries in write order (id asc). `since` returns only `id > since`. */
119
+ read(scope: string, opts?: {
120
+ since?: number;
121
+ }): BlackboardEntry[];
122
+ /**
123
+ * Prior `file-claim` entries by OTHER authors under this `scope` that overlap
124
+ * `files`. A writer's own earlier claim is never a conflict with itself. Pass
125
+ * `beforeId` to restrict to strictly prior claims (`id < beforeId`) — the
126
+ * family computes conflicts AFTER inserting its own claim and sets `beforeId`
127
+ * to that new id, so first-writer-wins is decided by insertion order and a
128
+ * sibling claim that raced in concurrently is still surfaced without matching
129
+ * the writer's own just-written row.
130
+ */
131
+ detectFileClaimConflicts(scope: string, opts: {
132
+ authorTask?: string;
133
+ files: readonly string[];
134
+ beforeId?: number;
135
+ }): ClaimConflict[];
136
+ /** Number of entries under a scope. */
137
+ count(scope: string): number;
138
+ }