chainlesschain 0.45.74 → 0.45.76

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.
Files changed (41) hide show
  1. package/README.md +52 -15
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-BhJ3YFWt.js → AppLayout-2RCrdXxl.js} +1 -1
  5. package/src/assets/web-panel/assets/AppLayout-D9pBLPC3.css +1 -0
  6. package/src/assets/web-panel/assets/{Chat-DaxTP3x8.js → Chat-B2nB8o_F.js} +1 -1
  7. package/src/assets/web-panel/assets/{Dashboard-CjlX4CrX.js → Dashboard-DanoHPSI.js} +1 -1
  8. package/src/assets/web-panel/assets/{Skills-BCvgBkD3.js → Skills-CLlblJcG.js} +1 -1
  9. package/src/assets/web-panel/assets/chat-DWBA4-cl.js +1 -0
  10. package/src/assets/web-panel/assets/{index-DrmEk9S3.js → index-CyGtHm63.js} +2 -2
  11. package/src/assets/web-panel/index.html +1 -1
  12. package/src/commands/learning.js +273 -0
  13. package/src/commands/lowcode.js +23 -8
  14. package/src/gateways/discord/discord-formatter.js +89 -0
  15. package/src/gateways/gateway-base.js +189 -0
  16. package/src/gateways/telegram/telegram-formatter.js +93 -0
  17. package/src/index.js +2 -0
  18. package/src/lib/app-builder.js +136 -8
  19. package/src/lib/autonomous-agent.js +8 -1
  20. package/src/lib/cli-context-engineering.js +15 -0
  21. package/src/lib/execution-backend.js +239 -0
  22. package/src/lib/hook-manager.js +2 -0
  23. package/src/lib/iteration-budget.js +175 -0
  24. package/src/lib/learning/learning-hooks.js +117 -0
  25. package/src/lib/learning/learning-tables.js +66 -0
  26. package/src/lib/learning/outcome-feedback.js +243 -0
  27. package/src/lib/learning/reflection-engine.js +323 -0
  28. package/src/lib/learning/skill-improver.js +536 -0
  29. package/src/lib/learning/skill-synthesizer.js +315 -0
  30. package/src/lib/learning/trajectory-store.js +409 -0
  31. package/src/lib/plugin-autodiscovery.js +224 -0
  32. package/src/lib/session-search.js +193 -0
  33. package/src/lib/sub-agent-context.js +7 -2
  34. package/src/lib/user-profile.js +172 -0
  35. package/src/lib/web-ui-server.js +1 -1
  36. package/src/repl/agent-repl.js +109 -0
  37. package/src/runtime/agent-core.js +75 -4
  38. package/src/runtime/coding-agent-contract-shared.cjs +35 -0
  39. package/src/runtime/coding-agent-policy.cjs +10 -0
  40. package/src/assets/web-panel/assets/AppLayout-Cr2lWhF-.css +0 -1
  41. package/src/assets/web-panel/assets/chat-BmwHBi9M.js +0 -1
