@aragon-agent/cli 0.6.1 → 0.6.3
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 +216 -0
- package/README.md +241 -18
- package/dist/agent/agent-mode.d.ts +39 -0
- package/dist/agent/agent-mode.js +36 -0
- package/dist/agent/controller.d.ts +74 -1
- package/dist/agent/controller.js +200 -1
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +120 -0
- package/dist/agent/reducer.js +201 -2
- package/dist/cli.js +297 -6
- package/dist/commands/builtins.js +73 -23
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +113 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +342 -0
- package/dist/compaction/compactor.d.ts +268 -0
- package/dist/compaction/compactor.js +689 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +179 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +255 -0
- package/dist/compaction/wiring.js +556 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +92 -2
- package/dist/config/load.d.ts +31 -0
- package/dist/config/load.js +63 -1
- package/dist/config/schema.d.ts +159 -0
- package/dist/config/schema.js +137 -0
- package/dist/config/store.js +17 -1
- package/dist/config/ui-state.d.ts +44 -6
- package/dist/config/ui-state.js +21 -12
- package/dist/diagnostics/doctor.js +41 -1
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/index.js +8 -4
- package/dist/exec/runner.js +44 -2
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/stdin-mouse-filter.d.ts +8 -3
- package/dist/logging/install.d.ts +51 -1
- package/dist/logging/install.js +28 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/cli-commands.js +23 -1
- package/dist/session/persist.js +19 -0
- package/dist/session/store.d.ts +18 -0
- package/dist/session/store.js +59 -2
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +48 -1
- package/dist/ui/App.js +317 -17
- package/dist/ui/Composer.d.ts +26 -1
- package/dist/ui/Composer.js +6 -6
- package/dist/ui/PromptInput.d.ts +34 -1
- package/dist/ui/PromptInput.js +54 -3
- package/dist/ui/StatusBar.d.ts +40 -0
- package/dist/ui/StatusBar.js +13 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +32 -2
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +10 -3
- package/dist/ui/layout/budget.js +10 -3
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +34 -0
- package/dist/ui/overlays/HelpOverlay.js +18 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +60 -1
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/screen.d.ts +22 -0
- package/dist/ui/screen.js +70 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +23 -0
- package/dist/ui/use-startup-notices.d.ts +62 -5
- package/dist/ui/use-startup-notices.js +81 -17
- package/dist/ui/use-wheel-routing.js +8 -0
- package/dist/ui/win-vt-force.d.ts +136 -0
- package/dist/ui/win-vt-force.js +237 -0
- package/package.json +4 -2
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* module exists so `stdin-mouse-filter.ts` can remove every byte of every
|
|
16
16
|
* report before Ink ever sees the chunk (invariant I-1).
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Every mouse report is CONSUMED here whatever it turns out to be; what changed
|
|
19
|
+
* with tui-selection-and-scroll-follow is that press / drag / release are now
|
|
20
|
+
* EMITTED as well as consumed, so `SelectionController` can build a selection
|
|
21
|
+
* out of them (§4.4.2). They were previously recognized and thrown away.
|
|
20
22
|
*/
|
|
21
23
|
/**
|
|
22
24
|
* Ceiling on the held-back tail. A malformed sequence that looks like a prefix
|
|
@@ -38,34 +40,80 @@ const SGR_PREFIX = /^\x1b\[<[\d;]*$/;
|
|
|
38
40
|
export function isMousePrefix(s) {
|
|
39
41
|
return /^\x1B(\[(<[\d;]*|M[\s\S]{0,2})?)?$/.test(s);
|
|
40
42
|
}
|
|
43
|
+
function modifiers(b) {
|
|
44
|
+
return { shift: (b & 4) !== 0, alt: (b & 8) !== 0, ctrl: (b & 16) !== 0 };
|
|
45
|
+
}
|
|
41
46
|
/**
|
|
42
|
-
* Decode
|
|
43
|
-
* not a wheel notch we act on.
|
|
47
|
+
* Decode one report, or `null` when it is a report we consume without acting on.
|
|
44
48
|
*
|
|
45
49
|
* THE ORDER OF THESE TESTS IS THE SPECIFICATION, not a presentation choice.
|
|
46
50
|
* Button 66 (wheel-left) has `b & 1 === 0` and would decode as `'up'`, so the
|
|
47
|
-
* horizontal test has to precede the direction decode
|
|
51
|
+
* horizontal test has to precede the direction decode; and `b & 32` (motion) has
|
|
52
|
+
* to precede the `final` test, because a drag is reported with `M` exactly as a
|
|
53
|
+
* press is.
|
|
54
|
+
*
|
|
55
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
+
* I-11 — BUTTON EVENTS COME FROM THE SGR PATH ONLY. X10 stays wheel-only, which
|
|
57
|
+
* is exactly the contract it has today.
|
|
58
|
+
*
|
|
59
|
+
* `matchX10` reconstructs a legacy report and hard-codes `final = 'M'`, because
|
|
60
|
+
* X10 HAS NO SEPARATE RELEASE FORM AT ALL — it encodes a release as button 3 of
|
|
61
|
+
* a press. Fed through the table below, an X10 release would become a `press`
|
|
62
|
+
* that is never followed by a `release`, whose `button` is 3 while the type says
|
|
63
|
+
* `0 | 1 | 2`, and whose consequence is a `hold` that is set and never cleared:
|
|
64
|
+
* content anchoring never lets go, the resume timer's third condition never
|
|
65
|
+
* passes, and the transcript is frozen for the rest of the session with nothing
|
|
66
|
+
* raised anywhere.
|
|
67
|
+
*
|
|
68
|
+
* This costs nothing real (D-16): we never REQUEST X10 — `?1006h` is over a
|
|
69
|
+
* decade old and every terminal that implements `?1000` implements it — and a
|
|
70
|
+
* terminal that ignored the SGR request also has no usable drag coordinates past
|
|
71
|
+
* column 223 (R-2). X10 exists in this parser only so a stray report cannot
|
|
72
|
+
* reach the user's draft.
|
|
73
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
48
74
|
*/
|
|
49
|
-
function
|
|
50
|
-
// 1.
|
|
51
|
-
if ((b & 64)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
function decodeReport(b, x, y, final, encoding) {
|
|
76
|
+
// 1. Wheel. Buttons 64-67 in both encodings.
|
|
77
|
+
if ((b & 64) !== 0) {
|
|
78
|
+
// Horizontal wheel (66 = left, 67 = right). Consumed and dropped (non-goal).
|
|
79
|
+
if ((b & 3) >= 2)
|
|
80
|
+
return null;
|
|
81
|
+
// Wheel buttons are press-only; a terminal that ever sent a release for one
|
|
82
|
+
// must not produce a second step for the same physical notch.
|
|
83
|
+
if (final !== 'M')
|
|
84
|
+
return null;
|
|
85
|
+
return { kind: 'wheel', dir: (b & 1) === 1 ? 'down' : 'up', x, y, ...modifiers(b) };
|
|
86
|
+
}
|
|
87
|
+
if (encoding !== 'sgr')
|
|
88
|
+
return null; // I-11
|
|
89
|
+
// 2. Motion while a button is held (`?1002h`). `3` is "no button", which
|
|
90
|
+
// `?1002` should never send and `?1003` sends constantly — dropping it is
|
|
91
|
+
// what keeps an any-event terminal from painting a selection with no button
|
|
92
|
+
// down at all.
|
|
93
|
+
if ((b & 32) !== 0) {
|
|
94
|
+
const button = b & 3;
|
|
95
|
+
if (button === 3)
|
|
96
|
+
return null;
|
|
97
|
+
return { kind: 'drag', button: button, x, y, ...modifiers(b) };
|
|
98
|
+
}
|
|
99
|
+
// 3. Release. In SGR the final byte carries the transition, so this is
|
|
100
|
+
// unambiguous — unlike X10, which is why step 1's guard exists.
|
|
101
|
+
//
|
|
102
|
+
// A release whose button field reads `3` is a terminal saying "some button
|
|
103
|
+
// came up" without naming it. It is mapped to button 0 rather than dropped:
|
|
104
|
+
// the ONE thing a release must always do is let go of `hold`, and dropping
|
|
105
|
+
// it would leave the viewport frozen — the exact failure I-11 is about. A
|
|
106
|
+
// spurious button-0 release is harmless, because a selection can only exist
|
|
107
|
+
// if button 0 was pressed in the first place.
|
|
108
|
+
if (final === 'm') {
|
|
109
|
+
const button = b & 3;
|
|
110
|
+
return { kind: 'release', button: (button === 3 ? 0 : button), x, y, ...modifiers(b) };
|
|
111
|
+
}
|
|
112
|
+
// 4. Press.
|
|
113
|
+
const button = b & 3;
|
|
114
|
+
if (button === 3)
|
|
59
115
|
return null;
|
|
60
|
-
return {
|
|
61
|
-
kind: 'wheel',
|
|
62
|
-
dir: (b & 1) === 1 ? 'down' : 'up',
|
|
63
|
-
x,
|
|
64
|
-
y,
|
|
65
|
-
shift: (b & 4) !== 0,
|
|
66
|
-
alt: (b & 8) !== 0,
|
|
67
|
-
ctrl: (b & 16) !== 0,
|
|
68
|
-
};
|
|
116
|
+
return { kind: 'press', button: button, x, y, ...modifiers(b) };
|
|
69
117
|
}
|
|
70
118
|
/**
|
|
71
119
|
* X10 legacy reports (`\x1b[M` + three chars, each `code - 32`).
|
|
@@ -84,7 +132,13 @@ function matchX10(s) {
|
|
|
84
132
|
if (codes.some((c) => c < 32 || c > 255))
|
|
85
133
|
return { kind: 'complete', length: 6, event: null };
|
|
86
134
|
const [b, x, y] = codes;
|
|
87
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
kind: 'complete',
|
|
137
|
+
length: 6,
|
|
138
|
+
// `'x10'` is what makes I-11 a property of the code rather than of somebody's
|
|
139
|
+
// memory: this encoding yields wheel events or nothing, ever.
|
|
140
|
+
event: decodeReport(b - 32, x - 32, y - 32, 'M', 'x10'),
|
|
141
|
+
};
|
|
88
142
|
}
|
|
89
143
|
/** Classify the escape sequence starting at `s[0] === ESC`. */
|
|
90
144
|
function matchReport(s) {
|
|
@@ -100,7 +154,7 @@ function matchReport(s) {
|
|
|
100
154
|
const b = Number(m[1]);
|
|
101
155
|
const x = Number(m[2]);
|
|
102
156
|
const y = Number(m[3]);
|
|
103
|
-
return { kind: 'complete', length: m[0].length, event:
|
|
157
|
+
return { kind: 'complete', length: m[0].length, event: decodeReport(b, x, y, m[4], 'sgr') };
|
|
104
158
|
}
|
|
105
159
|
return SGR_PREFIX.test(s) ? { kind: 'partial' } : { kind: 'none' };
|
|
106
160
|
}
|
|
@@ -109,12 +163,17 @@ function matchReport(s) {
|
|
|
109
163
|
return { kind: 'none' };
|
|
110
164
|
}
|
|
111
165
|
/**
|
|
112
|
-
* Split a decoded stdin chunk into
|
|
166
|
+
* Split a decoded stdin chunk into mouse events, passthrough text, and a
|
|
113
167
|
* trailing prefix to carry into the next chunk.
|
|
114
168
|
*
|
|
115
169
|
* The caller is expected to feed `pending + nextChunk` back in; ConPTY does
|
|
116
170
|
* split writes mid-sequence, and a report torn across two chunks would
|
|
117
171
|
* otherwise leak its tail into the composer (R-6).
|
|
172
|
+
*
|
|
173
|
+
* ONE CHANNEL, NOT TWO (§4.4.2). Widening this list to carry buttons alongside
|
|
174
|
+
* wheel notches is what keeps ARRIVAL ORDER intact between them; a second
|
|
175
|
+
* channel would let a press overtake the notch that preceded it, and the
|
|
176
|
+
* selection would then be anchored against rows that had already scrolled.
|
|
118
177
|
*/
|
|
119
178
|
export function splitMouseEvents(buffer) {
|
|
120
179
|
const events = [];
|
|
@@ -18,10 +18,15 @@
|
|
|
18
18
|
* than the bug this feature fixes. `cli.tsx::runInteractive` is the only caller
|
|
19
19
|
* and passes `filter !== null` to `enterAltScreen`.
|
|
20
20
|
*/
|
|
21
|
-
import { type
|
|
21
|
+
import { type MouseEvent } from './mouse-events.js';
|
|
22
22
|
export interface MouseSource {
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Returns an unsubscribe function. Multiple subscribers are allowed — the
|
|
25
|
+
* wheel router and (since tui-selection-and-scroll-follow) the selection
|
|
26
|
+
* controller both take this one channel, and each ignores the kinds it does
|
|
27
|
+
* not own.
|
|
28
|
+
*/
|
|
29
|
+
subscribe(listener: (event: MouseEvent) => void): () => void;
|
|
25
30
|
}
|
|
26
31
|
export interface MouseFilter {
|
|
27
32
|
/** Hand this to Ink: `render(<App/>, { stdin: filter.stdin })`. */
|
|
@@ -129,7 +129,17 @@ export interface AgentEventSource {
|
|
|
129
129
|
thinkingLevel: string;
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* The ELEVEN members of the core `AgentEvent` union, as this module reads them.
|
|
134
|
+
*
|
|
135
|
+
* THIS UNION MUST STAY A SUPERSET OF CORE'S, and the failure when it does not is
|
|
136
|
+
* a COMPILE error rather than a silent one (implementation finding IF-3): the
|
|
137
|
+
* `AgentEventSource` interface above takes a listener of THIS type, so a core
|
|
138
|
+
* union member missing here makes `AgentController` structurally incompatible
|
|
139
|
+
* with `attachAgentEvents` and every call site fails to type-check. That is the
|
|
140
|
+
* right failure mode, but it means adding an event to core is also a change
|
|
141
|
+
* here — the two `compaction_*` members below arrived that way.
|
|
142
|
+
*/
|
|
133
143
|
export type AgentLogEvent = {
|
|
134
144
|
type: 'agent_start';
|
|
135
145
|
} | {
|
|
@@ -183,6 +193,46 @@ export type AgentLogEvent = {
|
|
|
183
193
|
duration?: number;
|
|
184
194
|
error?: string;
|
|
185
195
|
output?: string;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Context compaction (context-auto-compaction §5.1).
|
|
199
|
+
*
|
|
200
|
+
* RECORDED UNDER THE `agent` SCOPE HERE, not `compaction`. This module logs the
|
|
201
|
+
* LEAD'S LIFECYCLE, and "the run paused to compact" is part of that story — a
|
|
202
|
+
* support reader following a run's `agent` records must not find an unexplained
|
|
203
|
+
* gap where a 40-second summarization was. The `compaction` scope carries the
|
|
204
|
+
* subsystem's own detail (`compaction/compactor.ts`), which is a different
|
|
205
|
+
* question.
|
|
206
|
+
*
|
|
207
|
+
* NO `summary` FIELD IS READ, ever: it can be tens of thousands of characters,
|
|
208
|
+
* and this file's `message_update` case already records why a log larger than
|
|
209
|
+
* the conversation is not a log.
|
|
210
|
+
*/
|
|
211
|
+
| {
|
|
212
|
+
type: 'compaction_start';
|
|
213
|
+
trigger?: string;
|
|
214
|
+
messageCount?: number;
|
|
215
|
+
} | {
|
|
216
|
+
type: 'compaction_end';
|
|
217
|
+
applied?: boolean;
|
|
218
|
+
mode?: string;
|
|
219
|
+
reason?: string;
|
|
220
|
+
messagesBefore?: number;
|
|
221
|
+
messagesAfter?: number;
|
|
222
|
+
droppedMessages?: number;
|
|
223
|
+
/**
|
|
224
|
+
* The tail-relief report (context-auto-compaction-hardening W2).
|
|
225
|
+
*
|
|
226
|
+
* THIS UNION MUST STAY A SUPERSET OF CORE'S (IF-3). A field added to
|
|
227
|
+
* `CompactionEndEvent` and not mirrored here does not fail the build — the
|
|
228
|
+
* structural type simply ignores it — so the only trace of a degradation
|
|
229
|
+
* the user is entitled to know about would be missing from the log.
|
|
230
|
+
*/
|
|
231
|
+
tailRelief?: {
|
|
232
|
+
messages: number;
|
|
233
|
+
charsRemoved: number;
|
|
234
|
+
};
|
|
235
|
+
durationMs?: number;
|
|
186
236
|
};
|
|
187
237
|
/**
|
|
188
238
|
* Subscribe to the agent's events and record them.
|
package/dist/logging/install.js
CHANGED
|
@@ -273,6 +273,34 @@ export function attachAgentEvents(logger, controller) {
|
|
|
273
273
|
ms: turnStartedAt > 0 ? Date.now() - turnStartedAt : 0,
|
|
274
274
|
});
|
|
275
275
|
break;
|
|
276
|
+
// Context compaction (context-auto-compaction §5.1). At `info`, like
|
|
277
|
+
// `turn_end`, so a run's `agent` records have no unexplained 40-second gap
|
|
278
|
+
// where a summarization was.
|
|
279
|
+
case 'compaction_start':
|
|
280
|
+
logger.info('agent', 'compaction_start', {
|
|
281
|
+
trigger: event.trigger,
|
|
282
|
+
messages: event.messageCount,
|
|
283
|
+
});
|
|
284
|
+
break;
|
|
285
|
+
case 'compaction_end':
|
|
286
|
+
// `applied: false` is a WARN, not an info: it means the run is still at
|
|
287
|
+
// the occupancy that triggered this and is about to send anyway.
|
|
288
|
+
logger[event.applied ? 'info' : 'warn']('agent', 'compaction_end', {
|
|
289
|
+
applied: event.applied,
|
|
290
|
+
mode: event.mode,
|
|
291
|
+
reason: event.reason,
|
|
292
|
+
before: event.messagesBefore,
|
|
293
|
+
after: event.messagesAfter,
|
|
294
|
+
dropped: event.droppedMessages,
|
|
295
|
+
...(event.tailRelief
|
|
296
|
+
? {
|
|
297
|
+
clipped: event.tailRelief.messages,
|
|
298
|
+
clippedChars: event.tailRelief.charsRemoved,
|
|
299
|
+
}
|
|
300
|
+
: {}),
|
|
301
|
+
ms: event.durationMs,
|
|
302
|
+
});
|
|
303
|
+
break;
|
|
276
304
|
case 'message_update': {
|
|
277
305
|
// API retry (llm-api-retry-backoff §6.8), ABOVE the `trace` line so a
|
|
278
306
|
// three-minute pause is in the log at the DEFAULT level. Redaction is
|
package/dist/logging/logger.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import type { LogConfig } from '../config/schema.js';
|
|
24
24
|
import { type LogLevelName } from './levels.js';
|
|
25
25
|
/** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
|
|
26
|
-
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update';
|
|
26
|
+
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction';
|
|
27
27
|
export interface LogRecord {
|
|
28
28
|
ts: string;
|
|
29
29
|
lv: LogLevelName;
|
|
@@ -16,7 +16,17 @@
|
|
|
16
16
|
* should make on its own.
|
|
17
17
|
*/
|
|
18
18
|
import process from 'node:process';
|
|
19
|
-
import { listSessions, pruneSessions, readSessionFile, removeSession, sessionPathFor, sessionsDir, } from './store.js';
|
|
19
|
+
import { isValidSessionId, listSessions, pruneSessions, readSessionFile, removeSession, sessionPathFor, sessionsDir, } from './store.js';
|
|
20
|
+
/**
|
|
21
|
+
* One wording for one rule, shared by `show` and `rm`.
|
|
22
|
+
*
|
|
23
|
+
* It is deliberately the same sentence `--session-id` produces: a caller who
|
|
24
|
+
* meets the rule once should recognise it everywhere, and two spellings of the
|
|
25
|
+
* same refusal read as two different rules.
|
|
26
|
+
*/
|
|
27
|
+
const INVALID_ID_MESSAGE = (id) => `Invalid session id "${id}". Use 1-64 characters matching ` +
|
|
28
|
+
'[A-Za-z0-9][A-Za-z0-9._-]* (no "/", "\\", ".." or a leading dot). ' +
|
|
29
|
+
'Pass a path to `aragon exec --resume` if you mean a file elsewhere.';
|
|
20
30
|
const SUBCOMMANDS = ['list', 'show', 'rm', 'prune', 'path'];
|
|
21
31
|
export function registerSessionsCommand(program) {
|
|
22
32
|
program
|
|
@@ -74,6 +84,10 @@ function runShow(argument, opts) {
|
|
|
74
84
|
process.stderr.write('Usage: aragon sessions show <id> [--json]\n');
|
|
75
85
|
return 2;
|
|
76
86
|
}
|
|
87
|
+
if (!isValidSessionId(argument)) {
|
|
88
|
+
process.stderr.write(`${INVALID_ID_MESSAGE(argument)}\n`);
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
77
91
|
const path = sessionPathFor(argument);
|
|
78
92
|
const session = readSessionFile(path);
|
|
79
93
|
if (!session) {
|
|
@@ -112,6 +126,14 @@ function runRemove(argument, opts) {
|
|
|
112
126
|
process.stderr.write('Refusing to delete without --yes.\n');
|
|
113
127
|
return 2;
|
|
114
128
|
}
|
|
129
|
+
// BEFORE `--yes` IS HONOURED AS PERMISSION TO DELETE. `sessions rm` is
|
|
130
|
+
// documented to accept any file IN THE SESSIONS DIRECTORY, because the user
|
|
131
|
+
// named it - not any file anywhere, which is what an unvalidated id joined to
|
|
132
|
+
// that directory would reach (R-7).
|
|
133
|
+
if (!isValidSessionId(argument)) {
|
|
134
|
+
process.stderr.write(`${INVALID_ID_MESSAGE(argument)}\n`);
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
115
137
|
if (!removeSession(argument)) {
|
|
116
138
|
process.stderr.write(`session_not_found: no session "${argument}".\n`);
|
|
117
139
|
return 2;
|
package/dist/session/persist.js
CHANGED
|
@@ -139,6 +139,25 @@ export function normalizeLoadedEntries(entries) {
|
|
|
139
139
|
detail: 'interrupted (session resumed)',
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
+
// THE FIFTH CLAUSE (context-auto-compaction §5.5 / C-8 / AC-19). A compaction
|
|
143
|
+
// card saved while its summarization was open resumes claiming to be running
|
|
144
|
+
// while nothing is, and — the part that actually costs — it never settles, so
|
|
145
|
+
// `Transcript`'s MONOTONIC boundary never advances past it and every frame for
|
|
146
|
+
// the rest of the session re-renders the whole tail.
|
|
147
|
+
//
|
|
148
|
+
// `applied: false` is also simply TRUE: whatever the call was going to
|
|
149
|
+
// return died with the process, and the history on disk is the PRE-compaction
|
|
150
|
+
// one. A card that claimed otherwise would tell the user their session had
|
|
151
|
+
// been compacted when it had not.
|
|
152
|
+
if (entry.kind === 'compaction' && entry.live) {
|
|
153
|
+
return {
|
|
154
|
+
...entry,
|
|
155
|
+
live: false,
|
|
156
|
+
applied: false,
|
|
157
|
+
mode: 'none',
|
|
158
|
+
reason: 'interrupted (session resumed)',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
142
161
|
return entry;
|
|
143
162
|
});
|
|
144
163
|
}
|
package/dist/session/store.d.ts
CHANGED
|
@@ -54,6 +54,24 @@ export interface SessionListEntry {
|
|
|
54
54
|
}
|
|
55
55
|
export declare function sessionsDir(): string;
|
|
56
56
|
export declare function sessionPathFor(id: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* The id a RESUMED session writes back under.
|
|
59
|
+
*
|
|
60
|
+
* `--resume` accepts `<id|path>`, so the value the caller typed is NOT usable as
|
|
61
|
+
* an id: `--resume ./sessions/hand-saved.json` would otherwise make the path
|
|
62
|
+
* itself the id, and every later `sessionPathFor` / lock call would build a
|
|
63
|
+
* filename out of it - outside the sessions directory whenever the path was
|
|
64
|
+
* relative. That is the same write primitive `--session-id` is validated to
|
|
65
|
+
* prevent, arriving through the flag next to it. `meta.id` is no safer: it comes
|
|
66
|
+
* out of a file, and files are inputs.
|
|
67
|
+
*
|
|
68
|
+
* The order is the one that keeps every legitimate resume landing where the user
|
|
69
|
+
* expects: an exec-written session names itself, a TUI `/save` is named by its
|
|
70
|
+
* file, and anything else gets a fresh id rather than a guess - reported back in
|
|
71
|
+
* `system.init.sessionId` and `result.sessionId`, so the caller is never left
|
|
72
|
+
* wondering which session it just wrote.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resumedSessionId(path: string, saved: SavedSession): string;
|
|
57
75
|
/** Read one file, or `null` for anything unreadable. NEVER THROWS. */
|
|
58
76
|
export declare function readSessionFile(path: string): SavedSession | null;
|
|
59
77
|
/**
|
package/dist/session/store.js
CHANGED
|
@@ -53,8 +53,58 @@ const POINTER_FILE = '.last.json';
|
|
|
53
53
|
export function sessionsDir() {
|
|
54
54
|
return getSessionsDir();
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* THE CHOKE POINT. Every string that becomes a session filename passes here.
|
|
58
|
+
*
|
|
59
|
+
* `isValidSessionId` is applied HERE rather than only at `--session-id`, because
|
|
60
|
+
* that flag is not the only way a caller-supplied string reaches this function:
|
|
61
|
+
* `--resume <id|path>` derives an id from the resumed FILE, `sessions rm` /
|
|
62
|
+
* `sessions show` take one straight from argv, and a session file can carry any
|
|
63
|
+
* `meta.id` at all. Guarding each caller instead would mean the boundary holds
|
|
64
|
+
* only for the callers someone remembered - and `join()` silently normalises
|
|
65
|
+
* `../..` away, so an escape leaves no trace to notice later. Validating at the
|
|
66
|
+
* single point that builds the path is what makes R-7 a property of the module
|
|
67
|
+
* rather than a habit of its callers.
|
|
68
|
+
*
|
|
69
|
+
* THROWS rather than coercing to a "safe" name: a caller who asked for one file
|
|
70
|
+
* and silently got another is a worse failure than a loud one, and every caller
|
|
71
|
+
* that can be handed untrusted input validates first and reports exit 2 itself.
|
|
72
|
+
*/
|
|
73
|
+
function assertSessionId(id) {
|
|
74
|
+
if (!isValidSessionId(id)) {
|
|
75
|
+
throw new Error(`Invalid session id "${id}": expected 1-64 characters matching ` +
|
|
76
|
+
'[A-Za-z0-9][A-Za-z0-9._-]* (no "/", "\\", ".." or a leading dot).');
|
|
77
|
+
}
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
56
80
|
export function sessionPathFor(id) {
|
|
57
|
-
return join(getSessionsDir(), `${id}.json`);
|
|
81
|
+
return join(getSessionsDir(), `${assertSessionId(id)}.json`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The id a RESUMED session writes back under.
|
|
85
|
+
*
|
|
86
|
+
* `--resume` accepts `<id|path>`, so the value the caller typed is NOT usable as
|
|
87
|
+
* an id: `--resume ./sessions/hand-saved.json` would otherwise make the path
|
|
88
|
+
* itself the id, and every later `sessionPathFor` / lock call would build a
|
|
89
|
+
* filename out of it - outside the sessions directory whenever the path was
|
|
90
|
+
* relative. That is the same write primitive `--session-id` is validated to
|
|
91
|
+
* prevent, arriving through the flag next to it. `meta.id` is no safer: it comes
|
|
92
|
+
* out of a file, and files are inputs.
|
|
93
|
+
*
|
|
94
|
+
* The order is the one that keeps every legitimate resume landing where the user
|
|
95
|
+
* expects: an exec-written session names itself, a TUI `/save` is named by its
|
|
96
|
+
* file, and anything else gets a fresh id rather than a guess - reported back in
|
|
97
|
+
* `system.init.sessionId` and `result.sessionId`, so the caller is never left
|
|
98
|
+
* wondering which session it just wrote.
|
|
99
|
+
*/
|
|
100
|
+
export function resumedSessionId(path, saved) {
|
|
101
|
+
const declared = saved.meta?.id;
|
|
102
|
+
if (typeof declared === 'string' && isValidSessionId(declared))
|
|
103
|
+
return declared;
|
|
104
|
+
const base = basenameId(path);
|
|
105
|
+
if (isValidSessionId(base))
|
|
106
|
+
return base;
|
|
107
|
+
return randomUUID();
|
|
58
108
|
}
|
|
59
109
|
function ensureDir() {
|
|
60
110
|
const dir = getSessionsDir();
|
|
@@ -117,6 +167,11 @@ export function writeSession(input) {
|
|
|
117
167
|
return path;
|
|
118
168
|
}
|
|
119
169
|
export function removeSession(id) {
|
|
170
|
+
// An id that cannot name a session in this directory has not "failed to be
|
|
171
|
+
// deleted" - there was never a file of that name to delete. `sessions rm`
|
|
172
|
+
// validates first and reports the reason, so this stays a boolean.
|
|
173
|
+
if (!isValidSessionId(id))
|
|
174
|
+
return false;
|
|
120
175
|
const path = sessionPathFor(id);
|
|
121
176
|
try {
|
|
122
177
|
unlinkSync(path);
|
|
@@ -329,7 +384,9 @@ export function tryAcquireSessionLock(id, opts = {}) {
|
|
|
329
384
|
const now = opts.now ?? Date.now;
|
|
330
385
|
const isAlive = opts.isProcessAlive ?? defaultIsProcessAlive;
|
|
331
386
|
ensureDir();
|
|
332
|
-
|
|
387
|
+
// Same boundary as `sessionPathFor` - a lock is a file in this directory too,
|
|
388
|
+
// and an id that may not name a session may not name its lock either.
|
|
389
|
+
const path = join(getSessionsDir(), `${assertSessionId(id)}.lock`);
|
|
333
390
|
const payload = {
|
|
334
391
|
pid: process.pid,
|
|
335
392
|
host: hostname(),
|
package/dist/team/report.js
CHANGED
|
@@ -143,9 +143,15 @@ function buildSection(run) {
|
|
|
143
143
|
// bytes, and it is the only place the lead learns which of its children were
|
|
144
144
|
// cheap — the fact the whole second cost line is about.
|
|
145
145
|
const tier = run.tier === 'fast' ? ' [fast]' : '';
|
|
146
|
+
// `compacted N` in the same position `retried Nx` uses, and for the same
|
|
147
|
+
// reason (context-auto-compaction-hardening §3.4.4 / W3): this is the ONLY
|
|
148
|
+
// place the lead learns that a child's own history was summarized mid-task, so
|
|
149
|
+
// a summary that reads thinner than the turn count suggests has a stated cause
|
|
150
|
+
// rather than an unexplained one. Absent when it never happened.
|
|
151
|
+
const compacted = run.compactions ? `, compacted ${run.compactions}` : '';
|
|
146
152
|
const head = [
|
|
147
153
|
`### ${run.label}${tier} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
|
|
148
|
-
`${run.turns} turns, ${run.toolCalls} tools${retried}`,
|
|
154
|
+
`${run.turns} turns, ${run.toolCalls} tools${retried}${compacted}`,
|
|
149
155
|
];
|
|
150
156
|
if (run.filesTouched.length > 0)
|
|
151
157
|
head.push(`files: ${run.filesTouched.join(', ')}`);
|
package/dist/team/runtime.d.ts
CHANGED
|
@@ -60,6 +60,16 @@ export interface TeamRuntimeDeps {
|
|
|
60
60
|
* is the pre-feature behaviour.
|
|
61
61
|
*/
|
|
62
62
|
resolveTier?: SubagentDeps['resolveTier'];
|
|
63
|
+
/**
|
|
64
|
+
* Per-child context compaction (context-auto-compaction-hardening §3.4 / W3).
|
|
65
|
+
*
|
|
66
|
+
* Passed straight through to `SubagentDeps`; absent means children get none,
|
|
67
|
+
* which is the pre-hardening behaviour exactly. `AgentController` supplies it
|
|
68
|
+
* from `CompactionWiring.childFactory()`, and the closure returns `undefined`
|
|
69
|
+
* when compaction is unregistered or `compaction.subagents` is false - which is
|
|
70
|
+
* what lets `subagent.ts` spread nothing at all.
|
|
71
|
+
*/
|
|
72
|
+
contextManagerFor?: SubagentDeps['contextManagerFor'];
|
|
63
73
|
}
|
|
64
74
|
export declare class TeamRuntime {
|
|
65
75
|
private readonly deps;
|
package/dist/team/runtime.js
CHANGED
|
@@ -132,6 +132,13 @@ export class TeamRuntime {
|
|
|
132
132
|
...(this.deps.humanQueue ? { humanQueue: this.deps.humanQueue } : {}),
|
|
133
133
|
agentFactory: this.deps.agentFactory ?? defaultSubagentFactory,
|
|
134
134
|
...(this.deps.resolveTier ? { resolveTier: this.deps.resolveTier } : {}),
|
|
135
|
+
// Site two of two, and the omission is silent in the same way the policy
|
|
136
|
+
// one above is: declaring it on `TeamRuntimeDeps` and forgetting this line
|
|
137
|
+
// type-checks and forwards nothing, so every child would quietly run with
|
|
138
|
+
// no compaction while the config key says it has one.
|
|
139
|
+
...(this.deps.contextManagerFor
|
|
140
|
+
? { contextManagerFor: this.deps.contextManagerFor }
|
|
141
|
+
: {}),
|
|
135
142
|
};
|
|
136
143
|
// Hoisted into a factory so a retry can build a fresh child with IDENTICAL
|
|
137
144
|
// wiring (F-4). Replacing the child rather than re-prompting it is the only
|
package/dist/team/subagent.d.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* them (§3.11 / R-6). What a child does NOT get is spelled out below, and each
|
|
11
11
|
* omission has a reason that is a correctness boundary rather than caution.
|
|
12
12
|
*/
|
|
13
|
-
import { type AgentConfig, type AgentEvent, type AgentTool, type ModelRef, type ProviderRegistry, type SkillRegistry, type ThinkingLevel, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
|
|
13
|
+
import { type AgentConfig, type AgentEvent, type AgentTool, type Message, type ModelRef, type ProviderRegistry, type SkillRegistry, type ThinkingLevel, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
|
|
14
14
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
15
|
+
import type { ChildContextManagerProvider } from '../compaction/child.js';
|
|
15
16
|
import type { CliConfig } from '../config/schema.js';
|
|
16
17
|
import type { ToolPermission } from '../exec/permission.js';
|
|
17
18
|
import type { FastTierName } from '../fast/types.js';
|
|
@@ -29,6 +30,24 @@ export interface SubagentAgentLike extends WatchdogPausable {
|
|
|
29
30
|
prompt(text: string): Promise<void>;
|
|
30
31
|
abort(): void;
|
|
31
32
|
subscribe(listener: (event: AgentEvent) => void): () => void;
|
|
33
|
+
/**
|
|
34
|
+
* The child's live history, for its own context manager
|
|
35
|
+
* (context-auto-compaction-hardening §3.4.3 / W3).
|
|
36
|
+
*
|
|
37
|
+
* NARROWED ON PURPOSE. `Agent` satisfies this with its full `AgentState`
|
|
38
|
+
* getter and a stub satisfies it with `{ messages: [] }`. Widening it to
|
|
39
|
+
* `AgentState` would make every test stub fabricate a systemPrompt, a model and
|
|
40
|
+
* a phase to compile, for fields none of them use.
|
|
41
|
+
*
|
|
42
|
+
* A PROPERTY READ, NOT A METHOD CALL, AND THAT IS NOT A STYLE CHOICE: core's
|
|
43
|
+
* `Agent` has NO `getMessages()`. It exposes `get state(): AgentState`;
|
|
44
|
+
* `AgentController.getMessages()` is the CONTROLLER's own one-line forwarder
|
|
45
|
+
* over `this.agent.state.messages`. Adding a method to the engine to serve a
|
|
46
|
+
* host-side convenience would move the core export surface for nothing.
|
|
47
|
+
*/
|
|
48
|
+
readonly state: {
|
|
49
|
+
readonly messages: readonly Message[];
|
|
50
|
+
};
|
|
32
51
|
}
|
|
33
52
|
export type SubagentAgentFactory = (config: AgentConfig) => SubagentAgentLike;
|
|
34
53
|
/** The default factory: a real `Agent` from `@aragon-agent/core`. */
|
|
@@ -91,6 +110,17 @@ export interface SubagentDeps {
|
|
|
91
110
|
ref: ModelRef;
|
|
92
111
|
thinkingLevel: ThinkingLevel;
|
|
93
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* Build this child its own context manager
|
|
115
|
+
* (context-auto-compaction-hardening §3.4.3 / W3).
|
|
116
|
+
*
|
|
117
|
+
* ABSENT means children get none, and `subagent.ts` then spreads NO
|
|
118
|
+
* `contextManager` key at all - so the child's loop gate tests a genuinely
|
|
119
|
+
* undefined field and a `compaction.subagents: false` session is byte-identical
|
|
120
|
+
* to round 1 (AC-H10). `AgentController` supplies it from
|
|
121
|
+
* `CompactionWiring.childFactory()`, which returns `null` when the key is off.
|
|
122
|
+
*/
|
|
123
|
+
contextManagerFor?: ChildContextManagerProvider;
|
|
94
124
|
}
|
|
95
125
|
export interface SubagentHooks {
|
|
96
126
|
/** Coalesced at the source (§5.2); phase transitions always pass. */
|
package/dist/team/subagent.js
CHANGED
|
@@ -183,22 +183,40 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
183
183
|
thinkingLevel: deps.config.thinkingLevel,
|
|
184
184
|
};
|
|
185
185
|
const model = resolved.ref;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
186
|
+
// HOISTED TO A LOCAL, AND THAT IS WHAT MAKES W3 POSSIBLE AT ALL. This was built
|
|
187
|
+
// INLINE inside the `agentFactory({...})` argument, so there was nothing for the
|
|
188
|
+
// child manager's `getSystemPrompt` accessor to close over - and the accessor
|
|
189
|
+
// cannot read it back off the agent, because `SubagentAgentLike` is narrowed to
|
|
190
|
+
// `state.messages` on purpose.
|
|
191
|
+
const systemPrompt = buildSystemPrompt({
|
|
192
|
+
cwd: deps.getCwd(),
|
|
193
|
+
tools,
|
|
194
|
+
skillsBlock: deps.alwaysBlock ?? '',
|
|
195
|
+
agentMode: spec.readOnly ? 'plan' : deps.getMode(),
|
|
196
|
+
// A child has no overlay, so the plan block must describe the headless
|
|
197
|
+
// shape: write the plan as the final message rather than calling tools it
|
|
198
|
+
// does not have.
|
|
199
|
+
planInteractive: false,
|
|
200
|
+
subagentBlock: buildSubagentBlock({
|
|
201
|
+
label: spec.label,
|
|
202
|
+
description: spec.description,
|
|
203
|
+
peers: deps.bus.peersOf(spec.label),
|
|
201
204
|
}),
|
|
205
|
+
});
|
|
206
|
+
// LAZY ACCESSORS OVER `agentRef`, WHICH IS ASSIGNED AFTER THIS RETURNS. A
|
|
207
|
+
// non-lazy read here is `undefined` at construction and the child's estimate
|
|
208
|
+
// branch would silently measure an empty history - CR-1 one module over.
|
|
209
|
+
const contextManager = deps.contextManagerFor?.({
|
|
210
|
+
label: spec.label,
|
|
211
|
+
model,
|
|
212
|
+
getMessages: () => agentRef?.state.messages ?? [],
|
|
213
|
+
getSystemPrompt: () => systemPrompt,
|
|
214
|
+
onCompacted: () => {
|
|
215
|
+
run.compactions = (run.compactions ?? 0) + 1;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
const agent = deps.agentFactory({
|
|
219
|
+
systemPrompt,
|
|
202
220
|
model,
|
|
203
221
|
tools,
|
|
204
222
|
thinkingLevel: resolved.thinkingLevel,
|
|
@@ -217,6 +235,11 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
217
235
|
// explicitly.
|
|
218
236
|
idleTimeout: deps.config.idleTimeoutMs,
|
|
219
237
|
},
|
|
238
|
+
// SPREAD CONDITIONALLY, never `contextManager: deps.contextManagerFor?.(...)`.
|
|
239
|
+
// With the feature off the options bag has no such key, so the loop's gate
|
|
240
|
+
// tests a field that is genuinely `undefined` rather than a property holding
|
|
241
|
+
// one - the identical argument `AgentController` makes for the lead (AC-H10).
|
|
242
|
+
...(contextManager ? { contextManager } : {}),
|
|
220
243
|
});
|
|
221
244
|
agentRef = agent;
|
|
222
245
|
let lastEmit = 0;
|
|
@@ -289,6 +312,10 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
289
312
|
outputTokens: run.usage.outputTokens + event.usage.outputTokens,
|
|
290
313
|
};
|
|
291
314
|
hooks.onUsage(spec.label, event.usage);
|
|
315
|
+
// W1's delta needs the history LENGTH the measurement covered, and the
|
|
316
|
+
// port carries no turn hook. `turn_end` is emitted BEFORE the assistant
|
|
317
|
+
// push, so this is exactly the history the reported `usage` describes.
|
|
318
|
+
contextManager?.onTurnEnd(event.usage, agentRef?.state.messages ?? [], systemPrompt);
|
|
292
319
|
const text = assistantText(event.message);
|
|
293
320
|
// The LAST non-empty assistant text wins, which is exactly the child's
|
|
294
321
|
// final message once the loop stops calling tools. Capturing it here
|