@aragon-agent/core 0.2.51 → 0.2.52

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.
@@ -21,6 +21,16 @@ export interface CompactionPlan {
21
21
  cutIndex: number;
22
22
  droppedMessages: number;
23
23
  retainedTurns: number;
24
+ /**
25
+ * Set when this plan exists only because `retainMessages` allowed the turn
26
+ * requirement to be relaxed.
27
+ *
28
+ * THE HOST MUST ANNOUNCE IT. A relaxed cut keeps fewer user turns than
29
+ * `keepRecentTurns` asked for, and the recent turns are the part of a history a
30
+ * user cares most about keeping word for word - so a compaction that quietly
31
+ * traded that away would be lying by omission about its own result.
32
+ */
33
+ keepRelaxed?: boolean;
24
34
  }
25
35
  export type HistoryCheck = {
26
36
  ok: true;
@@ -63,6 +73,32 @@ export interface PlanCompactionOptions {
63
73
  * to save a parameter.
64
74
  */
65
75
  protectedPrefix?: number;
76
+ /**
77
+ * The smallest tail - in MESSAGES - a second planning pass may settle for.
78
+ *
79
+ * OPTIONAL, AND THAT IS THE WHOLE CONTRACT (context-compaction-never-fires B).
80
+ * Without it `planCompaction` behaves exactly as it always has, which is what
81
+ * keeps the four existing assertions in `compaction.test.ts` ({keep: 4} null,
82
+ * {keep: 99} null, the `protectedPrefix` exit, and the idempotence guard) true
83
+ * without editing a line of them. The relaxation is a NEW MODE a host opts into,
84
+ * not a change to `keepRecentTurns`'s meaning.
85
+ *
86
+ * WHY IT IS NEEDED AT ALL. `keepRecentTurns` counts `user` messages, and this
87
+ * package's dominant session shape is one user prompt followed by a hundred
88
+ * turns of tool calls. Such a history retains 0 user turns behind any cut, so it
89
+ * has no legal plan under any `keepRecentTurns >= 1` - 107 of 119 long sessions
90
+ * on the reporting machine. Passing this field says "a cut with a genuinely
91
+ * useful tail beats no cut at all".
92
+ *
93
+ * WHY IT IS MESSAGES AND NOT TURNS. The tail has to remain a usable continuation
94
+ * window: the number is a structural bound (tens of messages), chosen by the
95
+ * host, not a policy about turns.
96
+ *
97
+ * THE EXIT CONDITION IS UNCHANGED. `cutIndex <= protectedPrefix` still returns
98
+ * `null` in both passes, so an already-compacted history with nothing new is
99
+ * still refused (the idempotence guard) and the anchor is still never dropped.
100
+ */
101
+ retainMessages?: number;
66
102
  }
67
103
  /**
68
104
  * The largest safe cut that still leaves at least `keepRecentTurns` complete
@@ -71,6 +107,15 @@ export interface PlanCompactionOptions {
71
107
  * `null` — rather than a zero-length head — is what lets §3.8 turn "the recent
72
108
  * turns alone exceed the threshold" into a bounded, loud outcome instead of a
73
109
  * retry loop against a paid API.
110
+ *
111
+ * TWO PASSES, AND THE SECOND ONE IS OPT-IN (`opts.retainMessages`). The first pass
112
+ * is unchanged: the largest safe cut that leaves `keepRecentTurns` user turns
113
+ * behind it. Only when that pass finds nothing AND the caller supplied a
114
+ * `retainMessages` budget does the second pass run, taking the largest safe cut
115
+ * that leaves at least that many MESSAGES in the tail - flagged `keepRelaxed` so
116
+ * the host can disclose the trade. A caller that passes nothing sees exactly the
117
+ * behaviour this function has always had, which is what makes the change
118
+ * backward-compatible by structure rather than by discipline.
74
119
  */
75
120
  export declare function planCompaction(messages: readonly Message[], opts: PlanCompactionOptions): CompactionPlan | null;
76
121
  export interface ValidateHistoryOptions {
@@ -66,6 +66,15 @@ function countTurns(messages, from) {
66
66
  * `null` — rather than a zero-length head — is what lets §3.8 turn "the recent
67
67
  * turns alone exceed the threshold" into a bounded, loud outcome instead of a
68
68
  * retry loop against a paid API.
69
+ *
70
+ * TWO PASSES, AND THE SECOND ONE IS OPT-IN (`opts.retainMessages`). The first pass
71
+ * is unchanged: the largest safe cut that leaves `keepRecentTurns` user turns
72
+ * behind it. Only when that pass finds nothing AND the caller supplied a
73
+ * `retainMessages` budget does the second pass run, taking the largest safe cut
74
+ * that leaves at least that many MESSAGES in the tail - flagged `keepRelaxed` so
75
+ * the host can disclose the trade. A caller that passes nothing sees exactly the
76
+ * behaviour this function has always had, which is what makes the change
77
+ * backward-compatible by structure rather than by discipline.
69
78
  */
70
79
  export function planCompaction(messages, opts) {
71
80
  const keep = Math.max(1, Math.floor(opts.keepRecentTurns));
@@ -87,6 +96,39 @@ export function planCompaction(messages, opts) {
87
96
  retainedTurns,
88
97
  };
89
98
  }
99
+ const retainMessages = opts.retainMessages;
100
+ if (retainMessages === undefined)
101
+ return null;
102
+ return planRelaxed(messages, safe, {
103
+ protectedPrefix,
104
+ retainMessages: Math.max(1, Math.floor(retainMessages)),
105
+ });
106
+ }
107
+ /**
108
+ * The second pass: the largest safe cut whose tail still holds `retainMessages`.
109
+ *
110
+ * THE TAIL LENGTH BOUND AND THE PREFIX EXIT ARE THE TWO RULES THAT MATTER. Without
111
+ * the length bound a relaxed plan could leave two messages behind, which is not a
112
+ * continuation window but a truncation; without the unchanged prefix exit a
113
+ * compacted history with nothing new would be re-summarized (the idempotence guard
114
+ * `compaction.test.ts` pins).
115
+ */
116
+ function planRelaxed(messages, safe, opts) {
117
+ for (let i = safe.length - 1; i >= 0; i -= 1) {
118
+ const cutIndex = safe[i];
119
+ if (cutIndex >= messages.length)
120
+ continue;
121
+ if (messages.length - cutIndex < opts.retainMessages)
122
+ continue;
123
+ if (cutIndex <= opts.protectedPrefix)
124
+ return null;
125
+ return {
126
+ cutIndex,
127
+ droppedMessages: cutIndex - opts.protectedPrefix,
128
+ retainedTurns: countTurns(messages, cutIndex),
129
+ keepRelaxed: true,
130
+ };
131
+ }
90
132
  return null;
91
133
  }
92
134
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/core",
3
- "version": "0.2.51",
3
+ "version": "0.2.52",
4
4
  "description": "AragonAgent core engine — LLM provider adapters, streaming, tool registry/execution, steering, idle watchdog, and an optional isolated-vm sandbox.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",