@aragon-agent/cli 0.6.13 → 0.6.15
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/CHANGELOG.md +41 -0
- package/README.md +250 -222
- package/dist/agent/controller.d.ts +36 -2
- package/dist/agent/controller.js +244 -57
- package/dist/agent/model-profile-settings.js +11 -2
- package/dist/agent/plan-prompt.d.ts +1 -1
- package/dist/agent/plan-prompt.js +4 -3
- package/dist/agent/project-guidance-prompt.d.ts +4 -0
- package/dist/agent/project-guidance-prompt.js +104 -0
- package/dist/agent/reducer.d.ts +2 -0
- package/dist/agent/reducer.js +5 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/boot/guard.d.ts +3 -3
- package/dist/boot/guard.js +3 -3
- package/dist/cli.js +33 -15
- package/dist/commands/builtins.js +2 -2
- package/dist/compaction/child.d.ts +9 -1
- package/dist/compaction/child.js +43 -13
- package/dist/compaction/command.js +20 -10
- package/dist/compaction/compactor.d.ts +33 -204
- package/dist/compaction/compactor.js +311 -723
- package/dist/compaction/context-command.js +5 -3
- package/dist/compaction/digest.d.ts +15 -51
- package/dist/compaction/digest.js +113 -148
- package/dist/compaction/limits.d.ts +19 -142
- package/dist/compaction/limits.js +20 -147
- package/dist/compaction/memory-identity.d.ts +16 -0
- package/dist/compaction/memory-identity.js +63 -0
- package/dist/compaction/memory-input.d.ts +25 -0
- package/dist/compaction/memory-input.js +106 -0
- package/dist/compaction/memory-render.d.ts +4 -0
- package/dist/compaction/memory-render.js +47 -0
- package/dist/compaction/memory-validation.d.ts +21 -0
- package/dist/compaction/memory-validation.js +268 -0
- package/dist/compaction/memory.d.ts +79 -0
- package/dist/compaction/memory.js +118 -0
- package/dist/compaction/meter.d.ts +14 -10
- package/dist/compaction/meter.js +55 -19
- package/dist/compaction/operation.d.ts +18 -0
- package/dist/compaction/operation.js +68 -0
- package/dist/compaction/pressure.d.ts +6 -20
- package/dist/compaction/pressure.js +14 -23
- package/dist/compaction/summarize-call.d.ts +3 -16
- package/dist/compaction/summarize-call.js +20 -20
- package/dist/compaction/summary-prompt.d.ts +29 -66
- package/dist/compaction/summary-prompt.js +132 -178
- package/dist/compaction/types.d.ts +23 -3
- package/dist/compaction/wiring.d.ts +10 -25
- package/dist/compaction/wiring.js +60 -67
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +8 -4
- package/dist/config/schema.d.ts +3 -3
- package/dist/config/schema.js +1 -1
- package/dist/exec/events.d.ts +8 -0
- package/dist/exec/index.js +13 -6
- package/dist/exec/runner.d.ts +8 -0
- package/dist/exec/runner.js +26 -2
- package/dist/session/persist.d.ts +3 -0
- package/dist/session/persist.js +9 -0
- package/dist/session/store.js +2 -0
- package/dist/session/validate-session.js +34 -0
- package/dist/team/subagent.js +8 -0
- package/dist/ui/App.js +221 -126
- package/dist/ui/BottomStatusRow.d.ts +5 -10
- package/dist/ui/BottomStatusRow.js +7 -11
- package/dist/ui/Composer.d.ts +4 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +6 -2
- package/dist/ui/Header.js +7 -6
- package/dist/ui/Logo.d.ts +3 -5
- package/dist/ui/Logo.js +4 -8
- package/dist/ui/PromptInput.d.ts +3 -1
- package/dist/ui/PromptInput.js +35 -42
- package/dist/ui/StatusBar.d.ts +6 -0
- package/dist/ui/StatusBar.js +8 -7
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/capabilities.d.ts +2 -0
- package/dist/ui/capabilities.js +6 -0
- package/dist/ui/color-runtime.d.ts +5 -0
- package/dist/ui/color-runtime.js +29 -0
- package/dist/ui/entries/CompactionCard.d.ts +3 -1
- package/dist/ui/entries/CompactionCard.js +7 -5
- package/dist/ui/frame-differ.js +27 -0
- package/dist/ui/frame-observer.js +18 -5
- package/dist/ui/glyphs.js +10 -8
- package/dist/ui/ink-runtime.d.ts +3 -0
- package/dist/ui/ink-runtime.js +30 -0
- package/dist/ui/interaction-copy.d.ts +73 -73
- package/dist/ui/interaction-copy.js +30 -38
- package/dist/ui/interaction-hints.d.ts +8 -1
- package/dist/ui/interaction-hints.js +56 -69
- package/dist/ui/layout/AppShell.d.ts +6 -2
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/OverlayFrame.d.ts +2 -0
- package/dist/ui/layout/OverlayFrame.js +23 -14
- package/dist/ui/layout/ScrollViewport.d.ts +3 -1
- package/dist/ui/layout/ScrollViewport.js +10 -2
- package/dist/ui/layout/budget.d.ts +25 -26
- package/dist/ui/layout/budget.js +29 -22
- package/dist/ui/layout/document-layout.d.ts +2 -2
- package/dist/ui/layout/document-layout.js +2 -3
- package/dist/ui/layout/queue-layout.js +4 -4
- package/dist/ui/layout/status-detail-layout.d.ts +9 -0
- package/dist/ui/layout/status-detail-layout.js +56 -0
- package/dist/ui/layout/status-layout.d.ts +19 -2
- package/dist/ui/layout/status-layout.js +95 -83
- package/dist/ui/layout/wrap-rows.d.ts +2 -0
- package/dist/ui/layout/wrap-rows.js +21 -7
- package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
- package/dist/ui/overlays/ConfirmDialog.js +22 -3
- package/dist/ui/overlays/HelpOverlay.js +9 -2
- package/dist/ui/overlays/ModelPicker.d.ts +5 -8
- package/dist/ui/overlays/ModelPicker.js +44 -10
- package/dist/ui/overlays/ModelProfileEditor.js +2 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
- package/dist/ui/overlays/ModelProfilePicker.js +4 -2
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
- package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
- package/dist/ui/overlays/QuestionOverlay.js +45 -32
- package/dist/ui/overlays/QueueOverlay.js +2 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
- package/dist/ui/overlays/SettingsScreen.js +10 -3
- package/dist/ui/render-cache.d.ts +2 -2
- package/dist/ui/render-cache.js +4 -3
- package/dist/ui/run-metrics.d.ts +23 -0
- package/dist/ui/run-metrics.js +27 -0
- package/dist/ui/status-feedback.d.ts +33 -0
- package/dist/ui/status-feedback.js +36 -0
- package/dist/ui/stdout-frame-writer.js +28 -0
- package/package.json +2 -2
|
@@ -1,172 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* COMPACTION_LIMITS — the single authority on every bound context compaction
|
|
3
|
-
* enforces (context-auto-compaction §5.4).
|
|
4
|
-
*
|
|
5
|
-
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
|
|
6
|
-
* (`glyphs.test.ts::inScope`), so no literal in this tree may hold a non-ASCII
|
|
7
|
-
* byte. That regex is a HARDCODED DIRECTORY LIST, which is why adding this tree
|
|
8
|
-
* and adding `compaction` to that list are the SAME change (C-7) - a scanner
|
|
9
|
-
* that silently stops scanning is worse than no scanner. `fast/limits.ts`
|
|
10
|
-
* records the identical trap one feature earlier.
|
|
11
|
-
*
|
|
12
|
-
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
|
|
13
|
-
* `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
|
|
14
|
-
*
|
|
15
|
-
* - The entries below are STRUCTURAL. They describe what a digest, a
|
|
16
|
-
* summarization call and a card can physically carry. A user has no business
|
|
17
|
-
* tuning them.
|
|
18
|
-
* - The six `compaction.*` keys in `config/schema.ts` are POLICY: when to fire,
|
|
19
|
-
* how much to keep, which model, what to do on failure. Those are clamped,
|
|
20
|
-
* persisted and user-facing.
|
|
21
|
-
*/
|
|
1
|
+
/** Structural limits for v2 compaction. All character counts are UTF-16 units. */
|
|
22
2
|
export const COMPACTION_LIMITS = {
|
|
23
|
-
/** Ceiling on the rendered digest, before the summarizer-window check. ~30k tokens. */
|
|
24
3
|
digestMaxChars: 120_000,
|
|
25
|
-
/** Output + system prompt + framing, kept out of the digest's share of the window. */
|
|
26
4
|
summarizerReserveTokens: 8_000,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
anchorChars: 4_000,
|
|
44
|
-
/** What the summarizer prompt asks for, and what the splice clips to. */
|
|
45
|
-
summaryMaxChars: 6_000,
|
|
46
|
-
summaryOutputTokens: 2_048,
|
|
47
|
-
/**
|
|
48
|
-
* Wall-clock budget for ONE summarization call.
|
|
49
|
-
*
|
|
50
|
-
* Longer than the fast reviewer's 20 s because the input is ~30x larger and the
|
|
51
|
-
* output ~4x larger, and because the cost of a timeout here is a DEGRADED RUN
|
|
52
|
-
* rather than a missing critique.
|
|
53
|
-
*
|
|
54
|
-
* COUPLED TO THE ENGINE'S `COMPACTION_HARD_TIMEOUT_MS` (120 s). The worst legal
|
|
55
|
-
* host budget is two calls at this value back to back plus the digest render,
|
|
56
|
-
* so `2 * callTimeoutMs` must stay strictly under the engine's ceiling. Raising
|
|
57
|
-
* this without raising that makes the §3.7 ladder unreachable and turns every
|
|
58
|
-
* slow-but-correct summarization into `manager_timeout`.
|
|
59
|
-
*/
|
|
5
|
+
summaryMaxChars: 32_000,
|
|
6
|
+
summaryOutputTokens: 4_096,
|
|
7
|
+
memoryJsonChars: 32_000,
|
|
8
|
+
memoryEnvelopeChars: 1_024,
|
|
9
|
+
protectedMemoryChars: 48_000,
|
|
10
|
+
memoryItems: 200,
|
|
11
|
+
protectedUserMessages: 200,
|
|
12
|
+
memoryAdditions: 64,
|
|
13
|
+
coverageClips: 200,
|
|
14
|
+
jsonDepth: 32,
|
|
15
|
+
instructionsChars: 4_000,
|
|
16
|
+
itemTextChars: 1_200,
|
|
17
|
+
sourceExcerptChars: 512,
|
|
18
|
+
itemSources: 8,
|
|
19
|
+
toolResultEdgeChars: 1_500,
|
|
20
|
+
operationTimeoutMs: 120_000,
|
|
60
21
|
callTimeoutMs: 45_000,
|
|
61
|
-
// --- Anti-loop guards (§3.8). ---
|
|
62
|
-
/** Turns between compactions. Never two in a row: a compaction that just ran
|
|
63
|
-
* needs a real turn before its effect is measurable, because `lastUsage` is
|
|
64
|
-
* still the pre-compaction number until the next `turn_end`. */
|
|
65
22
|
minTurnsBetween: 2,
|
|
66
|
-
/** Compactions STARTED per run, both triggers, hard. */
|
|
67
23
|
maxPerRun: 5,
|
|
68
|
-
/** A splice must project occupancy below `threshold - this`, or it bought two turns. */
|
|
69
24
|
minReclaimRatio: 0.15,
|
|
70
|
-
/** Consecutive no-progress PRESSURE compactions before the proactive trigger self-disables. */
|
|
71
25
|
stuckLimit: 2,
|
|
72
|
-
//
|
|
73
|
-
/**
|
|
74
|
-
* Clip bound on an oversized `tool_result` body inside the RETAINED tail.
|
|
75
|
-
*
|
|
76
|
-
* 2 000 rather than `toolResultChars`' 600, and the two are not the same
|
|
77
|
-
* question. That one bounds a message in the DIGEST, which the summarizer
|
|
78
|
-
* reads once and which exists to be lossy. This one bounds a message the model
|
|
79
|
-
* keeps WORKING FROM: it is the last two-to-four turns, and the run is about to
|
|
80
|
-
* continue on them. Relief only ever runs when the alternative is a request the
|
|
81
|
-
* provider cannot accept.
|
|
82
|
-
*/
|
|
26
|
+
// Legacy capacity helpers remain callable, but v2 never mutates retained tails.
|
|
83
27
|
tailToolResultChars: 2_000,
|
|
84
|
-
/**
|
|
85
|
-
* Room reserved for the `<compacted_context>` block in the tail-budget
|
|
86
|
-
* projection.
|
|
87
|
-
*
|
|
88
|
-
* THE BLOCK IS WRITTEN AFTER THE PROJECTION, which is why this exists at all:
|
|
89
|
-
* relief measures a spliced history whose summary body is already in it, but
|
|
90
|
-
* the `plan === null` path measures one where nothing has been written yet, and
|
|
91
|
-
* a budget with no allowance would let the block push the result back over.
|
|
92
|
-
*/
|
|
93
28
|
blockAllowanceTokens: 2_000,
|
|
94
|
-
// --- The archive (hardening §3.5 / W4). ---
|
|
95
|
-
/**
|
|
96
|
-
* Files kept for ONE `runId`, newest first.
|
|
97
|
-
*
|
|
98
|
-
* PER RUN, NOT PER DIRECTORY (RV-6). `<home>/compaction/` is shared by every
|
|
99
|
-
* `aragon` on the machine — `file-sink.ts:94-95` already treats concurrent
|
|
100
|
-
* processes as normal — so a global count-based prune would delete a live
|
|
101
|
-
* session's archives out from under it.
|
|
102
|
-
*/
|
|
103
29
|
archiveMaxFiles: 20,
|
|
104
|
-
/**
|
|
105
|
-
* The cross-run sweep: 7 days.
|
|
106
|
-
*
|
|
107
|
-
* AGE RATHER THAN COUNT, for the reason above. Age can only ever delete files
|
|
108
|
-
* whose run finished a week ago; a count can delete the newest file another
|
|
109
|
-
* process wrote a second ago.
|
|
110
|
-
*/
|
|
111
30
|
archiveMaxAgeMs: 604_800_000,
|
|
112
|
-
/**
|
|
113
|
-
* Per-file ceiling before dropped message bodies are clipped from the OLDEST
|
|
114
|
-
* end. The metadata and the summary are never clipped: a truncated archive
|
|
115
|
-
* that still says what happened beats none.
|
|
116
|
-
*/
|
|
117
31
|
archiveMaxBytes: 8_000_000,
|
|
118
|
-
// --- Sub-agent compaction (hardening §3.4 / W3). ---
|
|
119
|
-
/** A child's `keepRecentTurns`, delivered through its config view. */
|
|
120
32
|
childKeepRecentTurns: 2,
|
|
121
|
-
/**
|
|
122
|
-
* A child's guard-2 ceiling, delivered through `Compactor`'s instance bound.
|
|
123
|
-
*
|
|
124
|
-
* HERE RATHER THAN AS A `compaction.*` KEY, and the placement is the whole
|
|
125
|
-
* argument: `maxPerRun` is STRUCTURAL (see this file's header), there is no
|
|
126
|
-
* config path to it by design, and a child needs a tighter one. Both numbers
|
|
127
|
-
* therefore stay in the one file that owns structural bounds.
|
|
128
|
-
*/
|
|
129
33
|
childMaxPerRun: 2,
|
|
130
|
-
/** Rows the collapsed transcript card renders; Ctrl+O expands. */
|
|
131
34
|
cardTextRows: 6,
|
|
132
|
-
/**
|
|
133
|
-
* Below this many columns the status chip drops.
|
|
134
|
-
*
|
|
135
|
-
* 100, matching all four existing peers - `retry-view.ts`, `fast/limits.ts`,
|
|
136
|
-
* `team/limits.ts`, `todo/limits.ts` (P2-2). A status-bar degradation ladder
|
|
137
|
-
* that is nearly-but-not-quite uniform is a reading hazard bought for nothing.
|
|
138
|
-
* The guaranteed reporting surface is `/compact status`, which is what makes
|
|
139
|
-
* dropping the chip acceptable at all.
|
|
140
|
-
*/
|
|
141
35
|
statusCompactCols: 100,
|
|
142
36
|
};
|
|
143
|
-
|
|
144
|
-
* Bumped whenever the wording of the `<compacted_context>` block or the
|
|
145
|
-
* summarizer system prompt changes, so a behaviour report can be tied to a block
|
|
146
|
-
* revision with one grep - exactly what `FAST_BLOCK_VERSION` and
|
|
147
|
-
* `TEAM_BLOCK_VERSION` are for.
|
|
148
|
-
*/
|
|
149
|
-
export const COMPACTION_BLOCK_VERSION = 'v1-2026-08';
|
|
150
|
-
/** Opening tag of the verbatim goal anchor (§3.6.5). */
|
|
37
|
+
export const COMPACTION_BLOCK_VERSION = 'v2-2026-10';
|
|
151
38
|
export const ANCHOR_TAG_OPEN = '<original_task>';
|
|
152
39
|
export const ANCHOR_TAG_CLOSE = '</original_task>';
|
|
153
|
-
/** Opening tag of the spliced summary block. Recognized on the NEXT compaction. */
|
|
154
40
|
export const BLOCK_TAG_OPEN = '<compacted_context';
|
|
155
41
|
export const BLOCK_TAG_CLOSE = '</compacted_context>';
|
|
156
|
-
/**
|
|
157
|
-
export const ANCHOR_TRUNCATION_MARK = ' [truncated]';
|
|
158
|
-
/**
|
|
159
|
-
* What tail relief writes where it removed text (hardening §3.3.2 rule 4).
|
|
160
|
-
*
|
|
161
|
-
* IN THE HISTORY THE MODEL READS, at the exact place the data went missing. A
|
|
162
|
-
* model that sees this knows the output is partial and can re-run the tool; a
|
|
163
|
-
* model handed a silently truncated file cannot tell the difference between "the
|
|
164
|
-
* file ends here" and "the file was cut here".
|
|
165
|
-
*
|
|
166
|
-
* THE HOST OWNS IT, not core: `engine/compaction.ts` has no glyph vocabulary and
|
|
167
|
-
* must not learn one, so `relieveTail` takes this as a callback. ASCII, per this
|
|
168
|
-
* file's header.
|
|
169
|
-
*/
|
|
42
|
+
/** Retained legacy helper for explicit callers; v2 compression never clips the tail. */
|
|
170
43
|
export function tailClipMarker(removed) {
|
|
171
44
|
return `\n[... ${removed} characters removed by context compaction ...]`;
|
|
172
45
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Message } from '@aragon-agent/core';
|
|
2
|
+
export interface CompactionIdentity {
|
|
3
|
+
version: 1;
|
|
4
|
+
anchorIndex: 0;
|
|
5
|
+
blockIndex: 1;
|
|
6
|
+
generation: number;
|
|
7
|
+
prefixSha256: string;
|
|
8
|
+
}
|
|
9
|
+
/** Validate the optional session credential shape; throws on unknown or bad fields. */
|
|
10
|
+
export declare function validateCompactionIdentity(value: unknown): asserts value is CompactionIdentity;
|
|
11
|
+
/** Serialize the complete prefix, including content parts and timestamps, in fixed order. */
|
|
12
|
+
export declare function serializeCompactionPrefix(messages: readonly Message[]): string;
|
|
13
|
+
/** Create credentials only at the host's successful adoption boundary. */
|
|
14
|
+
export declare function createCompactionIdentity(messages: readonly Message[]): CompactionIdentity;
|
|
15
|
+
/** Verify persisted credentials without mutating history or repairing missing identity. */
|
|
16
|
+
export declare function verifyCompactionIdentity(messages: readonly Message[], identity: unknown): identity is CompactionIdentity;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Adopted-prefix credentials for process-local and saved-session continuity. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { canonicalUserMessage, MemoryError } from './memory.js';
|
|
4
|
+
import { parseCompactedBlock } from './summary-prompt.js';
|
|
5
|
+
/** Validate the optional session credential shape; throws on unknown or bad fields. */
|
|
6
|
+
export function validateCompactionIdentity(value) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
8
|
+
throw new MemoryError('invalid_prior_memory', '$.identity');
|
|
9
|
+
}
|
|
10
|
+
const record = value;
|
|
11
|
+
const keys = ['version', 'anchorIndex', 'blockIndex', 'generation', 'prefixSha256'];
|
|
12
|
+
if (Object.keys(record).length !== keys.length ||
|
|
13
|
+
Object.keys(record).some((key) => !keys.includes(key))) {
|
|
14
|
+
throw new MemoryError('invalid_prior_memory', '$.identity.keys');
|
|
15
|
+
}
|
|
16
|
+
if (record.version !== 1 || record.anchorIndex !== 0 || record.blockIndex !== 1) {
|
|
17
|
+
throw new MemoryError('invalid_prior_memory', '$.identity.prefix');
|
|
18
|
+
}
|
|
19
|
+
if (!Number.isSafeInteger(record.generation) || record.generation < 1) {
|
|
20
|
+
throw new MemoryError('invalid_prior_memory', '$.identity.generation');
|
|
21
|
+
}
|
|
22
|
+
if (typeof record.prefixSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(record.prefixSha256)) {
|
|
23
|
+
throw new MemoryError('invalid_prior_memory', '$.identity.hash');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function prefix(messages) {
|
|
27
|
+
const anchor = messages[0];
|
|
28
|
+
const block = messages[1];
|
|
29
|
+
if (anchor?.role !== 'user' || block?.role !== 'user') {
|
|
30
|
+
throw new MemoryError('invalid_prior_memory', '$.prefix.roles');
|
|
31
|
+
}
|
|
32
|
+
return [anchor, block];
|
|
33
|
+
}
|
|
34
|
+
/** Serialize the complete prefix, including content parts and timestamps, in fixed order. */
|
|
35
|
+
export function serializeCompactionPrefix(messages) {
|
|
36
|
+
return JSON.stringify(prefix(messages).map(canonicalUserMessage));
|
|
37
|
+
}
|
|
38
|
+
/** Create credentials only at the host's successful adoption boundary. */
|
|
39
|
+
export function createCompactionIdentity(messages) {
|
|
40
|
+
const [, block] = prefix(messages);
|
|
41
|
+
const { memory } = parseCompactedBlock(block);
|
|
42
|
+
return {
|
|
43
|
+
version: 1,
|
|
44
|
+
anchorIndex: 0,
|
|
45
|
+
blockIndex: 1,
|
|
46
|
+
generation: memory.generation,
|
|
47
|
+
prefixSha256: createHash('sha256').update(serializeCompactionPrefix(messages)).digest('hex'),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Verify persisted credentials without mutating history or repairing missing identity. */
|
|
51
|
+
export function verifyCompactionIdentity(messages, identity) {
|
|
52
|
+
try {
|
|
53
|
+
validateCompactionIdentity(identity);
|
|
54
|
+
const current = createCompactionIdentity(messages);
|
|
55
|
+
return current.generation === identity.generation
|
|
56
|
+
&& current.prefixSha256 === identity.prefixSha256;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof MemoryError)
|
|
60
|
+
return false;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Prepare protected host input and move multimodal messages behind safe cuts. */
|
|
2
|
+
import { type Message, type UserMessage } from '@aragon-agent/core';
|
|
3
|
+
import { type CompactionMemory, type ProtectedUserMessage } from './memory.js';
|
|
4
|
+
import { type AnchorShape } from './summary-prompt.js';
|
|
5
|
+
import { type CompactionIdentity } from './memory-identity.js';
|
|
6
|
+
export interface PreparedMemoryInput {
|
|
7
|
+
anchor: UserMessage;
|
|
8
|
+
anchorShape: AnchorShape;
|
|
9
|
+
prior?: CompactionMemory;
|
|
10
|
+
generation: number;
|
|
11
|
+
originalTaskSourceId: string;
|
|
12
|
+
userMessages: ProtectedUserMessage[];
|
|
13
|
+
head: Message[];
|
|
14
|
+
headStartIndex: number;
|
|
15
|
+
tail: Message[];
|
|
16
|
+
cutIndex: number;
|
|
17
|
+
legacySummary?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PrepareMemoryOptions {
|
|
20
|
+
messages: readonly Message[];
|
|
21
|
+
cutIndex: number;
|
|
22
|
+
identity?: CompactionIdentity;
|
|
23
|
+
}
|
|
24
|
+
/** Prepare a snapshot without mutation; throws before payment when protection cannot fit. */
|
|
25
|
+
export declare function prepareMemoryInput(options: PrepareMemoryOptions): PreparedMemoryInput;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Prepare protected host input and move multimodal messages behind safe cuts. */
|
|
2
|
+
import { findSafeCutIndices } from '@aragon-agent/core';
|
|
3
|
+
import { assertProtectedMemoryBudget, isLegacyMemoryText, MemoryError, } from './memory.js';
|
|
4
|
+
import { parseCompactedBlock } from './summary-prompt.js';
|
|
5
|
+
import { verifyCompactionIdentity } from './memory-identity.js';
|
|
6
|
+
import { COMPACTION_LIMITS as LIMITS } from './limits.js';
|
|
7
|
+
import { buildDigest } from './digest.js';
|
|
8
|
+
function hasImage(message) {
|
|
9
|
+
return message.role !== 'assistant' && Array.isArray(message.content)
|
|
10
|
+
&& message.content.some((part) => part.type === 'image');
|
|
11
|
+
}
|
|
12
|
+
function protectImages(messages, cutIndex, anchorIndex) {
|
|
13
|
+
const imageIndex = messages.findIndex((message, index) => index < cutIndex && index !== anchorIndex && hasImage(message));
|
|
14
|
+
if (imageIndex < 0)
|
|
15
|
+
return cutIndex;
|
|
16
|
+
return findSafeCutIndices(messages).filter((index) => index <= imageIndex).at(-1) ?? 0;
|
|
17
|
+
}
|
|
18
|
+
function legacyReference(messages) {
|
|
19
|
+
// This copy is read-only evidence of lost provenance; all originals stay protected.
|
|
20
|
+
const blocks = messages.filter(isLegacyMemoryText);
|
|
21
|
+
return blocks.length
|
|
22
|
+
? blocks.map((message) => message.content).join('\n\n')
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
function preflightBudget(input) {
|
|
26
|
+
if (input.userMessages.length > LIMITS.protectedUserMessages) {
|
|
27
|
+
throw new MemoryError('protected_memory_too_large', '$.userMessages');
|
|
28
|
+
}
|
|
29
|
+
const { coverage } = buildDigest({
|
|
30
|
+
head: input.head,
|
|
31
|
+
generation: input.generation,
|
|
32
|
+
startIndex: input.headStartIndex,
|
|
33
|
+
});
|
|
34
|
+
const clips = [
|
|
35
|
+
...(input.prior?.coverage.clippedToolResults ?? []),
|
|
36
|
+
...coverage.clippedToolResults,
|
|
37
|
+
];
|
|
38
|
+
if (clips.length > LIMITS.coverageClips) {
|
|
39
|
+
throw new MemoryError('protected_memory_too_large', '$.coverage');
|
|
40
|
+
}
|
|
41
|
+
const minimum = {
|
|
42
|
+
schemaVersion: 2,
|
|
43
|
+
generation: input.generation,
|
|
44
|
+
originalTask: { sourceId: input.originalTaskSourceId, location: 'anchor' },
|
|
45
|
+
userMessages: input.userMessages,
|
|
46
|
+
items: input.prior?.items ?? [],
|
|
47
|
+
...(input.legacySummary !== undefined ? { legacySummary: input.legacySummary } : {}),
|
|
48
|
+
coverage: {
|
|
49
|
+
summarizedMessages: (input.prior?.coverage.summarizedMessages ?? 0) + coverage.summarizedMessages,
|
|
50
|
+
clippedToolResults: clips,
|
|
51
|
+
legacyIncomplete: !!input.prior?.coverage.legacyIncomplete || input.legacySummary !== undefined,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
assertProtectedMemoryBudget(input.anchor, minimum);
|
|
55
|
+
}
|
|
56
|
+
/** Prepare a snapshot without mutation; throws before payment when protection cannot fit. */
|
|
57
|
+
export function prepareMemoryInput(options) {
|
|
58
|
+
const { messages, identity } = options;
|
|
59
|
+
if (identity && !verifyCompactionIdentity(messages, identity)) {
|
|
60
|
+
throw new MemoryError('invalid_prior_memory', '$.identity');
|
|
61
|
+
}
|
|
62
|
+
const parsed = identity ? parseCompactedBlock(messages[1]) : undefined;
|
|
63
|
+
const prior = parsed?.memory;
|
|
64
|
+
const anchorIndex = identity ? 0 : messages.findIndex((message) => message.role === 'user');
|
|
65
|
+
const anchor = messages[anchorIndex];
|
|
66
|
+
if (anchor?.role !== 'user')
|
|
67
|
+
throw new MemoryError('nothing_to_drop', '$.anchor');
|
|
68
|
+
if (!Number.isSafeInteger(options.cutIndex) ||
|
|
69
|
+
!findSafeCutIndices(messages).includes(options.cutIndex)) {
|
|
70
|
+
throw new MemoryError('invalid_history', '$.cutIndex');
|
|
71
|
+
}
|
|
72
|
+
const generation = (prior?.generation ?? 0) + 1;
|
|
73
|
+
if (!Number.isSafeInteger(generation)) {
|
|
74
|
+
throw new MemoryError('invalid_prior_memory', '$.generation');
|
|
75
|
+
}
|
|
76
|
+
const cutIndex = protectImages(messages, options.cutIndex, anchorIndex);
|
|
77
|
+
const headStartIndex = identity ? 2 : anchorIndex + 1;
|
|
78
|
+
// Core histories normally start with a user; never silently discard a preceding message.
|
|
79
|
+
if (anchorIndex !== 0)
|
|
80
|
+
throw new MemoryError('invalid_history', '$.anchorIndex');
|
|
81
|
+
if (cutIndex <= headStartIndex) {
|
|
82
|
+
throw new MemoryError(cutIndex < options.cutIndex ? 'protected_multimodal_tail' : 'nothing_to_drop', '$.cutIndex');
|
|
83
|
+
}
|
|
84
|
+
const userMessages = [...(prior?.userMessages ?? [])];
|
|
85
|
+
for (let index = headStartIndex; index < cutIndex; index += 1) {
|
|
86
|
+
const message = messages[index];
|
|
87
|
+
if (message.role === 'user')
|
|
88
|
+
userMessages.push({ id: `g${generation}:m${index}`, message });
|
|
89
|
+
}
|
|
90
|
+
const input = {
|
|
91
|
+
anchor,
|
|
92
|
+
anchorShape: parsed?.anchor ?? 'verbatim',
|
|
93
|
+
...(prior ? { prior } : {}),
|
|
94
|
+
generation,
|
|
95
|
+
originalTaskSourceId: prior?.originalTask.sourceId ?? `g${generation}:m${anchorIndex}`,
|
|
96
|
+
userMessages,
|
|
97
|
+
head: messages.slice(headStartIndex, cutIndex),
|
|
98
|
+
headStartIndex,
|
|
99
|
+
tail: messages.slice(cutIndex),
|
|
100
|
+
cutIndex,
|
|
101
|
+
legacySummary: prior?.legacySummary
|
|
102
|
+
?? (!identity ? legacyReference(messages.slice(0, cutIndex)) : undefined),
|
|
103
|
+
};
|
|
104
|
+
preflightBudget(input);
|
|
105
|
+
return input;
|
|
106
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Human-readable views; never the authoritative serialized working memory. */
|
|
2
|
+
import type { CompactionMemory } from './memory.js';
|
|
3
|
+
/** Render fixed sections and active chain tails, retaining full chains in JSON only. */
|
|
4
|
+
export declare function renderMemoryMarkdown(memory: CompactionMemory): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function itemText(item) {
|
|
2
|
+
const status = item.status ? `[${item.status}] ` : '';
|
|
3
|
+
const sources = item.sources
|
|
4
|
+
.map((source) => `${source.messageId} (${source.role}): ${JSON.stringify(source.excerpt)}`)
|
|
5
|
+
.join('; ');
|
|
6
|
+
return `- ${status}${item.text}\n Sources (model extracted): ${sources}`;
|
|
7
|
+
}
|
|
8
|
+
/** Render fixed sections and active chain tails, retaining full chains in JSON only. */
|
|
9
|
+
export function renderMemoryMarkdown(memory) {
|
|
10
|
+
const superseded = new Set(memory.items.flatMap((item) => item.supersedes ? [item.supersedes] : []));
|
|
11
|
+
const active = memory.items.filter((item) => !superseded.has(item.id));
|
|
12
|
+
const select = (...sections) => active
|
|
13
|
+
.filter((item) => sections.includes(item.section))
|
|
14
|
+
.map(itemText)
|
|
15
|
+
.join('\n');
|
|
16
|
+
const requirements = memory.userMessages.map((entry) => {
|
|
17
|
+
const content = entry.message.content;
|
|
18
|
+
return `- ${entry.id}: ${typeof content === 'string'
|
|
19
|
+
? content
|
|
20
|
+
: content.map((part) => part.type === 'text' ? part.text : '[image retained]').join('\n')}`;
|
|
21
|
+
}).join('\n');
|
|
22
|
+
const sections = [
|
|
23
|
+
['Goal', `Original task retained verbatim in anchor (${memory.originalTask.sourceId}).`],
|
|
24
|
+
['User requirements', requirements],
|
|
25
|
+
['Global context', select('global')],
|
|
26
|
+
['Decisions', select('decisions')],
|
|
27
|
+
['Files and facts', select('files', 'facts')],
|
|
28
|
+
['Tasks', select('tasks')],
|
|
29
|
+
['Verification', select('verification')],
|
|
30
|
+
['Pitfalls', select('pitfalls')],
|
|
31
|
+
['Next steps', select('next')],
|
|
32
|
+
[
|
|
33
|
+
'Coverage',
|
|
34
|
+
`Generation: ${memory.generation}`
|
|
35
|
+
+ `\nSummarized messages: ${memory.coverage.summarizedMessages}`
|
|
36
|
+
+ `\nClipped tool results: ${memory.coverage.clippedToolResults.length}`
|
|
37
|
+
+ `\nLegacy incomplete: ${memory.coverage.legacyIncomplete}`
|
|
38
|
+
+ memory.coverage.clippedToolResults
|
|
39
|
+
.map((clip) => `\n- ${clip.messageId}: ${clip.omittedChars} characters omitted`)
|
|
40
|
+
.join('')
|
|
41
|
+
+ (memory.legacySummary
|
|
42
|
+
? `\n\nLegacy reference (unverified):\n${memory.legacySummary}`
|
|
43
|
+
: ''),
|
|
44
|
+
],
|
|
45
|
+
];
|
|
46
|
+
return sections.map(([title, body]) => `## ${title}\n${body || '(none)'}`).join('\n\n');
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Bounded syntax, schema and evidence checks for untrusted memory input. */
|
|
2
|
+
import type { CompactionMemory, MemoryDelta, MemorySource } from './memory.js';
|
|
3
|
+
/** Stable failure reason and short field path; never includes protected content. */
|
|
4
|
+
export declare class MemoryError extends Error {
|
|
5
|
+
readonly reason: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
constructor(reason: string, path?: string);
|
|
8
|
+
}
|
|
9
|
+
/** Parse bounded JSON, rejecting duplicate decoded keys before JSON.parse. */
|
|
10
|
+
export declare function parseStrictJson(text: string): unknown;
|
|
11
|
+
/** Validate the model-owned schema only; throws MemoryError on invalid fields. */
|
|
12
|
+
export declare function validateDeltaSchema(value: unknown): asserts value is MemoryDelta;
|
|
13
|
+
/** Validate a persisted v2 body, including unique IDs and acyclic state chains. */
|
|
14
|
+
export declare function validateMemorySchema(value: unknown): asserts value is CompactionMemory;
|
|
15
|
+
export interface DeltaValidationContext {
|
|
16
|
+
sourceMap: ReadonlyMap<string, MemorySource>;
|
|
17
|
+
prior?: CompactionMemory;
|
|
18
|
+
hasSummarizableMessages: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Check current-source excerpts, completion evidence and append-only supersession. */
|
|
21
|
+
export declare function validateMemoryDelta(delta: MemoryDelta, context: DeltaValidationContext): void;
|