@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.
- package/README.md +22 -0
- package/dist/blackboard/family.d.ts +40 -0
- package/dist/blackboard/family.js +151 -0
- package/dist/blackboard/index.d.ts +21 -0
- package/dist/blackboard/index.js +19 -0
- package/dist/blackboard/schema.d.ts +30 -0
- package/dist/blackboard/schema.js +42 -0
- package/dist/blackboard/store.d.ts +138 -0
- package/dist/blackboard/store.js +216 -0
- package/dist/blackboard/test-db.d.ts +5 -0
- package/dist/blackboard/test-db.js +42 -0
- package/dist/channel/auth.d.ts +41 -0
- package/dist/channel/auth.js +67 -0
- package/dist/channel/clock.d.ts +11 -0
- package/dist/channel/clock.js +4 -0
- package/dist/channel/connection.d.ts +75 -0
- package/dist/channel/connection.js +14 -0
- package/dist/channel/dispatch.d.ts +46 -0
- package/dist/channel/dispatch.js +86 -0
- package/dist/channel/hub.d.ts +78 -0
- package/dist/channel/hub.js +157 -0
- package/dist/channel/index.d.ts +27 -0
- package/dist/channel/index.js +20 -0
- package/dist/channel/registry.d.ts +68 -0
- package/dist/channel/registry.js +84 -0
- package/dist/channel/ws-transport.d.ts +23 -0
- package/dist/channel/ws-transport.js +178 -0
- package/dist/cockpit/boot.d.ts +68 -0
- package/dist/cockpit/boot.js +202 -0
- package/dist/cockpit/fake-dom.d.ts +37 -0
- package/dist/cockpit/fake-dom.js +73 -0
- package/dist/cockpit/index.d.ts +27 -0
- package/dist/cockpit/index.js +27 -0
- package/dist/cockpit/relay-client.d.ts +52 -0
- package/dist/cockpit/relay-client.js +192 -0
- package/dist/cockpit/render.d.ts +58 -0
- package/dist/cockpit/render.js +122 -0
- package/dist/cockpit/terminal-session.d.ts +95 -0
- package/dist/cockpit/terminal-session.js +123 -0
- package/dist/cockpit/view.d.ts +79 -0
- package/dist/cockpit/view.js +58 -0
- package/dist/demand/c8-rest.d.ts +77 -0
- package/dist/demand/c8-rest.js +123 -0
- package/dist/demand/index.d.ts +24 -0
- package/dist/demand/index.js +24 -0
- package/dist/demand/model.d.ts +68 -0
- package/dist/demand/model.js +118 -0
- package/dist/demand/taskdef.d.ts +40 -0
- package/dist/demand/taskdef.js +67 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/presence/family.d.ts +40 -0
- package/dist/presence/family.js +166 -0
- package/dist/presence/index.d.ts +19 -0
- package/dist/presence/index.js +17 -0
- package/dist/presence/schema.d.ts +20 -0
- package/dist/presence/schema.js +32 -0
- package/dist/presence/store.d.ts +130 -0
- package/dist/presence/store.js +191 -0
- package/dist/presence/test-db.d.ts +5 -0
- package/dist/presence/test-db.js +42 -0
- package/dist/protocol/conformance/frames.d.ts +24 -0
- package/dist/protocol/conformance/frames.js +116 -0
- package/dist/protocol/conformance/index.d.ts +13 -0
- package/dist/protocol/conformance/index.js +13 -0
- package/dist/protocol/conformance/malformed.d.ts +14 -0
- package/dist/protocol/conformance/malformed.js +44 -0
- package/dist/protocol/conformance/tokens.d.ts +19 -0
- package/dist/protocol/conformance/tokens.js +49 -0
- package/dist/protocol/conformance/vocab.d.ts +23 -0
- package/dist/protocol/conformance/vocab.js +97 -0
- package/dist/protocol/families.d.ts +32 -0
- package/dist/protocol/families.js +45 -0
- package/dist/protocol/frame.d.ts +45 -0
- package/dist/protocol/frame.js +114 -0
- package/dist/protocol/hex.d.ts +7 -0
- package/dist/protocol/hex.js +26 -0
- package/dist/protocol/index.d.ts +23 -0
- package/dist/protocol/index.js +23 -0
- package/dist/protocol/lanes.d.ts +36 -0
- package/dist/protocol/lanes.js +40 -0
- package/dist/protocol/payloads.d.ts +64 -0
- package/dist/protocol/payloads.js +122 -0
- package/dist/protocol/token.d.ts +34 -0
- package/dist/protocol/token.js +81 -0
- package/dist/protocol/vocab/schema.d.ts +51 -0
- package/dist/protocol/vocab/schema.js +218 -0
- package/dist/relay/incarnation.d.ts +17 -0
- package/dist/relay/incarnation.js +50 -0
- package/dist/relay/index.d.ts +25 -0
- package/dist/relay/index.js +22 -0
- package/dist/relay/relay-family.d.ts +68 -0
- package/dist/relay/relay-family.js +272 -0
- package/dist/relay/ring.d.ts +49 -0
- package/dist/relay/ring.js +105 -0
- package/dist/relay/scheduler.d.ts +72 -0
- package/dist/relay/scheduler.js +180 -0
- package/dist/relay/validate.d.ts +29 -0
- package/dist/relay/validate.js +39 -0
- package/dist/transcript/index.d.ts +18 -0
- package/dist/transcript/index.js +17 -0
- package/dist/transcript/schema.d.ts +32 -0
- package/dist/transcript/schema.js +48 -0
- package/dist/transcript/store.d.ts +192 -0
- package/dist/transcript/store.js +347 -0
- package/dist/transcript/test-db.d.ts +5 -0
- package/dist/transcript/test-db.js +41 -0
- package/dist/vocab/core-vocab.d.ts +26 -0
- package/dist/vocab/core-vocab.js +67 -0
- package/dist/vocab/diversity.d.ts +78 -0
- package/dist/vocab/diversity.js +89 -0
- package/dist/vocab/index.d.ts +22 -0
- package/dist/vocab/index.js +22 -0
- package/dist/vocab/merge.d.ts +9 -0
- package/dist/vocab/merge.js +104 -0
- package/dist/vocab/requires.d.ts +49 -0
- package/dist/vocab/requires.js +107 -0
- package/dist/vocab/resolver.d.ts +62 -0
- package/dist/vocab/resolver.js +149 -0
- package/dist/vocab/serve.d.ts +39 -0
- package/dist/vocab/serve.js +36 -0
- package/package.json +108 -0
- package/page/cockpit.css +114 -0
- package/page/cockpit.page.json +33 -0
- package/page/embed.html +40 -0
- package/page/mount.js +78 -0
- package/page/standalone.html +43 -0
- package/src/blackboard/family.test.ts +280 -0
- package/src/blackboard/family.ts +208 -0
- package/src/blackboard/index.ts +42 -0
- package/src/blackboard/schema.test.ts +60 -0
- package/src/blackboard/schema.ts +44 -0
- package/src/blackboard/store.test.ts +189 -0
- package/src/blackboard/store.ts +331 -0
- package/src/blackboard/test-db.ts +47 -0
- package/src/channel/auth.test.ts +64 -0
- package/src/channel/auth.ts +101 -0
- package/src/channel/clock.ts +14 -0
- package/src/channel/connection.ts +77 -0
- package/src/channel/dispatch.test.ts +83 -0
- package/src/channel/dispatch.ts +102 -0
- package/src/channel/hub.test.ts +335 -0
- package/src/channel/hub.ts +222 -0
- package/src/channel/index.ts +55 -0
- package/src/channel/registry.test.ts +73 -0
- package/src/channel/registry.ts +137 -0
- package/src/channel/ws-transport.test.ts +234 -0
- package/src/channel/ws-transport.ts +212 -0
- package/src/cockpit/boot.test.ts +374 -0
- package/src/cockpit/boot.ts +280 -0
- package/src/cockpit/fake-dom.ts +90 -0
- package/src/cockpit/index.ts +63 -0
- package/src/cockpit/relay-client.test.ts +359 -0
- package/src/cockpit/relay-client.ts +234 -0
- package/src/cockpit/render.test.ts +149 -0
- package/src/cockpit/render.ts +194 -0
- package/src/cockpit/terminal-session.test.ts +252 -0
- package/src/cockpit/terminal-session.ts +194 -0
- package/src/cockpit/view.test.ts +117 -0
- package/src/cockpit/view.ts +140 -0
- package/src/demand/c8-rest.test.ts +140 -0
- package/src/demand/c8-rest.ts +167 -0
- package/src/demand/index.ts +42 -0
- package/src/demand/model.test.ts +197 -0
- package/src/demand/model.ts +183 -0
- package/src/demand/taskdef.test.ts +85 -0
- package/src/demand/taskdef.ts +78 -0
- package/src/index.ts +17 -0
- package/src/presence/family.test.ts +252 -0
- package/src/presence/family.ts +205 -0
- package/src/presence/index.ts +26 -0
- package/src/presence/schema.test.ts +53 -0
- package/src/presence/schema.ts +34 -0
- package/src/presence/store.test.ts +190 -0
- package/src/presence/store.ts +287 -0
- package/src/presence/test-db.test.ts +57 -0
- package/src/presence/test-db.ts +47 -0
- package/src/protocol/conformance/corpus.test.ts +66 -0
- package/src/protocol/conformance/frames.ts +142 -0
- package/src/protocol/conformance/index.ts +29 -0
- package/src/protocol/conformance/malformed.ts +59 -0
- package/src/protocol/conformance/tokens.ts +70 -0
- package/src/protocol/conformance/vocab.ts +122 -0
- package/src/protocol/families.ts +54 -0
- package/src/protocol/frame.test.ts +116 -0
- package/src/protocol/frame.ts +171 -0
- package/src/protocol/hex.ts +28 -0
- package/src/protocol/index.ts +84 -0
- package/src/protocol/lanes.test.ts +82 -0
- package/src/protocol/lanes.ts +54 -0
- package/src/protocol/payloads.test.ts +91 -0
- package/src/protocol/payloads.ts +201 -0
- package/src/protocol/token.test.ts +57 -0
- package/src/protocol/token.ts +123 -0
- package/src/protocol/vocab/schema.test.ts +67 -0
- package/src/protocol/vocab/schema.ts +281 -0
- package/src/relay/incarnation.test.ts +53 -0
- package/src/relay/incarnation.ts +54 -0
- package/src/relay/index.ts +34 -0
- package/src/relay/integration.test.ts +135 -0
- package/src/relay/relay-family.test.ts +236 -0
- package/src/relay/relay-family.ts +336 -0
- package/src/relay/ring.test.ts +138 -0
- package/src/relay/ring.ts +136 -0
- package/src/relay/scheduler.test.ts +233 -0
- package/src/relay/scheduler.ts +208 -0
- package/src/relay/validate.test.ts +43 -0
- package/src/relay/validate.ts +44 -0
- package/src/transcript/index.ts +33 -0
- package/src/transcript/integration.test.ts +108 -0
- package/src/transcript/schema.test.ts +69 -0
- package/src/transcript/schema.ts +51 -0
- package/src/transcript/store.test.ts +285 -0
- package/src/transcript/store.ts +530 -0
- package/src/transcript/test-db.ts +46 -0
- package/src/vocab/core-vocab.test.ts +34 -0
- package/src/vocab/core-vocab.ts +88 -0
- package/src/vocab/diversity.test.ts +153 -0
- package/src/vocab/diversity.ts +169 -0
- package/src/vocab/index.ts +55 -0
- package/src/vocab/merge.test.ts +73 -0
- package/src/vocab/merge.ts +117 -0
- package/src/vocab/requires.test.ts +69 -0
- package/src/vocab/requires.ts +155 -0
- package/src/vocab/resolver.test.ts +118 -0
- package/src/vocab/resolver.ts +187 -0
- package/src/vocab/serve.test.ts +64 -0
- 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
|
+
}
|