aiden-runtime 4.1.0 → 4.1.2
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 +89 -33
- package/dist/cli/v4/aidenCLI.js +162 -11
- package/dist/cli/v4/callbacks.js +5 -2
- package/dist/cli/v4/chatSession.js +525 -15
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -1
- package/dist/cli/v4/commands/reloadSoul.js +37 -0
- package/dist/cli/v4/commands/update.js +102 -0
- package/dist/cli/v4/defaultSoul.js +68 -2
- package/dist/cli/v4/display.js +28 -10
- package/dist/cli/v4/doctor.js +173 -1
- package/dist/cli/v4/doctorLiveness.js +384 -0
- package/dist/cli/v4/promotionPrompt.js +202 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/toolPreview.js +139 -0
- package/dist/core/v4/aidenAgent.js +91 -29
- package/dist/core/v4/capabilities.js +89 -0
- package/dist/core/v4/contextCompressor.js +25 -8
- package/dist/core/v4/distillationIndex.js +167 -0
- package/dist/core/v4/distillationStore.js +98 -0
- package/dist/core/v4/logger/logger.js +40 -9
- package/dist/core/v4/promotionCandidates.js +234 -0
- package/dist/core/v4/promptBuilder.js +145 -1
- package/dist/core/v4/sessionDistiller.js +405 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +28 -21
- package/dist/core/v4/skillMining/proposalBuilder.js +3 -2
- package/dist/core/v4/skillMining/skillMiner.js +43 -6
- package/dist/core/v4/skillOutcomeTracker.js +323 -0
- package/dist/core/v4/subsystemHealth.js +143 -0
- package/dist/core/v4/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/dangerousPatterns.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/codexResponsesAdapter.js +7 -2
- package/dist/providers/v4/errors.js +67 -1
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +57 -1
- package/dist/tools/v4/memory/memoryRemove.js +57 -2
- package/dist/tools/v4/memory/sessionSummary.js +151 -0
- package/dist/tools/v4/sessions/recallSession.js +163 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/subagent/subagentFanout.js +24 -0
- package/dist/tools/v4/system/_psHelpers.js +55 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appLaunch.js +92 -0
- package/dist/tools/v4/system/clipboardRead.js +54 -0
- package/dist/tools/v4/system/clipboardWrite.js +84 -0
- package/dist/tools/v4/system/mediaKey.js +78 -0
- package/dist/tools/v4/system/osProcessList.js +99 -0
- package/dist/tools/v4/system/screenshot.js +106 -0
- package/dist/tools/v4/system/volumeSet.js +157 -0
- package/package.json +4 -1
- package/skills/system_control.md +135 -69
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
* cli/v4/providerBootSelector.ts — Phase v4.1.2-bug1.
|
|
10
|
+
*
|
|
11
|
+
* Boot-time provider/model picker. Replaces the hardcoded
|
|
12
|
+
* `groq + llama-3.3-70b-versatile` fallback that bit new users:
|
|
13
|
+
* users authenticated with ChatGPT Plus OAuth (the post-v4.1.1
|
|
14
|
+
* onboarding default) booted into Groq anyway, and llama-3.3-70b's
|
|
15
|
+
* tool emission triggers Groq's first-party 400
|
|
16
|
+
* ("Failed to call a function. Please adjust your prompt.").
|
|
17
|
+
*
|
|
18
|
+
* Resolution precedence (caller in `aidenCLI.ts` enforces order):
|
|
19
|
+
* 1. Both CLI flags `--provider` + `--model` set → use as-is
|
|
20
|
+
* 2. One CLI flag set → use it, resolve other
|
|
21
|
+
* 3. Persisted config (model-selection.json) complete → use as-is
|
|
22
|
+
* 4. Persisted config partial → use it, resolve other
|
|
23
|
+
* 5. Neither → priority-list auto-pick → THIS MODULE
|
|
24
|
+
* 6. Nothing authed → hardcoded fallback
|
|
25
|
+
*
|
|
26
|
+
* `resolveBootProvider()` covers cases 1–5; the caller composes the
|
|
27
|
+
* input shape and falls back when this returns `null` (case 6).
|
|
28
|
+
*
|
|
29
|
+
* Test surface: the enumerator is injected so unit tests mock it.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.BOOT_PRIORITY = void 0;
|
|
33
|
+
exports.findProviderForModel = findProviderForModel;
|
|
34
|
+
exports.resolveBootProvider = resolveBootProvider;
|
|
35
|
+
const doctorLiveness_1 = require("./doctorLiveness");
|
|
36
|
+
const registry_1 = require("../../providers/v4/registry");
|
|
37
|
+
/**
|
|
38
|
+
* Provider id ordering for auto-pick. Higher in the list = preferred.
|
|
39
|
+
* OAuth subscription flows lead because they have no API-key
|
|
40
|
+
* onboarding friction and are the default v4.1.1 install path.
|
|
41
|
+
*/
|
|
42
|
+
exports.BOOT_PRIORITY = [
|
|
43
|
+
'chatgpt-plus', // OAuth — primary onboarding flow
|
|
44
|
+
'claude-pro', // OAuth — Anthropic equivalent
|
|
45
|
+
'anthropic', // API key — power-user tier
|
|
46
|
+
'openai', // API key — power-user tier
|
|
47
|
+
// Phase v4.1.2-deepseek: paid tier, strong tool-caller, ranked
|
|
48
|
+
// above groq for the same first-run-UX reason — groq's free-tier
|
|
49
|
+
// tool emission was the original bug1 (llama-3.3-70b 400s on
|
|
50
|
+
// tool calls).
|
|
51
|
+
'deepseek', // API key — paid, strong tool-caller (V4 Pro)
|
|
52
|
+
'groq', // free-tier API key — common but tool-emission flaky
|
|
53
|
+
'ollama', // local — only if daemon up
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Walk every provider's `modelIds` and return the entry that lists
|
|
57
|
+
* `modelId`. Used by the `--model`-only path to validate the model
|
|
58
|
+
* is at least known to one provider before we accept it.
|
|
59
|
+
*/
|
|
60
|
+
function findProviderForModel(modelId) {
|
|
61
|
+
for (const entry of Object.values(registry_1.PROVIDER_REGISTRY)) {
|
|
62
|
+
if (entry.modelIds.includes(modelId))
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the boot provider + model. Returns `null` when no choice
|
|
69
|
+
* could be inferred AND nothing is authed (the caller falls back to
|
|
70
|
+
* the hardcoded `groq + llama-3.3-70b-versatile` default in that
|
|
71
|
+
* one case).
|
|
72
|
+
*
|
|
73
|
+
* Throws (`Error`) when input is internally inconsistent — e.g. the
|
|
74
|
+
* user passed `--model foo` for a model no provider declares. Caller
|
|
75
|
+
* surfaces the message via the standard error path.
|
|
76
|
+
*/
|
|
77
|
+
async function resolveBootProvider(input, enumerate) {
|
|
78
|
+
const { cliProviderId, cliModelId, cfgProviderId, cfgModelId } = input;
|
|
79
|
+
// Case 1: both CLI flags set.
|
|
80
|
+
if (cliProviderId && cliModelId) {
|
|
81
|
+
return { providerId: cliProviderId, modelId: cliModelId, source: 'cli-flag' };
|
|
82
|
+
}
|
|
83
|
+
// Case 2a: `--provider` only. Use that provider + its first
|
|
84
|
+
// non-codex model. The registry might not know this provider; we
|
|
85
|
+
// accept it as-is (the runtime resolver will throw a clearer error
|
|
86
|
+
// later if it's bogus).
|
|
87
|
+
if (cliProviderId && !cliModelId) {
|
|
88
|
+
const entry = registry_1.PROVIDER_REGISTRY[cliProviderId];
|
|
89
|
+
const modelId = entry ? (0, doctorLiveness_1.pickProbeModel)(entry) : '';
|
|
90
|
+
if (!modelId) {
|
|
91
|
+
throw new Error(`--provider '${cliProviderId}' set but no model could be inferred. ` +
|
|
92
|
+
`Pass --model explicitly.`);
|
|
93
|
+
}
|
|
94
|
+
return { providerId: cliProviderId, modelId, source: 'cli-flag-partial' };
|
|
95
|
+
}
|
|
96
|
+
// Case 2b: `--model` only. Verify the model is known to some
|
|
97
|
+
// provider; pick the matching provider.
|
|
98
|
+
if (!cliProviderId && cliModelId) {
|
|
99
|
+
const entry = findProviderForModel(cliModelId);
|
|
100
|
+
if (!entry) {
|
|
101
|
+
throw new Error(`--model '${cliModelId}' is not declared by any provider in the ` +
|
|
102
|
+
`registry. Run \`aiden model\` to see available models, or pass ` +
|
|
103
|
+
`--provider explicitly.`);
|
|
104
|
+
}
|
|
105
|
+
return { providerId: entry.id, modelId: cliModelId, source: 'cli-flag-partial' };
|
|
106
|
+
}
|
|
107
|
+
// Case 3: persisted config — both fields set.
|
|
108
|
+
if (cfgProviderId && cfgModelId) {
|
|
109
|
+
return { providerId: cfgProviderId, modelId: cfgModelId, source: 'persisted-config' };
|
|
110
|
+
}
|
|
111
|
+
// Case 4a: config has provider, no model — resolve via pickProbeModel.
|
|
112
|
+
if (cfgProviderId && !cfgModelId) {
|
|
113
|
+
const entry = registry_1.PROVIDER_REGISTRY[cfgProviderId];
|
|
114
|
+
const modelId = entry ? (0, doctorLiveness_1.pickProbeModel)(entry) : '';
|
|
115
|
+
if (modelId) {
|
|
116
|
+
return { providerId: cfgProviderId, modelId, source: 'config-partial' };
|
|
117
|
+
}
|
|
118
|
+
// Unknown provider in config — fall through to auto-pick rather
|
|
119
|
+
// than refusing to boot.
|
|
120
|
+
}
|
|
121
|
+
// Case 4b: config has model only — same shape as Case 2b but
|
|
122
|
+
// softer: fall through to auto-pick if the model isn't found in
|
|
123
|
+
// the registry (config can lag the codebase).
|
|
124
|
+
if (!cfgProviderId && cfgModelId) {
|
|
125
|
+
const entry = findProviderForModel(cfgModelId);
|
|
126
|
+
if (entry) {
|
|
127
|
+
return { providerId: entry.id, modelId: cfgModelId, source: 'config-partial' };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Case 5: auto-pick from priority list.
|
|
131
|
+
const configured = await enumerate();
|
|
132
|
+
for (const id of exports.BOOT_PRIORITY) {
|
|
133
|
+
const hit = configured.find((c) => c.entry.id === id && c.configured);
|
|
134
|
+
if (hit) {
|
|
135
|
+
return {
|
|
136
|
+
providerId: id,
|
|
137
|
+
modelId: (0, doctorLiveness_1.pickProbeModel)(hit.entry),
|
|
138
|
+
source: 'auto-priority',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Case 6: nothing authed — caller falls back to hardcoded default.
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
* cli/v4/sessionSummaryGate.ts — Phase v4.1.2-followup-2.
|
|
10
|
+
*
|
|
11
|
+
* Pure decision helpers extracted from `ChatSession.maybeAutoSummarize`
|
|
12
|
+
* so the threshold + mtime/size-grew logic is unit-testable without
|
|
13
|
+
* standing up a full ChatSession + mocked agent loop.
|
|
14
|
+
*
|
|
15
|
+
* shouldAutoSummarize → returns {fire: true} or
|
|
16
|
+
* {fire: false, reason: 'short'|'unconfigured'|'no-paths'}.
|
|
17
|
+
* ChatSession uses the reason tag to log the right
|
|
18
|
+
* user-visible message.
|
|
19
|
+
*
|
|
20
|
+
* memoryGrewBetween → strict size-or-mtime comparison so the caller can
|
|
21
|
+
* detect "the model actually fired session_summary"
|
|
22
|
+
* even when the tool wrote without growing the file
|
|
23
|
+
* length (e.g. replaced a previous same-length entry).
|
|
24
|
+
*
|
|
25
|
+
* No I/O here. ChatSession owns the fs.stat + display.warn / display.dim.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.SESSION_SUMMARY_MIN_TURNS = void 0;
|
|
29
|
+
exports.shouldAutoSummarize = shouldAutoSummarize;
|
|
30
|
+
exports.memoryGrewBetween = memoryGrewBetween;
|
|
31
|
+
/** Minimum user-message turns required before auto-summary triggers. */
|
|
32
|
+
exports.SESSION_SUMMARY_MIN_TURNS = 3;
|
|
33
|
+
/**
|
|
34
|
+
* Decide whether the /quit auto-summary should fire. Threshold lives
|
|
35
|
+
* here as the single source of truth; ChatSession imports the constant
|
|
36
|
+
* so the user-facing log message ("need 3+") cites the exact value.
|
|
37
|
+
*/
|
|
38
|
+
function shouldAutoSummarize(input) {
|
|
39
|
+
if (input.userTurns < exports.SESSION_SUMMARY_MIN_TURNS) {
|
|
40
|
+
return { fire: false, reason: 'short' };
|
|
41
|
+
}
|
|
42
|
+
if (input.unconfigured) {
|
|
43
|
+
return { fire: false, reason: 'unconfigured' };
|
|
44
|
+
}
|
|
45
|
+
if (!input.memoryPath) {
|
|
46
|
+
return { fire: false, reason: 'no-paths' };
|
|
47
|
+
}
|
|
48
|
+
return { fire: true };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* True iff MEMORY.md grew (longer) or was touched (newer mtime) between
|
|
52
|
+
* the two snapshots. Used to detect whether the agent actually fired
|
|
53
|
+
* the session_summary tool inside the synthetic turn — if not, the
|
|
54
|
+
* user sees a warning instead of a misleading "saved" message.
|
|
55
|
+
*
|
|
56
|
+
* The size-OR-mtime disjunction (not just size>before) covers the case
|
|
57
|
+
* where session_summary replaces an existing same-length entry: file
|
|
58
|
+
* size stays the same but mtime advances.
|
|
59
|
+
*/
|
|
60
|
+
function memoryGrewBetween(before, after) {
|
|
61
|
+
if (after.size > before.size)
|
|
62
|
+
return true;
|
|
63
|
+
if (after.mtime > before.mtime)
|
|
64
|
+
return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
* cli/v4/toolPreview.ts — Phase v4.1.2 alive-core.
|
|
10
|
+
*
|
|
11
|
+
* Clean per-tool argument previews. Replaces the old
|
|
12
|
+
* `JSON.stringify(args)` blob in `display.toolPreview` with a
|
|
13
|
+
* tool-aware lookup that extracts the primary argument (the one
|
|
14
|
+
* actually useful at a glance — `command` for terminal, `path` for
|
|
15
|
+
* file ops, `query` for search, etc.).
|
|
16
|
+
*
|
|
17
|
+
* Falls back to the original full-JSON stringification when the tool
|
|
18
|
+
* isn't in the map or the primary arg is absent. This keeps unknown
|
|
19
|
+
* tools rendering exactly as before — additive only.
|
|
20
|
+
*
|
|
21
|
+
* Adding a new tool with a non-obvious primary arg? Add it here.
|
|
22
|
+
* Tools whose `args` shape is "the arg is meaningful at-a-glance"
|
|
23
|
+
* (a path, a query, a command, a URL, an id, a name) belong in this map.
|
|
24
|
+
* Tools whose args are a small flag bag (e.g. system_info has no args
|
|
25
|
+
* worth showing) can be omitted — the renderer hides the args block
|
|
26
|
+
* entirely when the map returns `null` and the arg object is empty.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.TOOL_PRIMARY_ARG = void 0;
|
|
30
|
+
exports.buildToolPreview = buildToolPreview;
|
|
31
|
+
/**
|
|
32
|
+
* Map of tool-name → name of the property in `args` that should render
|
|
33
|
+
* as the at-a-glance preview. Stable contract; tests assert specific
|
|
34
|
+
* entries.
|
|
35
|
+
*/
|
|
36
|
+
exports.TOOL_PRIMARY_ARG = {
|
|
37
|
+
// ── terminal / execution ─────────────────────────────────────────────
|
|
38
|
+
shell_exec: 'command',
|
|
39
|
+
execute_code: 'code',
|
|
40
|
+
// ── file ops ─────────────────────────────────────────────────────────
|
|
41
|
+
file_read: 'path',
|
|
42
|
+
file_write: 'path',
|
|
43
|
+
file_patch: 'path',
|
|
44
|
+
file_list: 'path',
|
|
45
|
+
file_copy: 'source',
|
|
46
|
+
file_move: 'source',
|
|
47
|
+
file_delete: 'path',
|
|
48
|
+
// ── web ──────────────────────────────────────────────────────────────
|
|
49
|
+
web_search: 'query',
|
|
50
|
+
deep_research: 'query',
|
|
51
|
+
youtube_search: 'query',
|
|
52
|
+
fetch_url: 'url',
|
|
53
|
+
fetch_page: 'url',
|
|
54
|
+
open_url: 'url',
|
|
55
|
+
// ── browser ──────────────────────────────────────────────────────────
|
|
56
|
+
browser_navigate: 'url',
|
|
57
|
+
browser_click: 'selector',
|
|
58
|
+
browser_fill: 'selector',
|
|
59
|
+
browser_type: 'selector',
|
|
60
|
+
browser_scroll: 'selector',
|
|
61
|
+
browser_extract: 'selector',
|
|
62
|
+
browser_get_url: '', // no args — present so map lookup hits
|
|
63
|
+
browser_screenshot: 'path',
|
|
64
|
+
browser_close: '',
|
|
65
|
+
// ── memory ───────────────────────────────────────────────────────────
|
|
66
|
+
memory_add: 'content',
|
|
67
|
+
memory_remove: 'content',
|
|
68
|
+
memory_replace: 'old',
|
|
69
|
+
// ── skills ───────────────────────────────────────────────────────────
|
|
70
|
+
skill_view: 'name',
|
|
71
|
+
skill_manage: 'action',
|
|
72
|
+
skills_list: '',
|
|
73
|
+
// ── sessions ─────────────────────────────────────────────────────────
|
|
74
|
+
session_search: 'query',
|
|
75
|
+
session_list: '',
|
|
76
|
+
session_summary: 'trigger',
|
|
77
|
+
// ── process ──────────────────────────────────────────────────────────
|
|
78
|
+
process_spawn: 'command',
|
|
79
|
+
process_kill: 'pid',
|
|
80
|
+
process_list: '',
|
|
81
|
+
process_wait: 'pid',
|
|
82
|
+
process_log_read: 'pid',
|
|
83
|
+
// ── subagent ─────────────────────────────────────────────────────────
|
|
84
|
+
subagent_fanout: 'mode',
|
|
85
|
+
// ── system / misc ────────────────────────────────────────────────────
|
|
86
|
+
system_info: '',
|
|
87
|
+
now_playing: '',
|
|
88
|
+
get_natural_events: '',
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Maximum visible characters for the preview value. Long commands /
|
|
92
|
+
* full file contents get truncated with an ellipsis so a single tool
|
|
93
|
+
* row stays on one line at typical terminal widths.
|
|
94
|
+
*/
|
|
95
|
+
const PREVIEW_MAX_CHARS = 120;
|
|
96
|
+
/**
|
|
97
|
+
* Build the per-tool preview string for `args`. Returns:
|
|
98
|
+
* - `null` when the tool isn't in the map (caller falls back to the
|
|
99
|
+
* legacy JSON.stringify path),
|
|
100
|
+
* - `''` (empty string) when the tool is in the map but has no
|
|
101
|
+
* meaningful primary arg (caller renders just the tool name),
|
|
102
|
+
* - the truncated string value of the primary arg otherwise.
|
|
103
|
+
*
|
|
104
|
+
* Exposed for unit tests. Pure function, no side effects.
|
|
105
|
+
*/
|
|
106
|
+
function buildToolPreview(toolName, args) {
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(exports.TOOL_PRIMARY_ARG, toolName)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const argKey = exports.TOOL_PRIMARY_ARG[toolName];
|
|
111
|
+
if (argKey === '')
|
|
112
|
+
return '';
|
|
113
|
+
if (!args || typeof args !== 'object')
|
|
114
|
+
return '';
|
|
115
|
+
const raw = args[argKey];
|
|
116
|
+
if (raw === undefined || raw === null)
|
|
117
|
+
return '';
|
|
118
|
+
let str;
|
|
119
|
+
if (typeof raw === 'string') {
|
|
120
|
+
str = raw;
|
|
121
|
+
}
|
|
122
|
+
else if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
123
|
+
str = String(raw);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
try {
|
|
127
|
+
str = JSON.stringify(raw);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
str = String(raw);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Collapse whitespace so multi-line commands stay on one preview row.
|
|
134
|
+
str = str.replace(/\s+/g, ' ').trim();
|
|
135
|
+
if (str.length > PREVIEW_MAX_CHARS) {
|
|
136
|
+
str = `${str.slice(0, PREVIEW_MAX_CHARS - 1)}…`;
|
|
137
|
+
}
|
|
138
|
+
return str;
|
|
139
|
+
}
|
|
@@ -61,7 +61,11 @@ class AidenAgent {
|
|
|
61
61
|
/** Cached system prompt — invalidated by setPersonalityOverlay/markMemoryDirty/explicit. */
|
|
62
62
|
this.cachedSystemPrompt = null;
|
|
63
63
|
this.compressionEvents = 0;
|
|
64
|
-
|
|
64
|
+
// Phase v4.1.2: tracks which identity / memory files need a system-
|
|
65
|
+
// prompt rebuild on the next turn. Empty set = clean. Plain Set keeps
|
|
66
|
+
// the membership-test path O(1) and avoids the combinatorial union
|
|
67
|
+
// type the previous representation grew when SOUL.md joined the list.
|
|
68
|
+
this.memoryDirty = new Set();
|
|
65
69
|
/** Process-scoped tracker metrics for `/doctor`. */
|
|
66
70
|
this.skillEnforcementMetrics = {
|
|
67
71
|
recovered: 0, failed: 0, armed: 0, preArmed: 0,
|
|
@@ -100,6 +104,17 @@ class AidenAgent {
|
|
|
100
104
|
this.refreshMemorySnapshot = opts.refreshMemorySnapshot;
|
|
101
105
|
this.onMemoryRefresh = opts.onMemoryRefresh;
|
|
102
106
|
this.lookupSkillRequiredTools = opts.lookupSkillRequiredTools;
|
|
107
|
+
// Phase v4.1.2-slice3: optional health registry (constructor-
|
|
108
|
+
// injected per the slice3 decision tree — no singleton). When
|
|
109
|
+
// wired, the caller already plumbed trackers into each subsystem
|
|
110
|
+
// via their own constructors; we just hold the read handle.
|
|
111
|
+
this
|
|
112
|
+
.subsystemHealthRegistry = opts.subsystemHealthRegistry;
|
|
113
|
+
// Phase v4.1.2-slice4: same pattern for the outcome tracker. The
|
|
114
|
+
// caller composes the tracker into `onToolCall`; we just keep a
|
|
115
|
+
// read handle for doctor.
|
|
116
|
+
this
|
|
117
|
+
.skillOutcomeTracker = opts.skillOutcomeTracker;
|
|
103
118
|
}
|
|
104
119
|
// ── Public method surface ────────────────────────────────────────────
|
|
105
120
|
setProvider(adapter) {
|
|
@@ -124,6 +139,38 @@ class AidenAgent {
|
|
|
124
139
|
this.cachedSystemPrompt = null;
|
|
125
140
|
return true;
|
|
126
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Phase v4.1.2-bug2: replace the active provider/model fed into the
|
|
144
|
+
* `## Runtime` slot of the system prompt. Mirrors
|
|
145
|
+
* `setPersonalityOverlay` shape — mutate the cached PromptBuilder
|
|
146
|
+
* options + null the system-prompt cache so the next runConversation
|
|
147
|
+
* rebuilds with fresh values. Returns `true` when at least one of
|
|
148
|
+
* `providerId`/`modelId` actually changed; `false` is a no-op
|
|
149
|
+
* (caller may skip downstream signalling).
|
|
150
|
+
*
|
|
151
|
+
* This is NOT a dirty-bit invalidation — provider/model are
|
|
152
|
+
* in-memory field updates, not disk-backed reloads. The existing
|
|
153
|
+
* MemoryFile dirty-bit (`memory|user|soul`) governs file reload
|
|
154
|
+
* semantics and is intentionally not extended here.
|
|
155
|
+
*
|
|
156
|
+
* Called by chatSession.setProvider() after the adapter swap so the
|
|
157
|
+
* prompt's self-description stays in lockstep with the routed
|
|
158
|
+
* provider. Without this, `/model groq → chatgpt-plus` swaps the
|
|
159
|
+
* adapter (real requests route correctly) but the prompt keeps
|
|
160
|
+
* claiming "Provider: groq" for the rest of the session.
|
|
161
|
+
*/
|
|
162
|
+
setActiveModel(providerId, modelId) {
|
|
163
|
+
const cur = this.promptBuilderOptions;
|
|
164
|
+
if (cur?.providerId === providerId && cur?.modelId === modelId)
|
|
165
|
+
return false;
|
|
166
|
+
this.promptBuilderOptions = {
|
|
167
|
+
...(cur ?? {}),
|
|
168
|
+
providerId,
|
|
169
|
+
modelId,
|
|
170
|
+
};
|
|
171
|
+
this.cachedSystemPrompt = null;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
127
174
|
/**
|
|
128
175
|
* Build (or return the cached) system prompt without driving the
|
|
129
176
|
* provider. Powers the `/debug-prompt` command. Returns `null` when no
|
|
@@ -138,23 +185,31 @@ class AidenAgent {
|
|
|
138
185
|
return this.cachedSystemPrompt;
|
|
139
186
|
}
|
|
140
187
|
/**
|
|
141
|
-
* Mark MEMORY.md / USER.md as dirty. The next
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
188
|
+
* Mark MEMORY.md / USER.md / SOUL.md as dirty. The next
|
|
189
|
+
* `runConversation` will rebuild the prompt, fire `onMemoryRefresh`,
|
|
190
|
+
* and clear the dirty set.
|
|
191
|
+
*
|
|
192
|
+
* - 'memory' / 'user' refresh through `refreshMemorySnapshot` (the
|
|
193
|
+
* in-memory MEMORY.md / USER.md blobs need a re-read). No-op when
|
|
194
|
+
* no refresh callback is wired (frozen-snapshot semantics).
|
|
195
|
+
* - 'soul' just invalidates the prompt cache; SOUL.md is re-read
|
|
196
|
+
* from disk by `PromptBuilder.build()` on the next rebuild. No
|
|
197
|
+
* snapshot callback required, so this kind always takes effect.
|
|
145
198
|
*/
|
|
146
199
|
markMemoryDirty(file) {
|
|
147
|
-
if (!this.refreshMemorySnapshot)
|
|
200
|
+
if ((file === 'memory' || file === 'user') && !this.refreshMemorySnapshot) {
|
|
148
201
|
return;
|
|
149
|
-
if (this.memoryDirty === null) {
|
|
150
|
-
this.memoryDirty = file;
|
|
151
|
-
}
|
|
152
|
-
else if (this.memoryDirty !== file) {
|
|
153
|
-
this.memoryDirty = 'both';
|
|
154
202
|
}
|
|
203
|
+
this.memoryDirty.add(file);
|
|
155
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Returns the set of dirty files as a stable-sorted readonly array.
|
|
207
|
+
* Empty array = clean. (Phase v4.1.2: replaces the older
|
|
208
|
+
* `'memory' | 'user' | 'both' | null` return type now that SOUL.md
|
|
209
|
+
* joins the rotation — a Set scales without union-type explosion.)
|
|
210
|
+
*/
|
|
156
211
|
getMemoryDirtyState() {
|
|
157
|
-
return this.memoryDirty;
|
|
212
|
+
return [...this.memoryDirty].sort();
|
|
158
213
|
}
|
|
159
214
|
/** /doctor accessor for cumulative skill-enforcement counters. */
|
|
160
215
|
getSkillEnforcementMetrics() {
|
|
@@ -319,28 +374,35 @@ class AidenAgent {
|
|
|
319
374
|
}
|
|
320
375
|
// ── Private helpers ──────────────────────────────────────────────────
|
|
321
376
|
async refreshSystemPromptIfDirty() {
|
|
322
|
-
if (this.memoryDirty ===
|
|
377
|
+
if (this.memoryDirty.size === 0)
|
|
323
378
|
return;
|
|
324
|
-
if (!this.
|
|
325
|
-
this.memoryDirty
|
|
379
|
+
if (!this.promptBuilder || !this.promptBuilderOptions) {
|
|
380
|
+
this.memoryDirty.clear();
|
|
326
381
|
return;
|
|
327
382
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
383
|
+
const dirtyFiles = [...this.memoryDirty].sort();
|
|
384
|
+
// 'soul' is satisfied by a cache invalidation alone — SOUL.md is
|
|
385
|
+
// re-read by PromptBuilder.build() on the next rebuild. 'memory'
|
|
386
|
+
// / 'user' need a snapshot refresh first.
|
|
387
|
+
const needsSnapshot = this.memoryDirty.has('memory') || this.memoryDirty.has('user');
|
|
388
|
+
if (needsSnapshot && this.refreshMemorySnapshot) {
|
|
389
|
+
let snapshot;
|
|
390
|
+
try {
|
|
391
|
+
snapshot = await this.refreshMemorySnapshot();
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// Leave the dirty set as-is so the next turn retries. We don't
|
|
395
|
+
// break this turn over a transient memory-read failure.
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.promptBuilderOptions = {
|
|
399
|
+
...this.promptBuilderOptions,
|
|
400
|
+
memorySnapshot: snapshot,
|
|
401
|
+
};
|
|
336
402
|
}
|
|
337
|
-
this.promptBuilderOptions = {
|
|
338
|
-
...this.promptBuilderOptions,
|
|
339
|
-
memorySnapshot: snapshot,
|
|
340
|
-
};
|
|
341
403
|
this.cachedSystemPrompt = null;
|
|
342
|
-
this.onMemoryRefresh?.(
|
|
343
|
-
this.memoryDirty
|
|
404
|
+
this.onMemoryRefresh?.(dirtyFiles);
|
|
405
|
+
this.memoryDirty.clear();
|
|
344
406
|
}
|
|
345
407
|
async ensureSystemPrompt() {
|
|
346
408
|
if (!this.promptBuilder || !this.promptBuilderOptions)
|
|
@@ -0,0 +1,89 @@
|
|
|
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/capabilities.ts — Phase v4.1.2-followup self-awareness.
|
|
10
|
+
*
|
|
11
|
+
* Runtime-computed manifest of what Aiden actually has loaded. Fed
|
|
12
|
+
* into the `## Runtime` slot of the system prompt so the model can
|
|
13
|
+
* answer questions like "what version are you" and "what tools do
|
|
14
|
+
* you have" from facts in its context, instead of hallucinating from
|
|
15
|
+
* whatever stale text used to live in SOUL.md.
|
|
16
|
+
*
|
|
17
|
+
* The manifest is computed at prompt-build time, never cached
|
|
18
|
+
* separately — it piggybacks on the existing system-prompt cache.
|
|
19
|
+
* On dirty-bit invalidation (memory / user / soul write, or
|
|
20
|
+
* personality overlay change) the prompt rebuilds and so do these
|
|
21
|
+
* numbers.
|
|
22
|
+
*
|
|
23
|
+
* No hardcoded "shipped vs deferred" framing here. The slot describes
|
|
24
|
+
* what IS loaded; absence is absence, not declared deferral.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.buildRuntimeManifest = buildRuntimeManifest;
|
|
28
|
+
exports.renderRuntimeSlot = renderRuntimeSlot;
|
|
29
|
+
const version_1 = require("../version");
|
|
30
|
+
/**
|
|
31
|
+
* TODO(v4.2): replace with a proper channel registry enumeration once
|
|
32
|
+
* channels expose a registration API. Today the gateway adapters are
|
|
33
|
+
* wired directly in `api/server.ts` (DiscordAdapter, SlackAdapter,
|
|
34
|
+
* TelegramAdapter, WhatsAppAdapter, EmailAdapter, WebhookAdapter,
|
|
35
|
+
* TwilioAdapter, IMessageAdapter, SignalAdapter — nine in total) and
|
|
36
|
+
* interaction surfaces (cli REPL, MCP server, OpenAI-compat HTTP, voice
|
|
37
|
+
* mode, headless --no-ui, web dashboard) are scattered across cli/v4
|
|
38
|
+
* and api/. This list conflates the two for the user-visible "channels
|
|
39
|
+
* Aiden is available on" count; a follow-up should pick a single
|
|
40
|
+
* definition and back this with a real registry.
|
|
41
|
+
*/
|
|
42
|
+
const KNOWN_CHANNELS = Object.freeze([
|
|
43
|
+
'cli',
|
|
44
|
+
'telegram',
|
|
45
|
+
'discord',
|
|
46
|
+
'slack',
|
|
47
|
+
'mcp',
|
|
48
|
+
'voice',
|
|
49
|
+
'headless',
|
|
50
|
+
'web',
|
|
51
|
+
'api',
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Build the manifest from caller-supplied counts + persistent imports.
|
|
55
|
+
* Pure function — no side effects, no async, no I/O — so PromptBuilder
|
|
56
|
+
* can call it inline and keep the same determinism contract for its
|
|
57
|
+
* prefix-cache friendliness.
|
|
58
|
+
*/
|
|
59
|
+
function buildRuntimeManifest(opts) {
|
|
60
|
+
return {
|
|
61
|
+
version: version_1.VERSION,
|
|
62
|
+
toolCount: opts.toolCount,
|
|
63
|
+
skillCount: opts.skillCount,
|
|
64
|
+
channels: KNOWN_CHANNELS,
|
|
65
|
+
providerId: opts.providerId,
|
|
66
|
+
modelId: opts.modelId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Render the manifest as the `## Runtime` prompt slot. Visual style
|
|
71
|
+
* mirrors the other slots in PromptBuilder — h2 header, simple
|
|
72
|
+
* `key: value` lines, no marketing speak.
|
|
73
|
+
*
|
|
74
|
+
* Always emits a complete block even when provider/model are unknown;
|
|
75
|
+
* the contract is "always present, even if some values are unknown"
|
|
76
|
+
* so the model doesn't second-guess whether the slot was suppressed.
|
|
77
|
+
*/
|
|
78
|
+
function renderRuntimeSlot(manifest) {
|
|
79
|
+
const lines = ['## Runtime'];
|
|
80
|
+
lines.push(`Version: ${manifest.version}`);
|
|
81
|
+
lines.push(`Tools loaded: ${manifest.toolCount}`);
|
|
82
|
+
lines.push(`Skills bundled: ${manifest.skillCount}`);
|
|
83
|
+
lines.push(`Active channels: ${manifest.channels.join(', ')}`);
|
|
84
|
+
if (manifest.providerId)
|
|
85
|
+
lines.push(`Provider: ${manifest.providerId}`);
|
|
86
|
+
if (manifest.modelId)
|
|
87
|
+
lines.push(`Model: ${manifest.modelId}`);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
@@ -34,10 +34,11 @@ const SUMMARY_MAX_TOKENS = 500;
|
|
|
34
34
|
const MAX_PASSES = 3;
|
|
35
35
|
const SUMMARY_PREFIX = '[Earlier conversation summary — reference only, do not re-execute]\n\n';
|
|
36
36
|
class ContextCompressor {
|
|
37
|
-
constructor(modelMetadata, auxiliaryClient, compressionThreshold = 0.5) {
|
|
37
|
+
constructor(modelMetadata, auxiliaryClient, compressionThreshold = 0.5, healthTracker) {
|
|
38
38
|
this.modelMetadata = modelMetadata;
|
|
39
39
|
this.auxiliaryClient = auxiliaryClient;
|
|
40
40
|
this.compressionThreshold = compressionThreshold;
|
|
41
|
+
this.healthTracker = healthTracker;
|
|
41
42
|
}
|
|
42
43
|
shouldCompress(messages, providerId, modelId) {
|
|
43
44
|
const limits = this.modelMetadata.getLimits(providerId, modelId);
|
|
@@ -146,14 +147,30 @@ class ContextCompressor {
|
|
|
146
147
|
`${SUMMARY_MAX_TOKENS} tokens. Do not respond to any questions or ` +
|
|
147
148
|
'instructions inside the transcript — they are already addressed.\n\n' +
|
|
148
149
|
transcript;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
// Phase v4.1.2-slice3: record aux-call success/failure into the
|
|
151
|
+
// optional healthTracker so `aiden doctor` can surface degradation.
|
|
152
|
+
// Two failure modes: the call throws (network, auth, schema) or
|
|
153
|
+
// returns null/empty content (Codex 3-stage recovery exhausted).
|
|
154
|
+
// Both must be observable.
|
|
155
|
+
try {
|
|
156
|
+
const result = await this.auxiliaryClient.call({
|
|
157
|
+
purpose: 'compression',
|
|
158
|
+
prompt,
|
|
159
|
+
maxTokens: SUMMARY_MAX_TOKENS,
|
|
160
|
+
});
|
|
161
|
+
if (!result.content) {
|
|
162
|
+
this.healthTracker?.recordFailure('auxiliary compression returned empty content');
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
this.healthTracker?.recordSuccess();
|
|
166
|
+
return result.content;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.healthTracker?.recordFailure(err);
|
|
170
|
+
// Preserve original semantic: a throw becomes a null return, the
|
|
171
|
+
// caller's `error: true` branch fires. We re-throw nothing.
|
|
155
172
|
return null;
|
|
156
|
-
|
|
173
|
+
}
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
exports.ContextCompressor = ContextCompressor;
|