@nanobpm/nano-workforce 0.170.1 → 0.171.1

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.
@@ -1,51 +1,50 @@
1
- // @generated from app/agentic/transcript-events.ts by scripts/build-cockpit-browser.ts — DO NOT EDIT.
1
+ // @generated from node_modules/@nanobpm/agentic/dist/transcript/events.js by scripts/build-cockpit-browser.ts — DO NOT EDIT.
2
2
  //
3
3
  // Browser ESM derived (type-strip only) from the typed transcript core so pages/cockpit/mount.js
4
4
  // renders the agentic transcript from ONE source of truth (#660). Regenerate with:
5
5
  // node --experimental-strip-types scripts/build-cockpit-browser.ts
6
6
 
7
- // nano-workforce — the transcript EVENT vocabulary + the single derive() fold (ADR 0056, #251).
8
- //
9
- // This is the "event-sourced session" layer over the H3 transcript store (#146/#222). The store is
10
- // already append-only and offset-keyed — chunks are appended, never mutated — which is half of the
11
- // dsh (DeepSeek Harness) event-sourced-session pattern. The gap it left is that chunks are opaque
12
- // `TEXT`: every richer view (structured message history, tool cards, per-turn boundaries, token
13
- // accounting) had to re-parse the raw frame bytes ad hoc, a DRIFT SURFACE (two parsers of the same
14
- // bytes), which our "Derivation Over Duplication" doctrine forbids.
15
- //
16
- // This module closes that gap the way dsh does: the append-only log of TYPED events is the single
17
- // source of truth, and every higher-level view is a DERIVATION of that one log via a single
18
- // {@link deriveView} fold — "the log IS the state, so divergence is structurally impossible". A raw
19
- // terminal chunk is retained verbatim as a `stream-chunk` event (byte-level replay fidelity is
20
- // preserved); a producer that emits a structured, marker-tagged JSON envelope is decoded into the
21
- // authoritative typed events (message / tool-call / tool-result / turn / step / lifecycle) the derived
22
- // views fold over — mirroring dsh (raw chunks for token-replay, `assistant/message` authoritative).
23
- //
24
- // THE ONE PARSER. {@link parseTranscriptEvent} is the SINGLE place a stored chunk is classified into a
25
- // typed event; every consumer (cockpit, search, token accounting, export) reads the derived view, not
26
- // the raw bytes. A drift-guard test (`transcript-events.drift.test.ts`) asserts the event marker — and
27
- // therefore the raw→event parse — appears in exactly this module, so a second parser cannot creep in.
28
- //
29
- // MERGE-EXTENSIBLE. The vocabulary is a small core ({@link CORE_TRANSCRIPT_VOCAB}) authors extend in the
30
- // same schema with {@link mergeTranscriptVocab} (cribbed from dsh's merge-extensible event taxonomy and
31
- // the S3 `mergeVocab`), so a new event kind is an additive merge, never a fork of the parser.
32
- //
33
- // Pure and side-effect-free: no I/O, unit-testable on Node, and it never touches the engine or a BPMN
34
- // flow (ADR 0056: app-tier only, advisory).
35
7
  /**
36
- * Runtime-safe UTF-8 byte length. This module is imported by cockpit code that runs in the BROWSER
37
- * (via `cockpit/transcript-derive.ts`), where Node's `Buffer` global is not available — a bare
38
- * `Buffer.byteLength` would throw at runtime when deriving the view for a replayed transcript. Prefer
39
- * `Buffer` when present (Node) and fall back to `TextEncoder` (a Web/Node standard) otherwise, so the
40
- * single derive fold is portable across both hosts. This is the one canonical UTF-8 byte-length
41
- * implementation the transcript plane derives from (reused by `transcript-read.ts`).
8
+ * The transcript EVENT vocabulary + the single derive() fold (ADR 0056, #251).
9
+ *
10
+ * This is the "event-sourced session" layer over the S6 transcript store ({@link ./store.ts}). The
11
+ * store is already append-only and offset-keyed chunks are appended, never mutated which is half of
12
+ * the event-sourced-session pattern. The gap it left is that chunks are opaque `TEXT`: every richer
13
+ * view (structured message history, tool cards, per-turn boundaries, token accounting) had to re-parse
14
+ * the raw frame bytes ad hoc, a DRIFT SURFACE (two parsers of the same bytes), which our "Derivation
15
+ * Over Duplication" doctrine forbids.
16
+ *
17
+ * This module closes that gap: the append-only log of TYPED events is the single source of truth, and
18
+ * every higher-level view is a DERIVATION of that one log via a single {@link deriveView} fold — "the
19
+ * log IS the state, so divergence is structurally impossible". A raw terminal chunk is retained
20
+ * verbatim as a `stream-chunk` event (byte-level replay fidelity is preserved); a producer that emits a
21
+ * structured, marker-tagged JSON envelope is decoded into the authoritative typed events (message /
22
+ * tool-call / tool-result / turn / step / lifecycle) the derived views fold over.
23
+ *
24
+ * THE ONE PARSER. {@link parseTranscriptEvent} is the SINGLE place a stored chunk is classified into a
25
+ * typed event; every consumer (cockpit, search, token accounting, export) reads the derived view, not
26
+ * the raw bytes. A drift-guard test (`events.drift.test.ts`) asserts the event marker — and therefore
27
+ * the raw→event parse — appears in exactly this module, so a second parser cannot creep in.
28
+ *
29
+ * MERGE-EXTENSIBLE. The vocabulary is a small core ({@link CORE_TRANSCRIPT_VOCAB}) authors extend in the
30
+ * same schema with {@link mergeTranscriptVocab}, so a new event kind is an additive merge, never a fork
31
+ * of the parser. A downstream app (e.g. nano-workforce#559) registers its own `permission` kind this
32
+ * way without editing this package.
33
+ *
34
+ * BROWSER-SAFE. This module is imported by cockpit code that runs in the BROWSER (the cockpit derive),
35
+ * so it takes no hard dependency on Node's `Buffer` or any Node-only API — {@link utf8ByteLength} uses
36
+ * the Web/Node standard `TextEncoder`. It is pure and side-effect-free: no I/O, and it never touches the
37
+ * engine or a BPMN flow (ADR 0056: app-tier only, advisory).
38
+ */
39
+ /**
40
+ * Runtime-safe UTF-8 byte length. The one canonical UTF-8 byte-length implementation the transcript
41
+ * plane derives from. Implemented with `TextEncoder` (a Web/Node standard) rather than Node's `Buffer`,
42
+ * so the single derive fold is portable across the browser (where `Buffer` is not available) and Node.
42
43
  */