@@ -0,0 +1,323 @@
1
+ /**
2
+ * ReflectionEngine — Periodic self-review of accumulated trajectory data.
3
+ *
4
+ * Generates structured reflection reports:
5
+ * - Tool usage patterns (most used, error-prone)
6
+ * - Score trends (improving / declining)
7
+ * - Skill coverage gaps
8
+ * - Improvement recommendations
9
+ *
10
+ * Trigger: manual via CLI command or scheduled (cron-style)
11
+ * Output: JSON report stored in DB + optional SKILL.md improvements
12
+ */
13
+
14
+ import { extractToolNames } from "./skill-synthesizer.js";
15
+
16
+ // ── _deps for test injection ────────────────────────
17
+ const _deps = {
18
+ now: () => Date.now(),
19
+ };
20
+
21
+ // ── Helpers ─────────────────────────────────────────
22
+
23
+ /**
24
+ * Compute tool usage statistics from trajectories.
25
+ * @param {Array<{toolChain:Array<{tool:string, status:string}>}>} trajectories
26
+ * @returns {{toolUsage:Record<string, {count:number, errorRate:number}>, totalTools:number}}
27
+ */
28
+ export function computeToolStats(trajectories) {
29
+ const stats = {};
30
+ let totalTools = 0;
31
+
32
+ for (const traj of trajectories) {
33
+ for (const step of traj.toolChain || []) {
34
+ totalTools++;
35
+ if (!stats[step.tool]) {
36
+ stats[step.tool] = { count: 0, errors: 0 };
37
+ }
38
+ stats[step.tool].count++;
39
+ if (step.status === "error" || step.status === "failed") {
40
+ stats[step.tool].errors++;
41
+ }
42
+ }
43
+ }
44
+
45
+ const toolUsage = {};
46
+ for (const [tool, s] of Object.entries(stats)) {
47
+ toolUsage[tool] = {
48
+ count: s.count,
49
+ errorRate: s.count > 0 ? s.errors / s.count : 0,
50
+ };
51
+ }
52
+
53
+ return { toolUsage, totalTools };
54
+ }
55
+
56
+ /**
57
+ * Compute score trend from trajectories (sorted by time).
58
+ * Returns "improving", "declining", or "stable".
59
+ * @param {Array<{outcomeScore:number|null}>} trajectories — ordered oldest→newest
60
+ * @returns {{trend:"improving"|"declining"|"stable", avgScore:number, recentAvg:number}}
61
+ */
62
+ export function computeScoreTrend(trajectories) {
63
+ const scored = trajectories.filter((t) => t.outcomeScore != null);
64
+ if (scored.length < 2) {
65
+ const avg = scored.length === 1 ? scored[0].outcomeScore : 0;
66
+ return { trend: "stable", avgScore: avg, recentAvg: avg };
67
+ }
68
+
69
+ const allAvg =
70
+ scored.reduce((sum, t) => sum + t.outcomeScore, 0) / scored.length;
71
+
72
+ // Split into halves
73
+ const mid = Math.floor(scored.length / 2);
74
+ const firstHalf = scored.slice(0, mid);
75
+ const secondHalf = scored.slice(mid);
76
+
77
+ const firstAvg =
78
+ firstHalf.reduce((sum, t) => sum + t.outcomeScore, 0) / firstHalf.length;
79
+ const secondAvg =
80
+ secondHalf.reduce((sum, t) => sum + t.outcomeScore, 0) / secondHalf.length;
81
+
82
+ const delta = secondAvg - firstAvg;
83
+ let trend = "stable";
84
+ if (delta > 0.05) trend = "improving";
85
+ else if (delta < -0.05) trend = "declining";
86
+
87
+ return { trend, avgScore: allAvg, recentAvg: secondAvg };
88
+ }
89
+
90
+ /**
91
+ * Identify error-prone tool patterns (tools with error rate > threshold).
92
+ * @param {Record<string, {count:number, errorRate:number}>} toolUsage
93
+ * @param {number} [threshold=0.3]
94
+ * @returns {Array<{tool:string, errorRate:number, count:number}>}
95
+ */
96
+ export function findErrorProneTools(toolUsage, threshold = 0.3) {
97
+ return Object.entries(toolUsage)
98
+ .filter(([, stats]) => stats.errorRate > threshold && stats.count >= 2)
99
+ .map(([tool, stats]) => ({
100
+ tool,
101
+ errorRate: stats.errorRate,
102
+ count: stats.count,
103
+ }))
104
+ .sort((a, b) => b.errorRate - a.errorRate);
105
+ }
106
+
107
+ /**
108
+ * Build LLM prompt for reflection analysis.
109
+ * @param {object} reportData — pre-computed stats
110
+ * @returns {Array<{role:string, content:string}>}
111
+ */
112
+ export function buildReflectionPrompt(reportData) {
113
+ return [
114
+ {
115
+ role: "system",
116
+ content: `You are a self-improvement analyst for an AI coding assistant.
117
+ Analyze execution statistics and provide actionable recommendations.
118
+ Output ONLY valid JSON:
119
+ {
120
+ "summary": "2-3 sentence overview",
121
+ "strengths": ["strength 1", ...],
122
+ "weaknesses": ["weakness 1", ...],
123
+ "recommendations": [
124
+ {"action": "what to do", "priority": "high|medium|low", "reason": "why"}
125
+ ]
126
+ }`,
127
+ },
128
+ {
129
+ role: "user",
130
+ content: `## Reflection Period Stats
131
+ Total trajectories: ${reportData.totalTrajectories}
132
+ Scored trajectories: ${reportData.scoredCount}
133
+ Average score: ${reportData.avgScore?.toFixed(2) || "N/A"}
134
+ Score trend: ${reportData.trend || "unknown"}
135
+ Recent average: ${reportData.recentAvg?.toFixed(2) || "N/A"}
136
+
137
+ ## Tool Usage (top 10)
138
+ ${reportData.topTools?.map((t) => `- ${t.tool}: ${t.count}x (error rate: ${(t.errorRate * 100).toFixed(0)}%)`).join("\n") || "No data"}
139
+
140
+ ## Error-prone Tools
141
+ ${reportData.errorProneTools?.map((t) => `- ${t.tool}: ${(t.errorRate * 100).toFixed(0)}% error rate (${t.count} calls)`).join("\n") || "None"}
142
+
143
+ ## Synthesized Skills
144
+ ${reportData.synthesizedCount || 0} skills auto-generated`,
145
+ },
146
+ ];
147
+ }
148
+
149
+ // ── ReflectionEngine class ─────────────────────────
150
+
151
+ export class ReflectionEngine {
152
+ /**
153
+ * @param {import("better-sqlite3").Database} db
154
+ * @param {function|null} llmChat — async (messages) => string
155
+ * @param {import("./trajectory-store.js").TrajectoryStore} trajectoryStore
156
+ * @param {{reflectionInterval?:number}} [config]
157
+ */
158
+ constructor(db, llmChat, trajectoryStore, config = {}) {
159
+ this.db = db;
160
+ this.llmChat = llmChat;
161
+ this.trajectoryStore = trajectoryStore;
162
+ this.reflectionInterval = config.reflectionInterval || 24 * 60 * 60 * 1000; // 24h default
163
+ }
164
+
165
+ /**
166
+ * Run a reflection cycle: gather stats, analyze, produce report.
167
+ * @param {{since?:string, limit?:number}} [options]
168
+ * @returns {Promise<object>} reflection report
169
+ */
170
+ async reflect(options = {}) {
171
+ const limit = options.limit || 200;
172
+
173
+ // Gather trajectories
174
+ const trajectories = this.trajectoryStore.getRecent({ limit });
175
+ if (trajectories.length === 0) {
176
+ return this._emptyReport("No trajectories to reflect on");
177
+ }
178
+
179
+ // Compute stats
180
+ const { toolUsage, totalTools } = computeToolStats(trajectories);
181
+ const scoreTrend = computeScoreTrend(trajectories);
182
+ const errorProneTools = findErrorProneTools(toolUsage);
183
+
184
+ // Top tools by usage
185
+ const topTools = Object.entries(toolUsage)
186
+ .map(([tool, stats]) => ({ tool, ...stats }))
187
+ .sort((a, b) => b.count - a.count)
188
+ .slice(0, 10);
189
+
190
+ const dbStats = this.trajectoryStore.getStats();
191
+
192
+ const reportData = {
193
+ totalTrajectories: trajectories.length,
194
+ scoredCount: trajectories.filter((t) => t.outcomeScore != null).length,
195
+ avgScore: scoreTrend.avgScore,
196
+ recentAvg: scoreTrend.recentAvg,
197
+ trend: scoreTrend.trend,
198
+ topTools,
199
+ errorProneTools,
200
+ totalToolCalls: totalTools,
201
+ synthesizedCount: dbStats.synthesized,
202
+ };
203
+
204
+ // LLM analysis (optional)
205
+ let llmAnalysis = null;
206
+ if (this.llmChat) {
207
+ llmAnalysis = await this._getLLMAnalysis(reportData);
208
+ }
209
+
210
+ const report = {
211
+ timestamp: new Date(_deps.now()).toISOString(),
212
+ ...reportData,
213
+ llmAnalysis,
214
+ };
215
+
216
+ // Persist report
217
+ this._saveReport(report);
218
+
219
+ return report;
220
+ }
221
+
222
+ /**
223
+ * Get the most recent reflection report.
224
+ * @returns {object|null}
225
+ */
226
+ getLatestReport() {
227
+ try {
228
+ const row = this.db
229
+ .prepare(
230
+ `SELECT * FROM skill_improvement_log
231
+ WHERE trigger_type = 'reflection'
232
+ ORDER BY created_at DESC
233
+ LIMIT 1`,
234
+ )
235
+ .get();
236
+
237
+ if (!row) return null;
238
+ try {
239
+ return JSON.parse(row.detail);
240
+ } catch {
241
+ return null;
242
+ }
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Check if a reflection is due based on interval.
250
+ * @returns {boolean}
251
+ */
252
+ isReflectionDue() {
253
+ const latest = this.getLatestReport();
254
+ if (!latest || !latest.timestamp) return true;
255
+
256
+ const lastTime = new Date(latest.timestamp).getTime();
257
+ return _deps.now() - lastTime >= this.reflectionInterval;
258
+ }
259
+
260
+ // ── Internal ────────────────────────────────────
261
+
262
+ /**
263
+ * Get LLM analysis of the report data.
264
+ * @param {object} reportData
265
+ * @returns {Promise<object|null>}
266
+ */
267
+ async _getLLMAnalysis(reportData) {
268
+ try {
269
+ const messages = buildReflectionPrompt(reportData);
270
+ const response = await this.llmChat(messages);
271
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
272
+ if (!jsonMatch) return null;
273
+ return JSON.parse(jsonMatch[0]);
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Save report to skill_improvement_log.
281
+ * @param {object} report
282
+ */
283
+ _saveReport(report) {
284
+ try {
285
+ this.db
286
+ .prepare(
287
+ `INSERT INTO skill_improvement_log (skill_name, trigger_type, detail)
288
+ VALUES (?, ?, ?)`,
289
+ )
290
+ .run(
291
+ "_reflection",
292
+ "reflection",
293
+ JSON.stringify(report).slice(0, 5000),
294
+ );
295
+ } catch {
296
+ // Non-critical
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Build an empty report for edge cases.
302
+ * @param {string} reason
303
+ * @returns {object}
304
+ */
305
+ _emptyReport(reason) {
306
+ return {
307
+ timestamp: new Date(_deps.now()).toISOString(),
308
+ totalTrajectories: 0,
309
+ scoredCount: 0,
310
+ avgScore: 0,
311
+ recentAvg: 0,
312
+ trend: "stable",
313
+ topTools: [],
314
+ errorProneTools: [],
315
+ totalToolCalls: 0,
316
+ synthesizedCount: 0,
317
+ llmAnalysis: null,
318
+ note: reason,
319
+ };
320
+ }
321
+ }
322
+
323
+ export { _deps };