aiden-runtime 4.5.0 → 4.6.1
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/README.md +17 -2
- package/dist/cli/v4/aidenCLI.js +207 -100
- package/dist/cli/v4/chatSession.js +120 -0
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +8 -0
- package/dist/cli/v4/commands/index.js +21 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +22 -2
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/walkthrough.js +140 -0
- package/dist/cli/v4/daemonAgentBuilder.js +4 -1
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/onboarding/disclaimer.js +162 -0
- package/dist/cli/v4/onboarding/loading.js +208 -0
- package/dist/cli/v4/onboarding/providerPicker.js +126 -0
- package/dist/cli/v4/onboarding/successScreen.js +68 -0
- package/dist/cli/v4/repl/firstRunHint.js +107 -0
- package/dist/cli/v4/setupWizard.js +201 -31
- package/dist/core/v4/aidenAgent.js +219 -1
- package/dist/core/v4/daemon/bootstrap.js +47 -0
- package/dist/core/v4/daemon/db/migrations.js +66 -0
- package/dist/core/v4/daemon/runStore.js +33 -3
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/providers/modelFetch.js +179 -0
- package/dist/core/v4/providers/probe.js +275 -0
- package/dist/core/v4/runtimeToggles.js +30 -3
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/toolRegistry.js +19 -3
- package/dist/core/v4/ui/banner.js +133 -0
- package/dist/core/v4/ui/theme.js +164 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/index.js +17 -3
- package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +53 -1
- package/dist/tools/v4/ui/_uiSmokeTool.js +60 -0
- package/package.json +7 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/ui/theme.ts — ONB1 (v4.7 onboarding rework).
|
|
10
|
+
*
|
|
11
|
+
* Self-contained theme module for the redesigned first-run experience.
|
|
12
|
+
* Lives alongside, NOT instead of, the existing skin engine
|
|
13
|
+
* (cli/v4/skinEngine.ts). The skin engine drives the REPL / boot card
|
|
14
|
+
* / every post-onboarding surface; this theme drives only the
|
|
15
|
+
* onboarding screens (slices 1–10 of dispatch ONB1).
|
|
16
|
+
*
|
|
17
|
+
* Why a separate module:
|
|
18
|
+
* - Onboarding palette is specified to a different muted/text spec
|
|
19
|
+
* than the existing skin (e.g. cool-grey #71717A vs warm-tan
|
|
20
|
+
* #B8A89A). Swapping the skin would re-paint every chat turn the
|
|
21
|
+
* user sees afterwards, surprising the eye on the *second* boot.
|
|
22
|
+
* - Onboarding is a single-shot surface — no per-user customisation,
|
|
23
|
+
* no YAML loader, no `monochrome`/`light` variants needed beyond
|
|
24
|
+
* graceful colour-depth degradation.
|
|
25
|
+
*
|
|
26
|
+
* Truecolor → 256 → 16 detection runs once at module load and is
|
|
27
|
+
* cached. Set `AIDEN_FORCE_COLOR_DEPTH=truecolor|256|16|none` to
|
|
28
|
+
* override (smoke tests rely on this).
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.SEP_LIGHT = exports.SEP_HEAVY = exports.dim = exports.italic = exports.bold = exports.c = exports.PALETTE = void 0;
|
|
32
|
+
exports.getColorDepth = getColorDepth;
|
|
33
|
+
exports.paint = paint;
|
|
34
|
+
exports.separator = separator;
|
|
35
|
+
exports.termWidth = termWidth;
|
|
36
|
+
/**
|
|
37
|
+
* The 8-colour onboarding palette. Hex strings are the source of
|
|
38
|
+
* truth; the emit functions below convert per detected depth.
|
|
39
|
+
*/
|
|
40
|
+
exports.PALETTE = {
|
|
41
|
+
primary: '#FF6B35', // brand orange — Aiden hero
|
|
42
|
+
accent: '#FFB088', // light orange — highlights
|
|
43
|
+
success: '#4ADE80', // green checkmarks
|
|
44
|
+
warning: '#FBBF24', // amber warnings
|
|
45
|
+
error: '#EF4444', // red errors
|
|
46
|
+
text: '#F5F5F5', // bright white — headers/titles
|
|
47
|
+
muted: '#71717A', // dim grey — secondary text/hints
|
|
48
|
+
rule: '#27272A', // dark grey — separators
|
|
49
|
+
};
|
|
50
|
+
/** Parse a `#RRGGBB` hex string into [r,g,b]. */
|
|
51
|
+
function hexToRgb(hex) {
|
|
52
|
+
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
|
53
|
+
if (!m)
|
|
54
|
+
return [255, 255, 255];
|
|
55
|
+
const n = parseInt(m[1], 16);
|
|
56
|
+
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Map a 24-bit RGB triple to the closest xterm-256 colour index.
|
|
60
|
+
* Uses the standard 6×6×6 cube + grey-ramp approximation.
|
|
61
|
+
*/
|
|
62
|
+
function rgbTo256(r, g, b) {
|
|
63
|
+
// Grey-ramp fast path: when r==g==b within 8, prefer the 24-step ramp.
|
|
64
|
+
if (Math.abs(r - g) < 8 && Math.abs(g - b) < 8) {
|
|
65
|
+
if (r < 8)
|
|
66
|
+
return 16;
|
|
67
|
+
if (r > 248)
|
|
68
|
+
return 231;
|
|
69
|
+
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
70
|
+
}
|
|
71
|
+
const q = (v) => Math.round(v / 51);
|
|
72
|
+
return 16 + 36 * q(r) + 6 * q(g) + q(b);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Map a 24-bit RGB triple to a low-fidelity 16-colour ANSI code
|
|
76
|
+
* (30–37 / 90–97). Picks the closest of the 16 standard slots.
|
|
77
|
+
*/
|
|
78
|
+
function rgbTo16(r, g, b) {
|
|
79
|
+
const STD = [
|
|
80
|
+
[30, 0, 0, 0], [31, 205, 49, 49], [32, 13, 188, 121], [33, 229, 229, 16],
|
|
81
|
+
[34, 36, 114, 200], [35, 188, 63, 188], [36, 17, 168, 205], [37, 229, 229, 229],
|
|
82
|
+
[90, 102, 102, 102], [91, 241, 76, 76], [92, 35, 209, 139], [93, 245, 245, 67],
|
|
83
|
+
[94, 59, 142, 234], [95, 214, 112, 214], [96, 41, 184, 219], [97, 229, 229, 229],
|
|
84
|
+
];
|
|
85
|
+
let best = STD[0];
|
|
86
|
+
let bestDist = Infinity;
|
|
87
|
+
for (const cand of STD) {
|
|
88
|
+
const [, cr, cg, cb] = cand;
|
|
89
|
+
const d = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
|
|
90
|
+
if (d < bestDist) {
|
|
91
|
+
bestDist = d;
|
|
92
|
+
best = cand;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return best[0];
|
|
96
|
+
}
|
|
97
|
+
/** Detect the terminal's effective colour depth. Cached at module load. */
|
|
98
|
+
function detectColorDepth() {
|
|
99
|
+
const forced = process.env.AIDEN_FORCE_COLOR_DEPTH?.toLowerCase();
|
|
100
|
+
if (forced === 'truecolor' || forced === '256' || forced === '16' || forced === 'none') {
|
|
101
|
+
return forced;
|
|
102
|
+
}
|
|
103
|
+
if (process.env.NO_COLOR && process.env.NO_COLOR !== '')
|
|
104
|
+
return 'none';
|
|
105
|
+
if (!process.stdout.isTTY)
|
|
106
|
+
return 'none';
|
|
107
|
+
const ct = (process.env.COLORTERM ?? '').toLowerCase();
|
|
108
|
+
if (ct === 'truecolor' || ct === '24bit')
|
|
109
|
+
return 'truecolor';
|
|
110
|
+
const term = (process.env.TERM ?? '').toLowerCase();
|
|
111
|
+
if (term.includes('256'))
|
|
112
|
+
return '256';
|
|
113
|
+
if (term === 'dumb' || term === '')
|
|
114
|
+
return 'none';
|
|
115
|
+
return '16';
|
|
116
|
+
}
|
|
117
|
+
const COLOR_DEPTH = detectColorDepth();
|
|
118
|
+
/** Public: report the depth (smoke tests + diagnostics). */
|
|
119
|
+
function getColorDepth() { return COLOR_DEPTH; }
|
|
120
|
+
/** Wrap `text` in the SGR sequence for `kind`, degrading per depth. */
|
|
121
|
+
function paint(text, kind) {
|
|
122
|
+
if (COLOR_DEPTH === 'none')
|
|
123
|
+
return text;
|
|
124
|
+
const [r, g, b] = hexToRgb(exports.PALETTE[kind]);
|
|
125
|
+
if (COLOR_DEPTH === 'truecolor')
|
|
126
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
|
|
127
|
+
if (COLOR_DEPTH === '256')
|
|
128
|
+
return `\x1b[38;5;${rgbTo256(r, g, b)}m${text}\x1b[39m`;
|
|
129
|
+
return `\x1b[${rgbTo16(r, g, b)}m${text}\x1b[39m`;
|
|
130
|
+
}
|
|
131
|
+
/** Convenience helpers — one per palette key. */
|
|
132
|
+
exports.c = {
|
|
133
|
+
primary: (s) => paint(s, 'primary'),
|
|
134
|
+
accent: (s) => paint(s, 'accent'),
|
|
135
|
+
success: (s) => paint(s, 'success'),
|
|
136
|
+
warning: (s) => paint(s, 'warning'),
|
|
137
|
+
error: (s) => paint(s, 'error'),
|
|
138
|
+
text: (s) => paint(s, 'text'),
|
|
139
|
+
muted: (s) => paint(s, 'muted'),
|
|
140
|
+
rule: (s) => paint(s, 'rule'),
|
|
141
|
+
};
|
|
142
|
+
/** SGR helpers for emphasis. Italic gracefully degrades when unsupported. */
|
|
143
|
+
const bold = (s) => (COLOR_DEPTH === 'none' ? s : `\x1b[1m${s}\x1b[22m`);
|
|
144
|
+
exports.bold = bold;
|
|
145
|
+
const italic = (s) => (COLOR_DEPTH === 'none' ? s : `\x1b[3m${s}\x1b[23m`);
|
|
146
|
+
exports.italic = italic;
|
|
147
|
+
const dim = (s) => (COLOR_DEPTH === 'none' ? s : `\x1b[2m${s}\x1b[22m`);
|
|
148
|
+
exports.dim = dim;
|
|
149
|
+
/**
|
|
150
|
+
* Common ornaments — single source so onboarding screens share rhythm.
|
|
151
|
+
*/
|
|
152
|
+
exports.SEP_HEAVY = '━';
|
|
153
|
+
exports.SEP_LIGHT = '─';
|
|
154
|
+
/** Render a full-width separator in RULE colour, optionally heavy. */
|
|
155
|
+
function separator(width, heavy = true) {
|
|
156
|
+
const w = Math.max(8, Math.min(width, 100));
|
|
157
|
+
const ch = heavy ? exports.SEP_HEAVY : exports.SEP_LIGHT;
|
|
158
|
+
return exports.c.rule(ch.repeat(w));
|
|
159
|
+
}
|
|
160
|
+
/** Effective terminal width clamped to a sane band. */
|
|
161
|
+
function termWidth() {
|
|
162
|
+
const raw = process.stdout.columns ?? 80;
|
|
163
|
+
return Math.max(40, Math.min(raw, 100));
|
|
164
|
+
}
|
package/dist/core/version.js
CHANGED
|
@@ -87,10 +87,39 @@ const RULES = [
|
|
|
87
87
|
toolsets: ['execute'],
|
|
88
88
|
},
|
|
89
89
|
// Process registry
|
|
90
|
+
//
|
|
91
|
+
// Note: the bare word "spawn" appears in this rule's keyword list
|
|
92
|
+
// (legacy — predates v4.6 sub-agents). The dedicated 'subagent'
|
|
93
|
+
// rule below ALSO matches "spawn" via a tighter delegation
|
|
94
|
+
// vocabulary, so a message like "spawn a background server" hits
|
|
95
|
+
// BOTH rules and adds both toolsets — UNION semantics make this
|
|
96
|
+
// additive, not conflicting.
|
|
90
97
|
{
|
|
91
98
|
keywords: /\b(process|background|long.?running|server|spawn|kill|daemon)\b/i,
|
|
92
99
|
toolsets: ['process'],
|
|
93
100
|
},
|
|
101
|
+
// v4.6 Phase 1 — sub-agent delegation surface (spawn_sub_agent +
|
|
102
|
+
// subagent_fanout). Both tools live in toolset `'subagent'`, which
|
|
103
|
+
// no pre-v4.6 rule mapped to — so PlannerGuard's per-turn narrowing
|
|
104
|
+
// silently stripped them from the model's catalog whenever any
|
|
105
|
+
// other rule fired. The model could see them via lookup_tool_schema
|
|
106
|
+
// but failed to actually invoke them because the provider tool list
|
|
107
|
+
// (post-narrow) didn't include them — see Dispatch 2H diagnostic.
|
|
108
|
+
//
|
|
109
|
+
// Regex notes:
|
|
110
|
+
// - `spawn_sub_agent` and `subagent_fanout` literals are listed
|
|
111
|
+
// explicitly because `\bspawn\b` does NOT match within
|
|
112
|
+
// `spawn_sub_agent` (underscore is a word char in JS regex,
|
|
113
|
+
// so there's no word boundary between `n` and `_`). Users who
|
|
114
|
+
// name the tool directly hit the literal arm.
|
|
115
|
+
// - The free-form vocabulary arm (`spawn`, `delegate`, etc.)
|
|
116
|
+
// catches natural-language delegation intent. UNION semantics
|
|
117
|
+
// with other rules let "spawn a child to read files" surface
|
|
118
|
+
// both 'subagent' AND 'files'.
|
|
119
|
+
{
|
|
120
|
+
keywords: /\b(spawn_sub_agent|subagent_fanout|spawn|subagent|sub.?agent|delegate|fanout|fan.?out|child.?agent|parallel|isolated)\b/i,
|
|
121
|
+
toolsets: ['subagent'],
|
|
122
|
+
},
|
|
94
123
|
// Media playback control (v4.1.4-media)
|
|
95
124
|
//
|
|
96
125
|
// Without this, intents like "list media sessions" matched the
|
|
@@ -92,14 +92,14 @@ class AnthropicAdapter {
|
|
|
92
92
|
// ── Public: non-streaming ────────────────────────────────────────────────
|
|
93
93
|
async call(input) {
|
|
94
94
|
const body = this.buildBody(input, /* streaming */ false);
|
|
95
|
-
const reply = await this.dispatch(body, /* streaming */ false);
|
|
95
|
+
const reply = await this.dispatch(body, /* streaming */ false, input.signal);
|
|
96
96
|
const json = (await reply.json());
|
|
97
97
|
return decodeResponse(json);
|
|
98
98
|
}
|
|
99
99
|
// ── Public: streaming ────────────────────────────────────────────────────
|
|
100
100
|
async *callStream(input) {
|
|
101
101
|
const body = this.buildBody(input, /* streaming */ true);
|
|
102
|
-
const reply = await this.dispatch(body, /* streaming */ true);
|
|
102
|
+
const reply = await this.dispatch(body, /* streaming */ true, input.signal);
|
|
103
103
|
if (!reply.body) {
|
|
104
104
|
// Server promised SSE but gave us nothing — fall through to a synthetic
|
|
105
105
|
// empty done event so the agent loop terminates rather than hangs.
|
|
@@ -163,7 +163,7 @@ class AnthropicAdapter {
|
|
|
163
163
|
// beta flags, or per-deployment routing tags without forking the adapter.
|
|
164
164
|
return { ...headers, ...this.extraHeaders };
|
|
165
165
|
}
|
|
166
|
-
async dispatch(body, streaming) {
|
|
166
|
+
async dispatch(body, streaming, externalSignal) {
|
|
167
167
|
// Resolved once per process via the userAgent module's cache, so paying
|
|
168
168
|
// for the version detection here is cheap on every retry/turn.
|
|
169
169
|
const userAgent = await (0, userAgent_1.getClaudeCliUserAgent)();
|
|
@@ -174,6 +174,22 @@ class AnthropicAdapter {
|
|
|
174
174
|
for (let attempt = 0; attempt < totalTries; attempt++) {
|
|
175
175
|
const controller = new AbortController();
|
|
176
176
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
177
|
+
// v4.6 prep — forward an external AbortSignal into this attempt's
|
|
178
|
+
// internal controller so a parent agent that aborts mid-flight
|
|
179
|
+
// cancels the in-flight fetch. External aborts surface as a raw
|
|
180
|
+
// AbortError (NOT ProviderTimeoutError) so AidenAgent can route
|
|
181
|
+
// them as `finishReason: 'interrupted'` instead of treating them
|
|
182
|
+
// as a retryable timeout.
|
|
183
|
+
let externalAbortHandler = null;
|
|
184
|
+
if (externalSignal) {
|
|
185
|
+
if (externalSignal.aborted) {
|
|
186
|
+
controller.abort();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
externalAbortHandler = () => controller.abort();
|
|
190
|
+
externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
177
193
|
let response;
|
|
178
194
|
try {
|
|
179
195
|
response = await fetch(this.endpoint, {
|
|
@@ -185,7 +201,16 @@ class AnthropicAdapter {
|
|
|
185
201
|
}
|
|
186
202
|
catch (err) {
|
|
187
203
|
clearTimeout(timer);
|
|
204
|
+
if (externalAbortHandler && externalSignal) {
|
|
205
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
206
|
+
}
|
|
188
207
|
if (err?.name === 'AbortError') {
|
|
208
|
+
// v4.6 prep — external abort takes priority over internal
|
|
209
|
+
// timeout. Surface the raw AbortError immediately (no retry)
|
|
210
|
+
// so AidenAgent's catch routes it as 'interrupted'.
|
|
211
|
+
if (externalSignal?.aborted) {
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
189
214
|
// Treat timeout as retryable; only surface ProviderTimeoutError if
|
|
190
215
|
// we've burned the last attempt.
|
|
191
216
|
lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
@@ -200,6 +225,9 @@ class AnthropicAdapter {
|
|
|
200
225
|
throw lastErr;
|
|
201
226
|
}
|
|
202
227
|
clearTimeout(timer);
|
|
228
|
+
if (externalAbortHandler && externalSignal) {
|
|
229
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
230
|
+
}
|
|
203
231
|
if (response.ok)
|
|
204
232
|
return response;
|
|
205
233
|
// Phase 25.1.5d diagnostic: gated dump of request + response so we
|
|
@@ -73,7 +73,7 @@ class ChatCompletionsAdapter {
|
|
|
73
73
|
// ── Non-streaming ────────────────────────────────────────────────────
|
|
74
74
|
async call(input) {
|
|
75
75
|
const body = this.buildBody(input, /* streaming */ false);
|
|
76
|
-
const reply = await this.dispatch(body, /* streaming */ false);
|
|
76
|
+
const reply = await this.dispatch(body, /* streaming */ false, input.signal);
|
|
77
77
|
const text = await reply.text();
|
|
78
78
|
let parsed;
|
|
79
79
|
try {
|
|
@@ -91,7 +91,7 @@ class ChatCompletionsAdapter {
|
|
|
91
91
|
// ── Streaming ────────────────────────────────────────────────────────
|
|
92
92
|
async *callStream(input) {
|
|
93
93
|
const body = this.buildBody(input, /* streaming */ true);
|
|
94
|
-
const reply = await this.dispatch(body, /* streaming */ true);
|
|
94
|
+
const reply = await this.dispatch(body, /* streaming */ true, input.signal);
|
|
95
95
|
if (!reply.body) {
|
|
96
96
|
yield {
|
|
97
97
|
type: 'done',
|
|
@@ -150,7 +150,7 @@ class ChatCompletionsAdapter {
|
|
|
150
150
|
headers['Accept'] = 'text/event-stream';
|
|
151
151
|
return { ...headers, ...this.extraHeaders };
|
|
152
152
|
}
|
|
153
|
-
async dispatch(body, streaming) {
|
|
153
|
+
async dispatch(body, streaming, externalSignal) {
|
|
154
154
|
const headers = this.buildHeaders(streaming);
|
|
155
155
|
const serialised = JSON.stringify(body);
|
|
156
156
|
const totalTries = this.maxRetries + 1;
|
|
@@ -158,6 +158,19 @@ class ChatCompletionsAdapter {
|
|
|
158
158
|
for (let attempt = 0; attempt < totalTries; attempt++) {
|
|
159
159
|
const controller = new AbortController();
|
|
160
160
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
161
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
162
|
+
// External aborts surface as raw AbortError so AidenAgent routes
|
|
163
|
+
// them as 'interrupted' rather than retrying as ProviderTimeoutError.
|
|
164
|
+
let externalAbortHandler = null;
|
|
165
|
+
if (externalSignal) {
|
|
166
|
+
if (externalSignal.aborted) {
|
|
167
|
+
controller.abort();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
externalAbortHandler = () => controller.abort();
|
|
171
|
+
externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
161
174
|
let response;
|
|
162
175
|
try {
|
|
163
176
|
response = await fetch(this.endpoint, {
|
|
@@ -169,7 +182,14 @@ class ChatCompletionsAdapter {
|
|
|
169
182
|
}
|
|
170
183
|
catch (err) {
|
|
171
184
|
clearTimeout(timer);
|
|
185
|
+
if (externalAbortHandler && externalSignal) {
|
|
186
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
187
|
+
}
|
|
172
188
|
if (err?.name === 'AbortError') {
|
|
189
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
190
|
+
if (externalSignal?.aborted) {
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
173
193
|
lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
174
194
|
}
|
|
175
195
|
else {
|
|
@@ -182,6 +202,9 @@ class ChatCompletionsAdapter {
|
|
|
182
202
|
throw lastErr;
|
|
183
203
|
}
|
|
184
204
|
clearTimeout(timer);
|
|
205
|
+
if (externalAbortHandler && externalSignal) {
|
|
206
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
207
|
+
}
|
|
185
208
|
if (response.ok)
|
|
186
209
|
return response;
|
|
187
210
|
const status = response.status;
|
|
@@ -104,7 +104,7 @@ class CodexResponsesAdapter {
|
|
|
104
104
|
// ── Public: non-streaming entry ─────────────────────────────────────
|
|
105
105
|
async call(input) {
|
|
106
106
|
const body = this.buildBody(input);
|
|
107
|
-
const reply = await this.dispatch(body);
|
|
107
|
+
const reply = await this.dispatch(body, input.signal);
|
|
108
108
|
// Codex backend always streams; aggregate the SSE frames into the
|
|
109
109
|
// same shape the JSON path returns. Plain api.openai.com path returns
|
|
110
110
|
// JSON directly.
|
|
@@ -175,7 +175,7 @@ class CodexResponsesAdapter {
|
|
|
175
175
|
}
|
|
176
176
|
return { ...headers, ...this.extraHeaders };
|
|
177
177
|
}
|
|
178
|
-
async dispatch(body) {
|
|
178
|
+
async dispatch(body, externalSignal) {
|
|
179
179
|
const headers = this.buildHeaders();
|
|
180
180
|
const serialised = JSON.stringify(body);
|
|
181
181
|
const totalTries = this.maxRetries + 1;
|
|
@@ -183,6 +183,19 @@ class CodexResponsesAdapter {
|
|
|
183
183
|
for (let attempt = 0; attempt < totalTries; attempt++) {
|
|
184
184
|
const controller = new AbortController();
|
|
185
185
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
186
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
187
|
+
// External aborts surface as raw AbortError so AidenAgent routes
|
|
188
|
+
// them as 'interrupted' rather than retrying as ProviderTimeoutError.
|
|
189
|
+
let externalAbortHandler = null;
|
|
190
|
+
if (externalSignal) {
|
|
191
|
+
if (externalSignal.aborted) {
|
|
192
|
+
controller.abort();
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
externalAbortHandler = () => controller.abort();
|
|
196
|
+
externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
186
199
|
let response;
|
|
187
200
|
try {
|
|
188
201
|
response = await fetch(this.endpoint, {
|
|
@@ -194,7 +207,14 @@ class CodexResponsesAdapter {
|
|
|
194
207
|
}
|
|
195
208
|
catch (err) {
|
|
196
209
|
clearTimeout(timer);
|
|
210
|
+
if (externalAbortHandler && externalSignal) {
|
|
211
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
212
|
+
}
|
|
197
213
|
if (err?.name === 'AbortError') {
|
|
214
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
215
|
+
if (externalSignal?.aborted) {
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
198
218
|
lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
199
219
|
}
|
|
200
220
|
else {
|
|
@@ -207,6 +227,9 @@ class CodexResponsesAdapter {
|
|
|
207
227
|
throw lastErr;
|
|
208
228
|
}
|
|
209
229
|
clearTimeout(timer);
|
|
230
|
+
if (externalAbortHandler && externalSignal) {
|
|
231
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
232
|
+
}
|
|
210
233
|
if (response.ok)
|
|
211
234
|
return response;
|
|
212
235
|
const status = response.status;
|
|
@@ -63,7 +63,7 @@ class OllamaPromptToolsAdapter {
|
|
|
63
63
|
let lastError = null;
|
|
64
64
|
for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {
|
|
65
65
|
try {
|
|
66
|
-
const response = await this.fetchWithTimeout(url, headers, body);
|
|
66
|
+
const response = await this.fetchWithTimeout(url, headers, body, input.signal);
|
|
67
67
|
if (response.ok) {
|
|
68
68
|
const json = (await response.json());
|
|
69
69
|
return this.parseResponse(json);
|
|
@@ -134,6 +134,17 @@ class OllamaPromptToolsAdapter {
|
|
|
134
134
|
const headers = { 'Content-Type': 'application/json' };
|
|
135
135
|
const controller = new AbortController();
|
|
136
136
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
137
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
138
|
+
let externalAbortHandler = null;
|
|
139
|
+
if (input.signal) {
|
|
140
|
+
if (input.signal.aborted) {
|
|
141
|
+
controller.abort();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
externalAbortHandler = () => controller.abort();
|
|
145
|
+
input.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
137
148
|
let response;
|
|
138
149
|
try {
|
|
139
150
|
response = await fetch(url, {
|
|
@@ -145,13 +156,23 @@ class OllamaPromptToolsAdapter {
|
|
|
145
156
|
}
|
|
146
157
|
catch (err) {
|
|
147
158
|
clearTimeout(timer);
|
|
159
|
+
if (externalAbortHandler && input.signal) {
|
|
160
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
161
|
+
}
|
|
148
162
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
163
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
164
|
+
if (input.signal?.aborted) {
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
149
167
|
throw new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
150
168
|
}
|
|
151
169
|
throw new errors_1.ProviderError(`Ollama not reachable at ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`, this.providerName, undefined, err, true);
|
|
152
170
|
}
|
|
153
171
|
if (!response.ok) {
|
|
154
172
|
clearTimeout(timer);
|
|
173
|
+
if (externalAbortHandler && input.signal) {
|
|
174
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
175
|
+
}
|
|
155
176
|
const status = response.status;
|
|
156
177
|
const rawText = await this.safeReadText(response);
|
|
157
178
|
// Phase v4.1.1-oauth-fix Phase 5: composeMessage handles body
|
|
@@ -160,8 +181,15 @@ class OllamaPromptToolsAdapter {
|
|
|
160
181
|
}
|
|
161
182
|
if (!response.body) {
|
|
162
183
|
clearTimeout(timer);
|
|
184
|
+
if (externalAbortHandler && input.signal) {
|
|
185
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
186
|
+
}
|
|
163
187
|
throw new errors_1.ProviderError(`Provider ${this.providerName} returned an empty stream body`, this.providerName);
|
|
164
188
|
}
|
|
189
|
+
// Response is good; the stream consumer will run for a while. The
|
|
190
|
+
// controller stays armed (with `externalSignal` still listening) so
|
|
191
|
+
// that mid-stream aborts cancel reader.read() via fetch's signal.
|
|
192
|
+
// Listener cleanup happens in the stream-consumer try/finally below.
|
|
165
193
|
const reader = response.body.getReader();
|
|
166
194
|
const decoder = new TextDecoder('utf-8');
|
|
167
195
|
let lineBuffer = '';
|
|
@@ -223,10 +251,18 @@ class OllamaPromptToolsAdapter {
|
|
|
223
251
|
}
|
|
224
252
|
catch (err) {
|
|
225
253
|
clearTimeout(timer);
|
|
254
|
+
// v4.6 prep — external abort during mid-stream read surfaces as
|
|
255
|
+
// AbortError; re-throw so AidenAgent routes it as 'interrupted'.
|
|
256
|
+
if (err instanceof Error && err.name === 'AbortError' && input.signal?.aborted) {
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
226
259
|
throw new errors_1.ProviderError(`Provider ${this.providerName} stream interrupted: ${err instanceof Error ? err.message : String(err)}`, this.providerName, undefined, err, true);
|
|
227
260
|
}
|
|
228
261
|
finally {
|
|
229
262
|
clearTimeout(timer);
|
|
263
|
+
if (externalAbortHandler && input.signal) {
|
|
264
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
265
|
+
}
|
|
230
266
|
try {
|
|
231
267
|
reader.releaseLock();
|
|
232
268
|
}
|
|
@@ -422,9 +458,20 @@ class OllamaPromptToolsAdapter {
|
|
|
422
458
|
const textBefore = firstTagIdx >= 0 ? text.slice(0, firstTagIdx).trim() : text;
|
|
423
459
|
return { textBefore, toolCalls };
|
|
424
460
|
}
|
|
425
|
-
async fetchWithTimeout(url, headers, body) {
|
|
461
|
+
async fetchWithTimeout(url, headers, body, externalSignal) {
|
|
426
462
|
const controller = new AbortController();
|
|
427
463
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
464
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
465
|
+
let externalAbortHandler = null;
|
|
466
|
+
if (externalSignal) {
|
|
467
|
+
if (externalSignal.aborted) {
|
|
468
|
+
controller.abort();
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
externalAbortHandler = () => controller.abort();
|
|
472
|
+
externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
428
475
|
try {
|
|
429
476
|
return await fetch(url, {
|
|
430
477
|
method: 'POST',
|
|
@@ -435,12 +482,20 @@ class OllamaPromptToolsAdapter {
|
|
|
435
482
|
}
|
|
436
483
|
catch (err) {
|
|
437
484
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
485
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
486
|
+
// Surface the raw AbortError so AidenAgent routes it as 'interrupted'.
|
|
487
|
+
if (externalSignal?.aborted) {
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
438
490
|
throw new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
439
491
|
}
|
|
440
492
|
throw err;
|
|
441
493
|
}
|
|
442
494
|
finally {
|
|
443
495
|
clearTimeout(timer);
|
|
496
|
+
if (externalAbortHandler && externalSignal) {
|
|
497
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
498
|
+
}
|
|
444
499
|
}
|
|
445
500
|
}
|
|
446
501
|
async safeReadText(response) {
|
package/dist/tools/v4/index.js
CHANGED
|
@@ -90,6 +90,9 @@ const memoryReplace_1 = require("./memory/memoryReplace");
|
|
|
90
90
|
const memoryRemove_1 = require("./memory/memoryRemove");
|
|
91
91
|
const sessionSummary_1 = require("./memory/sessionSummary");
|
|
92
92
|
const subagentFanout_1 = require("./subagent/subagentFanout");
|
|
93
|
+
// v4.6 Phase 1 — spawn_sub_agent stub registered alongside the
|
|
94
|
+
// fanout stub so the schema is visible at agent construction.
|
|
95
|
+
const spawnSubAgentTool_1 = require("./subagent/spawnSubAgentTool");
|
|
93
96
|
/**
|
|
94
97
|
* Register every read-only tool into `registry`. The
|
|
95
98
|
* `lookup_tool_schema` tool needs a registry reference, so it's
|
|
@@ -152,11 +155,25 @@ function registerReadOnlyTools(registry) {
|
|
|
152
155
|
// Until then, calling the stub returns a clear "not wired" error
|
|
153
156
|
// rather than crashing.
|
|
154
157
|
register(makeSubagentFanoutStub());
|
|
158
|
+
// v4.6 Phase 1 — register a stub for spawn_sub_agent. Same
|
|
159
|
+
// rationale: agent construction at `cli/v4/aidenCLI.ts` snapshots
|
|
160
|
+
// the tool array, so the schema must be in the registry by then.
|
|
161
|
+
// The REPL wiring at `buildAgentRuntime` calls
|
|
162
|
+
// `register(makeSpawnSubAgentTool({...real deps}))` to replace
|
|
163
|
+
// this stub once `parentAgent`, `runStore`, etc. are available.
|
|
164
|
+
// The stub carries `contexts: ['repl']` so it's excluded from the
|
|
165
|
+
// daemon agent's tool catalog via `getSchemas(_, 'daemon')`.
|
|
166
|
+
register((0, spawnSubAgentTool_1.makeSpawnSubAgentStub)());
|
|
155
167
|
}
|
|
156
168
|
/** Stub used until the runtime wires real provider / adapter / agent
|
|
157
169
|
* dependencies. Returns the SAME schema as the real tool so MCP and
|
|
158
170
|
* /tools see a consistent surface. */
|
|
159
171
|
function makeSubagentFanoutStub() {
|
|
172
|
+
// v4.6 Phase 2R — `runChild` removed from `SubagentFanoutFactoryOptions`.
|
|
173
|
+
// The stub returns a "no providers configured" error envelope on every
|
|
174
|
+
// call via `resolveProviders: () => []`. Production wires real
|
|
175
|
+
// `spawnDeps` post-runtime build (`cli/v4/aidenCLI.ts` for REPL,
|
|
176
|
+
// `cli/v4/commands/mcp.ts` for MCP serve).
|
|
160
177
|
return (0, subagentFanout_1.makeSubagentFanoutTool)({
|
|
161
178
|
resolveProviders: () => [],
|
|
162
179
|
resolveActiveModel: () => ({ providerId: 'unset', modelId: 'unset' }),
|
|
@@ -167,9 +184,6 @@ function makeSubagentFanoutStub() {
|
|
|
167
184
|
'Call register(makeSubagentFanoutTool({...})) after buildAgentRuntime.');
|
|
168
185
|
},
|
|
169
186
|
},
|
|
170
|
-
runChild: async () => {
|
|
171
|
-
throw new Error('subagent_fanout: tool not wired — runtime did not replace the stub.');
|
|
172
|
-
},
|
|
173
187
|
});
|
|
174
188
|
}
|
|
175
189
|
/**
|
|
@@ -64,7 +64,12 @@ function makeLookupToolSchema(registry) {
|
|
|
64
64
|
category: handler.category,
|
|
65
65
|
mutates: handler.mutates,
|
|
66
66
|
toolset: handler.toolset,
|
|
67
|
-
|
|
67
|
+
// v4.6 Phase 1 — read the queried tool's actual risk tier
|
|
68
|
+
// (was previously hardcoded to 'safe' regardless of the tool,
|
|
69
|
+
// which mis-reported caution/dangerous tools as safe in the
|
|
70
|
+
// /tools surface). Falls back to 'safe' for tools that never
|
|
71
|
+
// annotated their tier — matches the registry-level default.
|
|
72
|
+
riskTier: handler.riskTier ?? 'safe',
|
|
68
73
|
};
|
|
69
74
|
},
|
|
70
75
|
};
|