@genesislcap/ai-assistant 15.14.2 → 15.15.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/dist/ai-assistant.api.json +5 -5
- package/dist/chat-driver.cjs +285 -70
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +285 -70
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +32 -0
- package/dist/dts/chat-driver-node.d.ts +1 -1
- package/dist/dts/chat-driver-node.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/utils/condense-history.d.ts.map +1 -1
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +34 -7
- package/dist/esm/utils/condense-history.js +31 -7
- package/dist/esm/utils/condense-history.test.js +68 -1
- package/dist/esm/utils/history-transform.js +8 -1
- package/dist/esm/utils/history-transform.test.js +22 -1
- package/package.json +17 -17
- package/src/chat-driver-node.ts +7 -0
- package/src/components/chat-driver/chat-driver.ts +48 -3
- package/src/utils/condense-history.test.ts +84 -2
- package/src/utils/condense-history.ts +27 -4
- package/src/utils/history-transform.test.ts +31 -0
- package/src/utils/history-transform.ts +7 -1
|
@@ -497,8 +497,6 @@ suite('multi-trigger first-wins: the earliest-listed firing trigger is the reaso
|
|
|
497
497
|
);
|
|
498
498
|
});
|
|
499
499
|
|
|
500
|
-
suite.run();
|
|
501
|
-
|
|
502
500
|
// ── batched collapse (GENC-1476) ───────────────────────────────────────────
|
|
503
501
|
// Condensation rewrites history in place, so collapsing the instant a trigger fires
|
|
504
502
|
// breaks the prompt cache on nearly every call of a re-read loop. Batching holds the
|
|
@@ -662,3 +660,87 @@ batch('a mixed policy batches superseded but still honours agentEnd immediately'
|
|
|
662
660
|
});
|
|
663
661
|
|
|
664
662
|
batch.run();
|
|
663
|
+
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// image-bearing tool results (GENC-1508)
|
|
666
|
+
//
|
|
667
|
+
// A render tool returns a few words of text plus ~15KB of base64. Sizing the payload
|
|
668
|
+
// on `content` alone leaves it permanently under the floor, so the image rides every
|
|
669
|
+
// later turn — the exact bytes condensation exists to shed. And a stub that leaves the
|
|
670
|
+
// attachments in place claims a collapse that did not happen.
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
|
|
673
|
+
/** A tool result shaped like a mockup render: short text, one large image. */
|
|
674
|
+
const imageToolMsg = (toolCallId: string, dataLen: number): ChatMessage => ({
|
|
675
|
+
role: 'tool',
|
|
676
|
+
content: 'rendered',
|
|
677
|
+
toolResult: {
|
|
678
|
+
toolCallId,
|
|
679
|
+
content: 'rendered',
|
|
680
|
+
attachments: [
|
|
681
|
+
{ kind: 'image', name: 'mockup.png', mimeType: 'image/png', data: 'a'.repeat(dataLen) },
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
suite('image bytes count toward the condense floor, so a short-text render condenses', () => {
|
|
687
|
+
const history: ChatMessage[] = [
|
|
688
|
+
asstCall('m1', 'render_mockup', { html: '<p/>' }),
|
|
689
|
+
imageToolMsg('m1', CONDENSE_MIN_CHARS + 500),
|
|
690
|
+
asstCall('m2', 'render_mockup', { html: '<p/>' }),
|
|
691
|
+
imageToolMsg('m2', CONDENSE_MIN_CHARS + 500),
|
|
692
|
+
];
|
|
693
|
+
const policies = new Map<string, RegisteredCondensePolicy>([
|
|
694
|
+
['m1', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
|
|
695
|
+
['m2', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
|
|
696
|
+
]);
|
|
697
|
+
const { on } = sink();
|
|
698
|
+
const out = applyCondensation(history, policies, ctx(), on);
|
|
699
|
+
assert.ok(
|
|
700
|
+
out[1].toolResult!.content.startsWith('['),
|
|
701
|
+
'the superseded render is collapsed to a stub — sizing on content alone would skip it',
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
suite('a condensed image result drops its attachments, not just its text', () => {
|
|
706
|
+
const history: ChatMessage[] = [
|
|
707
|
+
asstCall('m1', 'render_mockup', { html: '<p/>' }),
|
|
708
|
+
imageToolMsg('m1', CONDENSE_MIN_CHARS + 500),
|
|
709
|
+
asstCall('m2', 'render_mockup', { html: '<p/>' }),
|
|
710
|
+
imageToolMsg('m2', CONDENSE_MIN_CHARS + 500),
|
|
711
|
+
];
|
|
712
|
+
const policies = new Map<string, RegisteredCondensePolicy>([
|
|
713
|
+
['m1', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
|
|
714
|
+
['m2', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
|
|
715
|
+
]);
|
|
716
|
+
const { on } = sink();
|
|
717
|
+
const out = applyCondensation(history, policies, ctx(), on);
|
|
718
|
+
assert.is(
|
|
719
|
+
out[1].toolResult!.attachments,
|
|
720
|
+
undefined,
|
|
721
|
+
'leaving the image attached would shed none of the bytes the stub claims were elided',
|
|
722
|
+
);
|
|
723
|
+
assert.equal(
|
|
724
|
+
out[3].toolResult!.attachments?.length,
|
|
725
|
+
1,
|
|
726
|
+
'the surviving latest render keeps its image',
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
suite('a short text-only result is still below the floor with no attachments', () => {
|
|
731
|
+
const history: ChatMessage[] = [
|
|
732
|
+
asstCall('s1', 'render_mockup', {}),
|
|
733
|
+
toolMsg('s1', 'rendered'),
|
|
734
|
+
asstCall('s2', 'render_mockup', {}),
|
|
735
|
+
toolMsg('s2', 'rendered'),
|
|
736
|
+
];
|
|
737
|
+
const policies = new Map<string, RegisteredCondensePolicy>([
|
|
738
|
+
['s1', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
|
|
739
|
+
['s2', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
|
|
740
|
+
]);
|
|
741
|
+
const { on } = sink();
|
|
742
|
+
const out = applyCondensation(history, policies, ctx(), on);
|
|
743
|
+
assert.is(out[1].toolResult!.content, 'rendered', 'the floor still protects small payloads');
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
suite.run();
|
|
@@ -169,11 +169,19 @@ function condenseStub(
|
|
|
169
169
|
trigger: CondenseTrigger,
|
|
170
170
|
origLen: number,
|
|
171
171
|
restorable: boolean,
|
|
172
|
+
/**
|
|
173
|
+
* Images elided alongside the text. Reported separately because the model reads this stub as
|
|
174
|
+
* fact: folding base64 bytes into the char count would tell it a 15KB render was "~15447
|
|
175
|
+
* chars" of prose. One variable cannot be both the size FLOOR (bytes, correctly) and a
|
|
176
|
+
* model-facing description (characters).
|
|
177
|
+
*/
|
|
178
|
+
imageCount = 0,
|
|
172
179
|
): string {
|
|
173
180
|
const what = target === 'args' ? 'args' : 'result';
|
|
174
181
|
const key = trigger.kind === 'superseded' ? ` ${trigger.by}` : '';
|
|
175
182
|
const restore = restorable ? '; re-call to restore' : '';
|
|
176
|
-
|
|
183
|
+
const images = imageCount > 0 ? ` + ${imageCount} image${imageCount === 1 ? '' : 's'}` : '';
|
|
184
|
+
return `[${tool}${key} — ${what} elided, ~${origLen} chars${images} (${triggerReason(trigger)})${restore}]`;
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
/** Stable label for the `context.condensed` meta-event's `trigger` field. */
|
|
@@ -392,7 +400,18 @@ export function applyCondensation(
|
|
|
392
400
|
const entry = policies.get(msg.toolResult.toolCallId);
|
|
393
401
|
// Defensive `?? 0` for restored/malformed history — `content` is typed
|
|
394
402
|
// `string`, but a 0-length payload is simply below the floor and skipped.
|
|
395
|
-
|
|
403
|
+
//
|
|
404
|
+
// Attachment bytes COUNT toward the floor (GENC-1508). An image-bearing result is
|
|
405
|
+
// typically a few words of text — "rendered the mockup" — plus ~15KB of base64, so
|
|
406
|
+
// sizing on `content` alone leaves it permanently under CONDENSE_MIN_CHARS and the
|
|
407
|
+
// image rides every subsequent turn. That is the exact payload condensation exists
|
|
408
|
+
// to shed, so it must be what is measured.
|
|
409
|
+
const textLen = msg.toolResult.content?.length ?? 0;
|
|
410
|
+
const images = msg.toolResult.attachments ?? [];
|
|
411
|
+
// Bytes gate the FLOOR (an image result is a few words of text plus ~15KB of base64, so
|
|
412
|
+
// sizing on text alone would never condense it); `textLen` and the image count are what
|
|
413
|
+
// the model is told. See condenseStub.
|
|
414
|
+
const origLen = textLen + images.reduce((n, att) => n + (att.data?.length ?? 0), 0);
|
|
396
415
|
const fired = entry?.policy.response
|
|
397
416
|
? firstFired(msg.toolResult.toolCallId, entry)
|
|
398
417
|
: undefined;
|
|
@@ -402,7 +421,7 @@ export function applyCondensation(
|
|
|
402
421
|
// Kept non-empty — Anthropic rejects empty tool_result content.
|
|
403
422
|
const content =
|
|
404
423
|
result === 'drop' || result === 'pointer'
|
|
405
|
-
? condenseStub('response', tool, fired,
|
|
424
|
+
? condenseStub('response', tool, fired, textLen, result === 'pointer', images.length)
|
|
406
425
|
: result.replaceWith;
|
|
407
426
|
if (!entry.reportedResponse) {
|
|
408
427
|
entry.reportedResponse = true;
|
|
@@ -416,7 +435,11 @@ export function applyCondensation(
|
|
|
416
435
|
tokensSaved: estimateTokensSaved(origLen, content.length),
|
|
417
436
|
});
|
|
418
437
|
}
|
|
419
|
-
|
|
438
|
+
// `attachments` is dropped, not spread through: the stub says the payload was
|
|
439
|
+
// elided, and leaving the images attached would make that a lie AND shed none of
|
|
440
|
+
// the bytes the condensation was performed to shed.
|
|
441
|
+
const { attachments: _elided, ...restOfResult } = msg.toolResult;
|
|
442
|
+
return { ...msg, toolResult: { ...restOfResult, content } };
|
|
420
443
|
}
|
|
421
444
|
}
|
|
422
445
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
COMPACT_MIN_MESSAGES_TO_COMPACT,
|
|
8
8
|
findCompactionCut,
|
|
9
9
|
normalizeForProvider,
|
|
10
|
+
transformHistoryForAgent,
|
|
10
11
|
} from './history-transform';
|
|
11
12
|
|
|
12
13
|
const asMsg = (o: Record<string, unknown>): ChatMessage => o as unknown as ChatMessage;
|
|
@@ -194,4 +195,34 @@ Suite('compactionDividerLabel pluralizes and falls back to 0', () => {
|
|
|
194
195
|
);
|
|
195
196
|
});
|
|
196
197
|
|
|
198
|
+
// ── masking another agent's tool payload (GENC-1508) ─────────────────────────
|
|
199
|
+
|
|
200
|
+
Suite('transformHistoryForAgent drops tool-result IMAGES, not just the text', () => {
|
|
201
|
+
const history: ChatMessage[] = [
|
|
202
|
+
asMsg({
|
|
203
|
+
role: 'tool',
|
|
204
|
+
content: '',
|
|
205
|
+
agentName: 'other-agent',
|
|
206
|
+
toolResult: {
|
|
207
|
+
toolCallId: 'r1',
|
|
208
|
+
content: 'rendered the mockup',
|
|
209
|
+
attachments: [
|
|
210
|
+
{ kind: 'image', name: 'mockup.png', mimeType: 'image/png', data: 'a'.repeat(2000) },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
];
|
|
215
|
+
const out = transformHistoryForAgent(history, 'this-agent');
|
|
216
|
+
assert.ok(
|
|
217
|
+
out[0].toolResult!.content.includes('omitted'),
|
|
218
|
+
'the text is stubbed, as it always was',
|
|
219
|
+
);
|
|
220
|
+
assert.is(
|
|
221
|
+
out[0].toolResult!.attachments,
|
|
222
|
+
undefined,
|
|
223
|
+
"masking exists to keep another agent's payload out of context — an image IS the payload, " +
|
|
224
|
+
'and stubbing the prose while shipping the render defeats the mask where it matters most',
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
197
228
|
Suite.run();
|
|
@@ -10,9 +10,15 @@ function maskToolPayload(msg: ChatMessage): ChatMessage {
|
|
|
10
10
|
return { ...msg, toolCalls: msg.toolCalls.map((tc) => ({ ...tc, args: {} })) };
|
|
11
11
|
}
|
|
12
12
|
if (msg.toolResult) {
|
|
13
|
+
// `attachments` is dropped, not spread through. Masking exists to keep another agent's
|
|
14
|
+
// payload out of this one's context, and an image is the largest payload there is — spreading
|
|
15
|
+
// it would stub the prose while still shipping the render, defeating the mask for exactly the
|
|
16
|
+
// case it matters most. `applyHistoryCap` masks everything past the cap for sub-agent context,
|
|
17
|
+
// so without this every older image rides every sub-agent turn under an "omitted" label.
|
|
18
|
+
const { attachments: _masked, ...restOfResult } = msg.toolResult;
|
|
13
19
|
return {
|
|
14
20
|
...msg,
|
|
15
|
-
toolResult: { ...
|
|
21
|
+
toolResult: { ...restOfResult, content: "[other agent's tool result omitted]" },
|
|
16
22
|
};
|
|
17
23
|
}
|
|
18
24
|
return msg;
|