aiden-runtime 4.1.1 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -26
- package/dist/cli/v4/aidenCLI.js +169 -9
- package/dist/cli/v4/callbacks.js +20 -2
- package/dist/cli/v4/chatSession.js +644 -16
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -1
- package/dist/cli/v4/commands/model.js +30 -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/capabilityCard.js +135 -0
- package/dist/cli/v4/display/sessionEndCard.js +127 -0
- package/dist/cli/v4/display/toolTrail.js +172 -0
- package/dist/cli/v4/display.js +492 -142
- package/dist/cli/v4/doctor.js +472 -58
- package/dist/cli/v4/doctorLiveness.js +65 -10
- package/dist/cli/v4/promotionPrompt.js +332 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +153 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- 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 +452 -0
- 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/toolRegistry.js +16 -1
- package/dist/core/v4/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/errors.js +112 -4
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +80 -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 +177 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/system/_psHelpers.js +123 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +218 -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 +109 -0
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -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 +185 -69
|
@@ -33,11 +33,6 @@ exports.LOG_LEVEL_ORDER = {
|
|
|
33
33
|
warn: 30,
|
|
34
34
|
error: 40,
|
|
35
35
|
};
|
|
36
|
-
/**
|
|
37
|
-
* Default `Logger` implementation. Holds a list of sinks and the
|
|
38
|
-
* current scope; child loggers share the same sink list (so updating
|
|
39
|
-
* the level / detaching at the root affects everything).
|
|
40
|
-
*/
|
|
41
36
|
class CoreLogger {
|
|
42
37
|
/**
|
|
43
38
|
* Construct a root logger. Use `child(segment)` for sub-loggers.
|
|
@@ -47,7 +42,11 @@ class CoreLogger {
|
|
|
47
42
|
this.scope = opts.scope ?? '';
|
|
48
43
|
this.sinks = opts.sinks;
|
|
49
44
|
this.level = opts.level ?? 'debug';
|
|
50
|
-
this.sinksOwner = {
|
|
45
|
+
this.sinksOwner = {
|
|
46
|
+
sinks: this.sinks,
|
|
47
|
+
level: this.level,
|
|
48
|
+
counters: opts.sinks.map(() => ({ totalWrites: 0, failures: 0 })),
|
|
49
|
+
};
|
|
51
50
|
}
|
|
52
51
|
/** Internal — used by `child()` to share state with the root. */
|
|
53
52
|
static childOf(parent, segment) {
|
|
@@ -73,6 +72,21 @@ class CoreLogger {
|
|
|
73
72
|
}
|
|
74
73
|
detachAll() {
|
|
75
74
|
this.sinksOwner.sinks.length = 0;
|
|
75
|
+
this.sinksOwner.counters.length = 0;
|
|
76
|
+
}
|
|
77
|
+
getSinkHealth() {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (let i = 0; i < this.sinksOwner.sinks.length; i += 1) {
|
|
80
|
+
const sink = this.sinksOwner.sinks[i];
|
|
81
|
+
const counter = this.sinksOwner.counters[i] ?? { totalWrites: 0, failures: 0 };
|
|
82
|
+
out.push({
|
|
83
|
+
name: sink.name ?? `sink:${i}`,
|
|
84
|
+
totalWrites: counter.totalWrites,
|
|
85
|
+
failures: counter.failures,
|
|
86
|
+
...(counter.lastError ? { lastError: counter.lastError } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
76
90
|
}
|
|
77
91
|
debug(msg, ctx) { this.write('debug', msg, ctx); }
|
|
78
92
|
info(msg, ctx) { this.write('info', msg, ctx); }
|
|
@@ -89,12 +103,29 @@ class CoreLogger {
|
|
|
89
103
|
ctx,
|
|
90
104
|
};
|
|
91
105
|
// Sinks must not throw — the helpers in ./sinks/* all wrap their
|
|
92
|
-
// I/O in try/catch. Be defensive anyway.
|
|
93
|
-
|
|
106
|
+
// I/O in try/catch. Be defensive anyway. Phase v4.1.2-slice3:
|
|
107
|
+
// bump the per-sink counter and capture the most recent failure
|
|
108
|
+
// message so `aiden doctor` can render it. The counter itself is
|
|
109
|
+
// never logged through this logger (would recurse).
|
|
110
|
+
for (let i = 0; i < this.sinksOwner.sinks.length; i += 1) {
|
|
111
|
+
const s = this.sinksOwner.sinks[i];
|
|
112
|
+
const c = this.sinksOwner.counters[i];
|
|
113
|
+
if (c)
|
|
114
|
+
c.totalWrites += 1;
|
|
94
115
|
try {
|
|
95
116
|
s.write(record);
|
|
96
117
|
}
|
|
97
|
-
catch {
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (c) {
|
|
120
|
+
c.failures += 1;
|
|
121
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
122
|
+
c.lastError = {
|
|
123
|
+
message: msg.length > 200 ? msg.slice(0, 197) + '...' : msg,
|
|
124
|
+
at: new Date(),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/* logging must not break callers */
|
|
128
|
+
}
|
|
98
129
|
}
|
|
99
130
|
}
|
|
100
131
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
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/promotionCandidates.ts — Phase v4.1.2-memory-D.
|
|
10
|
+
*
|
|
11
|
+
* Pure module that builds the list of "should we promote this to
|
|
12
|
+
* MEMORY.md `## Durable facts`?" candidates at session-end. The CLI
|
|
13
|
+
* surface (`cli/v4/promotionPrompt.ts`) reads the candidates, asks
|
|
14
|
+
* the user, and writes the approved subset.
|
|
15
|
+
*
|
|
16
|
+
* Sources combined (per Phase D's Q1 decision — A + B, defer C):
|
|
17
|
+
*
|
|
18
|
+
* A. Explicit user signals — regex over `history` user messages.
|
|
19
|
+
* "remember that X", "save this", "for next time", "don't forget"
|
|
20
|
+
* → the captured phrase becomes the candidate text. The
|
|
21
|
+
* surrounding user message is kept as `context` so the user can
|
|
22
|
+
* verify what they're promoting before approving.
|
|
23
|
+
*
|
|
24
|
+
* B. Distillation `decisions[]` + `open_items[]` — Phase A+B's
|
|
25
|
+
* structured output. Decisions are "X was chosen over Y"; open
|
|
26
|
+
* items are unfinished work / next-time prompts. Both are
|
|
27
|
+
* durable-worthy.
|
|
28
|
+
*
|
|
29
|
+
* C. Recurring facts across sessions — DEFERRED. Substring matching
|
|
30
|
+
* alone produces false positives ("any session mentioning Aiden"
|
|
31
|
+
* matches every other one). Semantic similarity belongs in Phase E
|
|
32
|
+
* alongside embeddings; lands when that slice ships.
|
|
33
|
+
*
|
|
34
|
+
* Priority ordering (drives the rendered list AND dedup-precedence):
|
|
35
|
+
* 1 — explicit (user EXPLICITLY asked to remember)
|
|
36
|
+
* 2 — decision (model identified as a settled decision)
|
|
37
|
+
* 3 — open_item (unfinished work — actionable next time)
|
|
38
|
+
*
|
|
39
|
+
* Dedup rules:
|
|
40
|
+
* - Within the candidate list: same-text (case-fold substring)
|
|
41
|
+
* candidates from multiple sources fold to highest priority.
|
|
42
|
+
* - Against existing durable body: substring-match every candidate
|
|
43
|
+
* against the caller's `existingDurableBody` (case-fold). Skipped
|
|
44
|
+
* candidates count toward `dedupedAgainstExisting` so the caller
|
|
45
|
+
* can surface the dim "N candidates already in durable facts"
|
|
46
|
+
* line per Phase D's Q5 first-run UX.
|
|
47
|
+
*
|
|
48
|
+
* Output cap: 10 candidates max (per Q3). Forces intentionality —
|
|
49
|
+
* sessions with 20+ durable-worthy items signal the user should
|
|
50
|
+
* reconsider what's actually durable.
|
|
51
|
+
*/
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.MAX_CANDIDATES = void 0;
|
|
54
|
+
exports.extractExplicitSignals = extractExplicitSignals;
|
|
55
|
+
exports.extractDistillationCandidates = extractDistillationCandidates;
|
|
56
|
+
exports.extractCandidates = extractCandidates;
|
|
57
|
+
exports.MAX_CANDIDATES = 10;
|
|
58
|
+
// ── Source A: explicit signals ────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Regex set for explicit promotion signals. Each pattern's capture
|
|
61
|
+
* group `[1]` is the phrase the user wants remembered. Anchored on
|
|
62
|
+
* word boundaries so partial-word matches don't fire ("remembering" ≠
|
|
63
|
+
* "remember").
|
|
64
|
+
*
|
|
65
|
+
* Capture semantics — Phase v4.1.2-bug-Z:
|
|
66
|
+
*
|
|
67
|
+
* Previous: capture terminated at any `[.!?\n]` (sentence boundary).
|
|
68
|
+
* That truncated common payloads with periods:
|
|
69
|
+
* - version strings: "gpt-5.5" → captured as "gpt-5"
|
|
70
|
+
* - URLs: "https://api.example.com/v1" → "https://api"
|
|
71
|
+
* - semver: "v1.2.3" → "v1"
|
|
72
|
+
* - filenames: "config.test.ts" → "config"
|
|
73
|
+
*
|
|
74
|
+
* Current: capture rest-of-payload until BLANK-LINE BOUNDARY
|
|
75
|
+
* (`\n\s*\n`) or end-of-string. Regex detects intent (the user said
|
|
76
|
+
* "remember"), not meaning (what counts as a sentence). The `s` flag
|
|
77
|
+
* makes `.` match newlines so multi-line payloads survive a single
|
|
78
|
+
* capture.
|
|
79
|
+
*
|
|
80
|
+
* Trade-off: when multiple markers fire within one sentence-run
|
|
81
|
+
* (no blank-line boundaries), the first-marker capture extends to
|
|
82
|
+
* end-of-payload and the second marker's narrower capture is
|
|
83
|
+
* dedup-folded into it. One candidate covers both facts. Cleanly
|
|
84
|
+
* paragraph-separated multi-markers still split because the
|
|
85
|
+
* blank-line boundary terminates the first capture before the
|
|
86
|
+
* second marker's position. The promotion prompt shows the full
|
|
87
|
+
* capture so the user approves knowingly.
|
|
88
|
+
*/
|
|
89
|
+
// Separator tolerance: between the verb-phrase ("remember that",
|
|
90
|
+
// "save this", "don't forget to") and the fact, accept whitespace
|
|
91
|
+
// AND optional punctuation (`:`, `,`). Some users naturally write
|
|
92
|
+
// "remember that: the port is 4200" or "save this — we use X".
|
|
93
|
+
const SEP = '[\\s:,—-]+';
|
|
94
|
+
// Capture-end: blank line (`\n` + optional whitespace + `\n`) OR
|
|
95
|
+
// end-of-string. The `s` flag lets `.` match newlines so single-marker
|
|
96
|
+
// multi-line payloads stay in one capture.
|
|
97
|
+
const END = '(?:\\n\\s*\\n|$)';
|
|
98
|
+
const EXPLICIT_SIGNAL_PATTERNS = Object.freeze([
|
|
99
|
+
new RegExp(`\\bremember${SEP}(?:that|this)${SEP}(.+?)${END}`, 'gis'),
|
|
100
|
+
new RegExp(`\\bsave${SEP}(?:this|that)${SEP}(?:to memory${SEP})?(.+?)${END}`, 'gis'),
|
|
101
|
+
new RegExp(`\\bfor next time${SEP}(.+?)${END}`, 'gis'),
|
|
102
|
+
new RegExp(`\\bdon'?t forget${SEP}(?:that|to)${SEP}(.+?)${END}`, 'gis'),
|
|
103
|
+
]);
|
|
104
|
+
/**
|
|
105
|
+
* Strip leading filler that often slips past the regex's "that|this"
|
|
106
|
+
* anchor ("that the", "this — "), and trim. Empty / too-short results
|
|
107
|
+
* are signalled by returning the empty string; caller drops them.
|
|
108
|
+
*/
|
|
109
|
+
function cleanCandidateText(raw) {
|
|
110
|
+
let s = raw.trim();
|
|
111
|
+
// Drop leading "that ", "this ", "to " (the regex caught them
|
|
112
|
+
// sometimes when they sat between the verb and the fact).
|
|
113
|
+
s = s.replace(/^(?:that|this|to)\s+/i, '').trim();
|
|
114
|
+
// Drop trailing punctuation noise.
|
|
115
|
+
s = s.replace(/[\s,;:]+$/, '');
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
function extractExplicitSignals(history) {
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const msg of history) {
|
|
121
|
+
if (msg.role !== 'user')
|
|
122
|
+
continue;
|
|
123
|
+
const text = typeof msg.content === 'string' ? msg.content : '';
|
|
124
|
+
if (!text)
|
|
125
|
+
continue;
|
|
126
|
+
for (const pat of EXPLICIT_SIGNAL_PATTERNS) {
|
|
127
|
+
// Recreate per-message so the global flag resets.
|
|
128
|
+
const re = new RegExp(pat.source, pat.flags);
|
|
129
|
+
let m;
|
|
130
|
+
while ((m = re.exec(text)) !== null) {
|
|
131
|
+
const cleaned = cleanCandidateText(m[1] ?? '');
|
|
132
|
+
if (cleaned.length < 4)
|
|
133
|
+
continue; // 1-3 char hits are noise
|
|
134
|
+
out.push({
|
|
135
|
+
text: cleaned,
|
|
136
|
+
source: 'explicit',
|
|
137
|
+
context: text.trim(),
|
|
138
|
+
priority: 1,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
// ── Source B: distillation decisions + open_items ─────────────────────────
|
|
146
|
+
function extractDistillationCandidates(dist) {
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const d of dist.decisions) {
|
|
149
|
+
const t = d.trim();
|
|
150
|
+
if (t.length >= 4) {
|
|
151
|
+
out.push({ text: t, source: 'decision', priority: 2 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const o of dist.open_items) {
|
|
155
|
+
const t = o.trim();
|
|
156
|
+
if (t.length >= 4) {
|
|
157
|
+
out.push({ text: t, source: 'open_item', priority: 3 });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
// ── Dedup + ranking ───────────────────────────────────────────────────────
|
|
163
|
+
function normalize(s) {
|
|
164
|
+
return s.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Within-session dedup: when the same fact surfaces from multiple
|
|
168
|
+
* sources, keep the highest-priority one. Substring-match in both
|
|
169
|
+
* directions so "Aiden runs on port 4200" and "Port 4200 for Aiden"
|
|
170
|
+
* collide on the longer-containing case.
|
|
171
|
+
*/
|
|
172
|
+
function dedupWithinSession(input) {
|
|
173
|
+
const sorted = [...input].sort((a, b) => a.priority - b.priority);
|
|
174
|
+
const kept = [];
|
|
175
|
+
let dropped = 0;
|
|
176
|
+
for (const c of sorted) {
|
|
177
|
+
const normC = normalize(c.text);
|
|
178
|
+
const collision = kept.some((k) => {
|
|
179
|
+
const normK = normalize(k.text);
|
|
180
|
+
return normK.includes(normC) || normC.includes(normK);
|
|
181
|
+
});
|
|
182
|
+
if (collision) {
|
|
183
|
+
dropped += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
kept.push(c);
|
|
187
|
+
}
|
|
188
|
+
return { kept, dropped };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Dedup against existing `## Durable facts` body. Substring-match
|
|
192
|
+
* each candidate against the body (case-fold). Skipped candidates
|
|
193
|
+
* count toward the returned `dropped` so the caller can render the
|
|
194
|
+
* "N already in durable facts" dim line.
|
|
195
|
+
*/
|
|
196
|
+
function dedupAgainstExisting(input, existing) {
|
|
197
|
+
if (!existing.trim())
|
|
198
|
+
return { kept: [...input], dropped: 0 };
|
|
199
|
+
const normExisting = normalize(existing);
|
|
200
|
+
const kept = [];
|
|
201
|
+
let dropped = 0;
|
|
202
|
+
for (const c of input) {
|
|
203
|
+
if (normExisting.includes(normalize(c.text))) {
|
|
204
|
+
dropped += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
kept.push(c);
|
|
208
|
+
}
|
|
209
|
+
return { kept, dropped };
|
|
210
|
+
}
|
|
211
|
+
// ── Public entry point ────────────────────────────────────────────────────
|
|
212
|
+
/**
|
|
213
|
+
* Build the full candidate list. Combines source A + source B,
|
|
214
|
+
* dedups within session, dedups against existing durable body, sorts
|
|
215
|
+
* by priority (stable within priority by source order — explicit
|
|
216
|
+
* signals before decisions before open items), and caps at 10.
|
|
217
|
+
*/
|
|
218
|
+
function extractCandidates(history, distillation, existingDurableBody) {
|
|
219
|
+
const rawA = extractExplicitSignals(history);
|
|
220
|
+
const rawB = extractDistillationCandidates(distillation);
|
|
221
|
+
const totalBeforeDedup = rawA.length + rawB.length;
|
|
222
|
+
const within = dedupWithinSession([...rawA, ...rawB]);
|
|
223
|
+
const against = dedupAgainstExisting(within.kept, existingDurableBody);
|
|
224
|
+
// Stable sort by priority — within a priority tier, preserve insertion
|
|
225
|
+
// order so explicit signals land in chronological message order and
|
|
226
|
+
// decisions land in distillation order.
|
|
227
|
+
const sorted = [...against.kept].sort((a, b) => a.priority - b.priority);
|
|
228
|
+
return {
|
|
229
|
+
candidates: sorted.slice(0, exports.MAX_CANDIDATES),
|
|
230
|
+
dedupedAgainstExisting: against.dropped,
|
|
231
|
+
dedupedWithinSession: within.dropped,
|
|
232
|
+
totalBeforeDedup,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -38,11 +38,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.PromptBuilder = void 0;
|
|
40
40
|
exports.shouldInjectLlama33ToolHint = shouldInjectLlama33ToolHint;
|
|
41
|
+
exports.shouldInjectExecutionDiscipline = shouldInjectExecutionDiscipline;
|
|
41
42
|
const node_fs_1 = require("node:fs");
|
|
42
43
|
const node_os_1 = __importDefault(require("node:os"));
|
|
43
44
|
// When SOUL.md is missing or whitespace-only the bundled default takes
|
|
44
45
|
// over so a fresh install still has a working identity.
|
|
45
46
|
const defaultSoul_1 = require("../../cli/v4/defaultSoul");
|
|
47
|
+
// Phase v4.1.2-followup: runtime-injected version + capabilities slot.
|
|
48
|
+
const capabilities_1 = require("./capabilities");
|
|
46
49
|
// ── Section header / sentinel string contract ─────────────────────────
|
|
47
50
|
//
|
|
48
51
|
// Every literal here is part of the API contract pinned by tests. Header
|
|
@@ -59,6 +62,74 @@ const NOTE_USER_LIVE = '[System note: Treat as live identity, not past conversat
|
|
|
59
62
|
const NOTE_MEMORY_LIVE = '[System note: Treat as live working memory, not past conversation.]';
|
|
60
63
|
const SKILLS_LOAD_NOTE = 'You MUST load it first via the `skill_view` tool before invoking ' +
|
|
61
64
|
'the underlying capability. Skills carry the procedure the tools alone don\'t.';
|
|
65
|
+
/**
|
|
66
|
+
* Phase v4.1.2 alive-core: when the user has authored a real SOUL.md
|
|
67
|
+
* (not the bundled default), prepend a one-line embodiment directive
|
|
68
|
+
* to its content. The directive tells the model to *be* the identity,
|
|
69
|
+
* not narrate about it — closes the most common "stiff generic reply"
|
|
70
|
+
* failure mode where the model paraphrases SOUL.md back at the user.
|
|
71
|
+
*
|
|
72
|
+
* Intentionally suppressed when the identity slot falls back to
|
|
73
|
+
* DEFAULT_SOUL_MD: that text is generic by design and the directive
|
|
74
|
+
* would coach the model to perform a flat persona.
|
|
75
|
+
*/
|
|
76
|
+
const EMBODIMENT_DIRECTIVE = 'Embody this identity and tone. Speak as Aiden, not about Aiden. ' +
|
|
77
|
+
'Avoid generic, stiff replies.';
|
|
78
|
+
/**
|
|
79
|
+
* Phase v4.1.2 alive-core: tool-conditional guidance blocks. Each one
|
|
80
|
+
* is injected only when the corresponding toolset tag is in
|
|
81
|
+
* `opts.toolsetsLoaded`. Replaces the "fixed slot order regardless of
|
|
82
|
+
* capability" assumption — persona shape-shifts per available
|
|
83
|
+
* capability (prior-art pattern surfaced during v4.2 recon).
|
|
84
|
+
*
|
|
85
|
+
* Key match strings:
|
|
86
|
+
* - 'memory' → MEMORY_GUIDANCE
|
|
87
|
+
* - 'session-search' → SESSION_SEARCH_GUIDANCE
|
|
88
|
+
* - 'skills' → SKILLS_GUIDANCE
|
|
89
|
+
*
|
|
90
|
+
* Match the strings in `ToolHandler.toolset` on the registered tools
|
|
91
|
+
* (tools/v4/memory/*.ts ships `toolset: 'memory'`,
|
|
92
|
+
* tools/v4/sessions/sessionSearch.ts ships `toolset: 'session-search'`,
|
|
93
|
+
* skill tools ship `toolset: 'skills'`).
|
|
94
|
+
*/
|
|
95
|
+
const MEMORY_GUIDANCE = [
|
|
96
|
+
'## Persistent memory',
|
|
97
|
+
'',
|
|
98
|
+
'You have persistent memory across sessions. Save durable facts using `memory_add`:',
|
|
99
|
+
'user preferences, environment details, stable conventions. Memory is injected into',
|
|
100
|
+
'every turn; keep it compact and focused on facts that will still matter later.',
|
|
101
|
+
'Prioritize what reduces future user steering.',
|
|
102
|
+
].join('\n');
|
|
103
|
+
const SESSION_SEARCH_GUIDANCE = [
|
|
104
|
+
'## Session recall',
|
|
105
|
+
'',
|
|
106
|
+
'When the user references something from a past conversation or you suspect',
|
|
107
|
+
'relevant cross-session context exists, use `session_search` to recall it before',
|
|
108
|
+
'asking them to repeat themselves.',
|
|
109
|
+
].join('\n');
|
|
110
|
+
const SKILLS_GUIDANCE = [
|
|
111
|
+
'## Skill upkeep',
|
|
112
|
+
'',
|
|
113
|
+
'After completing a complex task (5+ tool calls), fixing a tricky error, or',
|
|
114
|
+
'discovering a non-trivial workflow, save it as a skill so you can reuse it next',
|
|
115
|
+
'time. When using an existing skill and finding it outdated, patch it immediately',
|
|
116
|
+
'— don\'t wait to be asked.',
|
|
117
|
+
].join('\n');
|
|
118
|
+
/**
|
|
119
|
+
* Phase v4.1.2 alive-core: execution-discipline prose. Counters the
|
|
120
|
+
* "I'll run the tests" → no tool call → end-of-turn failure mode by
|
|
121
|
+
* making the contract explicit. Injected when
|
|
122
|
+
* `shouldInjectExecutionDiscipline(modelId)` is true (currently always).
|
|
123
|
+
*/
|
|
124
|
+
const EXECUTION_DISCIPLINE_PROSE = [
|
|
125
|
+
'## Tool use enforcement',
|
|
126
|
+
'',
|
|
127
|
+
'When you say you will perform an action ("I\'ll run the tests", "let me check the',
|
|
128
|
+
'file"), you MUST immediately make the corresponding tool call in the same response.',
|
|
129
|
+
'Never end your turn with a promise of future action — execute it now. Every',
|
|
130
|
+
'response should either contain tool calls that make progress, or deliver a final',
|
|
131
|
+
'result. Responses that only describe intentions without acting are not acceptable.',
|
|
132
|
+
].join('\n');
|
|
62
133
|
/**
|
|
63
134
|
* Llama-3.3-specific tool-call format guard. Adapter-side recovery picks
|
|
64
135
|
* up failures, but we'd rather avoid the 400 round-trip.
|
|
@@ -73,6 +144,16 @@ function shouldInjectLlama33ToolHint(modelId) {
|
|
|
73
144
|
return false;
|
|
74
145
|
return /llama-?3\.3/i.test(modelId);
|
|
75
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Phase v4.1.2: predicate for the execution-discipline prose slot.
|
|
149
|
+
* Currently always-on — the "act, don't narrate" directive helps every
|
|
150
|
+
* tool-using model we route through. Narrow this if a specific model
|
|
151
|
+
* proves counter-productive; better to over-apply a useful prompt than
|
|
152
|
+
* guess incorrectly which models need it.
|
|
153
|
+
*/
|
|
154
|
+
function shouldInjectExecutionDiscipline(_modelId) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
76
157
|
// ── Internal helpers ──────────────────────────────────────────────────
|
|
77
158
|
function detectPlatform() {
|
|
78
159
|
const p = node_os_1.default.platform();
|
|
@@ -171,9 +252,16 @@ class PromptBuilder {
|
|
|
171
252
|
if (!opts.skipFilesystem) {
|
|
172
253
|
identity = await readNonEmpty(opts.paths.soulMd);
|
|
173
254
|
}
|
|
255
|
+
// Phase v4.1.2: track whether the identity came from a real
|
|
256
|
+
// user-authored SOUL.md so the embodiment directive only fires
|
|
257
|
+
// when there's a meaningful persona to embody.
|
|
258
|
+
const identityFromDisk = identity !== null;
|
|
174
259
|
if (!identity)
|
|
175
260
|
identity = defaultSoul_1.DEFAULT_SOUL_MD;
|
|
176
|
-
|
|
261
|
+
const identityContent = identityFromDisk
|
|
262
|
+
? `${EMBODIMENT_DIRECTIVE}\n\n${identity.trim()}`
|
|
263
|
+
: identity.trim();
|
|
264
|
+
slots.push({ name: 'identity', content: identityContent, optional: false });
|
|
177
265
|
// ── 2. Personality overlay ────────────────────────────────────────
|
|
178
266
|
const overlay = opts.personalityOverlay?.trim();
|
|
179
267
|
if (overlay) {
|
|
@@ -197,6 +285,51 @@ class PromptBuilder {
|
|
|
197
285
|
optional: true,
|
|
198
286
|
});
|
|
199
287
|
}
|
|
288
|
+
// ── 4.25. Runtime manifest (self-awareness) ───────────────────────
|
|
289
|
+
// High-signal facts about what Aiden actually has loaded right now:
|
|
290
|
+
// version, tool count, skill count, channel/surface list, current
|
|
291
|
+
// provider/model. Always present so "what version are you" /
|
|
292
|
+
// "what tools do you have" answers come from facts in context,
|
|
293
|
+
// not from whatever stale text used to live in SOUL.md.
|
|
294
|
+
const runtimeManifest = (0, capabilities_1.buildRuntimeManifest)({
|
|
295
|
+
toolCount: opts.toolCount ?? 0,
|
|
296
|
+
skillCount: opts.skillsList?.length ?? 0,
|
|
297
|
+
providerId: opts.providerId,
|
|
298
|
+
modelId: opts.modelId,
|
|
299
|
+
});
|
|
300
|
+
slots.push({
|
|
301
|
+
name: 'runtime',
|
|
302
|
+
content: (0, capabilities_1.renderRuntimeSlot)(runtimeManifest),
|
|
303
|
+
optional: false,
|
|
304
|
+
});
|
|
305
|
+
// ── 4.5. Tool-conditional guidance ────────────────────────────────
|
|
306
|
+
// Each block fires only when its corresponding toolset is loaded.
|
|
307
|
+
// Order is deterministic so the prefix cache stays stable across
|
|
308
|
+
// turns with the same toolset set.
|
|
309
|
+
const toolsets = opts.toolsetsLoaded;
|
|
310
|
+
if (toolsets && toolsets.size > 0) {
|
|
311
|
+
if (toolsets.has('memory')) {
|
|
312
|
+
slots.push({
|
|
313
|
+
name: 'guidance.memory',
|
|
314
|
+
content: MEMORY_GUIDANCE,
|
|
315
|
+
optional: true,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (toolsets.has('session-search')) {
|
|
319
|
+
slots.push({
|
|
320
|
+
name: 'guidance.sessionSearch',
|
|
321
|
+
content: SESSION_SEARCH_GUIDANCE,
|
|
322
|
+
optional: true,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (toolsets.has('skills')) {
|
|
326
|
+
slots.push({
|
|
327
|
+
name: 'guidance.skills',
|
|
328
|
+
content: SKILLS_GUIDANCE,
|
|
329
|
+
optional: true,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
200
333
|
// ── 5. Skills ─────────────────────────────────────────────────────
|
|
201
334
|
if (opts.skillsList && opts.skillsList.length > 0) {
|
|
202
335
|
slots.push({
|
|
@@ -213,6 +346,17 @@ class PromptBuilder {
|
|
|
213
346
|
optional: true,
|
|
214
347
|
});
|
|
215
348
|
}
|
|
349
|
+
// ── 6.5. Execution discipline ─────────────────────────────────────
|
|
350
|
+
// Phase v4.1.2: closes the "promise without acting" failure mode.
|
|
351
|
+
// Model-conditional via shouldInjectExecutionDiscipline so we can
|
|
352
|
+
// narrow later if a specific model proves counter-productive.
|
|
353
|
+
if (shouldInjectExecutionDiscipline(opts.modelId)) {
|
|
354
|
+
slots.push({
|
|
355
|
+
name: 'executionDiscipline',
|
|
356
|
+
content: EXECUTION_DISCIPLINE_PROSE,
|
|
357
|
+
optional: true,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
216
360
|
// ── 7. Iteration budget ───────────────────────────────────────────
|
|
217
361
|
if (opts.initialBudget) {
|
|
218
362
|
const { used, max } = opts.initialBudget;
|