aiden-runtime 4.1.3 → 4.1.5
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/cli/v4/aidenCLI.js +28 -0
- package/dist/cli/v4/callbacks.js +148 -13
- package/dist/cli/v4/chatSession.js +283 -22
- package/dist/cli/v4/defaultSoul.js +143 -4
- package/dist/cli/v4/display/frame.js +234 -0
- package/dist/cli/v4/display/progressBar.js +170 -0
- package/dist/cli/v4/display.js +663 -24
- package/dist/cli/v4/replyRenderer.js +196 -26
- package/dist/cli/v4/skinEngine.js +15 -4
- package/dist/cli/v4/toolPreview.js +78 -19
- package/dist/core/toolRegistry.js +7 -1
- package/dist/core/v4/aidenAgent.js +72 -0
- package/dist/core/v4/loopTrace.js +257 -0
- package/dist/core/v4/promptBuilder.js +2 -1
- package/dist/core/version.js +1 -1
- package/dist/core/webSearch.js +64 -24
- package/dist/providers/v4/anthropicAdapter.js +25 -2
- package/package.json +2 -1
- package/plugins/aiden-plugin-cdp-browser/.granted-permissions.json +8 -0
package/dist/cli/v4/aidenCLI.js
CHANGED
|
@@ -1057,6 +1057,34 @@ async function buildAgentRuntime(cliOpts, opts) {
|
|
|
1057
1057
|
// diagnostics must not break the loop
|
|
1058
1058
|
}
|
|
1059
1059
|
},
|
|
1060
|
+
// v4.1.5 Issue K — phase lifecycle hooks for the activity indicator
|
|
1061
|
+
// verb mutation. Each fires at a specific point in runConversation:
|
|
1062
|
+
// - onMemoryRefreshStart: before memory I/O begins
|
|
1063
|
+
// - onPromptBuilt: after system prompt assembly
|
|
1064
|
+
// - onProviderRequestStart: just before the HTTP request opens
|
|
1065
|
+
// chatSession registers handlers that call `indicator.setVerb()` so
|
|
1066
|
+
// the user sees the model's actual workflow phase during the gap.
|
|
1067
|
+
// All three are forwarded through `callbacks` so chatSession owns
|
|
1068
|
+
// the indicator handle (created per-turn). Defensive try/catch on
|
|
1069
|
+
// each — a misbehaving display sink never blocks the agent loop.
|
|
1070
|
+
onMemoryRefreshStart: () => {
|
|
1071
|
+
try {
|
|
1072
|
+
callbacks.onMemoryRefreshStart?.();
|
|
1073
|
+
}
|
|
1074
|
+
catch { /* defensive */ }
|
|
1075
|
+
},
|
|
1076
|
+
onPromptBuilt: (info) => {
|
|
1077
|
+
try {
|
|
1078
|
+
callbacks.onPromptBuilt?.(info);
|
|
1079
|
+
}
|
|
1080
|
+
catch { /* defensive */ }
|
|
1081
|
+
},
|
|
1082
|
+
onProviderRequestStart: (id) => {
|
|
1083
|
+
try {
|
|
1084
|
+
callbacks.onProviderRequestStart?.(id);
|
|
1085
|
+
}
|
|
1086
|
+
catch { /* defensive */ }
|
|
1087
|
+
},
|
|
1060
1088
|
// Phase 23.4b — feed the agent's Stage-0 intent pre-arm with the
|
|
1061
1089
|
// skill's `required_tools` from its SKILL.md frontmatter. Returns
|
|
1062
1090
|
// null when the skill is unknown / unloaded / empty so the agent
|
package/dist/cli/v4/callbacks.js
CHANGED
|
@@ -63,6 +63,29 @@ class CliCallbacks {
|
|
|
63
63
|
this.toolRows = new Map();
|
|
64
64
|
this.toolStartTimes = new Map();
|
|
65
65
|
this.firstToolFiredThisTurn = false;
|
|
66
|
+
// v4.1.5 Issue K — `firePhaseVerb` is the public entry point for the
|
|
67
|
+
// AidenCLI bridge. AidenAgent fires `onMemoryRefreshStart` etc.,
|
|
68
|
+
// aidenCLI's adapter calls into one of these `onPhase…` shims, each
|
|
69
|
+
// mapping a lifecycle event to a verb string. Defensive try/catch so
|
|
70
|
+
// a misbehaving display sink can't unwind the agent loop.
|
|
71
|
+
this.onMemoryRefreshStart = () => {
|
|
72
|
+
try {
|
|
73
|
+
this.phaseVerbHook?.('refreshing memory');
|
|
74
|
+
}
|
|
75
|
+
catch { /* defensive */ }
|
|
76
|
+
};
|
|
77
|
+
this.onPromptBuilt = (_info) => {
|
|
78
|
+
try {
|
|
79
|
+
this.phaseVerbHook?.('preparing prompt');
|
|
80
|
+
}
|
|
81
|
+
catch { /* defensive */ }
|
|
82
|
+
};
|
|
83
|
+
this.onProviderRequestStart = (_providerId) => {
|
|
84
|
+
try {
|
|
85
|
+
this.phaseVerbHook?.('calling provider');
|
|
86
|
+
}
|
|
87
|
+
catch { /* defensive */ }
|
|
88
|
+
};
|
|
66
89
|
/**
|
|
67
90
|
* Phase 23.5 — bound to AidenAgent.onToolCall. Emits one event row
|
|
68
91
|
* per tool call: prints `[running]` on `before`, mutates the bracket
|
|
@@ -82,6 +105,23 @@ class CliCallbacks {
|
|
|
82
105
|
}
|
|
83
106
|
this.beforeFirstToolHook = undefined;
|
|
84
107
|
}
|
|
108
|
+
// v4.1.4 reply-quality polish — Part 1.6. Pause activity
|
|
109
|
+
// indicator BEFORE the tool row writes so the indicator's line
|
|
110
|
+
// is clean when the row lands. Fires for every tool, not just
|
|
111
|
+
// the first. Defensive try/catch — a misbehaving hook must not
|
|
112
|
+
// block tool dispatch.
|
|
113
|
+
try {
|
|
114
|
+
this.beforeToolHook?.();
|
|
115
|
+
}
|
|
116
|
+
catch { /* defensive */ }
|
|
117
|
+
// v4.1.5+ Path A — fire the loop-trace sink BEFORE row writes.
|
|
118
|
+
// Captures every tool's call.id + name (including hidden ones
|
|
119
|
+
// suppressed from the visible trail) so the trace covers the
|
|
120
|
+
// full agent loop, not just user-visible work.
|
|
121
|
+
try {
|
|
122
|
+
this.toolTraceBeforeHook?.(call.id, call.name);
|
|
123
|
+
}
|
|
124
|
+
catch { /* defensive */ }
|
|
85
125
|
const handle = this.display.toolRow(call.name, call.arguments);
|
|
86
126
|
this.toolRows.set(call.id, handle);
|
|
87
127
|
this.toolStartTimes.set(call.id, Date.now());
|
|
@@ -92,12 +132,57 @@ class CliCallbacks {
|
|
|
92
132
|
const startedAt = this.toolStartTimes.get(call.id);
|
|
93
133
|
this.toolRows.delete(call.id);
|
|
94
134
|
this.toolStartTimes.delete(call.id);
|
|
95
|
-
if (!handle || startedAt === undefined)
|
|
135
|
+
if (!handle || startedAt === undefined) {
|
|
136
|
+
// Even if we lost the handle, the indicator may still need to
|
|
137
|
+
// be re-armed so the next gap shows activity. Tool-name-aware
|
|
138
|
+
// verb selection happens in the hook itself.
|
|
139
|
+
try {
|
|
140
|
+
this.afterEachToolHook?.(call.name);
|
|
141
|
+
}
|
|
142
|
+
catch { /* defensive */ }
|
|
143
|
+
// v4.1.5+ Path A — loop-trace sink fires even when handle was
|
|
144
|
+
// lost (rare; happens if before/after pairing slipped) so the
|
|
145
|
+
// trace never under-counts tool calls.
|
|
146
|
+
try {
|
|
147
|
+
this.toolTraceAfterHook?.(call.id, call.name, call.arguments);
|
|
148
|
+
}
|
|
149
|
+
catch { /* defensive */ }
|
|
96
150
|
return;
|
|
151
|
+
}
|
|
97
152
|
const ms = Date.now() - startedAt;
|
|
98
153
|
const err = result?.error;
|
|
99
154
|
if (typeof err === 'string' && err.includes('URL provenance gate')) {
|
|
100
155
|
handle.blocked();
|
|
156
|
+
// v4.1.5+ Path A — blocked path still needs the trace sink so
|
|
157
|
+
// the URL-provenance failure mode shows up in loop diagnostics.
|
|
158
|
+
try {
|
|
159
|
+
this.toolTraceAfterHook?.(call.id, call.name, call.arguments);
|
|
160
|
+
}
|
|
161
|
+
catch { /* defensive */ }
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// v4.1.4 reply-quality polish — Part 1.6. Helper used by ALL
|
|
165
|
+
// outcome branches below so the activity indicator gets re-armed
|
|
166
|
+
// for the gap that follows this tool (next tool, or final reply).
|
|
167
|
+
// Tool-name-aware verb selection happens in the hook (chatSession
|
|
168
|
+
// wires it through `verbForActivity`).
|
|
169
|
+
const fireAfter = () => {
|
|
170
|
+
try {
|
|
171
|
+
this.afterEachToolHook?.(call.name);
|
|
172
|
+
}
|
|
173
|
+
catch { /* defensive */ }
|
|
174
|
+
// v4.1.5+ Path A — also fire the loop-trace `after` sink so the
|
|
175
|
+
// tracer can compute duration + capture args (hidden-from-trail
|
|
176
|
+
// tools still flow through here, by design — the trace must see
|
|
177
|
+
// them to detect lookup_tool_schema / skill_view loops).
|
|
178
|
+
try {
|
|
179
|
+
this.toolTraceAfterHook?.(call.id, call.name, call.arguments);
|
|
180
|
+
}
|
|
181
|
+
catch { /* defensive */ }
|
|
182
|
+
};
|
|
183
|
+
if (typeof err === 'string' && err.includes('URL provenance gate')) {
|
|
184
|
+
handle.blocked();
|
|
185
|
+
fireAfter();
|
|
101
186
|
return;
|
|
102
187
|
}
|
|
103
188
|
if (err) {
|
|
@@ -111,15 +196,18 @@ class CliCallbacks {
|
|
|
111
196
|
if (result?.capabilityCard) {
|
|
112
197
|
this.display.capabilityCard(result.capabilityCard);
|
|
113
198
|
}
|
|
199
|
+
fireAfter();
|
|
114
200
|
return;
|
|
115
201
|
}
|
|
116
202
|
// v4.1.3-repl-polish: degraded outcome — tool completed but with a
|
|
117
203
|
// partial / best-effort result. Show in trail yellow instead of silent.
|
|
118
204
|
if (result?.degraded) {
|
|
119
205
|
handle.degraded(ms, result.degradedReason);
|
|
206
|
+
fireAfter();
|
|
120
207
|
return;
|
|
121
208
|
}
|
|
122
209
|
handle.ok(ms);
|
|
210
|
+
fireAfter();
|
|
123
211
|
};
|
|
124
212
|
/** ApprovalEngine.callbacks.promptUser */
|
|
125
213
|
this.promptApproval = async (req) => {
|
|
@@ -186,23 +274,29 @@ Reply with ONE word: safe, caution, or dangerous.`;
|
|
|
186
274
|
return false;
|
|
187
275
|
}
|
|
188
276
|
};
|
|
189
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* PlannerGuard sink. v4.1.4 Phase 3b' (Q-Planner): moved to
|
|
279
|
+
* verbose-only. The default `normal` mode previously emitted
|
|
280
|
+
* `[planner] kept N tools (reason)` mid-execution, which collided
|
|
281
|
+
* visually with the activity indicator's single-line paint and
|
|
282
|
+
* with streamed deltas. Users running with the default verbose
|
|
283
|
+
* level should see a clean execution surface — planner-guard
|
|
284
|
+
* decisions are useful for debugging but noise during normal use.
|
|
285
|
+
*
|
|
286
|
+
* `verbose` mode keeps the full breakdown for debugging. `compact`
|
|
287
|
+
* stays silent (unchanged).
|
|
288
|
+
*/
|
|
190
289
|
this.onPlannerGuardDecision = (decision) => {
|
|
191
290
|
if (this.verboseMode === 'compact')
|
|
192
291
|
return;
|
|
193
|
-
if (
|
|
292
|
+
if (this.verboseMode !== 'verbose')
|
|
194
293
|
return;
|
|
195
|
-
if (
|
|
196
|
-
const conf = decision.confidence !== undefined
|
|
197
|
-
? ` (conf ${decision.confidence.toFixed(2)})`
|
|
198
|
-
: '';
|
|
199
|
-
this.display.dim(`[planner] ${decision.reason}${conf}: kept ${decision.selectedTools.length} / dropped ${decision.excludedTools.length}`);
|
|
294
|
+
if (decision.reason === 'no_filter')
|
|
200
295
|
return;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
296
|
+
const conf = decision.confidence !== undefined
|
|
297
|
+
? ` (conf ${decision.confidence.toFixed(2)})`
|
|
298
|
+
: '';
|
|
299
|
+
this.display.dim(`[planner] ${decision.reason}${conf}: kept ${decision.selectedTools.length} / dropped ${decision.excludedTools.length}`);
|
|
206
300
|
};
|
|
207
301
|
/**
|
|
208
302
|
* Phase v4.1-skill-mining — post-turn cue when the miner has
|
|
@@ -280,6 +374,47 @@ Reply with ONE word: safe, caution, or dangerous.`;
|
|
|
280
374
|
this.beforeFirstToolHook = fn;
|
|
281
375
|
this.firstToolFiredThisTurn = false;
|
|
282
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* v4.1.4 reply-quality polish — Part 1.6.
|
|
379
|
+
*
|
|
380
|
+
* Register paired hooks so chatSession can pause the activity
|
|
381
|
+
* indicator while a tool row writes, and resume it (with a fresh
|
|
382
|
+
* verb derived from the just-completed tool) in the gap before the
|
|
383
|
+
* next tool fires or the final reply arrives.
|
|
384
|
+
*
|
|
385
|
+
* Both fire for EVERY tool, not just the first. Either can be
|
|
386
|
+
* omitted independently. Cleared between turns by passing `undefined`.
|
|
387
|
+
*/
|
|
388
|
+
setActivityIndicatorHooks(opts) {
|
|
389
|
+
this.beforeToolHook = opts.beforeTool;
|
|
390
|
+
this.afterEachToolHook = opts.afterEachTool;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* v4.1.5 Issue K — set/clear the phase-verb sink. chatSession
|
|
394
|
+
* registers a closure that captures the per-turn indicator handle
|
|
395
|
+
* and forwards calls to `indicator.setVerb(verb)`. Cleared between
|
|
396
|
+
* turns by passing `undefined`. Optional — non-indicator callers
|
|
397
|
+
* (test harnesses with stub displays) get no-op behaviour.
|
|
398
|
+
*/
|
|
399
|
+
setPhaseVerbHook(fn) {
|
|
400
|
+
this.phaseVerbHook = fn;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* v4.1.5+ Path A — register a per-turn tool-trace sink for the
|
|
404
|
+
* loop-trace logger. `before` fires with the call's id+name BEFORE
|
|
405
|
+
* the row writes; `after` fires post-execution with the same id +
|
|
406
|
+
* the call's args (for skill-name extraction in trace context).
|
|
407
|
+
* Cleared between turns by passing `undefined`.
|
|
408
|
+
*
|
|
409
|
+
* Separate from `setActivityIndicatorHooks` because the activity
|
|
410
|
+
* hook is name-only and fires for visible-trail purposes; this
|
|
411
|
+
* one captures FULL call data including hidden tools (which the
|
|
412
|
+
* trail suppresses via TRAIL_HIDE_TOOLS but the trace must see).
|
|
413
|
+
*/
|
|
414
|
+
setToolTraceHook(opts) {
|
|
415
|
+
this.toolTraceBeforeHook = opts.before;
|
|
416
|
+
this.toolTraceAfterHook = opts.after;
|
|
417
|
+
}
|
|
283
418
|
}
|
|
284
419
|
exports.CliCallbacks = CliCallbacks;
|
|
285
420
|
// Tier-3.1 (v4.1-tier3.1): replaced 🟢/🟡/🔴 emoji badges with
|
|
@@ -69,7 +69,16 @@ exports.renderProgressBar = renderProgressBar;
|
|
|
69
69
|
exports.formatTokens = formatTokens;
|
|
70
70
|
exports.formatDuration = formatDuration;
|
|
71
71
|
exports.renderMemoryConfirmations = renderMemoryConfirmations;
|
|
72
|
+
// v4.1.5+ Path A: env-var-gated loop trace logger. Captures tool-call
|
|
73
|
+
// sequence + system prompt + memory hashes when a turn shows loop
|
|
74
|
+
// symptoms (10+ calls OR 5+ consecutive same-name). Default off via
|
|
75
|
+
// `AIDEN_DEBUG_LOOP=1` env-var. Zero overhead when disabled.
|
|
76
|
+
const loopTrace_1 = require("../../core/v4/loopTrace");
|
|
72
77
|
const display_1 = require("./display");
|
|
78
|
+
// v4.1.4 Part 1.6 — per-turn token progress bar. Fed by `onProgress`
|
|
79
|
+
// events from the streaming adapter; hidden when the adapter doesn't
|
|
80
|
+
// emit progress (honest degradation).
|
|
81
|
+
const progressBar_1 = require("./display/progressBar");
|
|
73
82
|
const uiBuild_1 = require("./uiBuild");
|
|
74
83
|
const sessionSummaryGate_1 = require("./sessionSummaryGate");
|
|
75
84
|
const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
|
|
@@ -414,9 +423,22 @@ class ChatSession {
|
|
|
414
423
|
// Tier-3-essentials: hard-clear the screen on terminal resize so
|
|
415
424
|
// dropdown re-renders + previous prompt frames don't ghost into
|
|
416
425
|
// the new viewport. No-op on non-TTY / MCP serve mode.
|
|
426
|
+
//
|
|
427
|
+
// v4.1.4 reply-quality polish: also drop the per-chunk stream row
|
|
428
|
+
// counter so a mid-stream resize doesn't try to erase rows that
|
|
429
|
+
// the hard-clear already removed. See `resetStreamFrameForResize`
|
|
430
|
+
// in display.ts for the rationale.
|
|
417
431
|
const restoreResizeGuard = this.opts.promptApi
|
|
418
432
|
? () => { }
|
|
419
|
-
: (0, resizeGuard_1.installResizeGuard)(
|
|
433
|
+
: (0, resizeGuard_1.installResizeGuard)({
|
|
434
|
+
onCleared: () => {
|
|
435
|
+
try {
|
|
436
|
+
this.opts.display
|
|
437
|
+
.resetStreamFrameForResize?.();
|
|
438
|
+
}
|
|
439
|
+
catch { /* defensive — never break the resize listener */ }
|
|
440
|
+
},
|
|
441
|
+
});
|
|
420
442
|
try {
|
|
421
443
|
while (iter < max) {
|
|
422
444
|
iter += 1;
|
|
@@ -885,40 +907,226 @@ class ChatSession {
|
|
|
885
907
|
const baseHistory = newHistory.length > 0
|
|
886
908
|
? [...this.history, ...newHistory, userMsg]
|
|
887
909
|
: [...this.history, userMsg];
|
|
888
|
-
// Phase 16c: streaming gated on display.streaming config
|
|
889
|
-
//
|
|
890
|
-
//
|
|
910
|
+
// Phase 16c: streaming gated on display.streaming config.
|
|
911
|
+
// v4.1.4 Part 1.6: PRODUCTION DEFAULT FLIPPED FROM FALSE TO TRUE.
|
|
912
|
+
// Streaming delivers the activity indicator, tool-row live tick,
|
|
913
|
+
// and token progress bar that the user feedback ("after prompt i
|
|
914
|
+
// just see output") was specifically asking for. Users who
|
|
915
|
+
// explicitly set `display.streaming: false` in config still opt
|
|
916
|
+
// out; the change affects only the default for users who never
|
|
917
|
+
// touched the flag.
|
|
918
|
+
//
|
|
919
|
+
// Test-stub fallback (no ConfigManager) stays at `false` so
|
|
920
|
+
// existing tests that depended on the non-streaming code path
|
|
921
|
+
// don't have to be rewritten in this slice — they exercise the
|
|
922
|
+
// batch-call path that production users on Ollama / non-streaming
|
|
923
|
+
// adapters still hit naturally.
|
|
891
924
|
const streamingEnabled = typeof this.opts.config?.getValue === 'function'
|
|
892
|
-
? this.opts.config.getValue('display.streaming',
|
|
925
|
+
? this.opts.config.getValue('display.streaming', true) === true
|
|
893
926
|
: false;
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
927
|
+
// v4.1.4 reply-quality polish — Part 1.6. Activity indicator
|
|
928
|
+
// replaces the prior single-shot spinner. Pause/resume hooks make
|
|
929
|
+
// the indicator cooperate with tool rows: it pauses before each
|
|
930
|
+
// tool row writes and resumes (with a tool-aware verb) in the
|
|
931
|
+
// gap that follows, so the user always sees activity feedback
|
|
932
|
+
// during model-thinking time — not just the pre-first-token gap.
|
|
933
|
+
//
|
|
934
|
+
// Initial verb is "thinking" (pre-tools phase). After each tool
|
|
935
|
+
// completes, `verbForActivity(toolName, 'post-tool')` picks a
|
|
936
|
+
// category-aware verb (reading / searching / analyzing / drafting).
|
|
937
|
+
// When the first stream delta arrives OR the final agentTurn is
|
|
938
|
+
// about to write, the indicator stops permanently.
|
|
939
|
+
const indicator = this.opts.display.activityIndicator('thinking');
|
|
940
|
+
let indicatorStopped = false;
|
|
898
941
|
let streamingActive = false;
|
|
899
|
-
|
|
900
|
-
|
|
942
|
+
// v4.1.5 Issue O — track whether this turn had any tool calls so
|
|
943
|
+
// we can emit a single muted rule between the tool trail and the
|
|
944
|
+
// reply header. Set true when the first tool's `before` phase
|
|
945
|
+
// fires (via the existing beforeFirstToolHook plumbing). Emitted
|
|
946
|
+
// once per turn — `separatorEmitted` gates idempotency against
|
|
947
|
+
// both streaming and non-streaming paths reaching the same hook.
|
|
948
|
+
//
|
|
949
|
+
// v4.1.5 Phase 1d (Q-OBV-b) — multi-tool separator regression:
|
|
950
|
+
// the prior v4.1.5 Phase 1c emission point was the streaming
|
|
951
|
+
// `onFirstDelta` callback, but that fires PER provider call
|
|
952
|
+
// (the agent resets `firstDeltaFired` each callProvider
|
|
953
|
+
// invocation), and on multi-tool turns where the model emits
|
|
954
|
+
// no preamble in early iterations + no preamble in the final
|
|
955
|
+
// reply iteration either, the relative ordering of "first
|
|
956
|
+
// delta" vs "first tool" could leave the flag/idempotency
|
|
957
|
+
// gate in an unexpected state. Definitive fix: tie emission
|
|
958
|
+
// to the FIRST STREAM BYTE LANDING ON SCREEN, which only
|
|
959
|
+
// happens once per turn regardless of how many provider
|
|
960
|
+
// iterations occurred. `firstStreamByteSeen` is the new gate;
|
|
961
|
+
// separator fires from inside `onDelta` BEFORE `streamPartial`
|
|
962
|
+
// writes the agent header.
|
|
963
|
+
let turnHadTools = false;
|
|
964
|
+
let separatorEmitted = false;
|
|
965
|
+
let firstStreamByteSeen = false;
|
|
966
|
+
// v4.1.5+ Path A: per-turn loop tracer (env-var gated, default off).
|
|
967
|
+
// Captures tool-call sequence + assembled system prompt + memory
|
|
968
|
+
// hashes + recent skills when a turn trips loop thresholds. The
|
|
969
|
+
// `onLoopWarning` callback surfaces a one-line dim hint to the
|
|
970
|
+
// user when consecutive-same-tool count crosses 8 — gives them a
|
|
971
|
+
// chance to Ctrl+C before the agent burns more budget.
|
|
972
|
+
const loopTracer = new loopTrace_1.LoopTracer({
|
|
973
|
+
paths: this.opts.paths,
|
|
974
|
+
providerId: this.currentProviderId,
|
|
975
|
+
modelId: this.currentModelId,
|
|
976
|
+
onLoopWarning: (line) => {
|
|
977
|
+
try {
|
|
978
|
+
this.opts.display.dim(line);
|
|
979
|
+
}
|
|
980
|
+
catch { /* defensive */ }
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
if (loopTracer.isEnabled()) {
|
|
984
|
+
loopTracer.setHistory(baseHistory);
|
|
985
|
+
}
|
|
986
|
+
const emitToolReplySeparator = () => {
|
|
987
|
+
if (separatorEmitted || !turnHadTools)
|
|
988
|
+
return;
|
|
989
|
+
separatorEmitted = true;
|
|
990
|
+
// Same chrome pattern as the existing pre-turn rule (line ~1100)
|
|
991
|
+
// and the post-reply rule (line ~1297): two-space indent + the
|
|
992
|
+
// body-width muted rule + newline. The 2-space indent is the
|
|
993
|
+
// legacy convention used by adjacent rules; the v4.1.5 frame
|
|
994
|
+
// gutter (3) is consciously NOT applied here so all three rules
|
|
995
|
+
// in a turn share one left edge.
|
|
996
|
+
this.opts.display.write(` ${this.opts.display.rule()}\n`);
|
|
997
|
+
};
|
|
998
|
+
const stopIndicatorOnce = () => {
|
|
999
|
+
if (indicatorStopped)
|
|
901
1000
|
return;
|
|
902
|
-
|
|
903
|
-
|
|
1001
|
+
indicatorStopped = true;
|
|
1002
|
+
indicator.stop();
|
|
1003
|
+
// Clear the per-turn pause/resume hooks so they don't fire
|
|
1004
|
+
// against a stopped indicator on a subsequent turn. The next
|
|
1005
|
+
// turn re-registers fresh hooks.
|
|
1006
|
+
try {
|
|
1007
|
+
this.opts.callbacks.setActivityIndicatorHooks?.({});
|
|
1008
|
+
}
|
|
1009
|
+
catch { /* defensive */ }
|
|
1010
|
+
// v4.1.5 Issue K — also clear the phase-verb sink so lifecycle
|
|
1011
|
+
// events fired during async cleanup don't try to update a
|
|
1012
|
+
// stopped indicator.
|
|
1013
|
+
try {
|
|
1014
|
+
this.opts.callbacks.setPhaseVerbHook?.(undefined);
|
|
1015
|
+
}
|
|
1016
|
+
catch { /* defensive */ }
|
|
1017
|
+
// v4.1.5+ Path A — clear the loop-trace sink so subsequent
|
|
1018
|
+
// turns don't fire into a stale tracer. Note: this clears the
|
|
1019
|
+
// HOOK, not the tracer's accumulated state — finalize() still
|
|
1020
|
+
// runs at end-of-try below to write the snapshot if thresholds
|
|
1021
|
+
// tripped.
|
|
1022
|
+
try {
|
|
1023
|
+
this.opts.callbacks.setToolTraceHook?.({});
|
|
1024
|
+
}
|
|
1025
|
+
catch { /* defensive */ }
|
|
904
1026
|
};
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
this.
|
|
1027
|
+
// v4.1.5 Issue K — wire the per-turn phase-verb sink. Each
|
|
1028
|
+
// AidenAgent lifecycle event (memory refresh start, prompt built,
|
|
1029
|
+
// provider request start) flows through CliCallbacks and lands
|
|
1030
|
+
// here as a verb string ("refreshing memory" / "preparing prompt"
|
|
1031
|
+
// / "calling provider"). The closure captures the per-turn
|
|
1032
|
+
// indicator handle so verb mutations stay scoped to this turn.
|
|
1033
|
+
this.opts.callbacks.setPhaseVerbHook?.((verb) => {
|
|
1034
|
+
if (indicatorStopped)
|
|
1035
|
+
return;
|
|
1036
|
+
indicator.setVerb(verb);
|
|
1037
|
+
});
|
|
1038
|
+
// v4.1.5+ Path A — wire the loop-trace sink. Fires for EVERY tool
|
|
1039
|
+
// call (including hidden ones) so the trace captures the full
|
|
1040
|
+
// agent loop. Defensive — when AIDEN_DEBUG_LOOP is unset, the
|
|
1041
|
+
// tracer's `startTool`/`endTool` short-circuit immediately.
|
|
1042
|
+
this.opts.callbacks.setToolTraceHook?.({
|
|
1043
|
+
before: (id, name) => loopTracer.startTool(id, name),
|
|
1044
|
+
after: (id, name, args) => loopTracer.endTool(id, name, args),
|
|
1045
|
+
});
|
|
1046
|
+
// Phase 23.5 carried forward: stop the indicator the moment the
|
|
1047
|
+
// first tool row prints — the row itself is the activity surface
|
|
1048
|
+
// during a tool. Part 1.6 then resumes via `afterEachTool` so the
|
|
1049
|
+
// post-tool gap has its own indicator paint.
|
|
1050
|
+
//
|
|
1051
|
+
// v4.1.5 Issue O — also flip `turnHadTools = true` so the
|
|
1052
|
+
// separator emits before the reply header. Single hook captures
|
|
1053
|
+
// "any tool ran this turn" cleanly (it only fires for the FIRST
|
|
1054
|
+
// tool of the turn — subsequent tools don't re-trigger).
|
|
1055
|
+
this.opts.callbacks.setBeforeFirstToolHook?.(() => {
|
|
1056
|
+
turnHadTools = true;
|
|
1057
|
+
stopIndicatorOnce();
|
|
1058
|
+
});
|
|
1059
|
+
// Part 1.6: pause/resume hooks around every tool row. The
|
|
1060
|
+
// `beforeTool` hook fires before EACH tool row writes (not just
|
|
1061
|
+
// the first), so multi-tool sequences also keep the indicator
|
|
1062
|
+
// off the tool-row line. `afterEachTool` resumes with a verb
|
|
1063
|
+
// chosen from the just-completed tool's category — best guess
|
|
1064
|
+
// for "what the model is doing next". `lastToolName` is captured
|
|
1065
|
+
// for tests / observability; the verb decision happens inline.
|
|
1066
|
+
this.opts.callbacks.setActivityIndicatorHooks?.({
|
|
1067
|
+
beforeTool: () => {
|
|
1068
|
+
if (indicatorStopped)
|
|
1069
|
+
return;
|
|
1070
|
+
indicator.pause();
|
|
1071
|
+
// v4.1.4 Part 1.6: hide the progress bar while the tool row
|
|
1072
|
+
// owns the screen. The bar paints below the indicator, so
|
|
1073
|
+
// it'd otherwise sit between the tool row and any subsequent
|
|
1074
|
+
// stream output — visual clutter for tool-heavy turns. The
|
|
1075
|
+
// bar is per-turn, not per-stream-segment; once hidden it
|
|
1076
|
+
// stays hidden until the next turn's bar is created.
|
|
1077
|
+
progressBar?.hide();
|
|
1078
|
+
},
|
|
1079
|
+
afterEachTool: (toolName) => {
|
|
1080
|
+
if (indicatorStopped)
|
|
1081
|
+
return;
|
|
1082
|
+
indicator.resume((0, display_1.verbForActivity)(toolName, 'post-tool'));
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
// v4.1.4 Part 1.6: per-turn progress bar. Created lazily on the
|
|
1086
|
+
// first `onProgress` event from the streaming adapter so the bar
|
|
1087
|
+
// line doesn't paint until there's something to show. Adapters
|
|
1088
|
+
// that don't emit progress (Ollama, most OpenAI-compat) never
|
|
1089
|
+
// trigger creation — honest degradation, no fake estimates.
|
|
1090
|
+
let progressBar = null;
|
|
911
1091
|
try {
|
|
912
1092
|
const result = await this.opts.agent.runConversation(baseHistory, {
|
|
913
1093
|
stream: streamingEnabled,
|
|
914
1094
|
onFirstDelta: streamingEnabled
|
|
915
1095
|
? () => {
|
|
916
|
-
|
|
1096
|
+
stopIndicatorOnce();
|
|
917
1097
|
streamingActive = true;
|
|
1098
|
+
// v4.1.5 Phase 1d (Q-OBV-b) — separator emission MOVED
|
|
1099
|
+
// out of onFirstDelta because that callback fires per
|
|
1100
|
+
// provider-call iteration (firstDeltaFired resets each
|
|
1101
|
+
// callProvider invocation). The separator-emit now
|
|
1102
|
+
// lives in onDelta below, gated by `firstStreamByteSeen`
|
|
1103
|
+
// which only flips once per turn.
|
|
918
1104
|
}
|
|
919
1105
|
: undefined,
|
|
920
1106
|
onDelta: streamingEnabled
|
|
921
1107
|
? (text) => {
|
|
1108
|
+
// v4.1.5 Phase 1d (Q-OBV-b) — definitive separator
|
|
1109
|
+
// emission point. This is the FIRST text byte landing
|
|
1110
|
+
// on screen this turn. Fires the muted rule BEFORE
|
|
1111
|
+
// streamPartial writes the `┃ Aiden` header so the
|
|
1112
|
+
// visual order is:
|
|
1113
|
+
// ┊ tool rows...
|
|
1114
|
+
// ──────────── ← separator
|
|
1115
|
+
// ┃ Aiden
|
|
1116
|
+
// {text}
|
|
1117
|
+
// Idempotent via `firstStreamByteSeen` + the
|
|
1118
|
+
// `separatorEmitted` flag inside emitToolReplySeparator.
|
|
1119
|
+
// No-op when no tool fired (turnHadTools=false).
|
|
1120
|
+
if (!firstStreamByteSeen) {
|
|
1121
|
+
firstStreamByteSeen = true;
|
|
1122
|
+
emitToolReplySeparator();
|
|
1123
|
+
}
|
|
1124
|
+
// v4.1.4 Part 1.6: bar lives ABOVE streamed text. Hide
|
|
1125
|
+
// it before each delta writes so the stream output
|
|
1126
|
+
// doesn't land on the bar's line. The bar repaints on
|
|
1127
|
+
// the next `onProgress` event (which Anthropic emits
|
|
1128
|
+
// frequently enough that the bar stays usefully visible).
|
|
1129
|
+
progressBar?.hide();
|
|
922
1130
|
this.opts.display.streamPartial(text);
|
|
923
1131
|
}
|
|
924
1132
|
: undefined,
|
|
@@ -927,8 +1135,30 @@ class ChatSession {
|
|
|
927
1135
|
this.opts.display.streamToolIndicator(call.name);
|
|
928
1136
|
}
|
|
929
1137
|
: undefined,
|
|
1138
|
+
onProgress: streamingEnabled
|
|
1139
|
+
? (outputTokens, maxTokens) => {
|
|
1140
|
+
if (indicatorStopped === false)
|
|
1141
|
+
return;
|
|
1142
|
+
// Lazy-create on first event. The indicator must already
|
|
1143
|
+
// be stopped (first delta arrived) so the bar paints on
|
|
1144
|
+
// its own line below where the indicator was. If the
|
|
1145
|
+
// indicator is still up, skip — the bar would land on
|
|
1146
|
+
// the indicator line and get clobbered by the next tick.
|
|
1147
|
+
if (!progressBar) {
|
|
1148
|
+
progressBar = (0, progressBar_1.createProgressBar)(process.stdout,
|
|
1149
|
+
// Display exposes its skin via getter on the
|
|
1150
|
+
// implementation; cast to any to avoid widening
|
|
1151
|
+
// the public Display surface for one-shot use.
|
|
1152
|
+
this.opts.display.skin);
|
|
1153
|
+
}
|
|
1154
|
+
progressBar.update(outputTokens, maxTokens);
|
|
1155
|
+
}
|
|
1156
|
+
: undefined,
|
|
930
1157
|
});
|
|
931
|
-
|
|
1158
|
+
stopIndicatorOnce();
|
|
1159
|
+
// Hide the progress bar before any post-stream content
|
|
1160
|
+
// (statusFooter, the next prompt) lands on its line.
|
|
1161
|
+
progressBar?.hide();
|
|
932
1162
|
if (streamingActive)
|
|
933
1163
|
this.opts.display.streamComplete();
|
|
934
1164
|
this.history = result.messages;
|
|
@@ -951,6 +1181,11 @@ class ChatSession {
|
|
|
951
1181
|
// When streaming was active and emitted the final content already,
|
|
952
1182
|
// skip the markdown re-render — we'd otherwise duplicate text.
|
|
953
1183
|
if (result.finalContent && !streamingActive) {
|
|
1184
|
+
// v4.1.5 Issue O — non-streaming reply path. Emit the muted
|
|
1185
|
+
// rule between the tool trail and the agent header before
|
|
1186
|
+
// the one-shot reply lands. Idempotent + tool-gated by
|
|
1187
|
+
// `emitToolReplySeparator`.
|
|
1188
|
+
emitToolReplySeparator();
|
|
954
1189
|
this.opts.display.write(this.opts.display.agentTurn(result.finalContent));
|
|
955
1190
|
}
|
|
956
1191
|
if (this.sessionId) {
|
|
@@ -964,9 +1199,25 @@ class ChatSession {
|
|
|
964
1199
|
// post-turn status footer.
|
|
965
1200
|
this.opts.display.write(` ${this.opts.display.rule()}\n`);
|
|
966
1201
|
this.renderStatusLine();
|
|
1202
|
+
// v4.1.5+ Path A — finalize the loop trace. No-op if the env
|
|
1203
|
+
// var is unset OR if the turn didn't trip any threshold. When
|
|
1204
|
+
// it DOES emit, the snapshot path goes to a dim status line so
|
|
1205
|
+
// the user (and any teammate they're sharing the log with)
|
|
1206
|
+
// knows where to grab the diagnostic file.
|
|
1207
|
+
try {
|
|
1208
|
+
const snapPath = await loopTracer.finalize();
|
|
1209
|
+
if (snapPath) {
|
|
1210
|
+
this.opts.display.dim(`[loop-trace] wrote ${snapPath}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
catch { /* defensive */ }
|
|
967
1214
|
}
|
|
968
1215
|
catch (err) {
|
|
969
|
-
|
|
1216
|
+
stopIndicatorOnce();
|
|
1217
|
+
// v4.1.4 Part 1.6: error path must also hide the progress bar
|
|
1218
|
+
// so it doesn't leak across the boundary into the error chrome
|
|
1219
|
+
// or the next prompt.
|
|
1220
|
+
progressBar?.hide();
|
|
970
1221
|
if (streamingActive)
|
|
971
1222
|
this.opts.display.streamComplete();
|
|
972
1223
|
const msg = err?.message ?? String(err);
|
|
@@ -1008,6 +1259,16 @@ class ChatSession {
|
|
|
1008
1259
|
}
|
|
1009
1260
|
this.setStatusState({ kind: 'ready' });
|
|
1010
1261
|
this.lastTurnElapsedMs = Date.now() - turnStartedAt;
|
|
1262
|
+
// v4.1.5+ Path A — finalize the loop trace on the error path
|
|
1263
|
+
// too. Loop patterns that ended in an error are exactly the
|
|
1264
|
+
// ones most worth capturing for diagnosis.
|
|
1265
|
+
try {
|
|
1266
|
+
const snapPath = await loopTracer.finalize();
|
|
1267
|
+
if (snapPath) {
|
|
1268
|
+
this.opts.display.dim(`[loop-trace] wrote ${snapPath}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
catch { /* defensive */ }
|
|
1011
1272
|
}
|
|
1012
1273
|
}
|
|
1013
1274
|
// ── Startup card (Phase 26.2.4: neofetch-style sectioned) ──────────
|