43
44
  let cachedTextEncoder;
44
45
  export function utf8ByteLength(text) {
45
- if (typeof Buffer !== "undefined")
46
- return Buffer.byteLength(text, "utf8");
47
- // Cache one TextEncoder in the browser hot path (folding many stream-chunk events) to avoid
48
- // allocating a new encoder — and the GC pressure it creates — on every call.
46
+ // Cache one TextEncoder in the hot path (folding many stream-chunk events) to avoid allocating a new
47
+ // encoder — and the GC pressure it creates — on every call.
49
48
  cachedTextEncoder ??= new TextEncoder();
50
49
  return cachedTextEncoder.encode(text).length;
51
50
  }
@@ -53,8 +52,9 @@ export function utf8ByteLength(text) {
53
52
  * The reserved marker field that distinguishes a structured transcript-event envelope from raw
54
53
  * terminal bytes. A stored chunk is decoded as a typed event ONLY when it is a JSON object carrying
55
54
  * this field set to the schema version — otherwise it is retained verbatim as a raw `stream-chunk`, so
56
- * a raw ANSI frame that happens to be valid JSON is never mis-classified. Namespaced to nano-workforce
57
- * so it cannot collide with a producer's own payload keys.
55
+ * a raw ANSI frame that happens to be valid JSON is never mis-classified. Namespaced so it cannot
56
+ * collide with a producer's own payload keys. This is the canonical single source of truth for the
57
+ * whole package family — consumers (e.g. the cockpit) import this identifier, never a private copy.
58
58
  */
59
59
  export const TRANSCRIPT_EVENT_MARKER = "nwfTranscriptEvent";
60
60
  /** The current transcript-event envelope schema version (the value {@link TRANSCRIPT_EVENT_MARKER} carries). */
@@ -117,7 +117,7 @@ function decodePermissionOptions(value) {
117
117
  * parser. (`stream-chunk` is not decoded here — it is the fallback the parser applies to any chunk
118
118
  * that is not a well-formed typed envelope, so raw fidelity needs no decoder.)
119
119
  */
120
- export const CORE_TRANSCRIPT_VOCAB = Object.freeze({
120
+ export const CORE_TRANSCRIPT_VOCAB = Object.freeze(Object.assign(Object.create(null), {
121
121
  message: (body, offset) => {
122
122
  const text = str(body, "text");
123
123
  if (text === undefined)
@@ -148,11 +148,6 @@ export const CORE_TRANSCRIPT_VOCAB = Object.freeze({
148
148
  ...(content !== undefined ? { content } : {}),
149
149
  };
150
150
  },
151
- // ACP `plan` mapping: ACP `session/update` plan updates map onto the EXISTING `step`/`turn`
152
- // vocabulary rather than a new kind — an ACP plan ENTRY becomes a `step` (its `label` is the plan
153
- // entry's title; the entry ordinal is not preserved, as `StepEvent` carries only a `label`), and a
154
- // plan/turn BOUNDARY becomes a `turn` (its `index` the ACP turn/plan ordinal). The decoders below
155
- // already cope with an ACP-shaped `label` (`step`) / `index` (`turn`), so no new kind is needed.
156
151
  turn: (body, offset) => {
157
152
  const index = num(body, "index");
158
153
  return index !== undefined ? { kind: "turn", offset, index } : { kind: "turn", offset };
@@ -184,6 +179,16 @@ export const CORE_TRANSCRIPT_VOCAB = Object.freeze({
184
179
  const toolName = str(body, "toolName");
185
180
  const title = str(body, "title");
186
181
  const reason = str(body, "reason");
182
+ // Reject a present-but-non-string optional field rather than silently dropping it, mirroring the
183
+ // `by` treatment in the resolution path below: a present-but-invalid value is a producer bug, and
184
+ // swallowing it would make the typed event diverge from the on-wire JSON ("malformed → raw
185
+ // fallback, never a silent mis-decode").
186
+ if (body.toolName !== undefined && toolName === undefined)
187
+ return undefined;
188
+ if (body.title !== undefined && title === undefined)
189
+ return undefined;
190
+ if (body.reason !== undefined && reason === undefined)
191
+ return undefined;
187
192
  const event = { kind: "permission", phase: "request", offset, callId, policy, options };
188
193
  return {
189
194
  ...event,
@@ -219,15 +224,28 @@ export const CORE_TRANSCRIPT_VOCAB = Object.freeze({
219
224
  }
220
225
  return undefined;
221
226
  },
222
- });
227
+ }));
228
+ /** The core event kinds the parser decodes from an envelope (every kind except the raw `stream-chunk`
229
+ * fallback). Kept as a runtime list so the drift-guard can assert the single fold handles them all. */
230
+ export const CORE_TRANSCRIPT_EVENT_KINDS = Object.freeze([
231
+ "message",
232
+ "tool-call",
233
+ "tool-result",
234
+ "turn",
235
+ "step",
236
+ "lifecycle",
237
+ "permission",
238
+ ]);
223
239
  /**
224
240
  * Extend a vocabulary additively: later entries win on a key clash, so an author can either register a
225
- * brand-new kind or deliberately override a core decoder. Returns a NEW frozen vocab — neither input is
226
- * mutated — so the core stays canonical. (Cribbed from dsh's merge-extensible taxonomy / the S3
227
- * `mergeVocab`: one schema, extended by merge, never a second parser.)
241
+ * brand-new kind or deliberately override a core decoder. Returns a NEW frozen, NULL-PROTOTYPE vocab —
242
+ * neither input is mutated — so the core stays canonical AND `kind in vocab` / `Object.keys(vocab)`
243
+ * only ever see own decoders (an inherited "toString"/"constructor" key can never masquerade as one).
244
+ * This is the EXTENSION POINT a downstream app uses to add its own kind (e.g. a synthetic `annotation`
245
+ * kind) without editing this package: one schema, extended by merge, never a second parser.
228
246
  */
229
247
  export function mergeTranscriptVocab(base, ...extensions) {
230
- return Object.freeze(Object.assign({}, base, ...extensions));
248
+ return Object.freeze(Object.assign(Object.create(null), base, ...extensions));
231
249
  }
232
250
  /**
233
251
  * THE ONE PARSER. Classify a single stored chunk into a typed {@link TranscriptEvent}.
@@ -247,8 +265,12 @@ export function parseTranscriptEvent(entry, vocab = CORE_TRANSCRIPT_VOCAB) {
247
265
  const kind = typeof body.kind === "string" ? body.kind : undefined;
248
266
  if (kind === undefined)
249
267
  return raw;
250
- const decoder = vocab[kind];
251
- if (decoder === undefined)
268
+ // Own-property + typeof-function guard: `kind` is untrusted, so a bare `vocab[kind]` would resolve
269
+ // inherited members like "constructor"/"toString"/"__proto__" up the prototype chain to a
270
+ // non-decoder function and call it — a crashable (DoS) / invariant-breaking path. Only an OWN
271
+ // decoder function is ever invoked; everything else falls back to the raw stream-chunk.
272
+ const decoder = Object.prototype.hasOwnProperty.call(vocab, kind) ? vocab[kind] : undefined;
273
+ if (typeof decoder !== "function")
252
274
  return raw;
253
275
  return decoder(body, entry.offset) ?? raw;
254
276
  }
@@ -291,8 +313,10 @@ export function encodeTranscriptEvent(event) {
291
313
  * message history, tool cards (each call paired to its result by `callId`, else the most recent open
292
314
  * call), raw-byte accounting for replay fidelity, and the session lifecycle. It is a pure reduction of
293
315
  * one log: the cockpit, search, token accounting and export all read THIS, so there is never a second
294
- * parser of the same bytes. Content that precedes the first explicit `turn` event opens an implicit
295
- * turn 0, so a producer that never emits turn boundaries still derives a coherent single-turn view.
316
+ * parser of the same bytes. Typed content a message, tool-call or step that precedes the first
317
+ * explicit `turn` event opens an implicit turn 0, so a producer that never emits turn boundaries still
318
+ * derives a coherent single-turn view. Raw `stream-chunk` events alone open no turn (they only feed the
319
+ * byte-replay accounting), so a log of only chunks derives zero turns.
296
320
  */
297
321
  export function deriveView(events) {
298
322
  const turns = [];
@@ -318,14 +342,7 @@ export function deriveView(events) {
318
342
  eventCount++;
319
343
  switch (event.kind) {
320
344
  case "turn": {
321
- current = {
322
- index: event.index ?? turns.length,
323
- startOffset: event.offset,
324
- messages: [],
325
- tools: [],
326
- permissions: [],
327
- steps: 0,
328
- };
345
+ current = { index: event.index ?? turns.length, startOffset: event.offset, messages: [], tools: [], permissions: [], steps: 0 };
329
346
  turns.push(current);
330
347
  break;
331
348
  }
@@ -346,19 +363,22 @@ export function deriveView(events) {
346
363
  ...(event.callId !== undefined ? { callId: event.callId } : {}),
347
364
  ...(event.args !== undefined ? { args: event.args } : {}),
348
365
  };
349
- tools.push(tool);
350
- ensureTurn(event.offset).tools.push(tool);
366
+ const toolsIndex = tools.push(tool) - 1;
367
+ const turn = ensureTurn(event.offset);
368
+ const turnToolIndex = turn.tools.push(tool) - 1;
369
+ const pending = { tool, toolsIndex, turn, turnToolIndex };
351
370
  if (event.callId !== undefined)
352
- openTools.set(event.callId, tool);
371
+ openTools.set(event.callId, pending);
353
372
  else
354
- anonymousTool = tool;
373
+ anonymousTool = pending;
355
374
  break;
356
375
  }
357
376
  case "tool-result": {
358
- const target = event.callId !== undefined ? openTools.get(event.callId) : anonymousTool;
359
- if (target !== undefined) {
360
- pairResult(tools, target, event);
361
- pairResultInTurns(turns, target, event);
377
+ const pending = event.callId !== undefined ? openTools.get(event.callId) : anonymousTool;
378
+ if (pending !== undefined) {
379
+ const resolved = withResult(pending.tool, event);
380
+ tools[pending.toolsIndex] = resolved;
381
+ pending.turn.tools[pending.turnToolIndex] = resolved;
362
382
  if (event.callId !== undefined)
363
383
  openTools.delete(event.callId);
364
384
  else
@@ -369,7 +389,7 @@ export function deriveView(events) {
369
389
  case "permission": {
370
390
  // A `permission` event is one of two phases (same discriminant `kind`); branch on `phase`. A
371
391
  // REQUEST opens a pending DerivedPermission (paired to its turn); a RESOLUTION folds back into
372
- // the open request by `callId` — mirroring the tool-call/tool-result open-map pairing above.
392
+ // the open request by `callId` in O(1) — mirroring the tool-call/tool-result pending-map pairing.
373
393
  if (event.phase === "request") {
374
394
  const permission = {
375
395
  policy: event.policy,
@@ -380,15 +400,17 @@ export function deriveView(events) {
380
400
  ...(event.title !== undefined ? { title: event.title } : {}),
381
401
  ...(event.reason !== undefined ? { reason: event.reason } : {}),
382
402
  };
383
- permissions.push(permission);
384
- ensureTurn(event.offset).permissions.push(permission);
385
- openPermissions.set(event.callId, permission);
403
+ const permissionsIndex = permissions.push(permission) - 1;
404
+ const turn = ensureTurn(event.offset);
405
+ const turnPermissionIndex = turn.permissions.push(permission) - 1;
406
+ openPermissions.set(event.callId, { permission, permissionsIndex, turn, turnPermissionIndex });
386
407
  }
387
408
  else {
388
- const target = openPermissions.get(event.callId);
389
- if (target !== undefined) {
390
- pairResolution(permissions, target, event);
391
- pairResolutionInTurns(turns, target, event);
409
+ const pending = openPermissions.get(event.callId);
410
+ if (pending !== undefined) {
411
+ const resolved = withResolution(pending.permission, event);
412
+ permissions[pending.permissionsIndex] = resolved;
413
+ pending.turn.permissions[pending.turnPermissionIndex] = resolved;
392
414
  openPermissions.delete(event.callId);
393
415
  }
394
416
  }
@@ -406,14 +428,7 @@ export function deriveView(events) {
406
428
  }
407
429
  }
408
430
  return {
409
- turns: turns.map((t) => ({
410
- index: t.index,
411
- startOffset: t.startOffset,
412
- messages: t.messages,
413
- tools: t.tools,
414
- permissions: t.permissions,
415
- steps: t.steps,
416
- })),
431
+ turns: turns.map((t) => ({ index: t.index, startOffset: t.startOffset, messages: t.messages, tools: t.tools, permissions: t.permissions, steps: t.steps })),
417
432
  messages,
418
433
  tools,
419
434
  permissions,
@@ -423,46 +438,14 @@ export function deriveView(events) {
423
438
  eventCount,
424
439
  };
425
440
  }
426
- /** Replace a pending tool with its result in the flat list. A pending tool starts as the same object in
427
- * both the flat list and its turn (pushed by reference), so {@link pairResultInTurns} locates it there by
428
- * identity; each list is then replaced independently with its own resolved copy via {@link withResult}. */
429
- function pairResult(list, target, result) {
430
- const idx = list.indexOf(target);
431
- if (idx >= 0)
432
- list[idx] = withResult(target, result);
433
- }
434
- /** Replace a pending tool with its result inside whichever turn holds it. */
435
- function pairResultInTurns(turns, target, result) {
436
- for (const turn of turns) {
437
- const idx = turn.tools.indexOf(target);
438
- if (idx >= 0) {
439
- turn.tools[idx] = withResult(target, result);
440
- return;
441
- }
442
- }
443
- }
441
+ /** Replace a pending tool with its result: a new resolved card that carries the result payload. */
444
442
  function withResult(tool, result) {
445
443
  return {
446
444
  ...tool,
447
445
  result: { ok: result.ok, offset: result.offset, ...(result.content !== undefined ? { content: result.content } : {}) },
448
446
  };
449
447
  }
450
- /** Replace a pending permission with its resolution in the flat list (by identity — see {@link pairResult}). */
451
- function pairResolution(list, target, resolution) {
452
- const idx = list.indexOf(target);
453
- if (idx >= 0)
454
- list[idx] = withResolution(target, resolution);
455
- }
456
- /** Replace a pending permission with its resolution inside whichever turn holds it. */
457
- function pairResolutionInTurns(turns, target, resolution) {
458
- for (const turn of turns) {
459
- const idx = turn.permissions.indexOf(target);
460
- if (idx >= 0) {
461
- turn.permissions[idx] = withResolution(target, resolution);
462
- return;
463
- }
464
- }
465
- }
448
+ /** Replace a pending permission with its resolution: a new card carrying the operator's decision. */
466
449
  function withResolution(permission, resolution) {
467
450
  return {
468
451
  ...permission,
@@ -30,12 +30,18 @@ interface BundleModule {
30
30
  }
31
31
 
32
32
  // The transcript RENDER path the browser needs is a two-module graph, both pure + DOM-agnostic:
33
- // transcript-events.ts — the ONE parser + derive() fold (no runtime imports).
34
- // transcript-derive.ts — renderDerivedTranscript(): message turns, tool/diff cards, permission prompts.
33
+ // transcript-events — the ONE parser + derive() fold (no runtime imports).
34
+ // transcript-derive.ts — renderDerivedTranscript(): message turns, tool/diff cards, permission prompts.
35
35
  // Their only non-type imports are between each other; every type-only import (DocumentLike, ElementLike,
36
36
  // TranscriptDataReport) is `import type` and is erased on transpile, so the emitted JS is import-clean.
37
+ //
38
+ // The grammar module is now DEFINED once, in `@nanobpm/agentic/transcript` (issue #676) — nano-workforce
39
+ // no longer forks it. So the browser bundle for the parser+fold is DERIVED from agentic's own published,
40
+ // self-contained ESM (`dist/transcript/events.js` has no runtime imports), the SAME source of truth the
41
+ // Node core re-exports via the `transcript-events.ts` barrel. This keeps "one grammar, no drift surface"
42
+ // true across BOTH hosts: the cockpit renders from agentic's grammar, never a hand-rolled second copy.
37
43
  const MODULES: readonly BundleModule[] = [
38
- { src: "app/agentic/transcript-events.ts", out: "pages/cockpit/generated/transcript-events.js" },
44
+ { src: "node_modules/@nanobpm/agentic/dist/transcript/events.js", out: "pages/cockpit/generated/transcript-events.js" },
39
45
  {
40
46
  src: "app/agentic/cockpit/transcript-derive.ts",
41
47
  out: "pages/cockpit/generated/transcript-derive.js",