@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
@@ -0,0 +1,58 @@
1
+ function tokenRow(token) {
2
+ return {
3
+ token: token.token,
4
+ supply: token.supply,
5
+ instances: token.instances,
6
+ satisfied: token.satisfied,
7
+ status: token.satisfied ? "green" : "red",
8
+ };
9
+ }
10
+ function networkRow(network) {
11
+ return {
12
+ network: network.network,
13
+ tokens: network.tokens.map(tokenRow),
14
+ missing: network.missing,
15
+ status: network.missing.length > 0 ? "red" : "green",
16
+ };
17
+ }
18
+ function missingLight(token) {
19
+ return {
20
+ id: `missing:${token}`,
21
+ label: token,
22
+ status: "red",
23
+ detail: "missing agent type — no registered worker serves this token",
24
+ };
25
+ }
26
+ function diversityDetail(diversity) {
27
+ const offenders = diversity.roles.filter((role) => role.status !== "green");
28
+ if (offenders.length === 0)
29
+ return "family(#red) ≠ family(#blue) holds for every role";
30
+ return offenders
31
+ .map((role) => `${role.token}: ${role.collidingFamilies.join(", ")} (${role.status})`)
32
+ .join("; ");
33
+ }
34
+ function diversityLight(diversity) {
35
+ return {
36
+ id: "diversity",
37
+ label: "diversity SLO",
38
+ status: diversity.status,
39
+ detail: diversityDetail(diversity),
40
+ };
41
+ }
42
+ /**
43
+ * Derive the renderable cockpit view from an S4 demand×supply report.
44
+ *
45
+ * Pure and total: every list is already sorted by S4, so the derived view is
46
+ * stable and diff-friendly; no input mutates and no I/O happens.
47
+ */
48
+ export function cockpitView(report) {
49
+ return {
50
+ status: report.status,
51
+ networks: report.networks.map(networkRow),
52
+ missing: report.missing,
53
+ missingLights: report.missing.map(missingLight),
54
+ diversity: report.diversity,
55
+ diversityLight: diversityLight(report.diversity),
56
+ nonAgentic: report.nonAgentic,
57
+ };
58
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Read deployed models' `taskDefinition` leaves from the engine's C8 REST API.
3
+ *
4
+ * S4 is a read-only mirror: it reads what the *engine* already has deployed and
5
+ * diffs it against live supply — it never places work. The Camunda-8 engine stays
6
+ * frozen (design invariant 2): we speak the ordinary C8 v2 REST API over the
7
+ * app's own HTTP client, on a connection entirely separate from the agentic
8
+ * channel and from the worker's C8 job protocol. Nothing here rides the engine's
9
+ * transport or mutates engine state.
10
+ *
11
+ * The concrete reader ({@link httpC8RestReader}) is a thin fetch adapter over two
12
+ * C8 v2 endpoints:
13
+ *
14
+ * - `POST {rest}/process-definitions/search` → the deployed definition keys.
15
+ * - `GET {rest}/process-definitions/{key}/xml` → each definition's BPMN XML.
16
+ *
17
+ * It is expressed against a small structural {@link C8RestReader} seam so the
18
+ * model can be driven by an in-memory reader in tests without a live engine.
19
+ */
20
+ import { type TaskDefinitionLeaf } from "./taskdef.ts";
21
+ /**
22
+ * The minimal C8 REST surface the demand reader needs: enumerate deployed
23
+ * process-definition keys and fetch each one's BPMN XML. Structural so tests can
24
+ * supply an in-memory implementation.
25
+ */
26
+ export interface C8RestReader {
27
+ /** Every deployed process-definition key currently known to the engine. */
28
+ searchProcessDefinitionKeys(): Promise<string[]>;
29
+ /** The BPMN XML of one process definition. */
30
+ getProcessDefinitionXml(processDefinitionKey: string): Promise<string>;
31
+ }
32
+ /** A `fetch`-shaped function, so the HTTP reader is testable without the global. */
33
+ export type FetchLike = (input: string, init?: {
34
+ method?: string;
35
+ headers?: Record<string, string>;
36
+ body?: string;
37
+ }) => Promise<{
38
+ ok: boolean;
39
+ status: number;
40
+ statusText: string;
41
+ json(): Promise<unknown>;
42
+ text(): Promise<string>;
43
+ }>;
44
+ /** Options for {@link httpC8RestReader}. */
45
+ export interface HttpC8RestReaderOptions {
46
+ /** The C8 REST base address, e.g. `http://localhost:8080/v2`. */
47
+ readonly restAddress: string;
48
+ /** Optional bearer token for the C8 REST API. */
49
+ readonly token?: string;
50
+ /** Injected `fetch` (defaults to the global). */
51
+ readonly fetch?: FetchLike;
52
+ /** Search page size (C8 caps the result set; the reader pages until drained). Must be a positive integer. */
53
+ readonly pageSize?: number;
54
+ }
55
+ /**
56
+ * Build a {@link C8RestReader} backed by the live C8 v2 REST API.
57
+ *
58
+ * `searchProcessDefinitionKeys` pages `POST /process-definitions/search` until the
59
+ * engine returns fewer than a full page, de-duplicating keys. `getProcessDefinitionXml`
60
+ * reads `GET /process-definitions/{key}/xml`. Both raise on a non-2xx response so a
61
+ * misconfigured endpoint fails loudly rather than reporting phantom zero demand.
62
+ */
63
+ export declare function httpC8RestReader(options: HttpC8RestReaderOptions): C8RestReader;
64
+ /**
65
+ * Read every deployed model's `taskDefinition` leaves through a {@link C8RestReader}.
66
+ *
67
+ * Enumerates the deployed definitions, fetches each one's BPMN XML, and scans out
68
+ * its service-task task-definition leaves. The result is the raw demand corpus the
69
+ * {@link ./model.ts} model buckets and diffs against supply.
70
+ */
71
+ export declare function readDeployedTaskDefinitions(reader: C8RestReader): Promise<TaskDefinitionLeaf[]>;
72
+ /**
73
+ * Read the distinct demanded job types (routing tokens) from the engine — the
74
+ * convenience path for callers that only need the demand token set, not element
75
+ * provenance.
76
+ */
77
+ export declare function readDeployedTaskTypes(reader: C8RestReader): Promise<string[]>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Read deployed models' `taskDefinition` leaves from the engine's C8 REST API.
3
+ *
4
+ * S4 is a read-only mirror: it reads what the *engine* already has deployed and
5
+ * diffs it against live supply — it never places work. The Camunda-8 engine stays
6
+ * frozen (design invariant 2): we speak the ordinary C8 v2 REST API over the
7
+ * app's own HTTP client, on a connection entirely separate from the agentic
8
+ * channel and from the worker's C8 job protocol. Nothing here rides the engine's
9
+ * transport or mutates engine state.
10
+ *
11
+ * The concrete reader ({@link httpC8RestReader}) is a thin fetch adapter over two
12
+ * C8 v2 endpoints:
13
+ *
14
+ * - `POST {rest}/process-definitions/search` → the deployed definition keys.
15
+ * - `GET {rest}/process-definitions/{key}/xml` → each definition's BPMN XML.
16
+ *
17
+ * It is expressed against a small structural {@link C8RestReader} seam so the
18
+ * model can be driven by an in-memory reader in tests without a live engine.
19
+ */
20
+ import { distinctTaskTypes, scanTaskDefinitions } from "./taskdef.js";
21
+ function trimSlash(address) {
22
+ return address.endsWith("/") ? address.slice(0, -1) : address;
23
+ }
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function asKey(item) {
28
+ if (!isRecord(item))
29
+ return undefined;
30
+ const key = item.processDefinitionKey;
31
+ if (typeof key === "string")
32
+ return key;
33
+ if (typeof key === "number")
34
+ return String(key);
35
+ return undefined;
36
+ }
37
+ function itemsOf(body) {
38
+ if (!isRecord(body))
39
+ return [];
40
+ return Array.isArray(body.items) ? body.items : [];
41
+ }
42
+ /**
43
+ * Build a {@link C8RestReader} backed by the live C8 v2 REST API.
44
+ *
45
+ * `searchProcessDefinitionKeys` pages `POST /process-definitions/search` until the
46
+ * engine returns fewer than a full page, de-duplicating keys. `getProcessDefinitionXml`
47
+ * reads `GET /process-definitions/{key}/xml`. Both raise on a non-2xx response so a
48
+ * misconfigured endpoint fails loudly rather than reporting phantom zero demand.
49
+ */
50
+ export function httpC8RestReader(options) {
51
+ const base = trimSlash(options.restAddress);
52
+ const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
53
+ const pageSize = options.pageSize ?? 100;
54
+ if (!Number.isInteger(pageSize) || pageSize <= 0) {
55
+ throw new Error(`httpC8RestReader: pageSize must be a positive integer, got ${pageSize}`);
56
+ }
57
+ const headers = { "content-type": "application/json" };
58
+ if (options.token !== undefined)
59
+ headers.authorization = `Bearer ${options.token}`;
60
+ return {
61
+ async searchProcessDefinitionKeys() {
62
+ const keys = [];
63
+ const seen = new Set();
64
+ let from = 0;
65
+ for (;;) {
66
+ const response = await doFetch(`${base}/process-definitions/search`, {
67
+ method: "POST",
68
+ headers,
69
+ body: JSON.stringify({ page: { from, limit: pageSize } }),
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error(`C8 REST process-definitions/search failed: ${response.status} ${response.statusText}`);
73
+ }
74
+ const items = itemsOf(await response.json());
75
+ for (const item of items) {
76
+ const key = asKey(item);
77
+ if (key !== undefined && !seen.has(key)) {
78
+ seen.add(key);
79
+ keys.push(key);
80
+ }
81
+ }
82
+ if (items.length < pageSize)
83
+ break;
84
+ from += items.length;
85
+ }
86
+ return keys;
87
+ },
88
+ async getProcessDefinitionXml(processDefinitionKey) {
89
+ const response = await doFetch(`${base}/process-definitions/${encodeURIComponent(processDefinitionKey)}/xml`, {
90
+ method: "GET",
91
+ headers,
92
+ });
93
+ if (!response.ok) {
94
+ throw new Error(`C8 REST process-definitions/${processDefinitionKey}/xml failed: ${response.status} ${response.statusText}`);
95
+ }
96
+ return response.text();
97
+ },
98
+ };
99
+ }
100
+ /**
101
+ * Read every deployed model's `taskDefinition` leaves through a {@link C8RestReader}.
102
+ *
103
+ * Enumerates the deployed definitions, fetches each one's BPMN XML, and scans out
104
+ * its service-task task-definition leaves. The result is the raw demand corpus the
105
+ * {@link ./model.ts} model buckets and diffs against supply.
106
+ */
107
+ export async function readDeployedTaskDefinitions(reader) {
108
+ const keys = await reader.searchProcessDefinitionKeys();
109
+ const leaves = [];
110
+ for (const key of keys) {
111
+ const xml = await reader.getProcessDefinitionXml(key);
112
+ leaves.push(...scanTaskDefinitions(xml));
113
+ }
114
+ return leaves;
115
+ }
116
+ /**
117
+ * Read the distinct demanded job types (routing tokens) from the engine — the
118
+ * convenience path for callers that only need the demand token set, not element
119
+ * provenance.
120
+ */
121
+ export async function readDeployedTaskTypes(reader) {
122
+ return distinctTaskTypes(await readDeployedTaskDefinitions(reader));
123
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @nanobpm/agentic-demand — the demand×supply model for the Nano agentic
3
+ * protocol (ADR 0056, slice S4).
4
+ *
5
+ * A **read-only mirror + enrolment gate**: it reads the deployed models'
6
+ * `taskDefinition` leaves from the engine's C8 REST API ({@link httpC8RestReader},
7
+ * {@link readDeployedTaskDefinitions}), buckets them by network prefix, and diffs
8
+ * that demand against the live supply — the S2 presence registry resolved through
9
+ * the S3 vocabulary ({@link computeDemandSupply}) — to surface, per network, the
10
+ * *missing agent types* (`demand ∖ supply`) and the SLO state (missing-agent RED
11
+ * folded together with the S3 diversity SLO).
12
+ *
13
+ * It does NOT match-make: it never places work on a worker or holds a seat's job.
14
+ * Active placement is explicitly out of scope for v1. Nothing here rides the
15
+ * Camunda-8 engine or its transport — the C8 REST read is an ordinary read over a
16
+ * separate connection; the engine and the C8 job protocol stay frozen.
17
+ *
18
+ * The wire contract (routing-token grammar, `demand` payload) lives in
19
+ * `@nanobpm/agentic-protocol`; the registry rows and vocab resolver come from
20
+ * `@nanobpm/agentic-vocab`. This package builds on both and never redefines them.
21
+ */
22
+ export { scanTaskDefinitions, distinctTaskTypes, type TaskDefinitionLeaf } from "./taskdef.ts";
23
+ export { httpC8RestReader, readDeployedTaskDefinitions, readDeployedTaskTypes, type C8RestReader, type FetchLike, type HttpC8RestReaderOptions, } from "./c8-rest.ts";
24
+ export { computeDemandSupply, toDemandPayloads, type DemandSupplyInput, type DemandSupplyReport, type NetworkDemand, type TokenDemand, type SloStatus, } from "./model.ts";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @nanobpm/agentic-demand — the demand×supply model for the Nano agentic
3
+ * protocol (ADR 0056, slice S4).
4
+ *
5
+ * A **read-only mirror + enrolment gate**: it reads the deployed models'
6
+ * `taskDefinition` leaves from the engine's C8 REST API ({@link httpC8RestReader},
7
+ * {@link readDeployedTaskDefinitions}), buckets them by network prefix, and diffs
8
+ * that demand against the live supply — the S2 presence registry resolved through
9
+ * the S3 vocabulary ({@link computeDemandSupply}) — to surface, per network, the
10
+ * *missing agent types* (`demand ∖ supply`) and the SLO state (missing-agent RED
11
+ * folded together with the S3 diversity SLO).
12
+ *
13
+ * It does NOT match-make: it never places work on a worker or holds a seat's job.
14
+ * Active placement is explicitly out of scope for v1. Nothing here rides the
15
+ * Camunda-8 engine or its transport — the C8 REST read is an ordinary read over a
16
+ * separate connection; the engine and the C8 job protocol stay frozen.
17
+ *
18
+ * The wire contract (routing-token grammar, `demand` payload) lives in
19
+ * `@nanobpm/agentic-protocol`; the registry rows and vocab resolver come from
20
+ * `@nanobpm/agentic-vocab`. This package builds on both and never redefines them.
21
+ */
22
+ export { scanTaskDefinitions, distinctTaskTypes } from "./taskdef.js";
23
+ export { httpC8RestReader, readDeployedTaskDefinitions, readDeployedTaskTypes, } from "./c8-rest.js";
24
+ export { computeDemandSupply, toDemandPayloads, } from "./model.js";
@@ -0,0 +1,68 @@
1
+ import type { DemandPayload } from "../protocol/index.ts";
2
+ import { type DiversityReport, type RegisteredWorker, type VocabResolver } from "../vocab/index.ts";
3
+ import { type TaskDefinitionLeaf } from "./taskdef.ts";
4
+ /** The SLO grade — same three-level scale as the S3 diversity SLO. */
5
+ export type SloStatus = "green" | "amber" | "red";
6
+ /** One demanded routing token and the live supply against it. */
7
+ export interface TokenDemand {
8
+ /** The demanded routing token (a `taskDefinition` leaf's type). */
9
+ readonly token: string;
10
+ /** How many registered workers currently serve this token. */
11
+ readonly supply: number;
12
+ /** The instances serving it, sorted — provenance for the cockpit drill-in. */
13
+ readonly instances: readonly string[];
14
+ /** False when no registered worker serves it — a missing agent type. */
15
+ readonly satisfied: boolean;
16
+ }
17
+ /** The demand×supply picture for one network prefix. */
18
+ export interface NetworkDemand {
19
+ /** The network prefix bucket (`network` segment, or a bare token's own name). */
20
+ readonly network: string;
21
+ /** Every demanded token in the bucket, sorted by token. */
22
+ readonly tokens: readonly TokenDemand[];
23
+ /** The demanded tokens in this bucket with zero supply, sorted. */
24
+ readonly missing: readonly string[];
25
+ }
26
+ /** The full demand×supply report. */
27
+ export interface DemandSupplyReport {
28
+ /** Per-network demand×supply, sorted by network. */
29
+ readonly networks: readonly NetworkDemand[];
30
+ /** Every missing agent type across all networks, sorted and de-duplicated. */
31
+ readonly missing: readonly string[];
32
+ /** The S3 diversity SLO over the correlated live registry. */
33
+ readonly diversity: DiversityReport;
34
+ /** The overall SLO state: worst of the missing-agent signal and diversity. */
35
+ readonly status: SloStatus;
36
+ /**
37
+ * Deployed `taskDefinition` types that are NOT valid routing tokens — ordinary
38
+ * (non-agentic) C8 jobs the engine also runs. Surfaced (not silently dropped)
39
+ * so an operator can spot a mistyped agentic token, but excluded from the
40
+ * agentic demand×supply accounting.
41
+ */
42
+ readonly nonAgentic: readonly string[];
43
+ }
44
+ /** Input to {@link computeDemandSupply}. */
45
+ export interface DemandSupplyInput {
46
+ /** Demand: the deployed `taskDefinition` leaves (order-insensitive). */
47
+ readonly taskDefinitions: readonly TaskDefinitionLeaf[];
48
+ /** Supply: the live S2 registry rows. */
49
+ readonly workers: readonly RegisteredWorker[];
50
+ /** The S3 resolver over the resolved vocabulary artifact. */
51
+ readonly resolver: VocabResolver;
52
+ }
53
+ /**
54
+ * Compute the demand×supply model from deployed demand and live supply.
55
+ *
56
+ * Pure and deterministic: the same demand + registry + vocab always yields the
57
+ * same report, with every list sorted, so it is safe to diff frame-to-frame and
58
+ * to render straight into the cockpit.
59
+ */
60
+ export declare function computeDemandSupply(input: DemandSupplyInput): DemandSupplyReport;
61
+ /**
62
+ * Project the report onto the S0 `demand` message family — one {@link DemandPayload}
63
+ * per network carrying its missing agent types, so the gap can cross the agentic
64
+ * channel's control/facts lane. Emits an entry for every network (missing may be
65
+ * empty), sorted by network, so a consumer sees a cleared network flip from a
66
+ * non-empty `missing` back to `[]`.
67
+ */
68
+ export declare function toDemandPayloads(report: DemandSupplyReport): DemandPayload[];
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The demand×supply model — S4.
3
+ *
4
+ * Given the demand (the routing tokens deployed models ask for, as
5
+ * `taskDefinition` leaves) and the supply (the live S2 registry, resolved to the
6
+ * SERVE tokens each worker fills through the S3 vocab), this computes, per
7
+ * network:
8
+ *
9
+ * - **demand** — the distinct routing tokens the deployed models declare,
10
+ * bucketed by the token's network prefix;
11
+ * - **supply** — how many registered workers currently serve each token;
12
+ * - **missing agent type** — `demand ∖ supply`: a demanded token no registered
13
+ * worker can fill (the enrolment gap the cockpit lights up red);
14
+ * - **the SLO state** — the worst of the missing-agent signal (a missing token
15
+ * is RED) and the S3 diversity SLO (`family(#red) ≠ family(#blue)`).
16
+ *
17
+ * This is a **read-only mirror + enrolment gate**. It reads what the engine has
18
+ * deployed and what the registry reports and reconciles the two; it does NOT
19
+ * match-make — it never places work on, or holds a seat for, a worker. Active
20
+ * placement is explicitly out of scope for v1.
21
+ *
22
+ * Supply resolution reuses the S3 {@link VocabResolver}: a worker *serves* a token
23
+ * iff that token is in `resolver.resolve(worker.capability).tokens` — capability
24
+ * is the enrolment attribute the vocab's `requires` gate reads, never part of the
25
+ * token itself (design invariant 3).
26
+ */
27
+ import { parseToken } from "../protocol/index.js";
28
+ import { correlateRegistry } from "../vocab/index.js";
29
+ import { distinctTaskTypes } from "./taskdef.js";
30
+ const SEVERITY = { green: 0, amber: 1, red: 2 };
31
+ function worst(a, b) {
32
+ return SEVERITY[a] >= SEVERITY[b] ? a : b;
33
+ }
34
+ /** A demanded token's network-prefix bucket, or `undefined` if not a routing token. */
35
+ function bucketOf(token) {
36
+ try {
37
+ const parsed = parseToken(token);
38
+ // A bare (network-less) token like `decide` buckets under its own role name.
39
+ return parsed.network ?? parsed.role;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function sortedUnique(values) {
46
+ return [...new Set(values)].sort();
47
+ }
48
+ /**
49
+ * Compute the demand×supply model from deployed demand and live supply.
50
+ *
51
+ * Pure and deterministic: the same demand + registry + vocab always yields the
52
+ * same report, with every list sorted, so it is safe to diff frame-to-frame and
53
+ * to render straight into the cockpit.
54
+ */
55
+ export function computeDemandSupply(input) {
56
+ const { workers, resolver } = input;
57
+ // Supply index: each worker's SERVE token set, resolved once.
58
+ const supplyByToken = new Map();
59
+ for (const worker of workers) {
60
+ const tokens = resolver.resolve(worker.capability).tokens;
61
+ for (const token of tokens) {
62
+ const instances = supplyByToken.get(token);
63
+ if (instances === undefined)
64
+ supplyByToken.set(token, [worker.instance]);
65
+ else
66
+ instances.push(worker.instance);
67
+ }
68
+ }
69
+ const demandTokens = distinctTaskTypes(input.taskDefinitions);
70
+ const byNetwork = new Map();
71
+ const nonAgentic = [];
72
+ for (const token of demandTokens) {
73
+ const network = bucketOf(token);
74
+ if (network === undefined) {
75
+ nonAgentic.push(token);
76
+ continue;
77
+ }
78
+ const instances = sortedUnique(supplyByToken.get(token) ?? []);
79
+ const demand = {
80
+ token,
81
+ supply: instances.length,
82
+ instances,
83
+ satisfied: instances.length > 0,
84
+ };
85
+ const bucket = byNetwork.get(network) ?? new Map();
86
+ // A token can only appear once (distinctTaskTypes), so no merge is needed.
87
+ bucket.set(token, demand);
88
+ byNetwork.set(network, bucket);
89
+ }
90
+ const networks = [...byNetwork.entries()]
91
+ .map(([network, tokenMap]) => {
92
+ const tokens = [...tokenMap.values()].sort((a, b) => (a.token < b.token ? -1 : a.token > b.token ? 1 : 0));
93
+ const missing = tokens.filter((token) => !token.satisfied).map((token) => token.token);
94
+ return { network, tokens, missing };
95
+ })
96
+ .sort((a, b) => (a.network < b.network ? -1 : a.network > b.network ? 1 : 0));
97
+ const missing = sortedUnique(networks.flatMap((network) => network.missing));
98
+ const diversity = correlateRegistry(resolver, workers);
99
+ const missingStatus = missing.length > 0 ? "red" : "green";
100
+ const status = worst(missingStatus, diversity.status);
101
+ return {
102
+ networks,
103
+ missing,
104
+ diversity,
105
+ status,
106
+ nonAgentic: [...nonAgentic].sort(),
107
+ };
108
+ }
109
+ /**
110
+ * Project the report onto the S0 `demand` message family — one {@link DemandPayload}
111
+ * per network carrying its missing agent types, so the gap can cross the agentic
112
+ * channel's control/facts lane. Emits an entry for every network (missing may be
113
+ * empty), sorted by network, so a consumer sees a cleared network flip from a
114
+ * non-empty `missing` back to `[]`.
115
+ */
116
+ export function toDemandPayloads(report) {
117
+ return report.networks.map((network) => ({ network: network.network, missing: network.missing }));
118
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Parse `taskDefinition` leaves out of a deployed BPMN model.
3
+ *
4
+ * The demand side of the demand×supply model (S4) is the set of routing tokens
5
+ * the deployed processes ask for. In a Nano/Camunda-8 model each demand is a
6
+ * service task's `<zeebe:taskDefinition type="…">` — the job type the engine
7
+ * matches 1:1 against a worker's routing token. This scanner reads those leaves
8
+ * out of the raw BPMN XML so the model can bucket them by network prefix and
9
+ * diff them against live supply.
10
+ *
11
+ * It is deliberately a small, dependency-free regex scan over the same surface
12
+ * the Urban toolkit's worker-io deriver reads, so it stays app-tier and never
13
+ * touches the engine internals — the XML arrives from the C8 REST API
14
+ * ({@link ./c8-rest.ts}).
15
+ */
16
+ /** One deployed `taskDefinition` leaf: a job type demanded by a model element. */
17
+ export interface TaskDefinitionLeaf {
18
+ /** The `zeebe:taskDefinition` `type` — the routing token the engine matches. */
19
+ readonly taskType: string;
20
+ /** The `bpmn:process` id the leaf was declared in (best-effort, may be empty). */
21
+ readonly process: string;
22
+ /** The service-task element id carrying the leaf (best-effort, may be empty). */
23
+ readonly elementId: string;
24
+ }
25
+ /**
26
+ * Scan one BPMN document for its service-task `taskDefinition` leaves.
27
+ *
28
+ * Every `<bpmn:serviceTask>` carrying a `<zeebe:taskDefinition type="…">` with a
29
+ * non-empty type yields a leaf; tasks without a task definition (or with an empty
30
+ * type) are skipped. The scan is order-preserving and tolerant of attribute
31
+ * ordering and self-closing task-definition tags.
32
+ */
33
+ export declare function scanTaskDefinitions(xml: string): TaskDefinitionLeaf[];
34
+ /**
35
+ * Scan many BPMN documents and return the distinct demanded job types, in first
36
+ * occurrence order. The demand×supply model only needs the distinct set of
37
+ * routing tokens; {@link scanTaskDefinitions} keeps the full leaves for callers
38
+ * that want element provenance.
39
+ */
40
+ export declare function distinctTaskTypes(leaves: readonly TaskDefinitionLeaf[]): string[];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Parse `taskDefinition` leaves out of a deployed BPMN model.
3
+ *
4
+ * The demand side of the demand×supply model (S4) is the set of routing tokens
5
+ * the deployed processes ask for. In a Nano/Camunda-8 model each demand is a
6
+ * service task's `<zeebe:taskDefinition type="…">` — the job type the engine
7
+ * matches 1:1 against a worker's routing token. This scanner reads those leaves
8
+ * out of the raw BPMN XML so the model can bucket them by network prefix and
9
+ * diff them against live supply.
10
+ *
11
+ * It is deliberately a small, dependency-free regex scan over the same surface
12
+ * the Urban toolkit's worker-io deriver reads, so it stays app-tier and never
13
+ * touches the engine internals — the XML arrives from the C8 REST API
14
+ * ({@link ./c8-rest.ts}).
15
+ */
16
+ function attr(tag, name) {
17
+ const match = tag.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
18
+ return match ? match[1] : "";
19
+ }
20
+ function processId(xml) {
21
+ const match = xml.match(/<bpmn:process\b[^>]*\bid\s*=\s*"([^"]*)"/);
22
+ return match ? match[1] : "";
23
+ }
24
+ /**
25
+ * Scan one BPMN document for its service-task `taskDefinition` leaves.
26
+ *
27
+ * Every `<bpmn:serviceTask>` carrying a `<zeebe:taskDefinition type="…">` with a
28
+ * non-empty type yields a leaf; tasks without a task definition (or with an empty
29
+ * type) are skipped. The scan is order-preserving and tolerant of attribute
30
+ * ordering and self-closing task-definition tags.
31
+ */
32
+ export function scanTaskDefinitions(xml) {
33
+ const proc = processId(xml);
34
+ const out = [];
35
+ const blockRe = /<bpmn:serviceTask\b([^>]*)>([\s\S]*?)<\/bpmn:serviceTask>/g;
36
+ let block;
37
+ while ((block = blockRe.exec(xml)) !== null) {
38
+ const openAttrs = block[1];
39
+ const body = block[2];
40
+ const elementId = attr(`<x ${openAttrs}>`, "id");
41
+ const tdMatch = body.match(/<zeebe:taskDefinition\b[^>]*>/);
42
+ if (!tdMatch)
43
+ continue;
44
+ const taskType = attr(tdMatch[0], "type");
45
+ if (!taskType)
46
+ continue;
47
+ out.push({ taskType, elementId, process: proc });
48
+ }
49
+ return out;
50
+ }
51
+ /**
52
+ * Scan many BPMN documents and return the distinct demanded job types, in first
53
+ * occurrence order. The demand×supply model only needs the distinct set of
54
+ * routing tokens; {@link scanTaskDefinitions} keeps the full leaves for callers
55
+ * that want element provenance.
56
+ */
57
+ export function distinctTaskTypes(leaves) {
58
+ const seen = new Set();
59
+ const out = [];
60
+ for (const leaf of leaves) {
61
+ if (seen.has(leaf.taskType))
62
+ continue;
63
+ seen.add(leaf.taskType);
64
+ out.push(leaf.taskType);
65
+ }
66
+ return out;
67
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @nanobpm/agentic — the Nano agentic protocol (ADR 0056).
3
+ *
4
+ * One app-tier channel carrying agent presence/registry, demand×supply, a shared
5
+ * blackboard and a live terminal relay. This barrel re-exports every family as a
6
+ * namespace; most consumers import the specific subpath they need instead, e.g.
7
+ * `@nanobpm/agentic/channel`, `@nanobpm/agentic/relay`, `@nanobpm/agentic/cockpit`.
8
+ */
9
+ export * as protocol from "./protocol/index.ts";
10
+ export * as channel from "./channel/index.ts";
11
+ export * as presence from "./presence/index.ts";
12
+ export * as vocab from "./vocab/index.ts";
13
+ export * as demand from "./demand/index.ts";
14
+ export * as relay from "./relay/index.ts";
15
+ export * as transcript from "./transcript/index.ts";
16
+ export * as blackboard from "./blackboard/index.ts";
17
+ export * as cockpit from "./cockpit/index.ts";
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @nanobpm/agentic — the Nano agentic protocol (ADR 0056).
3
+ *
4
+ * One app-tier channel carrying agent presence/registry, demand×supply, a shared
5
+ * blackboard and a live terminal relay. This barrel re-exports every family as a
6
+ * namespace; most consumers import the specific subpath they need instead, e.g.
7
+ * `@nanobpm/agentic/channel`, `@nanobpm/agentic/relay`, `@nanobpm/agentic/cockpit`.
8
+ */
9
+ export * as protocol from "./protocol/index.js";
10
+ export * as channel from "./channel/index.js";
11
+ export * as presence from "./presence/index.js";
12
+ export * as vocab from "./vocab/index.js";
13
+ export * as demand from "./demand/index.js";
14
+ export * as relay from "./relay/index.js";
15
+ export * as transcript from "./transcript/index.js";
16
+ export * as blackboard from "./blackboard/index.js";
17
+ export * as cockpit from "./cockpit/index.js";