@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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-side relay channel client — S8.
|
|
3
|
+
*
|
|
4
|
+
* Ties one relay stream's {@link TerminalSession} to a live WebSocket-style
|
|
5
|
+
* connection: it encodes the session's outbound relay messages into S0 frames
|
|
6
|
+
* ({@link encodeFrame}) on the control lane, decodes inbound frames
|
|
7
|
+
* ({@link decodeFrame}) and routes `relay`-family payloads back to the session,
|
|
8
|
+
* and — critically for "survives a cockpit reconnect" — re-opens the socket when
|
|
9
|
+
* it drops and fires {@link RelayChannelClientOptions.onOpen} on **every**
|
|
10
|
+
* (re)connect, so the caller re-attaches the session and resumes from its last
|
|
11
|
+
* offset.
|
|
12
|
+
*
|
|
13
|
+
* It is transport- and timer-injected: the socket comes from a
|
|
14
|
+
* {@link SocketFactory} and reconnect scheduling from an injected
|
|
15
|
+
* {@link Scheduler}, so the whole reconnect→resume path is exercised
|
|
16
|
+
* deterministically in tests with a fake socket and a manual scheduler — no real
|
|
17
|
+
* timers, no real network (AGENTS.md: no flaky tests, no test retries).
|
|
18
|
+
*/
|
|
19
|
+
import { isNonNegInt, RELAY_FAMILY } from "../relay/index.js";
|
|
20
|
+
import { decodeFrame, encodeFrame, MAX_SEQ } from "../protocol/index.js";
|
|
21
|
+
const CONTROL_LANE = "control";
|
|
22
|
+
function defaultSchedule(run) {
|
|
23
|
+
setTimeout(run, 0);
|
|
24
|
+
}
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Narrow a decoded `relay`-family payload to the inbound sub-protocol without an
|
|
30
|
+
* unchecked cast (the repo bans `as`): a data chunk `{ stream, offset, chunk }`
|
|
31
|
+
* or a resume ack `{ op: "subscribed", stream, gap, nextOffset }`. Any payload
|
|
32
|
+
* carrying an `op` field is treated as an op-message and rejected unless it is a
|
|
33
|
+
* well-formed known op, so a malformed/unknown op can never fall through and be
|
|
34
|
+
* mistaken for a data chunk.
|
|
35
|
+
*/
|
|
36
|
+
function asRelayInbound(payload) {
|
|
37
|
+
if (!isRecord(payload))
|
|
38
|
+
return null;
|
|
39
|
+
if ("op" in payload) {
|
|
40
|
+
if (payload.op === "subscribed" &&
|
|
41
|
+
typeof payload.stream === "string" &&
|
|
42
|
+
typeof payload.gap === "boolean" &&
|
|
43
|
+
isNonNegInt(payload.nextOffset)) {
|
|
44
|
+
return { op: "subscribed", stream: payload.stream, gap: payload.gap, nextOffset: payload.nextOffset };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (typeof payload.stream === "string" && isNonNegInt(payload.offset) && typeof payload.chunk === "string") {
|
|
49
|
+
const data = { stream: payload.stream, offset: payload.offset, chunk: payload.chunk };
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Manages one relay socket with automatic resume-on-reconnect. Construct once
|
|
56
|
+
* per drill-in and pair with a {@link TerminalSession}: wire the session's
|
|
57
|
+
* `send` to {@link sendRelay}, feed {@link RelayChannelClientOptions.onRelay} to
|
|
58
|
+
* `session.handle`, and call `session.attach()` from `onOpen`.
|
|
59
|
+
*/
|
|
60
|
+
export class RelayChannelClient {
|
|
61
|
+
#connect;
|
|
62
|
+
#onRelay;
|
|
63
|
+
#onOpen;
|
|
64
|
+
#onClose;
|
|
65
|
+
#onError;
|
|
66
|
+
#schedule;
|
|
67
|
+
#autoReconnect;
|
|
68
|
+
#socket;
|
|
69
|
+
#seq = 0;
|
|
70
|
+
#closed = false;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.#connect = options.connect;
|
|
73
|
+
this.#onRelay = options.onRelay;
|
|
74
|
+
this.#onOpen = options.onOpen;
|
|
75
|
+
this.#onClose = options.onClose;
|
|
76
|
+
this.#onError = options.onError;
|
|
77
|
+
this.#schedule = options.schedule ?? defaultSchedule;
|
|
78
|
+
this.#autoReconnect = options.autoReconnect ?? true;
|
|
79
|
+
}
|
|
80
|
+
/** True once {@link close} has been called (no further reconnects). */
|
|
81
|
+
get isClosed() {
|
|
82
|
+
return this.#closed;
|
|
83
|
+
}
|
|
84
|
+
/** Open the first socket and wire its lifecycle. Idempotent while connected. */
|
|
85
|
+
open() {
|
|
86
|
+
if (this.#closed || this.#socket !== undefined)
|
|
87
|
+
return;
|
|
88
|
+
// Guard the injected factory: connect() can throw synchronously (e.g. the
|
|
89
|
+
// browser adapter's WebSocket constructor on an invalid URL / CSP block).
|
|
90
|
+
// An unguarded throw here escapes open()/#reconnect() and kills the
|
|
91
|
+
// reconnect→resume loop with no error surfaced. Route it to onError and, if
|
|
92
|
+
// autoReconnect is enabled, schedule a retry so the loop survives a
|
|
93
|
+
// transient connect failure — the same way #handleClose recovers a drop.
|
|
94
|
+
let socket;
|
|
95
|
+
try {
|
|
96
|
+
socket = this.#connect();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.#onError?.(err);
|
|
100
|
+
if (!this.#closed && this.#autoReconnect)
|
|
101
|
+
this.#schedule(() => this.#reconnect());
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.#socket = socket;
|
|
105
|
+
socket.onMessage((bytes) => this.#receive(bytes));
|
|
106
|
+
// Guard the consumer callback the same way #receive() does: a consumer
|
|
107
|
+
// onOpen handler can throw, and an unguarded throw escapes the socket open
|
|
108
|
+
// callback. Route it to onError so a bad handler can't wedge the client.
|
|
109
|
+
socket.onOpen(() => {
|
|
110
|
+
try {
|
|
111
|
+
this.#onOpen?.();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
this.#onError?.(err);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
socket.onClose(() => this.#handleClose());
|
|
118
|
+
}
|
|
119
|
+
/** Encode and send one outbound relay message on the control lane. */
|
|
120
|
+
sendRelay(message) {
|
|
121
|
+
const socket = this.#socket;
|
|
122
|
+
if (socket === undefined)
|
|
123
|
+
return;
|
|
124
|
+
const frame = { lane: CONTROL_LANE, family: RELAY_FAMILY, seq: this.#nextSeq(), payload: message };
|
|
125
|
+
try {
|
|
126
|
+
socket.send(encodeFrame(frame));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
this.#onError?.(err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Close for good — no reconnect will follow. */
|
|
133
|
+
close() {
|
|
134
|
+
this.#closed = true;
|
|
135
|
+
const socket = this.#socket;
|
|
136
|
+
this.#socket = undefined;
|
|
137
|
+
socket?.close();
|
|
138
|
+
}
|
|
139
|
+
#nextSeq() {
|
|
140
|
+
const seq = this.#seq;
|
|
141
|
+
this.#seq = this.#seq >= MAX_SEQ ? 0 : this.#seq + 1;
|
|
142
|
+
return seq;
|
|
143
|
+
}
|
|
144
|
+
#receive(bytes) {
|
|
145
|
+
let frame;
|
|
146
|
+
try {
|
|
147
|
+
frame = decodeFrame(bytes);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
this.#onError?.(err);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (frame.family !== RELAY_FAMILY)
|
|
154
|
+
return;
|
|
155
|
+
const message = asRelayInbound(frame.payload);
|
|
156
|
+
if (message === null) {
|
|
157
|
+
this.#onError?.(new Error("malformed relay payload"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Guard the consumer the same way decode/malformed errors are guarded: a
|
|
161
|
+
// handler (e.g. TerminalSession.handle) can throw on an unsafe offset via
|
|
162
|
+
// addSafeInt, and an unguarded throw here escapes the socket message
|
|
163
|
+
// callback and can wedge the relay client / page. Route it to onError.
|
|
164
|
+
try {
|
|
165
|
+
this.#onRelay(message);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
this.#onError?.(err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
#handleClose() {
|
|
172
|
+
this.#socket = undefined;
|
|
173
|
+
// Guard the consumer onClose: if it throws, an unguarded exception here
|
|
174
|
+
// skips the reconnect scheduling below and breaks the "survives reconnect"
|
|
175
|
+
// invariant for reasons unrelated to transport. Route it to onError, then
|
|
176
|
+
// continue with reconnect regardless.
|
|
177
|
+
try {
|
|
178
|
+
this.#onClose?.();
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
this.#onError?.(err);
|
|
182
|
+
}
|
|
183
|
+
if (this.#closed || !this.#autoReconnect)
|
|
184
|
+
return;
|
|
185
|
+
this.#schedule(() => this.#reconnect());
|
|
186
|
+
}
|
|
187
|
+
#reconnect() {
|
|
188
|
+
if (this.#closed || this.#socket !== undefined)
|
|
189
|
+
return;
|
|
190
|
+
this.open();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cockpit DOM renderer — S8.
|
|
3
|
+
*
|
|
4
|
+
* Renders a {@link CockpitView} into a host element: the demand×supply networks
|
|
5
|
+
* matrix, a red light per *missing agent type*, and the diversity-SLO light.
|
|
6
|
+
* Clicking a worker instance calls {@link RenderOptions.onDrill} with that
|
|
7
|
+
* instance's relay stream id. This renders only the *volatile* part of the page
|
|
8
|
+
* (the part that refreshes each poll); the drill-in terminal is owned by
|
|
9
|
+
* {@link mountCockpit} in a persistent region so it survives a matrix refresh.
|
|
10
|
+
*
|
|
11
|
+
* It renders against a **structural** DOM subset ({@link ElementLike} /
|
|
12
|
+
* {@link DocumentLike}) rather than the global `document`, for two reasons that
|
|
13
|
+
* matter to this slice:
|
|
14
|
+
* 1. it is the *same* function the standalone shell and the embedded (App View)
|
|
15
|
+
* host both call — via the plain-JS `page/mount.js` adapter, which passes the
|
|
16
|
+
* browser's real `document` and their own host element — so the two render
|
|
17
|
+
* **identically** by construction (one code path, no standalone/embedded
|
|
18
|
+
* branch); and
|
|
19
|
+
* 2. a plain in-memory fake satisfies the structural type, so the renderer is
|
|
20
|
+
* unit-tested on Node with no DOM library and no `as` cast.
|
|
21
|
+
*
|
|
22
|
+
* These interfaces are a deliberately *minimal* subset, NOT lib.dom's `Element`/
|
|
23
|
+
* `Document`: the real DOM satisfies them at runtime (as `page/mount.js` — plain
|
|
24
|
+
* JS, untyped — relies on), but a real `HTMLElement` is not TS-assignable to
|
|
25
|
+
* {@link ElementLike} (lib.dom's `appendChild` is `Node`-constrained). Keeping the
|
|
26
|
+
* subset this narrow — rather than widening to lib.dom, which the in-memory fake
|
|
27
|
+
* could not satisfy without a banned `as` cast — is the tradeoff that buys the
|
|
28
|
+
* DOM-free Node tests. A TypeScript browser caller wanting to invoke
|
|
29
|
+
* {@link bootCockpit} directly supplies a thin structural adapter over the real DOM.
|
|
30
|
+
*/
|
|
31
|
+
import type { CockpitView } from "./view.ts";
|
|
32
|
+
/** The minimal element surface the renderer builds against (the in-memory fake, and the real DOM at runtime, satisfy it — see file header). */
|
|
33
|
+
export interface ElementLike {
|
|
34
|
+
className: string;
|
|
35
|
+
textContent: string | null;
|
|
36
|
+
setAttribute(name: string, value: string): void;
|
|
37
|
+
appendChild(child: ElementLike): ElementLike;
|
|
38
|
+
replaceChildren(): void;
|
|
39
|
+
addEventListener(type: string, handler: () => void): void;
|
|
40
|
+
}
|
|
41
|
+
/** The minimal document surface the renderer builds against (the in-memory fake, and the real DOM at runtime, satisfy it — see file header). */
|
|
42
|
+
export interface DocumentLike {
|
|
43
|
+
createElement(tagName: string): ElementLike;
|
|
44
|
+
}
|
|
45
|
+
export interface RenderOptions {
|
|
46
|
+
/** Called with a worker instance's relay stream id when the operator drills in. */
|
|
47
|
+
readonly onDrill?: (stream: string) => void;
|
|
48
|
+
}
|
|
49
|
+
/** Handles into the rendered tree the caller may need. */
|
|
50
|
+
export interface CockpitDom {
|
|
51
|
+
/** The freshly built root the view was rendered into. */
|
|
52
|
+
readonly root: ElementLike;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render `view` into `host`, replacing whatever was there. Idempotent: call it
|
|
56
|
+
* again on every refresh to reflect the latest demand×supply snapshot.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderCockpit(host: ElementLike, doc: DocumentLike, view: CockpitView, options?: RenderOptions): CockpitDom;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
function el(doc, tag, className, text) {
|
|
2
|
+
const node = doc.createElement(tag);
|
|
3
|
+
if (className !== undefined)
|
|
4
|
+
node.className = className;
|
|
5
|
+
if (text !== undefined)
|
|
6
|
+
node.textContent = text;
|
|
7
|
+
return node;
|
|
8
|
+
}
|
|
9
|
+
function lightNode(doc, light) {
|
|
10
|
+
const row = el(doc, "div", "cockpit-light");
|
|
11
|
+
row.setAttribute("data-status", light.status);
|
|
12
|
+
row.setAttribute("data-light-id", light.id);
|
|
13
|
+
const dot = el(doc, "span", "cockpit-dot");
|
|
14
|
+
dot.setAttribute("data-status", light.status);
|
|
15
|
+
row.appendChild(dot);
|
|
16
|
+
row.appendChild(el(doc, "span", "cockpit-light-label", light.label));
|
|
17
|
+
if (light.detail !== undefined) {
|
|
18
|
+
const detail = el(doc, "span", "cockpit-light-detail", light.detail);
|
|
19
|
+
detail.setAttribute("title", light.detail);
|
|
20
|
+
row.appendChild(detail);
|
|
21
|
+
}
|
|
22
|
+
return row;
|
|
23
|
+
}
|
|
24
|
+
function statusBadge(doc, id, label, status) {
|
|
25
|
+
const badge = el(doc, "div", "cockpit-status", `${label}: ${status}`);
|
|
26
|
+
badge.setAttribute("data-status", status);
|
|
27
|
+
badge.setAttribute("data-badge-id", id);
|
|
28
|
+
return badge;
|
|
29
|
+
}
|
|
30
|
+
function tokenCell(doc, network, options) {
|
|
31
|
+
const table = el(doc, "table", "cockpit-tokens");
|
|
32
|
+
const thead = el(doc, "thead", "cockpit-tokens-thead");
|
|
33
|
+
const head = el(doc, "tr", "cockpit-tokens-head");
|
|
34
|
+
head.appendChild(el(doc, "th", "cockpit-th", "token"));
|
|
35
|
+
head.appendChild(el(doc, "th", "cockpit-th", "supply"));
|
|
36
|
+
head.appendChild(el(doc, "th", "cockpit-th", "workers"));
|
|
37
|
+
thead.appendChild(head);
|
|
38
|
+
table.appendChild(thead);
|
|
39
|
+
const tbody = el(doc, "tbody", "cockpit-tokens-tbody");
|
|
40
|
+
for (const token of network.tokens) {
|
|
41
|
+
const row = el(doc, "tr", "cockpit-token");
|
|
42
|
+
row.setAttribute("data-status", token.status);
|
|
43
|
+
row.setAttribute("data-token", token.token);
|
|
44
|
+
const dot = el(doc, "td", "cockpit-td");
|
|
45
|
+
const light = el(doc, "span", "cockpit-dot");
|
|
46
|
+
light.setAttribute("data-status", token.status);
|
|
47
|
+
dot.appendChild(light);
|
|
48
|
+
dot.appendChild(el(doc, "span", "cockpit-token-name", token.token));
|
|
49
|
+
row.appendChild(dot);
|
|
50
|
+
row.appendChild(el(doc, "td", "cockpit-td", String(token.supply)));
|
|
51
|
+
const workers = el(doc, "td", "cockpit-td cockpit-workers");
|
|
52
|
+
for (const instance of token.instances) {
|
|
53
|
+
const chip = el(doc, "button", "cockpit-worker", instance);
|
|
54
|
+
chip.setAttribute("data-stream", instance);
|
|
55
|
+
chip.setAttribute("type", "button");
|
|
56
|
+
const onDrill = options.onDrill;
|
|
57
|
+
if (onDrill !== undefined) {
|
|
58
|
+
chip.addEventListener("click", () => onDrill(instance));
|
|
59
|
+
}
|
|
60
|
+
workers.appendChild(chip);
|
|
61
|
+
}
|
|
62
|
+
row.appendChild(workers);
|
|
63
|
+
tbody.appendChild(row);
|
|
64
|
+
}
|
|
65
|
+
table.appendChild(tbody);
|
|
66
|
+
return table;
|
|
67
|
+
}
|
|
68
|
+
function networkSection(doc, network, options) {
|
|
69
|
+
const section = el(doc, "section", "cockpit-network");
|
|
70
|
+
section.setAttribute("data-status", network.status);
|
|
71
|
+
section.setAttribute("data-network", network.network);
|
|
72
|
+
const header = el(doc, "div", "cockpit-network-head");
|
|
73
|
+
const dot = el(doc, "span", "cockpit-dot");
|
|
74
|
+
dot.setAttribute("data-status", network.status);
|
|
75
|
+
header.appendChild(dot);
|
|
76
|
+
header.appendChild(el(doc, "span", "cockpit-network-name", network.network));
|
|
77
|
+
if (network.missing.length > 0) {
|
|
78
|
+
header.appendChild(el(doc, "span", "cockpit-network-missing", `missing: ${network.missing.join(", ")}`));
|
|
79
|
+
}
|
|
80
|
+
section.appendChild(header);
|
|
81
|
+
section.appendChild(tokenCell(doc, network, options));
|
|
82
|
+
return section;
|
|
83
|
+
}
|
|
84
|
+
function missingPanel(doc, view) {
|
|
85
|
+
const panel = el(doc, "aside", "cockpit-missing");
|
|
86
|
+
panel.setAttribute("data-empty", view.missingLights.length === 0 ? "true" : "false");
|
|
87
|
+
panel.appendChild(el(doc, "h2", "cockpit-panel-title", "Missing agent types"));
|
|
88
|
+
if (view.missingLights.length === 0) {
|
|
89
|
+
panel.appendChild(el(doc, "div", "cockpit-missing-none", "none — every demanded token is served"));
|
|
90
|
+
return panel;
|
|
91
|
+
}
|
|
92
|
+
for (const light of view.missingLights) {
|
|
93
|
+
panel.appendChild(lightNode(doc, light));
|
|
94
|
+
}
|
|
95
|
+
return panel;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Render `view` into `host`, replacing whatever was there. Idempotent: call it
|
|
99
|
+
* again on every refresh to reflect the latest demand×supply snapshot.
|
|
100
|
+
*/
|
|
101
|
+
export function renderCockpit(host, doc, view, options = {}) {
|
|
102
|
+
host.replaceChildren();
|
|
103
|
+
const root = el(doc, "div", "cockpit");
|
|
104
|
+
root.setAttribute("data-status", view.status);
|
|
105
|
+
const header = el(doc, "header", "cockpit-header");
|
|
106
|
+
header.appendChild(el(doc, "h1", "cockpit-title", "Agent networks — the cockpit"));
|
|
107
|
+
header.appendChild(statusBadge(doc, "overall", "overall", view.status));
|
|
108
|
+
header.appendChild(lightNode(doc, view.diversityLight));
|
|
109
|
+
root.appendChild(header);
|
|
110
|
+
root.appendChild(missingPanel(doc, view));
|
|
111
|
+
const matrix = el(doc, "div", "cockpit-matrix");
|
|
112
|
+
for (const network of view.networks) {
|
|
113
|
+
matrix.appendChild(networkSection(doc, network, options));
|
|
114
|
+
}
|
|
115
|
+
root.appendChild(matrix);
|
|
116
|
+
if (view.nonAgentic.length > 0) {
|
|
117
|
+
const footer = el(doc, "footer", "cockpit-nonagentic", `non-agentic task types: ${view.nonAgentic.join(", ")}`);
|
|
118
|
+
root.appendChild(footer);
|
|
119
|
+
}
|
|
120
|
+
host.appendChild(root);
|
|
121
|
+
return { root };
|
|
122
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker terminal session — S8's drill-into-a-worker live terminal.
|
|
3
|
+
*
|
|
4
|
+
* A consumer of the S5 relay sub-protocol that **survives a cockpit reconnect**
|
|
5
|
+
* via resume-from-offset. It is deliberately timer-free and transport-free: it
|
|
6
|
+
* emits the relay messages to send ({@link RelaySend}) and consumes the relay
|
|
7
|
+
* messages received ({@link TerminalSession.handle}), tracking exactly one piece
|
|
8
|
+
* of durable state — `nextOffset`, the offset to resume from. On any (re)attach
|
|
9
|
+
* it re-subscribes from `nextOffset`, and it drops any replayed chunk it has
|
|
10
|
+
* already applied, so a reconnect neither loses nor double-writes output (within
|
|
11
|
+
* the relay ring's retained window). If the hub's `subscribed` ack reports a
|
|
12
|
+
* `nextOffset` (stream head) *below* our resume point — a hub restart/reset left
|
|
13
|
+
* us ahead of the stream — it clamps `nextOffset` back down so fresh chunks are
|
|
14
|
+
* not dropped as stale.
|
|
15
|
+
*
|
|
16
|
+
* The S5 relay wire (see `@nanobpm/agentic-relay`):
|
|
17
|
+
* - outbound `{ op: "subscribe", stream, from, credit }` — (re)attach and resume,
|
|
18
|
+
* - outbound `{ op: "credit", credit }` — grant more bulk credit,
|
|
19
|
+
* - inbound `{ op: "subscribed", stream, gap, nextOffset }` — the resume ack
|
|
20
|
+
* (`gap: boolean` — the S5 wire flags whether chunks aged out),
|
|
21
|
+
* - inbound {@link RelayPayload} `{ stream, offset, chunk }` — a data chunk.
|
|
22
|
+
*/
|
|
23
|
+
import type { RelayPayload } from "../protocol/index.ts";
|
|
24
|
+
/** The terminal sink the session writes decoded output to (xterm.js satisfies this). */
|
|
25
|
+
export interface TerminalSink {
|
|
26
|
+
/** Append a chunk of terminal output. */
|
|
27
|
+
write(chunk: string): void;
|
|
28
|
+
/** Tear down the underlying terminal widget and its listeners, if any. */
|
|
29
|
+
dispose?(): void;
|
|
30
|
+
}
|
|
31
|
+
/** An outbound relay message the session asks its transport to send. */
|
|
32
|
+
export type RelayOutbound = {
|
|
33
|
+
readonly op: "subscribe";
|
|
34
|
+
readonly stream: string;
|
|
35
|
+
readonly from: number;
|
|
36
|
+
readonly credit: number;
|
|
37
|
+
} | {
|
|
38
|
+
readonly op: "credit";
|
|
39
|
+
readonly credit: number;
|
|
40
|
+
};
|
|
41
|
+
/** An inbound relay message the session consumes (a data chunk or a resume ack). */
|
|
42
|
+
export type RelayInbound = RelayPayload | {
|
|
43
|
+
readonly op: "subscribed";
|
|
44
|
+
readonly stream: string;
|
|
45
|
+
readonly gap: boolean;
|
|
46
|
+
readonly nextOffset: number;
|
|
47
|
+
};
|
|
48
|
+
/** Sends one outbound relay message over the channel. */
|
|
49
|
+
export type RelaySend = (message: RelayOutbound) => void;
|
|
50
|
+
export interface TerminalSessionOptions {
|
|
51
|
+
/** The relay stream id (one worker's terminal). */
|
|
52
|
+
readonly stream: string;
|
|
53
|
+
/** Where decoded output is written. */
|
|
54
|
+
readonly sink: TerminalSink;
|
|
55
|
+
/** Emits outbound relay messages. */
|
|
56
|
+
readonly send: RelaySend;
|
|
57
|
+
/** Bulk credit requested on each (re)subscribe. Default 1024. */
|
|
58
|
+
readonly credit?: number;
|
|
59
|
+
/** Offset to resume from on first attach. Default 0 (from the start). */
|
|
60
|
+
readonly from?: number;
|
|
61
|
+
/** Notified when the resume ack reports a gap (chunks aged out of the ring). */
|
|
62
|
+
readonly onGap?: () => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A resume-from-offset consumer of one relay stream. Construct once per worker
|
|
66
|
+
* drill-in; call {@link attach} on every (re)connection and feed every inbound
|
|
67
|
+
* relay message to {@link handle}.
|
|
68
|
+
*/
|
|
69
|
+
export declare class TerminalSession {
|
|
70
|
+
#private;
|
|
71
|
+
constructor(options: TerminalSessionOptions);
|
|
72
|
+
/** The stream id this session follows. */
|
|
73
|
+
get stream(): string;
|
|
74
|
+
/** The offset the session will resume from on the next {@link attach}. */
|
|
75
|
+
get nextOffset(): number;
|
|
76
|
+
/** Whether the most recent resume ack reported a gap (false when none was lost). */
|
|
77
|
+
get gap(): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* (Re)subscribe to the stream, resuming from {@link nextOffset}. Call this on
|
|
80
|
+
* first connect AND after every reconnect — because it always resumes from the
|
|
81
|
+
* offset just past the last applied chunk, a reconnect replays only the
|
|
82
|
+
* un-applied tail (no loss) and re-delivered chunks below `nextOffset` are
|
|
83
|
+
* dropped by {@link handle} (no duplication).
|
|
84
|
+
*/
|
|
85
|
+
attach(): void;
|
|
86
|
+
/** Grant additional bulk credit (backpressure release) mid-stream. */
|
|
87
|
+
grant(credit: number): void;
|
|
88
|
+
/**
|
|
89
|
+
* Process one inbound relay message. A data chunk at or beyond `nextOffset` is
|
|
90
|
+
* written and advances the resume point; a chunk below it (a duplicate replay
|
|
91
|
+
* after a reconnect) is dropped. A resume ack records any gap and clamps the
|
|
92
|
+
* resume point down to the hub's head when we are ahead of it.
|
|
93
|
+
*/
|
|
94
|
+
handle(message: RelayInbound): void;
|
|
95
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { addSafeInt, isNonNegInt, isPosInt } from "../relay/index.js";
|
|
2
|
+
const DEFAULT_CREDIT = 1024;
|
|
3
|
+
function isRelayData(message) {
|
|
4
|
+
return !("op" in message);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A resume-from-offset consumer of one relay stream. Construct once per worker
|
|
8
|
+
* drill-in; call {@link attach} on every (re)connection and feed every inbound
|
|
9
|
+
* relay message to {@link handle}.
|
|
10
|
+
*/
|
|
11
|
+
export class TerminalSession {
|
|
12
|
+
#stream;
|
|
13
|
+
#sink;
|
|
14
|
+
#send;
|
|
15
|
+
#credit;
|
|
16
|
+
#onGap;
|
|
17
|
+
/** The next offset we still need — everything below it has been applied. */
|
|
18
|
+
#nextOffset;
|
|
19
|
+
/** Whether the most recent resume ack reported a gap (retained window overrun). */
|
|
20
|
+
#gap = false;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.#stream = options.stream;
|
|
23
|
+
this.#sink = options.sink;
|
|
24
|
+
this.#send = options.send;
|
|
25
|
+
this.#credit = options.credit ?? DEFAULT_CREDIT;
|
|
26
|
+
if (!isPosInt(this.#credit)) {
|
|
27
|
+
throw new RangeError(`TerminalSession.credit must be a positive safe integer, got ${this.#credit}`);
|
|
28
|
+
}
|
|
29
|
+
this.#onGap = options.onGap;
|
|
30
|
+
const from = options.from ?? 0;
|
|
31
|
+
if (!isNonNegInt(from)) {
|
|
32
|
+
throw new RangeError(`TerminalSession.from must be a non-negative safe integer, got ${from}`);
|
|
33
|
+
}
|
|
34
|
+
this.#nextOffset = from;
|
|
35
|
+
}
|
|
36
|
+
/** The stream id this session follows. */
|
|
37
|
+
get stream() {
|
|
38
|
+
return this.#stream;
|
|
39
|
+
}
|
|
40
|
+
/** The offset the session will resume from on the next {@link attach}. */
|
|
41
|
+
get nextOffset() {
|
|
42
|
+
return this.#nextOffset;
|
|
43
|
+
}
|
|
44
|
+
/** Whether the most recent resume ack reported a gap (false when none was lost). */
|
|
45
|
+
get gap() {
|
|
46
|
+
return this.#gap;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* (Re)subscribe to the stream, resuming from {@link nextOffset}. Call this on
|
|
50
|
+
* first connect AND after every reconnect — because it always resumes from the
|
|
51
|
+
* offset just past the last applied chunk, a reconnect replays only the
|
|
52
|
+
* un-applied tail (no loss) and re-delivered chunks below `nextOffset` are
|
|
53
|
+
* dropped by {@link handle} (no duplication).
|
|
54
|
+
*/
|
|
55
|
+
attach() {
|
|
56
|
+
this.#send({ op: "subscribe", stream: this.#stream, from: this.#nextOffset, credit: this.#credit });
|
|
57
|
+
}
|
|
58
|
+
/** Grant additional bulk credit (backpressure release) mid-stream. */
|
|
59
|
+
grant(credit) {
|
|
60
|
+
if (!isPosInt(credit)) {
|
|
61
|
+
throw new RangeError(`TerminalSession.grant credit must be a positive safe integer, got ${credit}`);
|
|
62
|
+
}
|
|
63
|
+
this.#send({ op: "credit", credit });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Process one inbound relay message. A data chunk at or beyond `nextOffset` is
|
|
67
|
+
* written and advances the resume point; a chunk below it (a duplicate replay
|
|
68
|
+
* after a reconnect) is dropped. A resume ack records any gap and clamps the
|
|
69
|
+
* resume point down to the hub's head when we are ahead of it.
|
|
70
|
+
*/
|
|
71
|
+
handle(message) {
|
|
72
|
+
if (isRelayData(message)) {
|
|
73
|
+
this.#onData(message);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (message.op === "subscribed" && message.stream === this.#stream) {
|
|
77
|
+
this.#onSubscribed(message.gap, message.nextOffset);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
#onData(data) {
|
|
81
|
+
if (data.stream !== this.#stream)
|
|
82
|
+
return;
|
|
83
|
+
// Idempotent apply: a reconnect resubscribes from nextOffset, so the hub may
|
|
84
|
+
// re-deliver the boundary chunk; anything we have already applied is dropped.
|
|
85
|
+
if (data.offset < this.#nextOffset)
|
|
86
|
+
return;
|
|
87
|
+
// Compute the next resume point BEFORE writing so the apply is atomic: a
|
|
88
|
+
// chunk at Number.MAX_SAFE_INTEGER makes addSafeInt throw, and it must throw
|
|
89
|
+
// before we touch the sink — otherwise the chunk is written but #nextOffset
|
|
90
|
+
// is not advanced, leaving a partially-applied state that re-delivers (and
|
|
91
|
+
// so duplicates) the chunk on reconnect. Advancing via addSafeInt also fails
|
|
92
|
+
// fast rather than overflowing into an unsafe nextOffset — that value would
|
|
93
|
+
// later be echoed in subscribe.from and lose precision on any JSON
|
|
94
|
+
// round-trip, silently corrupting resume semantics.
|
|
95
|
+
const nextOffset = addSafeInt(data.offset, 1, "nextOffset");
|
|
96
|
+
this.#sink.write(data.chunk);
|
|
97
|
+
this.#nextOffset = nextOffset;
|
|
98
|
+
}
|
|
99
|
+
#onSubscribed(gap, nextOffset) {
|
|
100
|
+
// Validate the ack's resume point exactly as the constructor validates
|
|
101
|
+
// `from`: `nextOffset` is echoed back into subscribe.from on the next
|
|
102
|
+
// attach, so a negative/NaN/unsafe value (from a buggy or malicious
|
|
103
|
+
// transport that bypasses RelayChannelClient's validation) would corrupt
|
|
104
|
+
// resume semantics or lose precision on a JSON round-trip. Fail fast.
|
|
105
|
+
if (!isNonNegInt(nextOffset)) {
|
|
106
|
+
throw new RangeError(`TerminalSession.subscribed nextOffset must be a non-negative safe integer, got ${nextOffset}`);
|
|
107
|
+
}
|
|
108
|
+
// Clamp our resume point down to the hub's current head when we are ahead of
|
|
109
|
+
// it. Without this, a hub restart/reset (or a `from` seeded past the head)
|
|
110
|
+
// leaves #nextOffset above every offset the hub will now emit, so #onData
|
|
111
|
+
// silently drops all fresh chunks until the stream catches back up — losing
|
|
112
|
+
// terminal output, possibly indefinitely. Only clamp DOWN: a head at or above
|
|
113
|
+
// #nextOffset is the normal case and must not skip un-applied chunks.
|
|
114
|
+
if (nextOffset < this.#nextOffset) {
|
|
115
|
+
this.#nextOffset = nextOffset;
|
|
116
|
+
}
|
|
117
|
+
// Record the gap on every ack so a later no-gap resume clears a prior gap.
|
|
118
|
+
this.#gap = gap;
|
|
119
|
+
if (gap) {
|
|
120
|
+
this.#onGap?.();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cockpit view-model — S8.
|
|
3
|
+
*
|
|
4
|
+
* A pure, deterministic projection of the S4 {@link DemandSupplyReport} onto the
|
|
5
|
+
* shape the cockpit renders: a per-network demand×supply matrix, a red light per
|
|
6
|
+
* *missing agent type*, and the diversity-SLO state (from S3, carried through S4).
|
|
7
|
+
*
|
|
8
|
+
* It is deliberately framework-free and side-effect-free: the same report always
|
|
9
|
+
* yields the same {@link CockpitView}, so it is safe to diff frame-to-frame, to
|
|
10
|
+
* snapshot in a test, and to render identically whether the page is embedded in
|
|
11
|
+
* the console (App View) or served standalone — the view is the single source of
|
|
12
|
+
* truth both render paths consume.
|
|
13
|
+
*/
|
|
14
|
+
import type { DemandSupplyReport, SloStatus } from "../demand/index.ts";
|
|
15
|
+
import type { DiversityReport } from "../vocab/index.ts";
|
|
16
|
+
export type { SloStatus } from "../demand/index.ts";
|
|
17
|
+
/** A three-state status light rendered as a coloured dot + label. */
|
|
18
|
+
export interface CockpitLight {
|
|
19
|
+
/** Stable id for the DOM node / keyed diff. */
|
|
20
|
+
readonly id: string;
|
|
21
|
+
/** Human label shown next to the dot. */
|
|
22
|
+
readonly label: string;
|
|
23
|
+
/** The grade the dot colours to. */
|
|
24
|
+
readonly status: SloStatus;
|
|
25
|
+
/** Optional one-line detail (tooltip / secondary text). */
|
|
26
|
+
readonly detail?: string;
|
|
27
|
+
}
|
|
28
|
+
/** One demanded routing token as a matrix cell. */
|
|
29
|
+
export interface CockpitTokenRow {
|
|
30
|
+
/** The demanded routing token. */
|
|
31
|
+
readonly token: string;
|
|
32
|
+
/** How many registered workers currently serve it. */
|
|
33
|
+
readonly supply: number;
|
|
34
|
+
/** The serving worker instances (provenance for the drill-in), sorted. */
|
|
35
|
+
readonly instances: readonly string[];
|
|
36
|
+
/** False when no worker serves it — a missing agent type. */
|
|
37
|
+
readonly satisfied: boolean;
|
|
38
|
+
/** GREEN when served, RED when missing. */
|
|
39
|
+
readonly status: SloStatus;
|
|
40
|
+
}
|
|
41
|
+
/** One network prefix as a matrix row. */
|
|
42
|
+
export interface CockpitNetworkRow {
|
|
43
|
+
/** The network prefix bucket. */
|
|
44
|
+
readonly network: string;
|
|
45
|
+
/** Every demanded token in the bucket, sorted by token. */
|
|
46
|
+
readonly tokens: readonly CockpitTokenRow[];
|
|
47
|
+
/** The demanded tokens in this bucket with zero supply, sorted. */
|
|
48
|
+
readonly missing: readonly string[];
|
|
49
|
+
/** RED when the row has any missing agent type, else GREEN. */
|
|
50
|
+
readonly status: SloStatus;
|
|
51
|
+
}
|
|
52
|
+
/** The full renderable cockpit view. */
|
|
53
|
+
export interface CockpitView {
|
|
54
|
+
/** The overall SLO state: worst of missing-agent and diversity. */
|
|
55
|
+
readonly status: SloStatus;
|
|
56
|
+
/** The demand×supply matrix, one row per network, sorted by network. */
|
|
57
|
+
readonly networks: readonly CockpitNetworkRow[];
|
|
58
|
+
/** Every missing agent type across all networks, sorted and de-duplicated. */
|
|
59
|
+
readonly missing: readonly string[];
|
|
60
|
+
/** One RED light per missing agent type (empty when nothing is missing). */
|
|
61
|
+
readonly missingLights: readonly CockpitLight[];
|
|
62
|
+
/** The S3 diversity report, carried through unchanged for detail rendering. */
|
|
63
|
+
readonly diversity: DiversityReport;
|
|
64
|
+
/** A single light summarising the diversity SLO. */
|
|
65
|
+
readonly diversityLight: CockpitLight;
|
|
66
|
+
/**
|
|
67
|
+
* Deployed task types that are not valid routing tokens (ordinary,
|
|
68
|
+
* non-agentic C8 jobs) — surfaced so an operator can spot a mistyped token,
|
|
69
|
+
* excluded from the agentic accounting.
|
|
70
|
+
*/
|
|
71
|
+
readonly nonAgentic: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Derive the renderable cockpit view from an S4 demand×supply report.
|
|
75
|
+
*
|
|
76
|
+
* Pure and total: every list is already sorted by S4, so the derived view is
|
|
77
|
+
* stable and diff-friendly; no input mutates and no I/O happens.
|
|
78
|
+
*/
|
|
79
|
+
export declare function cockpitView(report: DemandSupplyReport): CockpitView;
|