@nanobpm/agentic 0.6.0 → 0.7.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 +58 -1
- package/dist/transcript/events.d.ts +213 -0
- package/dist/transcript/events.js +322 -0
- package/dist/transcript/index.d.ts +8 -0
- package/dist/transcript/index.js +7 -0
- package/package.json +1 -1
- package/src/transcript/events.drift.test.ts +68 -0
- package/src/transcript/events.test.ts +308 -0
- package/src/transcript/events.ts +490 -0
- package/src/transcript/index.ts +38 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The **Nano agentic protocol** (ADR 0056): one app-tier channel carrying agent pr
|
|
|
13
13
|
| `@nanobpm/agentic/vocab` | Vocab resolver + core vocabulary (S3) |
|
|
14
14
|
| `@nanobpm/agentic/demand` | Demand×supply model (S4) |
|
|
15
15
|
| `@nanobpm/agentic/relay` | Relay ring + QoS scheduler (S5) |
|
|
16
|
-
| `@nanobpm/agentic/transcript` | Transcript store, retention-by-lifecycle (S6) + turn-structured view (Camunda `AgentHistoryRecordValue` parity) |
|
|
16
|
+
| `@nanobpm/agentic/transcript` | Transcript store, retention-by-lifecycle (S6) + turn-structured view (Camunda `AgentHistoryRecordValue` parity) + the typed transcript-event **vocabulary** & single `parseTranscriptEvent` fold |
|
|
17
17
|
| `@nanobpm/agentic/blackboard` | Blackboard channel family (S7) |
|
|
18
18
|
| `@nanobpm/agentic/cockpit` | Operator visibility page — the cockpit (S8) |
|
|
19
19
|
| `@nanobpm/agentic/session` | Canonical `SessionEvent` + authoritative session log for durable agent-session resume (ADR 0062) |
|
|
@@ -21,3 +21,60 @@ The **Nano agentic protocol** (ADR 0056): one app-tier channel carrying agent pr
|
|
|
21
21
|
The barrel `@nanobpm/agentic` re-exports each family as a namespace (`protocol`, `channel`, …). The worker-side client ships separately as `@nanobpm/urban-agent-client`.
|
|
22
22
|
|
|
23
23
|
The wire contract is the single source of truth; nothing here rides the Camunda-8 engine or its transport.
|
|
24
|
+
|
|
25
|
+
## Transcript event vocabulary (`@nanobpm/agentic/transcript`)
|
|
26
|
+
|
|
27
|
+
The transcript subpath now ships the canonical typed transcript-event **vocabulary** alongside the S6
|
|
28
|
+
store, so every Urban app derives ACP-rich transcripts from **one** parser instead of forking its own:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
parseTranscriptEvent, // THE ONE PARSER: stored chunk → typed TranscriptEvent
|
|
33
|
+
deriveView, // THE ONE FOLD: typed events → per-turn structured view
|
|
34
|
+
deriveViewFromChunks, // parse + fold in one call
|
|
35
|
+
mergeTranscriptVocab, // additive EXTENSION POINT (register a new kind, never fork)
|
|
36
|
+
CORE_TRANSCRIPT_VOCAB,
|
|
37
|
+
encodeTranscriptEvent,
|
|
38
|
+
utf8ByteLength, // browser-safe (TextEncoder, no Buffer)
|
|
39
|
+
TRANSCRIPT_EVENT_MARKER, // "nwfTranscriptEvent" — canonical single source of truth
|
|
40
|
+
TRANSCRIPT_EVENT_VERSION, // 1
|
|
41
|
+
} from "@nanobpm/agentic/transcript";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The vocab/parser modules are **browser-safe** (no `Buffer`, no Node-only imports), so the cockpit derive
|
|
45
|
+
runs in-browser. Kinds: `message`, `tool-call`, `tool-result`, `turn`, `step`, `lifecycle`, plus the
|
|
46
|
+
raw `stream-chunk` fallback that preserves byte-level replay fidelity. A stored chunk is decoded as a
|
|
47
|
+
structured event **only** when it is a JSON object carrying `TRANSCRIPT_EVENT_MARKER` at the current
|
|
48
|
+
version — otherwise it is retained verbatim, so a raw ANSI frame that happens to be JSON is never
|
|
49
|
+
mis-classified. `TRANSCRIPT_EVENT_MARKER` + `parseTranscriptEvent` are the canonical detection surface
|
|
50
|
+
the whole package family (e.g. the cockpit's structured-stream drill-in) imports from here — never a
|
|
51
|
+
private copy.
|
|
52
|
+
|
|
53
|
+
### Extending the vocabulary (merge, don't fork)
|
|
54
|
+
|
|
55
|
+
A downstream app adds its own kind + parse handler **without editing this package**:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const appVocab = mergeTranscriptVocab(CORE_TRANSCRIPT_VOCAB, {
|
|
59
|
+
permission: (body, offset) =>
|
|
60
|
+
typeof body.requestId === "string"
|
|
61
|
+
? { kind: "message", offset, role: "system", text: `permission(${body.requestId})` }
|
|
62
|
+
: undefined, // reject malformed → raw fallback
|
|
63
|
+
});
|
|
64
|
+
parseTranscriptEvent({ offset, chunk }, appVocab); // the one parser, now aware of `permission`
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Migration for nano-workforce
|
|
68
|
+
|
|
69
|
+
`nano-workforce` currently forks this vocabulary at `app/agentic/transcript-events.ts`. To consume the
|
|
70
|
+
shared copy:
|
|
71
|
+
|
|
72
|
+
1. Replace imports of the local `./transcript-events.ts` with `@nanobpm/agentic/transcript`
|
|
73
|
+
(`parseTranscriptEvent`, `deriveView`, `mergeTranscriptVocab`, `TRANSCRIPT_EVENT_MARKER`,
|
|
74
|
+
`TRANSCRIPT_EVENT_VERSION`, `utf8ByteLength`, the event types, …). The public API — symbol names,
|
|
75
|
+
the `"nwfTranscriptEvent"` marker string, and version `1` — is preserved verbatim, so this is a
|
|
76
|
+
drop-in.
|
|
77
|
+
2. Register nano-workforce's app-specific `permission` kind (nano-workforce#559) via
|
|
78
|
+
`mergeTranscriptVocab(CORE_TRANSCRIPT_VOCAB, { permission: … })` instead of editing a forked parser.
|
|
79
|
+
3. **Delete** `app/agentic/transcript-events.ts` (and fold its `transcript-events.drift.test.ts` marker
|
|
80
|
+
guard into the shared package's guard, already ported here as `events.drift.test.ts`).
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transcript EVENT vocabulary + the single derive() fold (ADR 0056, #251).
|
|
3
|
+
*
|
|
4
|
+
* This is the "event-sourced session" layer over the S6 transcript store ({@link ./store.ts}). The
|
|
5
|
+
* store is already append-only and offset-keyed — chunks are appended, never mutated — which is half of
|
|
6
|
+
* the event-sourced-session pattern. The gap it left is that chunks are opaque `TEXT`: every richer
|
|
7
|
+
* view (structured message history, tool cards, per-turn boundaries, token accounting) had to re-parse
|
|
8
|
+
* the raw frame bytes ad hoc, a DRIFT SURFACE (two parsers of the same bytes), which our "Derivation
|
|
9
|
+
* Over Duplication" doctrine forbids.
|
|
10
|
+
*
|
|
11
|
+
* This module closes that gap: the append-only log of TYPED events is the single source of truth, and
|
|
12
|
+
* every higher-level view is a DERIVATION of that one log via a single {@link deriveView} fold — "the
|
|
13
|
+
* log IS the state, so divergence is structurally impossible". A raw terminal chunk is retained
|
|
14
|
+
* verbatim as a `stream-chunk` event (byte-level replay fidelity is preserved); a producer that emits a
|
|
15
|
+
* structured, marker-tagged JSON envelope is decoded into the authoritative typed events (message /
|
|
16
|
+
* tool-call / tool-result / turn / step / lifecycle) the derived views fold over.
|
|
17
|
+
*
|
|
18
|
+
* THE ONE PARSER. {@link parseTranscriptEvent} is the SINGLE place a stored chunk is classified into a
|
|
19
|
+
* typed event; every consumer (cockpit, search, token accounting, export) reads the derived view, not
|
|
20
|
+
* the raw bytes. A drift-guard test (`events.drift.test.ts`) asserts the event marker — and therefore
|
|
21
|
+
* the raw→event parse — appears in exactly this module, so a second parser cannot creep in.
|
|
22
|
+
*
|
|
23
|
+
* MERGE-EXTENSIBLE. The vocabulary is a small core ({@link CORE_TRANSCRIPT_VOCAB}) authors extend in the
|
|
24
|
+
* same schema with {@link mergeTranscriptVocab}, so a new event kind is an additive merge, never a fork
|
|
25
|
+
* of the parser. A downstream app (e.g. nano-workforce#559) registers its own `permission` kind this
|
|
26
|
+
* way without editing this package.
|
|
27
|
+
*
|
|
28
|
+
* BROWSER-SAFE. This module is imported by cockpit code that runs in the BROWSER (the cockpit derive),
|
|
29
|
+
* so it takes no hard dependency on Node's `Buffer` or any Node-only API — {@link utf8ByteLength} uses
|
|
30
|
+
* the Web/Node standard `TextEncoder`. It is pure and side-effect-free: no I/O, and it never touches the
|
|
31
|
+
* engine or a BPMN flow (ADR 0056: app-tier only, advisory).
|
|
32
|
+
*/
|
|
33
|
+
export declare function utf8ByteLength(text: string): number;
|
|
34
|
+
/**
|
|
35
|
+
* The reserved marker field that distinguishes a structured transcript-event envelope from raw
|
|
36
|
+
* terminal bytes. A stored chunk is decoded as a typed event ONLY when it is a JSON object carrying
|
|
37
|
+
* this field set to the schema version — otherwise it is retained verbatim as a raw `stream-chunk`, so
|
|
38
|
+
* a raw ANSI frame that happens to be valid JSON is never mis-classified. Namespaced so it cannot
|
|
39
|
+
* collide with a producer's own payload keys. This is the canonical single source of truth for the
|
|
40
|
+
* whole package family — consumers (e.g. the cockpit) import this identifier, never a private copy.
|
|
41
|
+
*/
|
|
42
|
+
export declare const TRANSCRIPT_EVENT_MARKER: "nwfTranscriptEvent";
|
|
43
|
+
/** The current transcript-event envelope schema version (the value {@link TRANSCRIPT_EVENT_MARKER} carries). */
|
|
44
|
+
export declare const TRANSCRIPT_EVENT_VERSION: 1;
|
|
45
|
+
/** The core, closed set of typed transcript-event kinds. Downstream apps register extra *envelope*
|
|
46
|
+
* kinds via {@link mergeTranscriptVocab}, but those decoders must still return one of these core
|
|
47
|
+
* variants — this union itself does not grow for TypeScript consumers. */
|
|
48
|
+
export type TranscriptEventKind = "stream-chunk" | "message" | "tool-call" | "tool-result" | "turn" | "step" | "lifecycle";
|
|
49
|
+
/** The message roles the derived history distinguishes (assistant is authoritative for derivation). */
|
|
50
|
+
export type TranscriptRole = "assistant" | "user" | "system" | "tool";
|
|
51
|
+
/** Fields every typed event carries: the store offset it was decoded from. */
|
|
52
|
+
interface TranscriptEventBase {
|
|
53
|
+
readonly offset: number;
|
|
54
|
+
}
|
|
55
|
+
/** A raw terminal chunk retained verbatim for byte-level replay fidelity (the default classification). */
|
|
56
|
+
export interface StreamChunkEvent extends TranscriptEventBase {
|
|
57
|
+
readonly kind: "stream-chunk";
|
|
58
|
+
/** The exact stored bytes — unmodified, so raw-byte replay stays faithful. */
|
|
59
|
+
readonly chunk: string;
|
|
60
|
+
}
|
|
61
|
+
/** An assistant/user/system message — authoritative for the derived message history. */
|
|
62
|
+
export interface MessageEvent extends TranscriptEventBase {
|
|
63
|
+
readonly kind: "message";
|
|
64
|
+
readonly role: TranscriptRole;
|
|
65
|
+
readonly text: string;
|
|
66
|
+
}
|
|
67
|
+
/** A tool invocation the agent issued. */
|
|
68
|
+
export interface ToolCallEvent extends TranscriptEventBase {
|
|
69
|
+
readonly kind: "tool-call";
|
|
70
|
+
readonly name: string;
|
|
71
|
+
/** A stable id linking this call to its {@link ToolResultEvent}, when the producer supplies one. */
|
|
72
|
+
readonly callId?: string;
|
|
73
|
+
readonly args?: unknown;
|
|
74
|
+
}
|
|
75
|
+
/** A tool result, paired back to its {@link ToolCallEvent} by `callId` (else the most recent open call). */
|
|
76
|
+
export interface ToolResultEvent extends TranscriptEventBase {
|
|
77
|
+
readonly kind: "tool-result";
|
|
78
|
+
readonly callId?: string;
|
|
79
|
+
readonly ok: boolean;
|
|
80
|
+
readonly content?: string;
|
|
81
|
+
}
|
|
82
|
+
/** A turn boundary — the start of a new request/response cycle. */
|
|
83
|
+
export interface TurnEvent extends TranscriptEventBase {
|
|
84
|
+
readonly kind: "turn";
|
|
85
|
+
/** The producer's turn index, when supplied (else derived positionally). */
|
|
86
|
+
readonly index?: number;
|
|
87
|
+
}
|
|
88
|
+
/** A step boundary within a turn (a tool loop iteration, a sub-agent hop, …). */
|
|
89
|
+
export interface StepEvent extends TranscriptEventBase {
|
|
90
|
+
readonly kind: "step";
|
|
91
|
+
readonly label?: string;
|
|
92
|
+
}
|
|
93
|
+
/** A session lifecycle transition (open → completed, or an explicit exit). */
|
|
94
|
+
export interface LifecycleEvent extends TranscriptEventBase {
|
|
95
|
+
readonly kind: "lifecycle";
|
|
96
|
+
readonly phase: "open" | "completed" | "exited";
|
|
97
|
+
}
|
|
98
|
+
/** The core, closed typed transcript-event union. Merging vocab lets an app decode custom *envelope*
|
|
99
|
+
* kinds, but each decoder returns one of these variants — the union itself does not grow for consumers. */
|
|
100
|
+
export type TranscriptEvent = StreamChunkEvent | MessageEvent | ToolCallEvent | ToolResultEvent | TurnEvent | StepEvent | LifecycleEvent;
|
|
101
|
+
/** A stored chunk as the store/read path exposes it (mirrors `TranscriptChunk`). */
|
|
102
|
+
export interface StoredChunk {
|
|
103
|
+
readonly offset: number;
|
|
104
|
+
readonly chunk: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A decoder for one event kind: given the parsed envelope body and the chunk offset, it returns the
|
|
108
|
+
* typed event (or `undefined` to reject a malformed envelope, which then falls back to `stream-chunk`).
|
|
109
|
+
* A vocabulary is the map kind → decoder; {@link mergeTranscriptVocab} extends it additively.
|
|
110
|
+
*/
|
|
111
|
+
export type TranscriptEventDecoder = (body: Record<string, unknown>, offset: number) => TranscriptEvent | undefined;
|
|
112
|
+
/** A transcript-event vocabulary: the ONE registry of kind → decoder the single parser consults. */
|
|
113
|
+
export type TranscriptVocab = Readonly<Record<string, TranscriptEventDecoder>>;
|
|
114
|
+
/**
|
|
115
|
+
* The opinionated core vocabulary — the built-in event kinds every consumer understands out of the
|
|
116
|
+
* box. Authors extend it in the SAME schema via {@link mergeTranscriptVocab}; they never fork the
|
|
117
|
+
* parser. (`stream-chunk` is not decoded here — it is the fallback the parser applies to any chunk
|
|
118
|
+
* that is not a well-formed typed envelope, so raw fidelity needs no decoder.)
|
|
119
|
+
*/
|
|
120
|
+
export declare const CORE_TRANSCRIPT_VOCAB: TranscriptVocab;
|
|
121
|
+
/** The core event kinds the parser decodes from an envelope (every kind except the raw `stream-chunk`
|
|
122
|
+
* fallback). Kept as a runtime list so the drift-guard can assert the single fold handles them all. */
|
|
123
|
+
export declare const CORE_TRANSCRIPT_EVENT_KINDS: readonly Exclude<TranscriptEventKind, "stream-chunk">[];
|
|
124
|
+
/**
|
|
125
|
+
* Extend a vocabulary additively: later entries win on a key clash, so an author can either register a
|
|
126
|
+
* brand-new kind or deliberately override a core decoder. Returns a NEW frozen, NULL-PROTOTYPE vocab —
|
|
127
|
+
* neither input is mutated — so the core stays canonical AND `kind in vocab` / `Object.keys(vocab)`
|
|
128
|
+
* only ever see own decoders (an inherited "toString"/"constructor" key can never masquerade as one).
|
|
129
|
+
* This is the EXTENSION POINT a downstream app uses to add its own kind (e.g. nano-workforce#559's
|
|
130
|
+
* `permission`) without editing this package: one schema, extended by merge, never a second parser.
|
|
131
|
+
*/
|
|
132
|
+
export declare function mergeTranscriptVocab(base: TranscriptVocab, ...extensions: TranscriptVocab[]): TranscriptVocab;
|
|
133
|
+
/**
|
|
134
|
+
* THE ONE PARSER. Classify a single stored chunk into a typed {@link TranscriptEvent}.
|
|
135
|
+
*
|
|
136
|
+
* A chunk is decoded as a structured event ONLY when it is a JSON object carrying the
|
|
137
|
+
* {@link TRANSCRIPT_EVENT_MARKER} at the current version AND a `kind` the vocab knows AND its decoder
|
|
138
|
+
* accepts the body. Anything else — raw terminal bytes, non-JSON, a JSON value without the marker, an
|
|
139
|
+
* unknown kind, a decoder rejection — is retained verbatim as a `stream-chunk`, so byte-level replay
|
|
140
|
+
* fidelity is never lost. This is the SINGLE point at which raw bytes become typed events; every view
|
|
141
|
+
* folds over the result of this function, so there is exactly one parser of the log.
|
|
142
|
+
*/
|
|
143
|
+
export declare function parseTranscriptEvent(entry: StoredChunk, vocab?: TranscriptVocab): TranscriptEvent;
|
|
144
|
+
/**
|
|
145
|
+
* Encode a typed event into the stored-chunk wire form a structured producer appends. The inverse of
|
|
146
|
+
* {@link parseTranscriptEvent} for every non-raw kind (a `stream-chunk` is stored as its own raw bytes,
|
|
147
|
+
* so it is returned verbatim). Provided so producers and tests speak the one envelope grammar rather
|
|
148
|
+
* than hand-rolling the marker — the derivation-over-duplication rule applied to the write side too.
|
|
149
|
+
*/
|
|
150
|
+
export declare function encodeTranscriptEvent(event: TranscriptEvent): string;
|
|
151
|
+
/** A derived tool card: a tool-call paired with its result (result absent while the call is pending). */
|
|
152
|
+
export interface DerivedTool {
|
|
153
|
+
readonly name: string;
|
|
154
|
+
readonly callId?: string;
|
|
155
|
+
readonly args?: unknown;
|
|
156
|
+
readonly offset: number;
|
|
157
|
+
readonly result?: {
|
|
158
|
+
readonly ok: boolean;
|
|
159
|
+
readonly content?: string;
|
|
160
|
+
readonly offset: number;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** A derived message in the folded history. */
|
|
164
|
+
export interface DerivedMessage {
|
|
165
|
+
readonly role: TranscriptRole;
|
|
166
|
+
readonly text: string;
|
|
167
|
+
readonly offset: number;
|
|
168
|
+
}
|
|
169
|
+
/** A derived turn: the messages, tool cards and step count folded within one turn boundary. */
|
|
170
|
+
export interface DerivedTurn {
|
|
171
|
+
readonly index: number;
|
|
172
|
+
readonly startOffset: number;
|
|
173
|
+
readonly messages: readonly DerivedMessage[];
|
|
174
|
+
readonly tools: readonly DerivedTool[];
|
|
175
|
+
readonly steps: number;
|
|
176
|
+
}
|
|
177
|
+
/** The single derived view every higher-level consumer reads instead of re-parsing raw bytes. */
|
|
178
|
+
export interface DerivedView {
|
|
179
|
+
/** The per-turn structure (a turn is opened implicitly before the first turn event when typed content — a message, tool-call or step — precedes it; raw `stream-chunk`s alone open no turn). */
|
|
180
|
+
readonly turns: readonly DerivedTurn[];
|
|
181
|
+
/** Every message across all turns, in offset order (the flat derived history). */
|
|
182
|
+
readonly messages: readonly DerivedMessage[];
|
|
183
|
+
/** Every tool card across all turns, in offset order. */
|
|
184
|
+
readonly tools: readonly DerivedTool[];
|
|
185
|
+
/** Total retained raw bytes (UTF-8) across `stream-chunk` events — the byte-replay fidelity accounting. */
|
|
186
|
+
readonly rawByteLength: number;
|
|
187
|
+
/** Number of retained raw chunks. */
|
|
188
|
+
readonly rawChunkCount: number;
|
|
189
|
+
/** The session lifecycle as the last lifecycle event reports it (defaults to `open`). */
|
|
190
|
+
readonly lifecycle: "open" | "completed" | "exited";
|
|
191
|
+
/** Number of events folded — every event in the log, including raw `stream-chunk`s. */
|
|
192
|
+
readonly eventCount: number;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* THE SINGLE FOLD. Derive every higher-level view from the typed event log — "the log IS the state".
|
|
196
|
+
*
|
|
197
|
+
* Folds the events (assumed in offset order — the store's append order) into per-turn structure, a flat
|
|
198
|
+
* message history, tool cards (each call paired to its result by `callId`, else the most recent open
|
|
199
|
+
* call), raw-byte accounting for replay fidelity, and the session lifecycle. It is a pure reduction of
|
|
200
|
+
* one log: the cockpit, search, token accounting and export all read THIS, so there is never a second
|
|
201
|
+
* parser of the same bytes. Typed content — a message, tool-call or step — that precedes the first
|
|
202
|
+
* explicit `turn` event opens an implicit turn 0, so a producer that never emits turn boundaries still
|
|
203
|
+
* derives a coherent single-turn view. Raw `stream-chunk` events alone open no turn (they only feed the
|
|
204
|
+
* byte-replay accounting), so a log of only chunks derives zero turns.
|
|
205
|
+
*/
|
|
206
|
+
export declare function deriveView(events: Iterable<TranscriptEvent>): DerivedView;
|
|
207
|
+
/**
|
|
208
|
+
* Convenience: parse a run of stored chunks into typed events through {@link parseTranscriptEvent} (the
|
|
209
|
+
* one parser) and fold them with {@link deriveView} in a single call — the entry point a consumer uses
|
|
210
|
+
* to go from stored bytes to a derived view without ever touching a second parser.
|
|
211
|
+
*/
|
|
212
|
+
export declare function deriveViewFromChunks(chunks: Iterable<StoredChunk>, vocab?: TranscriptVocab): DerivedView;
|
|
213
|
+
export {};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transcript EVENT vocabulary + the single derive() fold (ADR 0056, #251).
|
|
3
|
+
*
|
|
4
|
+
* This is the "event-sourced session" layer over the S6 transcript store ({@link ./store.ts}). The
|
|
5
|
+
* store is already append-only and offset-keyed — chunks are appended, never mutated — which is half of
|
|
6
|
+
* the event-sourced-session pattern. The gap it left is that chunks are opaque `TEXT`: every richer
|
|
7
|
+
* view (structured message history, tool cards, per-turn boundaries, token accounting) had to re-parse
|
|
8
|
+
* the raw frame bytes ad hoc, a DRIFT SURFACE (two parsers of the same bytes), which our "Derivation
|
|
9
|
+
* Over Duplication" doctrine forbids.
|
|
10
|
+
*
|
|
11
|
+
* This module closes that gap: the append-only log of TYPED events is the single source of truth, and
|
|
12
|
+
* every higher-level view is a DERIVATION of that one log via a single {@link deriveView} fold — "the
|
|
13
|
+
* log IS the state, so divergence is structurally impossible". A raw terminal chunk is retained
|
|
14
|
+
* verbatim as a `stream-chunk` event (byte-level replay fidelity is preserved); a producer that emits a
|
|
15
|
+
* structured, marker-tagged JSON envelope is decoded into the authoritative typed events (message /
|
|
16
|
+
* tool-call / tool-result / turn / step / lifecycle) the derived views fold over.
|
|
17
|
+
*
|
|
18
|
+
* THE ONE PARSER. {@link parseTranscriptEvent} is the SINGLE place a stored chunk is classified into a
|
|
19
|
+
* typed event; every consumer (cockpit, search, token accounting, export) reads the derived view, not
|
|
20
|
+
* the raw bytes. A drift-guard test (`events.drift.test.ts`) asserts the event marker — and therefore
|
|
21
|
+
* the raw→event parse — appears in exactly this module, so a second parser cannot creep in.
|
|
22
|
+
*
|
|
23
|
+
* MERGE-EXTENSIBLE. The vocabulary is a small core ({@link CORE_TRANSCRIPT_VOCAB}) authors extend in the
|
|
24
|
+
* same schema with {@link mergeTranscriptVocab}, so a new event kind is an additive merge, never a fork
|
|
25
|
+
* of the parser. A downstream app (e.g. nano-workforce#559) registers its own `permission` kind this
|
|
26
|
+
* way without editing this package.
|
|
27
|
+
*
|
|
28
|
+
* BROWSER-SAFE. This module is imported by cockpit code that runs in the BROWSER (the cockpit derive),
|
|
29
|
+
* so it takes no hard dependency on Node's `Buffer` or any Node-only API — {@link utf8ByteLength} uses
|
|
30
|
+
* the Web/Node standard `TextEncoder`. It is pure and side-effect-free: no I/O, and it never touches the
|
|
31
|
+
* engine or a BPMN flow (ADR 0056: app-tier only, advisory).
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Runtime-safe UTF-8 byte length. The one canonical UTF-8 byte-length implementation the transcript
|
|
35
|
+
* plane derives from. Implemented with `TextEncoder` (a Web/Node standard) rather than Node's `Buffer`,
|
|
36
|
+
* so the single derive fold is portable across the browser (where `Buffer` is not available) and Node.
|
|
37
|
+
*/
|
|
38
|
+
let cachedTextEncoder;
|
|
39
|
+
export function utf8ByteLength(text) {
|
|
40
|
+
// Cache one TextEncoder in the hot path (folding many stream-chunk events) to avoid allocating a new
|
|
41
|
+
// encoder — and the GC pressure it creates — on every call.
|
|
42
|
+
cachedTextEncoder ??= new TextEncoder();
|
|
43
|
+
return cachedTextEncoder.encode(text).length;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The reserved marker field that distinguishes a structured transcript-event envelope from raw
|
|
47
|
+
* terminal bytes. A stored chunk is decoded as a typed event ONLY when it is a JSON object carrying
|
|
48
|
+
* this field set to the schema version — otherwise it is retained verbatim as a raw `stream-chunk`, so
|
|
49
|
+
* a raw ANSI frame that happens to be valid JSON is never mis-classified. Namespaced so it cannot
|
|
50
|
+
* collide with a producer's own payload keys. This is the canonical single source of truth for the
|
|
51
|
+
* whole package family — consumers (e.g. the cockpit) import this identifier, never a private copy.
|
|
52
|
+
*/
|
|
53
|
+
export const TRANSCRIPT_EVENT_MARKER = "nwfTranscriptEvent";
|
|
54
|
+
/** The current transcript-event envelope schema version (the value {@link TRANSCRIPT_EVENT_MARKER} carries). */
|
|
55
|
+
export const TRANSCRIPT_EVENT_VERSION = 1;
|
|
56
|
+
function str(body, key) {
|
|
57
|
+
const v = body[key];
|
|
58
|
+
return typeof v === "string" ? v : undefined;
|
|
59
|
+
}
|
|
60
|
+
function num(body, key) {
|
|
61
|
+
const v = body[key];
|
|
62
|
+
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
63
|
+
}
|
|
64
|
+
const ROLES = ["assistant", "user", "system", "tool"];
|
|
65
|
+
/** Narrow an arbitrary string to a known {@link TranscriptRole}, defaulting to `assistant`. */
|
|
66
|
+
function toRole(value) {
|
|
67
|
+
return ROLES.find((role) => role === value) ?? "assistant";
|
|
68
|
+
}
|
|
69
|
+
/** A structural guard: a non-null, non-array object is a plain record of unknown values. */
|
|
70
|
+
function isRecord(value) {
|
|
71
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The opinionated core vocabulary — the built-in event kinds every consumer understands out of the
|
|
75
|
+
* box. Authors extend it in the SAME schema via {@link mergeTranscriptVocab}; they never fork the
|
|
76
|
+
* parser. (`stream-chunk` is not decoded here — it is the fallback the parser applies to any chunk
|
|
77
|
+
* that is not a well-formed typed envelope, so raw fidelity needs no decoder.)
|
|
78
|
+
*/
|
|
79
|
+
export const CORE_TRANSCRIPT_VOCAB = Object.freeze(Object.assign(Object.create(null), {
|
|
80
|
+
message: (body, offset) => {
|
|
81
|
+
const text = str(body, "text");
|
|
82
|
+
if (text === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
const roleRaw = str(body, "role");
|
|
85
|
+
return { kind: "message", offset, role: toRole(roleRaw), text };
|
|
86
|
+
},
|
|
87
|
+
"tool-call": (body, offset) => {
|
|
88
|
+
const name = str(body, "name");
|
|
89
|
+
if (name === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
const event = { kind: "tool-call", offset, name };
|
|
92
|
+
const callId = str(body, "callId");
|
|
93
|
+
return {
|
|
94
|
+
...event,
|
|
95
|
+
...(callId !== undefined ? { callId } : {}),
|
|
96
|
+
...("args" in body ? { args: body.args } : {}),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
"tool-result": (body, offset) => {
|
|
100
|
+
const ok = typeof body.ok === "boolean" ? body.ok : true;
|
|
101
|
+
const event = { kind: "tool-result", offset, ok };
|
|
102
|
+
const callId = str(body, "callId");
|
|
103
|
+
const content = str(body, "content");
|
|
104
|
+
return {
|
|
105
|
+
...event,
|
|
106
|
+
...(callId !== undefined ? { callId } : {}),
|
|
107
|
+
...(content !== undefined ? { content } : {}),
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
turn: (body, offset) => {
|
|
111
|
+
const index = num(body, "index");
|
|
112
|
+
return index !== undefined ? { kind: "turn", offset, index } : { kind: "turn", offset };
|
|
113
|
+
},
|
|
114
|
+
step: (body, offset) => {
|
|
115
|
+
const label = str(body, "label");
|
|
116
|
+
return label !== undefined ? { kind: "step", offset, label } : { kind: "step", offset };
|
|
117
|
+
},
|
|
118
|
+
lifecycle: (body, offset) => {
|
|
119
|
+
const phase = str(body, "phase");
|
|
120
|
+
if (phase !== "open" && phase !== "completed" && phase !== "exited")
|
|
121
|
+
return undefined;
|
|
122
|
+
return { kind: "lifecycle", offset, phase };
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
/** The core event kinds the parser decodes from an envelope (every kind except the raw `stream-chunk`
|
|
126
|
+
* fallback). Kept as a runtime list so the drift-guard can assert the single fold handles them all. */
|
|
127
|
+
export const CORE_TRANSCRIPT_EVENT_KINDS = Object.freeze([
|
|
128
|
+
"message",
|
|
129
|
+
"tool-call",
|
|
130
|
+
"tool-result",
|
|
131
|
+
"turn",
|
|
132
|
+
"step",
|
|
133
|
+
"lifecycle",
|
|
134
|
+
]);
|
|
135
|
+
/**
|
|
136
|
+
* Extend a vocabulary additively: later entries win on a key clash, so an author can either register a
|
|
137
|
+
* brand-new kind or deliberately override a core decoder. Returns a NEW frozen, NULL-PROTOTYPE vocab —
|
|
138
|
+
* neither input is mutated — so the core stays canonical AND `kind in vocab` / `Object.keys(vocab)`
|
|
139
|
+
* only ever see own decoders (an inherited "toString"/"constructor" key can never masquerade as one).
|
|
140
|
+
* This is the EXTENSION POINT a downstream app uses to add its own kind (e.g. nano-workforce#559's
|
|
141
|
+
* `permission`) without editing this package: one schema, extended by merge, never a second parser.
|
|
142
|
+
*/
|
|
143
|
+
export function mergeTranscriptVocab(base, ...extensions) {
|
|
144
|
+
return Object.freeze(Object.assign(Object.create(null), base, ...extensions));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* THE ONE PARSER. Classify a single stored chunk into a typed {@link TranscriptEvent}.
|
|
148
|
+
*
|
|
149
|
+
* A chunk is decoded as a structured event ONLY when it is a JSON object carrying the
|
|
150
|
+
* {@link TRANSCRIPT_EVENT_MARKER} at the current version AND a `kind` the vocab knows AND its decoder
|
|
151
|
+
* accepts the body. Anything else — raw terminal bytes, non-JSON, a JSON value without the marker, an
|
|
152
|
+
* unknown kind, a decoder rejection — is retained verbatim as a `stream-chunk`, so byte-level replay
|
|
153
|
+
* fidelity is never lost. This is the SINGLE point at which raw bytes become typed events; every view
|
|
154
|
+
* folds over the result of this function, so there is exactly one parser of the log.
|
|
155
|
+
*/
|
|
156
|
+
export function parseTranscriptEvent(entry, vocab = CORE_TRANSCRIPT_VOCAB) {
|
|
157
|
+
const raw = { kind: "stream-chunk", offset: entry.offset, chunk: entry.chunk };
|
|
158
|
+
const body = decodeEnvelope(entry.chunk);
|
|
159
|
+
if (body === undefined)
|
|
160
|
+
return raw;
|
|
161
|
+
const kind = typeof body.kind === "string" ? body.kind : undefined;
|
|
162
|
+
if (kind === undefined)
|
|
163
|
+
return raw;
|
|
164
|
+
// Own-property + typeof-function guard: `kind` is untrusted, so a bare `vocab[kind]` would resolve
|
|
165
|
+
// inherited members like "constructor"/"toString"/"__proto__" up the prototype chain to a
|
|
166
|
+
// non-decoder function and call it — a crashable (DoS) / invariant-breaking path. Only an OWN
|
|
167
|
+
// decoder function is ever invoked; everything else falls back to the raw stream-chunk.
|
|
168
|
+
const decoder = Object.prototype.hasOwnProperty.call(vocab, kind) ? vocab[kind] : undefined;
|
|
169
|
+
if (typeof decoder !== "function")
|
|
170
|
+
return raw;
|
|
171
|
+
return decoder(body, entry.offset) ?? raw;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Decode a chunk into a marker-tagged envelope body, or `undefined` when it is not one. Kept private
|
|
175
|
+
* so `JSON.parse` of a chunk lives in exactly one place (the drift-guard depends on this).
|
|
176
|
+
*/
|
|
177
|
+
function decodeEnvelope(chunk) {
|
|
178
|
+
// Cheap reject before the parse: a valid envelope is a JSON object mentioning the marker key.
|
|
179
|
+
const trimmed = chunk.trimStart();
|
|
180
|
+
if (!trimmed.startsWith("{") || !chunk.includes(TRANSCRIPT_EVENT_MARKER))
|
|
181
|
+
return undefined;
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(chunk);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
if (!isRecord(parsed))
|
|
190
|
+
return undefined;
|
|
191
|
+
return parsed[TRANSCRIPT_EVENT_MARKER] === TRANSCRIPT_EVENT_VERSION ? parsed : undefined;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Encode a typed event into the stored-chunk wire form a structured producer appends. The inverse of
|
|
195
|
+
* {@link parseTranscriptEvent} for every non-raw kind (a `stream-chunk` is stored as its own raw bytes,
|
|
196
|
+
* so it is returned verbatim). Provided so producers and tests speak the one envelope grammar rather
|
|
197
|
+
* than hand-rolling the marker — the derivation-over-duplication rule applied to the write side too.
|
|
198
|
+
*/
|
|
199
|
+
export function encodeTranscriptEvent(event) {
|
|
200
|
+
if (event.kind === "stream-chunk")
|
|
201
|
+
return event.chunk;
|
|
202
|
+
const { offset: _offset, ...rest } = event;
|
|
203
|
+
return JSON.stringify({ [TRANSCRIPT_EVENT_MARKER]: TRANSCRIPT_EVENT_VERSION, ...rest });
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* THE SINGLE FOLD. Derive every higher-level view from the typed event log — "the log IS the state".
|
|
207
|
+
*
|
|
208
|
+
* Folds the events (assumed in offset order — the store's append order) into per-turn structure, a flat
|
|
209
|
+
* message history, tool cards (each call paired to its result by `callId`, else the most recent open
|
|
210
|
+
* call), raw-byte accounting for replay fidelity, and the session lifecycle. It is a pure reduction of
|
|
211
|
+
* one log: the cockpit, search, token accounting and export all read THIS, so there is never a second
|
|
212
|
+
* parser of the same bytes. Typed content — a message, tool-call or step — that precedes the first
|
|
213
|
+
* explicit `turn` event opens an implicit turn 0, so a producer that never emits turn boundaries still
|
|
214
|
+
* derives a coherent single-turn view. Raw `stream-chunk` events alone open no turn (they only feed the
|
|
215
|
+
* byte-replay accounting), so a log of only chunks derives zero turns.
|
|
216
|
+
*/
|
|
217
|
+
export function deriveView(events) {
|
|
218
|
+
const turns = [];
|
|
219
|
+
const messages = [];
|
|
220
|
+
const tools = [];
|
|
221
|
+
const openTools = new Map();
|
|
222
|
+
let anonymousTool;
|
|
223
|
+
let rawByteLength = 0;
|
|
224
|
+
let rawChunkCount = 0;
|
|
225
|
+
let lifecycle = "open";
|
|
226
|
+
let eventCount = 0;
|
|
227
|
+
let current;
|
|
228
|
+
const ensureTurn = (offset) => {
|
|
229
|
+
if (current === undefined) {
|
|
230
|
+
current = { index: turns.length, startOffset: offset, messages: [], tools: [], steps: 0 };
|
|
231
|
+
turns.push(current);
|
|
232
|
+
}
|
|
233
|
+
return current;
|
|
234
|
+
};
|
|
235
|
+
for (const event of events) {
|
|
236
|
+
eventCount++;
|
|
237
|
+
switch (event.kind) {
|
|
238
|
+
case "turn": {
|
|
239
|
+
current = { index: event.index ?? turns.length, startOffset: event.offset, messages: [], tools: [], steps: 0 };
|
|
240
|
+
turns.push(current);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "step": {
|
|
244
|
+
ensureTurn(event.offset).steps++;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case "message": {
|
|
248
|
+
const msg = { role: event.role, text: event.text, offset: event.offset };
|
|
249
|
+
messages.push(msg);
|
|
250
|
+
ensureTurn(event.offset).messages.push(msg);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "tool-call": {
|
|
254
|
+
const tool = {
|
|
255
|
+
name: event.name,
|
|
256
|
+
offset: event.offset,
|
|
257
|
+
...(event.callId !== undefined ? { callId: event.callId } : {}),
|
|
258
|
+
...(event.args !== undefined ? { args: event.args } : {}),
|
|
259
|
+
};
|
|
260
|
+
const toolsIndex = tools.push(tool) - 1;
|
|
261
|
+
const turn = ensureTurn(event.offset);
|
|
262
|
+
const turnToolIndex = turn.tools.push(tool) - 1;
|
|
263
|
+
const pending = { tool, toolsIndex, turn, turnToolIndex };
|
|
264
|
+
if (event.callId !== undefined)
|
|
265
|
+
openTools.set(event.callId, pending);
|
|
266
|
+
else
|
|
267
|
+
anonymousTool = pending;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "tool-result": {
|
|
271
|
+
const pending = event.callId !== undefined ? openTools.get(event.callId) : anonymousTool;
|
|
272
|
+
if (pending !== undefined) {
|
|
273
|
+
const resolved = withResult(pending.tool, event);
|
|
274
|
+
tools[pending.toolsIndex] = resolved;
|
|
275
|
+
pending.turn.tools[pending.turnToolIndex] = resolved;
|
|
276
|
+
if (event.callId !== undefined)
|
|
277
|
+
openTools.delete(event.callId);
|
|
278
|
+
else
|
|
279
|
+
anonymousTool = undefined;
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "lifecycle": {
|
|
284
|
+
lifecycle = event.phase;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "stream-chunk": {
|
|
288
|
+
rawByteLength += utf8ByteLength(event.chunk);
|
|
289
|
+
rawChunkCount++;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
turns: turns.map((t) => ({ index: t.index, startOffset: t.startOffset, messages: t.messages, tools: t.tools, steps: t.steps })),
|
|
296
|
+
messages,
|
|
297
|
+
tools,
|
|
298
|
+
rawByteLength,
|
|
299
|
+
rawChunkCount,
|
|
300
|
+
lifecycle,
|
|
301
|
+
eventCount,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** Replace a pending tool with its result: a new resolved card that carries the result payload. */
|
|
305
|
+
function withResult(tool, result) {
|
|
306
|
+
return {
|
|
307
|
+
...tool,
|
|
308
|
+
result: { ok: result.ok, offset: result.offset, ...(result.content !== undefined ? { content: result.content } : {}) },
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Convenience: parse a run of stored chunks into typed events through {@link parseTranscriptEvent} (the
|
|
313
|
+
* one parser) and fold them with {@link deriveView} in a single call — the entry point a consumer uses
|
|
314
|
+
* to go from stored bytes to a derived view without ever touching a second parser.
|
|
315
|
+
*/
|
|
316
|
+
export function deriveViewFromChunks(chunks, vocab = CORE_TRANSCRIPT_VOCAB) {
|
|
317
|
+
function* parsed() {
|
|
318
|
+
for (const entry of chunks)
|
|
319
|
+
yield parseTranscriptEvent(entry, vocab);
|
|
320
|
+
}
|
|
321
|
+
return deriveView(parsed());
|
|
322
|
+
}
|
|
@@ -16,3 +16,11 @@
|
|
|
16
16
|
export { TranscriptStore, TranscriptCorruptionError, TranscriptLifecycleError, systemClock } from "./store.ts";
|
|
17
17
|
export type { Clock, SqliteDb, TranscriptChunk, TranscriptContentBlock, TranscriptContentType, TranscriptLifecycle, TranscriptRing, TranscriptSlice, TranscriptStatus, TranscriptStoreOptions, TranscriptStream, TranscriptToolCall, TranscriptTurn, TranscriptTurnMetrics, TranscriptTurnRole, } from "./store.ts";
|
|
18
18
|
export { TRANSCRIPT_CHUNK_TABLE, TRANSCRIPT_SCHEMA_SQL, TRANSCRIPT_STREAM_TABLE, TRANSCRIPT_TURN_SCHEMA_SQL, TRANSCRIPT_TURN_TABLE, } from "./schema.ts";
|
|
19
|
+
/**
|
|
20
|
+
* The transcript EVENT vocabulary + the single derive() fold (ADR 0056, #251) — the canonical,
|
|
21
|
+
* merge-extensible typed event grammar every Urban app consumes and extends (rather than forking).
|
|
22
|
+
* The marker + version constants and {@link parseTranscriptEvent} are the single source of truth the
|
|
23
|
+
* whole package family (e.g. the cockpit's structured-stream detection) imports from here.
|
|
24
|
+
*/
|
|
25
|
+
export { CORE_TRANSCRIPT_EVENT_KINDS, CORE_TRANSCRIPT_VOCAB, TRANSCRIPT_EVENT_MARKER, TRANSCRIPT_EVENT_VERSION, deriveView, deriveViewFromChunks, encodeTranscriptEvent, mergeTranscriptVocab, parseTranscriptEvent, utf8ByteLength, } from "./events.ts";
|
|
26
|
+
export type { DerivedMessage, DerivedTool, DerivedTurn, DerivedView, LifecycleEvent, MessageEvent, StepEvent, StoredChunk, StreamChunkEvent, ToolCallEvent, ToolResultEvent, TranscriptEvent, TranscriptEventDecoder, TranscriptEventKind, TranscriptRole, TranscriptVocab, TurnEvent, } from "./events.ts";
|
package/dist/transcript/index.js
CHANGED
|
@@ -15,3 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export { TranscriptStore, TranscriptCorruptionError, TranscriptLifecycleError, systemClock } from "./store.js";
|
|
17
17
|
export { TRANSCRIPT_CHUNK_TABLE, TRANSCRIPT_SCHEMA_SQL, TRANSCRIPT_STREAM_TABLE, TRANSCRIPT_TURN_SCHEMA_SQL, TRANSCRIPT_TURN_TABLE, } from "./schema.js";
|
|
18
|
+
/**
|
|
19
|
+
* The transcript EVENT vocabulary + the single derive() fold (ADR 0056, #251) — the canonical,
|
|
20
|
+
* merge-extensible typed event grammar every Urban app consumes and extends (rather than forking).
|
|
21
|
+
* The marker + version constants and {@link parseTranscriptEvent} are the single source of truth the
|
|
22
|
+
* whole package family (e.g. the cockpit's structured-stream detection) imports from here.
|
|
23
|
+
*/
|
|
24
|
+
export { CORE_TRANSCRIPT_EVENT_KINDS, CORE_TRANSCRIPT_VOCAB, TRANSCRIPT_EVENT_MARKER, TRANSCRIPT_EVENT_VERSION, deriveView, deriveViewFromChunks, encodeTranscriptEvent, mergeTranscriptVocab, parseTranscriptEvent, utf8ByteLength, } from "./events.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/agentic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The Nano agentic protocol (ADR 0056): one app-tier channel carrying agent presence/registry, demand×supply, a shared blackboard and live terminal relay — with the wire contract, channel/hub, family modules and the operator cockpit, as subpath exports.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|