@genesislcap/ai-assistant 15.6.0 → 15.6.2

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.
@@ -76,6 +76,10 @@ export interface CondenseContext {
76
76
  * contain. Batching holds the collapse set still between boundaries so the prefix is
77
77
  * append-only in between, trading up to `batchCalls` calls' worth of extra context for
78
78
  * one cache break per batch instead of one per call.
79
+ *
80
+ * `agentEnd` and `phaseEnd` are exempt and still collapse as they fire: they mark a
81
+ * discontinuity that invalidates the cached prefix anyway, so deferring them would hold
82
+ * stale payloads longer for nothing.
79
83
  */
80
84
  batchCalls?: number;
81
85
  }
@@ -1 +1 @@
1
- {"version":3,"file":"condense-history.d.ts","sourceRoot":"","sources":["../../../src/utils/condense-history.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAmB,MAAM,4BAA4B,CAAC;AAE/F;;;;;;;;;;;;;GAaG;AAEH;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAQ9C;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD;;;OAGG;IACH,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5B,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;CACrB;AA8CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EAAE,EACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAC/C,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GAClD,WAAW,EAAE,CAyJf"}
1
+ {"version":3,"file":"condense-history.d.ts","sourceRoot":"","sources":["../../../src/utils/condense-history.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAmB,MAAM,4BAA4B,CAAC;AAE/F;;;;;;;;;;;;;GAaG;AAEH;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAQ9C;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD;;;OAGG;IACH,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5B,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;CACrB;AAgED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EAAE,EACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAC/C,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GAClD,WAAW,EAAE,CA0Jf"}
@@ -60,6 +60,23 @@ function triggerLabel(trigger) {
60
60
  function estimateTokensSaved(origLen, stubLen) {
61
61
  return Math.max(0, Math.ceil((origLen - stubLen) / APPROX_CHARS_PER_TOKEN));
62
62
  }
63
+ /**
64
+ * Whether a trigger marks a genuine discontinuity rather than firing continuously, and so is
65
+ * exempt from batching.
66
+ *
67
+ * `agentEnd` advances only when the active agent's name changes (or a sub-agent completes /
68
+ * the agent is released), which swaps the system prompt and tool definitions — those render
69
+ * before the messages, so the cached prefix is invalidated at that point regardless. `phaseEnd`
70
+ * is the app declaring the same kind of boundary explicitly, and falls back to `agentEnd`.
71
+ * Deferring either to the next batch would buy no cache and only hold stale payloads longer.
72
+ *
73
+ * The rest fire continuously (`superseded`, `age`) or at a point where the history is otherwise
74
+ * append-only (`turnEnd` — a new turn appends a user message, leaving the previous entry live),
75
+ * so batching them is exactly where the cache is won.
76
+ */
77
+ function marksDiscontinuity(kind) {
78
+ return kind === 'agentEnd' || kind === 'phaseEnd';
79
+ }
63
80
  /**
64
81
  * Collapse stale tool payloads (declared via `condenseWhen`) out of the
65
82
  * model-bound history. Pure over the `history` array — it emits new message
@@ -155,9 +172,7 @@ export function applyCondensation(history, policies, ctx, onCondensed) {
155
172
  };
156
173
  // The first listed trigger that fires — drives the collapse, the stub reason,
157
174
  // and the event label. `undefined` means the payload stays full.
158
- const firstFired = (toolCallId, entry) => withinBatch(entry)
159
- ? triggersOf(entry).find((t) => triggerFires(toolCallId, entry, t))
160
- : undefined;
175
+ const firstFired = (toolCallId, entry) => triggersOf(entry).find((t) => (marksDiscontinuity(t.kind) || withinBatch(entry)) && triggerFires(toolCallId, entry, t));
161
176
  return history.map((msg) => {
162
177
  var _a, _b, _c, _d;
163
178
  // Tool-call ARGS live on the assistant message.
@@ -402,4 +402,34 @@ batch('batchCalls of 1, 0 or absent all keep the historical behaviour', () => {
402
402
  assert.is(out.filter((c) => c.startsWith('[')).length, 2, `batchCalls=${batchCalls}`);
403
403
  }
404
404
  });
405
+ batch('agentEnd is exempt from batching — it collapses as it fires', () => {
406
+ // An agent swap changes the system prompt and tool defs, which invalidates the cached
407
+ // prefix anyway, so deferring the collapse would hold a stale payload for nothing.
408
+ const history = [asstCall('a1', 'read_context', {}), toolMsg('a1', big())];
409
+ const policies = new Map([
410
+ ['a1', reg({ on: { kind: 'agentEnd' }, response: 'pointer' }, { iteration: 7, activation: 1 })],
411
+ ]);
412
+ const out = results(applyCondensation(history, policies, ctx({ modelCall: 7, batchCalls: 10, activationEnded: () => true }), sink().on));
413
+ // Boundary is 0 and the call sits at 7, so a batched trigger would have been deferred.
414
+ assert.ok(out[0].startsWith('['));
415
+ });
416
+ batch('phaseEnd is exempt too', () => {
417
+ const history = [asstCall('p1', 'read_docs', {}), toolMsg('p1', big())];
418
+ const policies = new Map([
419
+ ['p1', reg({ on: { kind: 'phaseEnd' }, response: 'pointer' }, { iteration: 7, phaseEpoch: 1 })],
420
+ ]);
421
+ const out = results(applyCondensation(history, policies, ctx({ modelCall: 7, batchCalls: 10, phaseEnded: () => true }), sink().on));
422
+ assert.ok(out[0].startsWith('['));
423
+ });
424
+ batch('a mixed policy batches superseded but still honours agentEnd immediately', () => {
425
+ // The vfs shape: `on: [{ superseded }, { agentEnd }]`. Mid-run the supersession is
426
+ // deferred; once the agent ends the payload collapses regardless of the boundary.
427
+ const { history, policies } = reReadLoop(2);
428
+ for (const id of ['r1', 'r2'])
429
+ policies.set(id, reg({ on: [{ kind: 'superseded', by: 'A.tsx' }, { kind: 'agentEnd' }], response: 'pointer' }, { iteration: id === 'r1' ? 1 : 2, activation: 1 }));
430
+ const midRun = results(applyCondensation(history, policies, ctx({ modelCall: 4, batchCalls: 10 }), sink().on));
431
+ assert.is(midRun.filter((c) => c.startsWith('[')).length, 0, 'supersession is deferred');
432
+ const afterSwap = results(applyCondensation(history, policies, ctx({ modelCall: 4, batchCalls: 10, activationEnded: () => true }), sink().on));
433
+ assert.is(afterSwap.filter((c) => c.startsWith('[')).length, 2, 'agentEnd collapses both');
434
+ });
405
435
  batch.run();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "15.6.0",
4
+ "version": "15.6.2",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -73,26 +73,26 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@genesislcap/foundation-testing": "15.6.0",
77
- "@genesislcap/genx": "15.6.0",
78
- "@genesislcap/rollup-builder": "15.6.0",
79
- "@genesislcap/ts-builder": "15.6.0",
80
- "@genesislcap/uvu-playwright-builder": "15.6.0",
81
- "@genesislcap/vite-builder": "15.6.0",
82
- "@genesislcap/webpack-builder": "15.6.0",
76
+ "@genesislcap/foundation-testing": "15.6.2",
77
+ "@genesislcap/genx": "15.6.2",
78
+ "@genesislcap/rollup-builder": "15.6.2",
79
+ "@genesislcap/ts-builder": "15.6.2",
80
+ "@genesislcap/uvu-playwright-builder": "15.6.2",
81
+ "@genesislcap/vite-builder": "15.6.2",
82
+ "@genesislcap/webpack-builder": "15.6.2",
83
83
  "@types/dompurify": "^3.0.5",
84
84
  "@types/marked": "^5.0.2",
85
85
  "esbuild": "0.25.12"
86
86
  },
87
87
  "dependencies": {
88
- "@genesislcap/foundation-ai": "15.6.0",
89
- "@genesislcap/foundation-logger": "15.6.0",
90
- "@genesislcap/foundation-notifications": "15.6.0",
91
- "@genesislcap/foundation-redux": "15.6.0",
92
- "@genesislcap/foundation-ui": "15.6.0",
93
- "@genesislcap/foundation-utils": "15.6.0",
94
- "@genesislcap/rapid-design-system": "15.6.0",
95
- "@genesislcap/web-core": "15.6.0",
88
+ "@genesislcap/foundation-ai": "15.6.2",
89
+ "@genesislcap/foundation-logger": "15.6.2",
90
+ "@genesislcap/foundation-notifications": "15.6.2",
91
+ "@genesislcap/foundation-redux": "15.6.2",
92
+ "@genesislcap/foundation-ui": "15.6.2",
93
+ "@genesislcap/foundation-utils": "15.6.2",
94
+ "@genesislcap/rapid-design-system": "15.6.2",
95
+ "@genesislcap/web-core": "15.6.2",
96
96
  "dompurify": "^3.3.1",
97
97
  "marked": "^17.0.3"
98
98
  },
@@ -105,5 +105,5 @@
105
105
  "access": "public"
106
106
  },
107
107
  "customElements": "dist/custom-elements.json",
108
- "gitHead": "dbf070bac8a4bfc2a178e335b1faffb130e8f761"
108
+ "gitHead": "a5781780cb56906755ddbfc4be75e221719ae4b4"
109
109
  }
@@ -536,4 +536,67 @@ batch('batchCalls of 1, 0 or absent all keep the historical behaviour', () => {
536
536
  }
537
537
  });
538
538
 
539
+ batch('agentEnd is exempt from batching — it collapses as it fires', () => {
540
+ // An agent swap changes the system prompt and tool defs, which invalidates the cached
541
+ // prefix anyway, so deferring the collapse would hold a stale payload for nothing.
542
+ const history = [asstCall('a1', 'read_context', {}), toolMsg('a1', big())];
543
+ const policies = new Map([
544
+ ['a1', reg({ on: { kind: 'agentEnd' }, response: 'pointer' }, { iteration: 7, activation: 1 })],
545
+ ]);
546
+ const out = results(
547
+ applyCondensation(
548
+ history,
549
+ policies,
550
+ ctx({ modelCall: 7, batchCalls: 10, activationEnded: () => true }),
551
+ sink().on,
552
+ ),
553
+ );
554
+ // Boundary is 0 and the call sits at 7, so a batched trigger would have been deferred.
555
+ assert.ok(out[0].startsWith('['));
556
+ });
557
+
558
+ batch('phaseEnd is exempt too', () => {
559
+ const history = [asstCall('p1', 'read_docs', {}), toolMsg('p1', big())];
560
+ const policies = new Map([
561
+ ['p1', reg({ on: { kind: 'phaseEnd' }, response: 'pointer' }, { iteration: 7, phaseEpoch: 1 })],
562
+ ]);
563
+ const out = results(
564
+ applyCondensation(
565
+ history,
566
+ policies,
567
+ ctx({ modelCall: 7, batchCalls: 10, phaseEnded: () => true }),
568
+ sink().on,
569
+ ),
570
+ );
571
+ assert.ok(out[0].startsWith('['));
572
+ });
573
+
574
+ batch('a mixed policy batches superseded but still honours agentEnd immediately', () => {
575
+ // The vfs shape: `on: [{ superseded }, { agentEnd }]`. Mid-run the supersession is
576
+ // deferred; once the agent ends the payload collapses regardless of the boundary.
577
+ const { history, policies } = reReadLoop(2);
578
+ for (const id of ['r1', 'r2'])
579
+ policies.set(
580
+ id,
581
+ reg(
582
+ { on: [{ kind: 'superseded', by: 'A.tsx' }, { kind: 'agentEnd' }], response: 'pointer' },
583
+ { iteration: id === 'r1' ? 1 : 2, activation: 1 },
584
+ ),
585
+ );
586
+ const midRun = results(
587
+ applyCondensation(history, policies, ctx({ modelCall: 4, batchCalls: 10 }), sink().on),
588
+ );
589
+ assert.is(midRun.filter((c) => c.startsWith('[')).length, 0, 'supersession is deferred');
590
+
591
+ const afterSwap = results(
592
+ applyCondensation(
593
+ history,
594
+ policies,
595
+ ctx({ modelCall: 4, batchCalls: 10, activationEnded: () => true }),
596
+ sink().on,
597
+ ),
598
+ );
599
+ assert.is(afterSwap.filter((c) => c.startsWith('[')).length, 2, 'agentEnd collapses both');
600
+ });
601
+
539
602
  batch.run();
@@ -87,6 +87,10 @@ export interface CondenseContext {
87
87
  * contain. Batching holds the collapse set still between boundaries so the prefix is
88
88
  * append-only in between, trading up to `batchCalls` calls' worth of extra context for
89
89
  * one cache break per batch instead of one per call.
90
+ *
91
+ * `agentEnd` and `phaseEnd` are exempt and still collapse as they fire: they mark a
92
+ * discontinuity that invalidates the cached prefix anyway, so deferring them would hold
93
+ * stale payloads longer for nothing.
90
94
  */
91
95
  batchCalls?: number;
92
96
  }
@@ -155,6 +159,24 @@ function estimateTokensSaved(origLen: number, stubLen: number): number {
155
159
  return Math.max(0, Math.ceil((origLen - stubLen) / APPROX_CHARS_PER_TOKEN));
156
160
  }
157
161
 
162
+ /**
163
+ * Whether a trigger marks a genuine discontinuity rather than firing continuously, and so is
164
+ * exempt from batching.
165
+ *
166
+ * `agentEnd` advances only when the active agent's name changes (or a sub-agent completes /
167
+ * the agent is released), which swaps the system prompt and tool definitions — those render
168
+ * before the messages, so the cached prefix is invalidated at that point regardless. `phaseEnd`
169
+ * is the app declaring the same kind of boundary explicitly, and falls back to `agentEnd`.
170
+ * Deferring either to the next batch would buy no cache and only hold stale payloads longer.
171
+ *
172
+ * The rest fire continuously (`superseded`, `age`) or at a point where the history is otherwise
173
+ * append-only (`turnEnd` — a new turn appends a user message, leaving the previous entry live),
174
+ * so batching them is exactly where the cache is won.
175
+ */
176
+ function marksDiscontinuity(kind: CondenseTrigger['kind']): boolean {
177
+ return kind === 'agentEnd' || kind === 'phaseEnd';
178
+ }
179
+
158
180
  /**
159
181
  * Collapse stale tool payloads (declared via `condenseWhen`) out of the
160
182
  * model-bound history. Pure over the `history` array — it emits new message
@@ -266,9 +288,10 @@ export function applyCondensation(
266
288
  toolCallId: string,
267
289
  entry: RegisteredCondensePolicy,
268
290
  ): CondenseTrigger | undefined =>
269
- withinBatch(entry)
270
- ? triggersOf(entry).find((t) => triggerFires(toolCallId, entry, t))
271
- : undefined;
291
+ triggersOf(entry).find(
292
+ (t) =>
293
+ (marksDiscontinuity(t.kind) || withinBatch(entry)) && triggerFires(toolCallId, entry, t),
294
+ );
272
295
 
273
296
  return history.map((msg) => {
274
297
  // Tool-call ARGS live on the assistant message.