aiden-runtime 4.0.2 → 4.1.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 +19 -11
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +424 -7
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +256 -55
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +297 -13
- package/dist/cli/v4/doctor.js +102 -1
- package/dist/cli/v4/doctorLiveness.js +329 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +118 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +140 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/moat/dangerousPatterns.js +1 -1
- package/dist/providers/v4/codexResponsesAdapter.js +7 -2
- package/dist/providers/v4/errors.js +51 -1
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +190 -0
- package/package.json +11 -2
|
@@ -0,0 +1,118 @@
|
|
|
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/skillMining/extractorPrompt.ts — Phase v4.1-skill-mining
|
|
10
|
+
*
|
|
11
|
+
* Optional LLM refinement pass on top of `proposalBuilder.draft()`.
|
|
12
|
+
* The skeleton SKILL.md the builder produces is already a valid
|
|
13
|
+
* v4 skill; the refiner only polishes prose. Two hard rules:
|
|
14
|
+
*
|
|
15
|
+
* 1. Structural fields are sacred — name, description, version,
|
|
16
|
+
* metadata.aiden.* must round-trip unchanged. We re-parse the
|
|
17
|
+
* refined output and discard it if any required field drifts.
|
|
18
|
+
*
|
|
19
|
+
* 2. Never write attribution tokens or "portions adapted from..." /
|
|
20
|
+
* "original copyright" strings. The permanent attribution
|
|
21
|
+
* sweep validates this; if a refined output contains any
|
|
22
|
+
* forbidden token, we fall back to the skeleton.
|
|
23
|
+
*
|
|
24
|
+
* If the auxiliary client is unavailable, the call times out, or
|
|
25
|
+
* the refined output fails validation, the function returns the
|
|
26
|
+
* skeleton unchanged. Mining works fully offline — refinement is
|
|
27
|
+
* best-effort polish.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.refine = refine;
|
|
31
|
+
const skillSpec_1 = require("../skillSpec");
|
|
32
|
+
const BANNED_TOKENS = [
|
|
33
|
+
'portions adapted from',
|
|
34
|
+
'original copyright',
|
|
35
|
+
'derived from',
|
|
36
|
+
'based on the',
|
|
37
|
+
'adapted from',
|
|
38
|
+
];
|
|
39
|
+
const FORBIDDEN_TOKENS_RE = new RegExp(`\\b(${BANNED_TOKENS.join('|')})\\b`, 'i');
|
|
40
|
+
const REFINER_SYSTEM_PROMPT = `
|
|
41
|
+
You polish auto-generated skill markdown for a local-first AI agent.
|
|
42
|
+
|
|
43
|
+
Your job is to improve the WORDING ONLY of an already-valid SKILL.md
|
|
44
|
+
file. The frontmatter (everything between the leading "---" markers)
|
|
45
|
+
must round-trip BYTE-FOR-BYTE unchanged. The "# <name>" heading must
|
|
46
|
+
stay first in the body. The numbered "## Steps" list must remain
|
|
47
|
+
numbered and in the same order; you may rephrase step descriptions
|
|
48
|
+
but must NOT add or remove steps.
|
|
49
|
+
|
|
50
|
+
Hard rules:
|
|
51
|
+
- Output the COMPLETE SKILL.md, not a diff.
|
|
52
|
+
- Do not add boilerplate, citations, or attribution to any other
|
|
53
|
+
agent or codebase. The skill is 100% the user's own.
|
|
54
|
+
- Do not introduce mock/fake values into commands.
|
|
55
|
+
- Keep total length under 6000 characters.
|
|
56
|
+
`.trim();
|
|
57
|
+
/**
|
|
58
|
+
* Refine `skeleton` via the auxiliary client. Always returns a
|
|
59
|
+
* valid SKILL.md string — either the refined output (if it passes
|
|
60
|
+
* round-trip + no-attribution validation) or the original skeleton.
|
|
61
|
+
*/
|
|
62
|
+
async function refine(skeleton, opts = {}) {
|
|
63
|
+
const client = opts.client;
|
|
64
|
+
if (!client || client.isUnavailable()) {
|
|
65
|
+
return skeleton;
|
|
66
|
+
}
|
|
67
|
+
// Parse the skeleton up front to lock the canonical frontmatter
|
|
68
|
+
// shape we'll require the refined output to match.
|
|
69
|
+
let skeletonParsed;
|
|
70
|
+
try {
|
|
71
|
+
skeletonParsed = (0, skillSpec_1.parseSkillContent)(skeleton);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// If the skeleton itself is invalid, refining can only make it
|
|
75
|
+
// worse; bubble the skeleton out and let the caller decide.
|
|
76
|
+
return skeleton;
|
|
77
|
+
}
|
|
78
|
+
const prompt = `${REFINER_SYSTEM_PROMPT}\n\n` +
|
|
79
|
+
`INPUT SKILL.md:\n\`\`\`\n${skeleton}\n\`\`\`\n\n` +
|
|
80
|
+
`Output the refined SKILL.md only — no commentary, no code fences.`;
|
|
81
|
+
let refined;
|
|
82
|
+
try {
|
|
83
|
+
const result = await client.call({
|
|
84
|
+
purpose: 'skill_describe',
|
|
85
|
+
prompt,
|
|
86
|
+
maxTokens: opts.maxTokens ?? 1500,
|
|
87
|
+
timeoutMs: opts.timeoutMs ?? 20000,
|
|
88
|
+
});
|
|
89
|
+
refined = (result.content ?? '').trim();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return skeleton;
|
|
93
|
+
}
|
|
94
|
+
if (refined.length === 0)
|
|
95
|
+
return skeleton;
|
|
96
|
+
// Strip a wrapping ```...``` if the model returned one.
|
|
97
|
+
refined = refined.replace(/^```(?:markdown|md)?\s*\n/, '').replace(/\n```\s*$/, '');
|
|
98
|
+
// Validation 1 — attribution sweep. Refined output must not
|
|
99
|
+
// contain any forbidden token. The permanent sweep would catch
|
|
100
|
+
// this at ship time; we catch it earlier so a single noisy
|
|
101
|
+
// refinement doesn't pollute the candidate queue.
|
|
102
|
+
if (FORBIDDEN_TOKENS_RE.test(refined))
|
|
103
|
+
return skeleton;
|
|
104
|
+
// Validation 2 — round-trip through parseSkillContent and verify
|
|
105
|
+
// the canonical fields match the skeleton.
|
|
106
|
+
let refinedParsed;
|
|
107
|
+
try {
|
|
108
|
+
refinedParsed = (0, skillSpec_1.parseSkillContent)(refined);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return skeleton;
|
|
112
|
+
}
|
|
113
|
+
if (refinedParsed.frontmatter.name !== skeletonParsed.frontmatter.name ||
|
|
114
|
+
refinedParsed.frontmatter.version !== skeletonParsed.frontmatter.version) {
|
|
115
|
+
return skeleton;
|
|
116
|
+
}
|
|
117
|
+
return refined;
|
|
118
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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/skillMining/proposalBuilder.ts — Phase v4.1-skill-mining
|
|
10
|
+
*
|
|
11
|
+
* Pure-function skeleton SKILL.md generator from a successful tool-
|
|
12
|
+
* call trace. Used by skillMiner BEFORE optional LLM refinement —
|
|
13
|
+
* the skeleton must already be a valid v4 skill (parseSkillContent
|
|
14
|
+
* round-trips), so that mining works offline when the auxiliary
|
|
15
|
+
* client is unavailable.
|
|
16
|
+
*
|
|
17
|
+
* Invariants:
|
|
18
|
+
* - emits required fields `name`, `description`, `version`
|
|
19
|
+
* - emits `metadata.aiden` with the mining provenance fields
|
|
20
|
+
* - body is numbered tool-call steps in markdown
|
|
21
|
+
* - never writes attribution strings — the banned-token regex
|
|
22
|
+
* strips them at extraction time and the permanent attribution
|
|
23
|
+
* sweep validates the result
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.deriveName = deriveName;
|
|
27
|
+
exports.deriveDescription = deriveDescription;
|
|
28
|
+
exports.draft = draft;
|
|
29
|
+
const STOP_WORDS = new Set([
|
|
30
|
+
'the', 'a', 'an', 'to', 'for', 'of', 'on', 'at', 'in', 'by', 'and', 'or', 'but', 'with',
|
|
31
|
+
'from', 'please', 'can', 'you', 'will', 'do', 'does', 'my', 'our', 'this', 'that', 'these',
|
|
32
|
+
'is', 'are', 'be', 'was', 'were', 'it', 'its', 'as', 'if', 'then', 'else', 'some', 'any',
|
|
33
|
+
'me', 'i', 'your', 'their', 'his', 'her', 'him',
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Derive a kebab-case skill-name from the first user message.
|
|
37
|
+
* Conservative — keeps only [a-z0-9-], collapses runs, max 40 chars.
|
|
38
|
+
* Falls back to `learned-skill-<short-fingerprint>` if extraction
|
|
39
|
+
* yields nothing usable.
|
|
40
|
+
*/
|
|
41
|
+
function deriveName(firstUserPrompt, fingerprint) {
|
|
42
|
+
const lowered = firstUserPrompt.toLowerCase();
|
|
43
|
+
// Pull out non-stopword tokens, max 5 of them.
|
|
44
|
+
const tokens = lowered
|
|
45
|
+
.split(/[^a-z0-9]+/)
|
|
46
|
+
.filter((t) => t.length >= 3 && !STOP_WORDS.has(t))
|
|
47
|
+
.slice(0, 5);
|
|
48
|
+
let stem = tokens.join('-');
|
|
49
|
+
// Strict alphanum-and-dash, collapse multiple dashes, trim leading/trailing.
|
|
50
|
+
stem = stem.replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
51
|
+
if (stem.length === 0) {
|
|
52
|
+
return `learned-skill-${fingerprint.slice(0, 8)}`;
|
|
53
|
+
}
|
|
54
|
+
return stem.slice(0, 40);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the description line — single sentence summarising what the
|
|
58
|
+
* trace did. Keep it under 200 chars (the skill loader caps the
|
|
59
|
+
* displayed description in /skills list).
|
|
60
|
+
*/
|
|
61
|
+
function deriveDescription(trace) {
|
|
62
|
+
if (trace.length === 0)
|
|
63
|
+
return 'Learned workflow.';
|
|
64
|
+
const tools = trace.map((e) => e.name).filter(Boolean);
|
|
65
|
+
const distinct = Array.from(new Set(tools));
|
|
66
|
+
if (distinct.length === 1) {
|
|
67
|
+
return `Learned workflow: ${distinct[0]} (${tools.length}x).`;
|
|
68
|
+
}
|
|
69
|
+
const head = distinct.slice(0, 4).join(' → ');
|
|
70
|
+
const more = distinct.length > 4 ? ` (+${distinct.length - 4} more)` : '';
|
|
71
|
+
return `Learned workflow: ${head}${more}.`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render the body as numbered tool-call steps. Args are JSON-stringified
|
|
75
|
+
* and clipped at 120 chars per step so a chatty trace doesn't bloat
|
|
76
|
+
* SKILL.md to multi-MB.
|
|
77
|
+
*/
|
|
78
|
+
function renderBody(trace) {
|
|
79
|
+
if (trace.length === 0) {
|
|
80
|
+
return '## Steps\n\n_(empty trace)_\n';
|
|
81
|
+
}
|
|
82
|
+
const out = ['## Steps', ''];
|
|
83
|
+
trace.forEach((entry, i) => {
|
|
84
|
+
const idx = i + 1;
|
|
85
|
+
const argSummary = (() => {
|
|
86
|
+
if (entry.args == null || typeof entry.args !== 'object')
|
|
87
|
+
return '';
|
|
88
|
+
try {
|
|
89
|
+
const json = JSON.stringify(entry.args);
|
|
90
|
+
if (json.length <= 120)
|
|
91
|
+
return ` \\\`${json}\\\``;
|
|
92
|
+
return ` \\\`${json.slice(0, 117)}…\\\``;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
out.push(`${idx}. **${entry.name}**${argSummary}`);
|
|
99
|
+
});
|
|
100
|
+
out.push('');
|
|
101
|
+
out.push('## Notes', '');
|
|
102
|
+
out.push('Mined from a successful turn — review the steps above before relying on it.');
|
|
103
|
+
out.push('');
|
|
104
|
+
return out.join('\n');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build a full SKILL.md (frontmatter + body) ready for parseSkillContent.
|
|
108
|
+
* The output is deterministic for a given (trace, context) input —
|
|
109
|
+
* smokes assert this for stable round-trip behaviour.
|
|
110
|
+
*/
|
|
111
|
+
function draft(trace, ctx) {
|
|
112
|
+
const name = ctx.nameOverride?.trim() || deriveName(ctx.firstUserPrompt, ctx.traceFingerprint);
|
|
113
|
+
const description = deriveDescription(trace);
|
|
114
|
+
const version = '0.1.0';
|
|
115
|
+
const createdAt = new Date().toISOString();
|
|
116
|
+
// YAML frontmatter — keep field order stable so smokes can pin
|
|
117
|
+
// shape without depending on YAML library output ordering.
|
|
118
|
+
const frontmatter = [
|
|
119
|
+
'---',
|
|
120
|
+
`name: ${name}`,
|
|
121
|
+
`description: ${JSON.stringify(description)}`,
|
|
122
|
+
`version: ${version}`,
|
|
123
|
+
'category: learned',
|
|
124
|
+
'metadata:',
|
|
125
|
+
' aiden:',
|
|
126
|
+
' learned: true',
|
|
127
|
+
` sourceSessionId: ${JSON.stringify(ctx.sourceSessionId)}`,
|
|
128
|
+
` sourceTurnIdx: ${ctx.sourceTurnIdx}`,
|
|
129
|
+
` createdAt: ${JSON.stringify(createdAt)}`,
|
|
130
|
+
` traceFingerprint: ${JSON.stringify(ctx.traceFingerprint)}`,
|
|
131
|
+
` candidateConfidence: ${ctx.candidateConfidence.toFixed(3)}`,
|
|
132
|
+
'---',
|
|
133
|
+
'',
|
|
134
|
+
`# ${name}`,
|
|
135
|
+
'',
|
|
136
|
+
description,
|
|
137
|
+
'',
|
|
138
|
+
].join('\n');
|
|
139
|
+
return frontmatter + renderBody(trace);
|
|
140
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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/skillMining/skillMiner.ts — Phase v4.1-skill-mining
|
|
10
|
+
*
|
|
11
|
+
* Orchestrator for the skill-mining post-turn observation. Sits next
|
|
12
|
+
* to `moat/skillTeacher.ts:observeTurn` in the agent loop hook
|
|
13
|
+
* (`core/v4/aidenAgent.ts:440-468`); the two are complementary.
|
|
14
|
+
* SkillTeacher proposes inline (immediate accept/reject); SkillMiner
|
|
15
|
+
* stages a candidate for `/skills review` so the user can audit
|
|
16
|
+
* before any disk-mutation lands in the live skills directory.
|
|
17
|
+
*
|
|
18
|
+
* Programmatic gates (in order — first failure short-circuits):
|
|
19
|
+
* 1. trace length < 3 → skip
|
|
20
|
+
* 2. any tool errored → skip
|
|
21
|
+
* 3. finishReason !== 'stop' → skip
|
|
22
|
+
* 4. session candidate count >= SESSION_SKILL_LIMIT → skip
|
|
23
|
+
* 5. user opt-out phrase in conversation → skip (reuse OPT_OUT_RE)
|
|
24
|
+
* 6. fingerprint matches pending candidate → skip (dedup)
|
|
25
|
+
* 7. fingerprint matches rejected list → skip (dedup)
|
|
26
|
+
*
|
|
27
|
+
* Pass-through pipeline:
|
|
28
|
+
* - compute confidence score from programmatic features
|
|
29
|
+
* - draft skeleton via proposalBuilder
|
|
30
|
+
* - refine via extractorPrompt (best-effort; falls back to
|
|
31
|
+
* skeleton)
|
|
32
|
+
* - validate via parseSkillContent round-trip
|
|
33
|
+
* - append to candidateStore
|
|
34
|
+
* - return ObservationResult so chatSession can notify
|
|
35
|
+
*
|
|
36
|
+
* MCP serve mode: caller (aidenAgent) MUST gate on
|
|
37
|
+
* !isMcpServeMode() before invoking observeTurn — the mining
|
|
38
|
+
* subsystem itself doesn't write to stdout but a candidate
|
|
39
|
+
* notification would, and serve mode owns stdout for JSON-RPC.
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.SkillMiner = exports.SESSION_SKILL_LIMIT = void 0;
|
|
43
|
+
exports.computeConfidence = computeConfidence;
|
|
44
|
+
const node_crypto_1 = require("node:crypto");
|
|
45
|
+
const skillSpec_1 = require("../skillSpec");
|
|
46
|
+
const candidateStore_1 = require("./candidateStore");
|
|
47
|
+
const traceFingerprint_1 = require("./traceFingerprint");
|
|
48
|
+
const proposalBuilder_1 = require("./proposalBuilder");
|
|
49
|
+
const extractorPrompt_1 = require("./extractorPrompt");
|
|
50
|
+
/** Per-session candidate cap — port from v3's `SESSION_SKILL_LIMIT`. */
|
|
51
|
+
exports.SESSION_SKILL_LIMIT = 2;
|
|
52
|
+
/** Minimum trace length to consider mining at all. */
|
|
53
|
+
const MIN_TRACE_LENGTH = 3;
|
|
54
|
+
/**
|
|
55
|
+
* Opt-out phrases that suppress mining for the current turn. Match
|
|
56
|
+
* the SkillTeacher pattern at `moat/skillTeacher.ts:110` so a user
|
|
57
|
+
* who silences SkillTeacher also silences mining.
|
|
58
|
+
*/
|
|
59
|
+
const OPT_OUT_RE = /\b(stop|don['']?t|no|never)\s+(suggest|propose|create|save|learn|remember)\w*\b/i;
|
|
60
|
+
/**
|
|
61
|
+
* Compute a 0..1 confidence score from programmatic trace features.
|
|
62
|
+
* Used to sort `/skills review` so the most promising candidates
|
|
63
|
+
* surface first.
|
|
64
|
+
*
|
|
65
|
+
* Components:
|
|
66
|
+
* - lengthScore : trace length sweet spot is 5..15 (peaks at 10)
|
|
67
|
+
* - errorRate : already gated to 0 (no errors in trace) but the
|
|
68
|
+
* feature is computed for forward-compat with
|
|
69
|
+
* future "soft" mining that admits some errors
|
|
70
|
+
* - distinctSet : more distinct tools = more reusable workflow
|
|
71
|
+
* - distinctTools: simple toolset diversity
|
|
72
|
+
*/
|
|
73
|
+
function computeConfidence(trace) {
|
|
74
|
+
if (trace.length === 0)
|
|
75
|
+
return 0;
|
|
76
|
+
// Length score — peaks at 10, falls off at extremes.
|
|
77
|
+
const len = trace.length;
|
|
78
|
+
const lengthScore = len < 3 ? 0 :
|
|
79
|
+
len <= 10 ? len / 10 :
|
|
80
|
+
len <= 15 ? 1 - (len - 10) * 0.04 :
|
|
81
|
+
Math.max(0.4, 1 - (len - 10) * 0.05);
|
|
82
|
+
// Error rate (0 = best, 1 = worst).
|
|
83
|
+
const errors = trace.filter((e) => e.error != null).length;
|
|
84
|
+
const errorRate = errors / trace.length;
|
|
85
|
+
// Distinct tool diversity.
|
|
86
|
+
const distinctTools = new Set(trace.map((e) => e.name)).size;
|
|
87
|
+
const diversityScore = Math.min(1, distinctTools / 4);
|
|
88
|
+
// Distinct toolsets diversity (some traces tag entries).
|
|
89
|
+
const distinctSets = new Set(trace.map((e) => e.toolset ?? '').filter(Boolean)).size;
|
|
90
|
+
const setBonus = Math.min(0.2, distinctSets * 0.1);
|
|
91
|
+
// Weighted average — length and diversity dominate, error penalty
|
|
92
|
+
// proportional to rate.
|
|
93
|
+
const raw = 0.55 * lengthScore + 0.35 * diversityScore + setBonus - errorRate;
|
|
94
|
+
return Math.max(0, Math.min(1, Number(raw.toFixed(3))));
|
|
95
|
+
}
|
|
96
|
+
/** Single-trace orchestrator. Stateless except for the per-session counter. */
|
|
97
|
+
class SkillMiner {
|
|
98
|
+
constructor(opts = {}) {
|
|
99
|
+
this.perSessionCount = new Map();
|
|
100
|
+
this.store = opts.store ?? new candidateStore_1.CandidateStore();
|
|
101
|
+
this.auxiliaryClient = opts.auxiliaryClient;
|
|
102
|
+
this.sessionCap = opts.sessionCap ?? exports.SESSION_SKILL_LIMIT;
|
|
103
|
+
this.skeletonOnly = opts.skeletonOnly ?? false;
|
|
104
|
+
}
|
|
105
|
+
/** Test/reset hook. */
|
|
106
|
+
_resetForTests() {
|
|
107
|
+
this.perSessionCount.clear();
|
|
108
|
+
}
|
|
109
|
+
/** Returns the count of pending candidates already attributed to a session. */
|
|
110
|
+
countForSession(sessionId) {
|
|
111
|
+
return this.perSessionCount.get(sessionId) ?? 0;
|
|
112
|
+
}
|
|
113
|
+
async observeTurn(obs) {
|
|
114
|
+
// Gate 1 — short trace.
|
|
115
|
+
if (!obs.trace || obs.trace.length < MIN_TRACE_LENGTH) {
|
|
116
|
+
return { status: 'short-trace' };
|
|
117
|
+
}
|
|
118
|
+
// Gate 2 — any tool errored.
|
|
119
|
+
if (obs.trace.some((e) => e.error != null)) {
|
|
120
|
+
return { status: 'tool-error' };
|
|
121
|
+
}
|
|
122
|
+
// Gate 3 — turn was aborted.
|
|
123
|
+
if (obs.finishReason !== 'stop') {
|
|
124
|
+
return { status: 'abort' };
|
|
125
|
+
}
|
|
126
|
+
// Gate 4 — session cap.
|
|
127
|
+
if (this.countForSession(obs.sessionId) >= this.sessionCap) {
|
|
128
|
+
return { status: 'session-cap' };
|
|
129
|
+
}
|
|
130
|
+
// Gate 5 — user opt-out anywhere in this turn's conversation.
|
|
131
|
+
for (const msg of obs.history) {
|
|
132
|
+
if (msg.role === 'user' && typeof msg.content === 'string' && OPT_OUT_RE.test(msg.content)) {
|
|
133
|
+
return { status: 'opt-out' };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Fingerprint + dedup.
|
|
137
|
+
const fpEntries = obs.trace.map((e) => ({ name: e.name, args: e.args }));
|
|
138
|
+
const fingerprint = (0, traceFingerprint_1.traceFingerprint)(fpEntries);
|
|
139
|
+
const pending = await this.store.list();
|
|
140
|
+
if (pending.some((c) => c.fingerprint === fingerprint)) {
|
|
141
|
+
return { status: 'dedup-pending' };
|
|
142
|
+
}
|
|
143
|
+
const rejected = await this.store.loadRejected();
|
|
144
|
+
if (rejected.has(fingerprint)) {
|
|
145
|
+
return { status: 'dedup-rejected' };
|
|
146
|
+
}
|
|
147
|
+
// Compute confidence + first user prompt for skeleton seeding.
|
|
148
|
+
const confidence = computeConfidence(obs.trace);
|
|
149
|
+
const firstUserPrompt = (() => {
|
|
150
|
+
for (const m of obs.history) {
|
|
151
|
+
if (m.role === 'user' && typeof m.content === 'string' && m.content.trim().length > 0) {
|
|
152
|
+
return m.content.trim();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
})();
|
|
157
|
+
const ctx = {
|
|
158
|
+
firstUserPrompt,
|
|
159
|
+
sourceSessionId: obs.sessionId,
|
|
160
|
+
sourceTurnIdx: obs.sourceTurnIdx,
|
|
161
|
+
traceFingerprint: fingerprint,
|
|
162
|
+
candidateConfidence: confidence,
|
|
163
|
+
};
|
|
164
|
+
let skill = (0, proposalBuilder_1.draft)(obs.trace, ctx);
|
|
165
|
+
if (!this.skeletonOnly) {
|
|
166
|
+
skill = await (0, extractorPrompt_1.refine)(skill, { client: this.auxiliaryClient });
|
|
167
|
+
}
|
|
168
|
+
// Final validation — must round-trip through parseSkillContent
|
|
169
|
+
// (the canonical loader parser). If it doesn't, drop the
|
|
170
|
+
// candidate rather than poison the queue.
|
|
171
|
+
try {
|
|
172
|
+
(0, skillSpec_1.parseSkillContent)(skill);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return { status: 'invalid-skill', confidence };
|
|
176
|
+
}
|
|
177
|
+
const candidate = {
|
|
178
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
179
|
+
fingerprint,
|
|
180
|
+
sourceSessionId: obs.sessionId,
|
|
181
|
+
sourceTurnIdx: obs.sourceTurnIdx,
|
|
182
|
+
createdAt: new Date().toISOString(),
|
|
183
|
+
candidateConfidence: confidence,
|
|
184
|
+
skillContent: skill,
|
|
185
|
+
};
|
|
186
|
+
await this.store.append(candidate);
|
|
187
|
+
this.perSessionCount.set(obs.sessionId, this.countForSession(obs.sessionId) + 1);
|
|
188
|
+
return { status: 'queued', candidate, confidence };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.SkillMiner = SkillMiner;
|
|
@@ -0,0 +1,51 @@
|
|
|
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/skillMining/traceFingerprint.ts — Phase v4.1-skill-mining
|
|
10
|
+
*
|
|
11
|
+
* Deterministic content-addressing for tool-call traces.
|
|
12
|
+
*
|
|
13
|
+
* The fingerprint is the sha256 hex of the normalized
|
|
14
|
+
* (toolName, sorted-arg-keys) sequence joined by `|`.
|
|
15
|
+
*
|
|
16
|
+
* Properties (verified by smoke):
|
|
17
|
+
* - identical traces produce identical hashes
|
|
18
|
+
* - traces that differ only in arg *values* (same arg keys)
|
|
19
|
+
* produce identical hashes — this is the desired behavior:
|
|
20
|
+
* "search github for X" and "search github for Y" should
|
|
21
|
+
* dedup to one candidate
|
|
22
|
+
* - traces with different tool sequences or arg keys produce
|
|
23
|
+
* different hashes
|
|
24
|
+
* - deterministic across runs (no salt, no time)
|
|
25
|
+
*
|
|
26
|
+
* The candidateStore + skillMiner use this to dedup proposals;
|
|
27
|
+
* the rejected list also tracks fingerprints so a user-rejected
|
|
28
|
+
* workflow doesn't get re-proposed on the next run.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.traceFingerprint = traceFingerprint;
|
|
32
|
+
const node_crypto_1 = require("node:crypto");
|
|
33
|
+
/** Normalize a single trace entry to its fingerprint contribution. */
|
|
34
|
+
function normalizeEntry(entry) {
|
|
35
|
+
const name = String(entry.name ?? '').trim().toLowerCase();
|
|
36
|
+
let argKeys = [];
|
|
37
|
+
if (entry.args && typeof entry.args === 'object' && !Array.isArray(entry.args)) {
|
|
38
|
+
argKeys = Object.keys(entry.args).sort();
|
|
39
|
+
}
|
|
40
|
+
return `${name}(${argKeys.join(',')})`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fingerprint a trace. Returns a 64-char lowercase sha256 hex.
|
|
44
|
+
* Empty trace is a valid input (returns the hash of the empty
|
|
45
|
+
* normalized string) — callers should reject empty traces before
|
|
46
|
+
* fingerprinting to keep the pending queue free of useless entries.
|
|
47
|
+
*/
|
|
48
|
+
function traceFingerprint(trace) {
|
|
49
|
+
const normalized = trace.map(normalizeEntry).join('|');
|
|
50
|
+
return (0, node_crypto_1.createHash)('sha256').update(normalized, 'utf8').digest('hex');
|
|
51
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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/subagent/budget.ts — Phase v4.1-subagent
|
|
10
|
+
*
|
|
11
|
+
* Per-subagent timeouts and iteration caps. Two budgets layered:
|
|
12
|
+
*
|
|
13
|
+
* - `perSubagentTimeoutMs` — hard wall-clock cap on a single
|
|
14
|
+
* subagent. Fired via AbortController; AidenAgent's provider
|
|
15
|
+
* adapter receives the abort and the in-flight HTTP call is
|
|
16
|
+
* cancelled (the v3 lesson — flag-only cancellation leaks
|
|
17
|
+
* tokens, AbortController plumbed through is the v4 fix).
|
|
18
|
+
*
|
|
19
|
+
* - `wallClockCapMs` — outer cap on the whole fanout. Defaults
|
|
20
|
+
* to 5× the per-subagent timeout because parallel subagents
|
|
21
|
+
* should finish faster than 5× one-at-a-time, but variance
|
|
22
|
+
* (provider rate limits, retry backoff) can extend the tail.
|
|
23
|
+
*
|
|
24
|
+
* - `maxIterations` — fresh per subagent. v3 starved nested
|
|
25
|
+
* spawns by dividing a global budget; v4 hands each subagent a
|
|
26
|
+
* full fresh budget and relies on the wall-clock cap for the
|
|
27
|
+
* outer bound.
|
|
28
|
+
*
|
|
29
|
+
* Read at fanout start. Each subagent gets its own AbortSignal
|
|
30
|
+
* derived from the timeout; abort propagates from parent down via
|
|
31
|
+
* `parentAbort.aborted`.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.DEFAULT_FANOUT_N = exports.MAX_FANOUT_N = exports.WALL_CLOCK_CAP_MULT = exports.DEFAULT_SUBAGENT_MAX_ITERATIONS = exports.DEFAULT_SUBAGENT_TIMEOUT_MS = void 0;
|
|
35
|
+
exports.resolveBudget = resolveBudget;
|
|
36
|
+
exports.validateN = validateN;
|
|
37
|
+
/** Default per-subagent timeout (ms). Override via env
|
|
38
|
+
* `AIDEN_SUBAGENT_TIMEOUT_MS` or `timeoutMs` argument on the tool. */
|
|
39
|
+
exports.DEFAULT_SUBAGENT_TIMEOUT_MS = 90000;
|
|
40
|
+
/** Default per-subagent iteration cap. Fresh per subagent. */
|
|
41
|
+
exports.DEFAULT_SUBAGENT_MAX_ITERATIONS = 20;
|
|
42
|
+
/** Wall-clock cap multiplier — outer cap = `perSubagentTimeoutMs * MULT`. */
|
|
43
|
+
exports.WALL_CLOCK_CAP_MULT = 5;
|
|
44
|
+
/** Max N — hard refuse beyond. */
|
|
45
|
+
exports.MAX_FANOUT_N = 5;
|
|
46
|
+
/** Default N when the caller doesn't specify. */
|
|
47
|
+
exports.DEFAULT_FANOUT_N = 3;
|
|
48
|
+
/** Resolve the live budget. Tool-call argument > env > module default. */
|
|
49
|
+
function resolveBudget(opts = {}) {
|
|
50
|
+
const env = opts.env ?? process.env;
|
|
51
|
+
const envTimeoutRaw = env.AIDEN_SUBAGENT_TIMEOUT_MS;
|
|
52
|
+
const envTimeout = envTimeoutRaw && /^\d+$/.test(envTimeoutRaw)
|
|
53
|
+
? Number.parseInt(envTimeoutRaw, 10)
|
|
54
|
+
: null;
|
|
55
|
+
const perSubagentTimeoutMs = opts.timeoutMs ?? envTimeout ?? exports.DEFAULT_SUBAGENT_TIMEOUT_MS;
|
|
56
|
+
return {
|
|
57
|
+
perSubagentTimeoutMs,
|
|
58
|
+
wallClockCapMs: perSubagentTimeoutMs * exports.WALL_CLOCK_CAP_MULT,
|
|
59
|
+
maxIterations: exports.DEFAULT_SUBAGENT_MAX_ITERATIONS,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Validate the requested N — throws with a clear message when out of
|
|
63
|
+
* bounds. Caller surfaces the error as a tool-result error string. */
|
|
64
|
+
function validateN(n) {
|
|
65
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
66
|
+
throw new Error(`subagent_fanout: n must be an integer, got ${n}`);
|
|
67
|
+
}
|
|
68
|
+
if (n < 1) {
|
|
69
|
+
throw new Error(`subagent_fanout: n must be >= 1, got ${n}`);
|
|
70
|
+
}
|
|
71
|
+
if (n > exports.MAX_FANOUT_N) {
|
|
72
|
+
throw new Error(`subagent_fanout: n=${n} exceeds hard cap ${exports.MAX_FANOUT_N}. ` +
|
|
73
|
+
`Higher concurrency hits provider RPM limits and increases tail latency variance.`);
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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/subagent/diagnostics.ts — Phase v4.1-subagent
|
|
10
|
+
*
|
|
11
|
+
* Build fingerprint + counts surfaced by `aiden subagent status` and
|
|
12
|
+
* the per-fanout result envelope. The fingerprint follows the same
|
|
13
|
+
* convention every Aiden phase since v4.1-3.2 has used: a constant
|
|
14
|
+
* string the user can grep for to verify the running build matches
|
|
15
|
+
* the phase they expected. Bump on every shipped phase. Format:
|
|
16
|
+
* `v4.1-subagent[+suffix]`.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.AIDEN_SUBAGENT_BUILD = void 0;
|
|
20
|
+
/** Build fingerprint — bump per phase. Surfaced in `aiden subagent
|
|
21
|
+
* status` and the post-fanout summary line. */
|
|
22
|
+
exports.AIDEN_SUBAGENT_BUILD = 'v4.1-subagent.2';
|