chainlesschain 0.45.75 → 0.45.77

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 (76) 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/{Analytics-sBrYoc3A.js → Analytics-Dd2DjBH5.js} +2 -2
  5. package/src/assets/web-panel/assets/AppLayout-CP9fATUN.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +1 -0
  7. package/src/assets/web-panel/assets/Backup-D6Tc7sf3.js +1 -0
  8. package/src/assets/web-panel/assets/Chat-DDUJZJ9I.js +1 -0
  9. package/src/assets/web-panel/assets/Chat-DfR76jyX.css +1 -0
  10. package/src/assets/web-panel/assets/Cowork-CPqYhoMI.css +1 -0
  11. package/src/assets/web-panel/assets/Cowork-XRFqGfqJ.js +48 -0
  12. package/src/assets/web-panel/assets/{Cron-CNs03iHJ.js → Cron-BnWzy_ZB.js} +2 -2
  13. package/src/assets/web-panel/assets/{Dashboard-DanoHPSI.js → Dashboard-D2vCkoGu.js} +1 -1
  14. package/src/assets/web-panel/assets/{Git-CCMVr3Y8.js → Git-DYlvK4sh.js} +2 -2
  15. package/src/assets/web-panel/assets/{Logs-BY6A0UNG.js → Logs-4VgUbfP0.js} +2 -2
  16. package/src/assets/web-panel/assets/{McpTools-CrBVYlg6.js → McpTools-ChaiHoWY.js} +2 -2
  17. package/src/assets/web-panel/assets/{Memory-CWx3SpUt.js → Memory-PFtpuOwf.js} +2 -2
  18. package/src/assets/web-panel/assets/{Notes-1LcGD49x.js → Notes-wc_n6Rh1.js} +2 -2
  19. package/src/assets/web-panel/assets/{Organization-Dx2DhbkM.js → Organization-D1qUa8NQ.js} +4 -4
  20. package/src/assets/web-panel/assets/{P2P-B16fjqfJ.js → P2P-DIG2gnR8.js} +2 -2
  21. package/src/assets/web-panel/assets/{Permissions-BQbC9FzG.js → Permissions-CpE-Ar1e.js} +3 -3
  22. package/src/assets/web-panel/assets/{Projects-CjhZbNYm.js → Projects-GjuS-C6U.js} +2 -2
  23. package/src/assets/web-panel/assets/{Providers-ivOAQtHM.js → Providers-CCfGeqh_.js} +2 -2
  24. package/src/assets/web-panel/assets/{RssFeed-BrsErdrU.js → RssFeed-5TkrXK7Z.js} +1 -1
  25. package/src/assets/web-panel/assets/{Security-DnEvJU5h.js → Security-CcfBWT1D.js} +3 -3
  26. package/src/assets/web-panel/assets/{Services-7jQywNbl.js → Services-Cnm5Zs5h.js} +1 -1
  27. package/src/assets/web-panel/assets/{Skills-CLlblJcG.js → Skills-BHapMb9h.js} +1 -1
  28. package/src/assets/web-panel/assets/{Tasks-CmJBC1cf.js → Tasks-DPb9OMck.js} +1 -1
  29. package/src/assets/web-panel/assets/Templates-Dij5t-rf.js +1 -0
  30. package/src/assets/web-panel/assets/{Wallet-3iYASEx_.js → Wallet-BJV5KmWA.js} +4 -4
  31. package/src/assets/web-panel/assets/{WebAuthn-s3Hzd9db.js → WebAuthn-DLkvYwSc.js} +5 -5
  32. package/src/assets/web-panel/assets/{antd-gZyc63Qr.js → antd-BQNxIyr-.js} +82 -82
  33. package/src/assets/web-panel/assets/github-dark-Dfs9RUU9.css +1 -0
  34. package/src/assets/web-panel/assets/index-CB5YlndO.js +2 -0
  35. package/src/assets/web-panel/assets/{markdown-Bv7nG63L.js → markdown-BeVIhIzs.js} +1 -1
  36. package/src/assets/web-panel/index.html +2 -2
  37. package/src/commands/learning.js +273 -0
  38. package/src/commands/lowcode.js +23 -8
  39. package/src/gateways/discord/discord-formatter.js +89 -0
  40. package/src/gateways/gateway-base.js +189 -0
  41. package/src/gateways/telegram/telegram-formatter.js +93 -0
  42. package/src/gateways/ws/action-protocol.js +54 -1
  43. package/src/gateways/ws/message-dispatcher.js +1 -0
  44. package/src/gateways/ws/ws-server.js +10 -1
  45. package/src/index.js +2 -0
  46. package/src/lib/app-builder.js +136 -8
  47. package/src/lib/autonomous-agent.js +8 -1
  48. package/src/lib/cli-context-engineering.js +15 -0
  49. package/src/lib/cowork-task-runner.js +101 -0
  50. package/src/lib/cowork-task-templates.js +493 -0
  51. package/src/lib/execution-backend.js +239 -0
  52. package/src/lib/hook-manager.js +2 -0
  53. package/src/lib/iteration-budget.js +175 -0
  54. package/src/lib/learning/learning-hooks.js +117 -0
  55. package/src/lib/learning/learning-tables.js +66 -0
  56. package/src/lib/learning/outcome-feedback.js +243 -0
  57. package/src/lib/learning/reflection-engine.js +323 -0
  58. package/src/lib/learning/skill-improver.js +536 -0
  59. package/src/lib/learning/skill-synthesizer.js +315 -0
  60. package/src/lib/learning/trajectory-store.js +409 -0
  61. package/src/lib/plugin-autodiscovery.js +224 -0
  62. package/src/lib/session-search.js +193 -0
  63. package/src/lib/sub-agent-context.js +7 -2
  64. package/src/lib/user-profile.js +172 -0
  65. package/src/lib/web-ui-server.js +1 -1
  66. package/src/repl/agent-repl.js +109 -0
  67. package/src/runtime/agent-core.js +75 -4
  68. package/src/runtime/coding-agent-contract-shared.cjs +35 -0
  69. package/src/runtime/coding-agent-policy.cjs +10 -0
  70. package/src/assets/web-panel/assets/AppLayout-2RCrdXxl.js +0 -1
  71. package/src/assets/web-panel/assets/AppLayout-D9pBLPC3.css +0 -1
  72. package/src/assets/web-panel/assets/Backup-D68fenbD.js +0 -1
  73. package/src/assets/web-panel/assets/Chat-B2nB8o_F.js +0 -1
  74. package/src/assets/web-panel/assets/Chat-DB46afPg.css +0 -1
  75. package/src/assets/web-panel/assets/Templates-RXT8-DNk.js +0 -1
  76. package/src/assets/web-panel/assets/index-CyGtHm63.js +0 -2
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Execution Backend — abstraction for command execution environments.
3
+ *
4
+ * Provides LocalBackend (default), DockerBackend, and SSHBackend.
5
+ * The agent's run_shell and run_code tools delegate to the active backend.
6
+ *
7
+ * Config: .chainlesschain/config.json → agent.executionBackend
8
+ * Feature flag: EXECUTION_BACKENDS (off by default)
9
+ *
10
+ * @module execution-backend
11
+ */
12
+
13
+ import { execSync } from "node:child_process";
14
+
15
+ // ─── Exported for test injection ────────────────────────────────────
16
+
17
+ export const _deps = {
18
+ execSync,
19
+ };
20
+
21
+ // ─── Base class ─────────────────────────────────────────────────────
22
+
23
+ export class ExecutionBackend {
24
+ constructor(options = {}) {
25
+ this.type = "base";
26
+ this.options = options;
27
+ }
28
+
29
+ /**
30
+ * Execute a command.
31
+ * @param {string} command - The command to run
32
+ * @param {object} [opts]
33
+ * @param {string} [opts.cwd] - Working directory
34
+ * @param {number} [opts.timeout] - Timeout in ms
35
+ * @param {number} [opts.maxBuffer] - Max output buffer size
36
+ * @returns {{ stdout: string, stderr: string, exitCode: number }}
37
+ */
38
+ execute(command, opts = {}) {
39
+ throw new Error("execute() must be implemented by subclass");
40
+ }
41
+
42
+ /** @returns {string} Backend description for logging */
43
+ describe() {
44
+ return `${this.type} backend`;
45
+ }
46
+ }
47
+
48
+ // ─── Local Backend ──────────────────────────────────────────────────
49
+
50
+ export class LocalBackend extends ExecutionBackend {
51
+ constructor(options = {}) {
52
+ super(options);
53
+ this.type = "local";
54
+ }
55
+
56
+ execute(command, opts = {}) {
57
+ const cwd = opts.cwd || process.cwd();
58
+ const timeout = opts.timeout || 60000;
59
+ const maxBuffer = opts.maxBuffer || 1024 * 1024;
60
+
61
+ try {
62
+ const stdout = _deps.execSync(command, {
63
+ cwd,
64
+ encoding: "utf8",
65
+ timeout,
66
+ maxBuffer,
67
+ });
68
+ return { stdout: stdout || "", stderr: "", exitCode: 0 };
69
+ } catch (err) {
70
+ return {
71
+ stdout: (err.stdout || "").toString(),
72
+ stderr: (err.stderr || err.message || "").toString(),
73
+ exitCode: err.status || 1,
74
+ };
75
+ }
76
+ }
77
+
78
+ describe() {
79
+ return "local (direct execution)";
80
+ }
81
+ }
82
+
83
+ // ─── Docker Backend ─────────────────────────────────────────────────
84
+
85
+ export class DockerBackend extends ExecutionBackend {
86
+ /**
87
+ * @param {object} options
88
+ * @param {string} options.container - Container name or ID (for exec mode)
89
+ * @param {string} [options.image] - Image name (for run mode — ephemeral containers)
90
+ * @param {string} [options.workdir] - Working directory inside container
91
+ * @param {string[]} [options.volumes] - Volume mounts (host:container format)
92
+ * @param {string} [options.shell] - Shell to use (default: sh)
93
+ */
94
+ constructor(options = {}) {
95
+ super(options);
96
+ this.type = "docker";
97
+ this.container = options.container || null;
98
+ this.image = options.image || null;
99
+ this.workdir = options.workdir || "/workspace";
100
+ this.volumes = options.volumes || [];
101
+ this.shell = options.shell || "sh";
102
+ }
103
+
104
+ execute(command, opts = {}) {
105
+ const timeout = opts.timeout || 60000;
106
+ const maxBuffer = opts.maxBuffer || 1024 * 1024;
107
+ const cwd = opts.cwd || this.workdir;
108
+
109
+ let dockerCmd;
110
+ if (this.container) {
111
+ // Exec into existing container
112
+ dockerCmd = `docker exec -w "${cwd}" ${this.container} ${this.shell} -c "${this._escapeCommand(command)}"`;
113
+ } else if (this.image) {
114
+ // Run ephemeral container
115
+ const volumeArgs = this.volumes.map((v) => `-v "${v}"`).join(" ");
116
+ dockerCmd = `docker run --rm -w "${cwd}" ${volumeArgs} ${this.image} ${this.shell} -c "${this._escapeCommand(command)}"`;
117
+ } else {
118
+ return {
119
+ stdout: "",
120
+ stderr: "Docker backend: neither container nor image specified",
121
+ exitCode: 1,
122
+ };
123
+ }
124
+
125
+ try {
126
+ const stdout = _deps.execSync(dockerCmd, {
127
+ encoding: "utf8",
128
+ timeout,
129
+ maxBuffer,
130
+ });
131
+ return { stdout: stdout || "", stderr: "", exitCode: 0 };
132
+ } catch (err) {
133
+ return {
134
+ stdout: (err.stdout || "").toString(),
135
+ stderr: (err.stderr || err.message || "").toString(),
136
+ exitCode: err.status || 1,
137
+ };
138
+ }
139
+ }
140
+
141
+ _escapeCommand(cmd) {
142
+ return cmd.replace(/"/g, '\\"');
143
+ }
144
+
145
+ describe() {
146
+ if (this.container) return `docker exec (container: ${this.container})`;
147
+ return `docker run (image: ${this.image})`;
148
+ }
149
+ }
150
+
151
+ // ─── SSH Backend ────────────────────────────────────────────────────
152
+
153
+ export class SSHBackend extends ExecutionBackend {
154
+ /**
155
+ * @param {object} options
156
+ * @param {string} options.host - Remote host
157
+ * @param {string} [options.user] - SSH user
158
+ * @param {string} [options.key] - Path to SSH private key
159
+ * @param {number} [options.port] - SSH port (default: 22)
160
+ * @param {string} [options.workdir] - Remote working directory
161
+ */
162
+ constructor(options = {}) {
163
+ super(options);
164
+ this.type = "ssh";
165
+ this.host = options.host;
166
+ this.user = options.user || "";
167
+ this.key = options.key || "";
168
+ this.port = options.port || 22;
169
+ this.workdir = options.workdir || "~";
170
+ }
171
+
172
+ execute(command, opts = {}) {
173
+ const timeout = opts.timeout || 60000;
174
+ const maxBuffer = opts.maxBuffer || 1024 * 1024;
175
+ const cwd = opts.cwd || this.workdir;
176
+
177
+ if (!this.host) {
178
+ return {
179
+ stdout: "",
180
+ stderr: "SSH backend: host not specified",
181
+ exitCode: 1,
182
+ };
183
+ }
184
+
185
+ const userHost = this.user ? `${this.user}@${this.host}` : this.host;
186
+ const keyArg = this.key ? `-i "${this.key}"` : "";
187
+ const portArg = this.port !== 22 ? `-p ${this.port}` : "";
188
+ const remoteCmd = `cd "${cwd}" && ${command}`;
189
+
190
+ const sshCmd = `ssh ${keyArg} ${portArg} -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${userHost} "${this._escapeCommand(remoteCmd)}"`;
191
+
192
+ try {
193
+ const stdout = _deps.execSync(sshCmd, {
194
+ encoding: "utf8",
195
+ timeout,
196
+ maxBuffer,
197
+ });
198
+ return { stdout: stdout || "", stderr: "", exitCode: 0 };
199
+ } catch (err) {
200
+ return {
201
+ stdout: (err.stdout || "").toString(),
202
+ stderr: (err.stderr || err.message || "").toString(),
203
+ exitCode: err.status || 1,
204
+ };
205
+ }
206
+ }
207
+
208
+ _escapeCommand(cmd) {
209
+ return cmd.replace(/"/g, '\\"');
210
+ }
211
+
212
+ describe() {
213
+ const userHost = this.user ? `${this.user}@${this.host}` : this.host;
214
+ return `ssh (${userHost}:${this.port})`;
215
+ }
216
+ }
217
+
218
+ // ─── Factory ────────────────────────────────────────────────────────
219
+
220
+ /**
221
+ * Create an execution backend from config.
222
+ * @param {object} [config] - Backend config from config.json
223
+ * @param {string} [config.type] - "local" | "docker" | "ssh"
224
+ * @param {object} [config.options] - Backend-specific options
225
+ * @returns {ExecutionBackend}
226
+ */
227
+ export function createBackend(config = {}) {
228
+ const type = (config.type || "local").toLowerCase();
229
+
230
+ switch (type) {
231
+ case "docker":
232
+ return new DockerBackend(config.options || {});
233
+ case "ssh":
234
+ return new SSHBackend(config.options || {});
235
+ case "local":
236
+ default:
237
+ return new LocalBackend(config.options || {});
238
+ }
239
+ }
@@ -58,6 +58,8 @@ export const HookEvents = {
58
58
  PostGitCommit: "PostGitCommit",
59
59
  PreGitPush: "PreGitPush",
60
60
  CIFailure: "CIFailure",
61
+ IterationWarning: "IterationWarning",
62
+ IterationBudgetExhausted: "IterationBudgetExhausted",
61
63
  };
62
64
 
63
65
  /**
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Iteration Budget — shared, configurable iteration limit for agent loops.
3
+ *
4
+ * Replaces the hardcoded MAX_ITERATIONS constant with a first-class budget
5
+ * object that is shared across parent and child agents, supports progressive
6
+ * warnings, and can be configured via config.json or environment variable.
7
+ *
8
+ * Inspired by Hermes Agent's shared iteration budget system.
9
+ *
10
+ * @module iteration-budget
11
+ */
12
+
13
+ // ─── Constants ──────────────────────────────────────────────────────────────
14
+
15
+ const DEFAULT_BUDGET = 50;
16
+ const WARNING_THRESHOLD = 0.7; // 70%
17
+ const WRAPPING_UP_THRESHOLD = 0.9; // 90%
18
+
19
+ /**
20
+ * Warning level enum.
21
+ */
22
+ export const WarningLevel = {
23
+ NONE: "none",
24
+ WARNING: "warning", // 70-89%
25
+ WRAPPING_UP: "wrapping-up", // 90-99%
26
+ EXHAUSTED: "exhausted", // 100%
27
+ };
28
+
29
+ // ─── IterationBudget ────────────────────────────────────────────────────────
30
+
31
+ export class IterationBudget {
32
+ /**
33
+ * @param {object} [options]
34
+ * @param {number} [options.limit] - Maximum iterations (default: 50)
35
+ * @param {string} [options.owner] - Identifier for the budget creator (e.g. session ID)
36
+ */
37
+ constructor(options = {}) {
38
+ this._limit = options.limit || IterationBudget.resolveLimit();
39
+ this._consumed = 0;
40
+ this._owner = options.owner || null;
41
+ this._warnings = []; // timestamps of emitted warnings
42
+ }
43
+
44
+ /**
45
+ * Resolve the budget limit from config/env/default.
46
+ * Priority: CC_ITERATION_BUDGET env > default
47
+ */
48
+ static resolveLimit() {
49
+ const env = process.env.CC_ITERATION_BUDGET;
50
+ if (env) {
51
+ const parsed = parseInt(env, 10);
52
+ if (!isNaN(parsed) && parsed > 0) return parsed;
53
+ }
54
+ return DEFAULT_BUDGET;
55
+ }
56
+
57
+ /** Total iteration limit. */
58
+ get limit() {
59
+ return this._limit;
60
+ }
61
+
62
+ /** Number of iterations consumed so far. */
63
+ get consumed() {
64
+ return this._consumed;
65
+ }
66
+
67
+ /**
68
+ * Consume one iteration. Returns the current warning level after consumption.
69
+ * @returns {string} WarningLevel value
70
+ */
71
+ consume() {
72
+ this._consumed++;
73
+ return this.warningLevel();
74
+ }
75
+
76
+ /**
77
+ * Number of iterations remaining.
78
+ * @returns {number}
79
+ */
80
+ remaining() {
81
+ return Math.max(0, this._limit - this._consumed);
82
+ }
83
+
84
+ /**
85
+ * Percentage of budget consumed (0.0 – 1.0+).
86
+ * @returns {number}
87
+ */
88
+ percentage() {
89
+ if (this._limit === 0) return 1;
90
+ return this._consumed / this._limit;
91
+ }
92
+
93
+ /**
94
+ * Whether the budget is exhausted.
95
+ * @returns {boolean}
96
+ */
97
+ isExhausted() {
98
+ return this._consumed >= this._limit;
99
+ }
100
+
101
+ /**
102
+ * Whether there is still budget remaining.
103
+ * @returns {boolean}
104
+ */
105
+ hasRemaining() {
106
+ return this._consumed < this._limit;
107
+ }
108
+
109
+ /**
110
+ * Current warning level based on consumption percentage.
111
+ * @returns {string} WarningLevel value
112
+ */
113
+ warningLevel() {
114
+ const pct = this.percentage();
115
+ if (pct >= 1) return WarningLevel.EXHAUSTED;
116
+ if (pct >= WRAPPING_UP_THRESHOLD) return WarningLevel.WRAPPING_UP;
117
+ if (pct >= WARNING_THRESHOLD) return WarningLevel.WARNING;
118
+ return WarningLevel.NONE;
119
+ }
120
+
121
+ /**
122
+ * Record that a warning was emitted (for dedup in the agent loop).
123
+ * @param {string} level - WarningLevel value
124
+ */
125
+ recordWarning(level) {
126
+ this._warnings.push({ level, at: this._consumed });
127
+ }
128
+
129
+ /**
130
+ * Whether a warning at this level has already been recorded.
131
+ * @param {string} level
132
+ * @returns {boolean}
133
+ */
134
+ hasWarned(level) {
135
+ return this._warnings.some((w) => w.level === level);
136
+ }
137
+
138
+ /**
139
+ * Generate a human-readable summary of budget usage.
140
+ * Useful when the budget is exhausted and the agent needs to report status.
141
+ * @returns {string}
142
+ */
143
+ toSummary() {
144
+ const pct = Math.round(this.percentage() * 100);
145
+ return (
146
+ `Iteration budget: ${this._consumed}/${this._limit} (${pct}%). ` +
147
+ `${this.remaining()} iterations remaining.`
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Create a warning message suitable for appending to tool results.
153
+ * @returns {string|null} Warning message or null if no warning needed
154
+ */
155
+ toWarningMessage() {
156
+ const level = this.warningLevel();
157
+ const remaining = this.remaining();
158
+ switch (level) {
159
+ case WarningLevel.WARNING:
160
+ return `[Budget Warning] ${remaining} iterations remaining out of ${this._limit}. Start wrapping up your work.`;
161
+ case WarningLevel.WRAPPING_UP:
162
+ return `[Budget Critical] Only ${remaining} iterations remaining! Finish immediately and return your results.`;
163
+ case WarningLevel.EXHAUSTED:
164
+ return `[Budget Exhausted] No iterations remaining. Returning work summary.`;
165
+ default:
166
+ return null;
167
+ }
168
+ }
169
+ }
170
+
171
+ // ─── Defaults ───────────────────────────────────────────────────────────────
172
+
173
+ export const DEFAULT_ITERATION_BUDGET = DEFAULT_BUDGET;
174
+ export const BUDGET_WARNING_THRESHOLD = WARNING_THRESHOLD;
175
+ export const BUDGET_WRAPPING_UP_THRESHOLD = WRAPPING_UP_THRESHOLD;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Learning Loop — Hook registration.
3
+ *
4
+ * Wires the TrajectoryStore into the existing hook infrastructure:
5
+ * - UserPromptSubmit → startTrajectory()
6
+ * - PostToolUse → appendToolCall()
7
+ * - SessionEnd → (reserved for ReflectionEngine in P3)
8
+ *
9
+ * This module does NOT modify agent-core.js or agent-repl.js.
10
+ * Instead it provides an init function that the REPL calls once
11
+ * during session setup, passing a LearningLoop context object.
12
+ *
13
+ * Design:
14
+ * - Fire-and-forget: learning failures never break the host flow
15
+ * - No-op when learningCtx is null or disabled
16
+ * - Stateless — the trajectory ID is tracked on learningCtx
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} LearningContext
21
+ * @property {import("./trajectory-store.js").TrajectoryStore} trajectoryStore
22
+ * @property {string|null} currentTrajectoryId — set per user turn
23
+ * @property {boolean} enabled
24
+ * @property {string} sessionId
25
+ */
26
+
27
+ /**
28
+ * Called on UserPromptSubmit — starts a new trajectory for this turn.
29
+ * @param {LearningContext} ctx
30
+ * @param {string} prompt — the user's raw input
31
+ */
32
+ export function onUserPromptSubmit(ctx, prompt) {
33
+ if (!ctx || !ctx.enabled || !ctx.trajectoryStore) return;
34
+ try {
35
+ ctx.currentTrajectoryId = ctx.trajectoryStore.startTrajectory(
36
+ ctx.sessionId,
37
+ prompt,
38
+ );
39
+ } catch (_err) {
40
+ // Learning failures never break the REPL
41
+ ctx.currentTrajectoryId = null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Called on PostToolUse — appends a tool call to the current trajectory.
47
+ * @param {LearningContext} ctx
48
+ * @param {{tool:string, args:any, result:any, durationMs?:number, status?:string}} record
49
+ */
50
+ export function onPostToolUse(ctx, record) {
51
+ if (!ctx || !ctx.enabled || !ctx.trajectoryStore || !ctx.currentTrajectoryId)
52
+ return;
53
+ try {
54
+ ctx.trajectoryStore.appendToolCall(ctx.currentTrajectoryId, record);
55
+ } catch (_err) {
56
+ // Swallow — never break the agent loop
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Called on response-complete — completes the current trajectory.
62
+ * @param {LearningContext} ctx
63
+ * @param {{finalResponse?:string, tags?:string[]}} data
64
+ * @returns {object|null} completed trajectory or null
65
+ */
66
+ export function onResponseComplete(ctx, data) {
67
+ if (!ctx || !ctx.enabled || !ctx.trajectoryStore || !ctx.currentTrajectoryId)
68
+ return null;
69
+ try {
70
+ const trajectory = ctx.trajectoryStore.completeTrajectory(
71
+ ctx.currentTrajectoryId,
72
+ data,
73
+ );
74
+ // Reset for next turn
75
+ const finishedId = ctx.currentTrajectoryId;
76
+ ctx.currentTrajectoryId = null;
77
+ return trajectory;
78
+ } catch (_err) {
79
+ ctx.currentTrajectoryId = null;
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Called on SessionEnd — reserved for ReflectionEngine (P3).
86
+ * Currently a no-op placeholder.
87
+ * @param {LearningContext} ctx
88
+ */
89
+ export function onSessionEnd(ctx) {
90
+ if (!ctx || !ctx.enabled) return;
91
+ // P3: will call reflectionEngine.onSessionEnd(ctx.sessionId)
92
+ }
93
+
94
+ /**
95
+ * Create a LearningContext for a session.
96
+ * Returns null if db is not available or learning is disabled.
97
+ *
98
+ * @param {import("better-sqlite3").Database|null} db
99
+ * @param {string} sessionId
100
+ * @param {{enabled?:boolean}} [config]
101
+ * @returns {LearningContext|null}
102
+ */
103
+ export function createLearningContext(db, sessionId, config = {}) {
104
+ if (!db) return null;
105
+ const enabled = config.enabled !== false;
106
+ if (!enabled) return null;
107
+
108
+ // Lazy import to avoid circular deps — TrajectoryStore calls ensureLearningTables
109
+ const { TrajectoryStore } = require("./trajectory-store.js");
110
+
111
+ return {
112
+ trajectoryStore: new TrajectoryStore(db),
113
+ currentTrajectoryId: null,
114
+ enabled: true,
115
+ sessionId,
116
+ };
117
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Learning Loop — Database table definitions.
3
+ *
4
+ * Tables:
5
+ * - learning_trajectories — full execution traces (intent → tools → response)
6
+ * - learning_trajectory_tags — per-trajectory tag index for pattern matching
7
+ * - skill_improvement_log — skill patch history (Phase P2)
8
+ */
9
+
10
+ /**
11
+ * Create all learning-related tables. Idempotent (IF NOT EXISTS).
12
+ * @param {import("better-sqlite3").Database} db
13
+ */
14
+ export function ensureLearningTables(db) {
15
+ db.exec(`
16
+ CREATE TABLE IF NOT EXISTS learning_trajectories (
17
+ id TEXT PRIMARY KEY,
18
+ session_id TEXT NOT NULL,
19
+ user_intent TEXT,
20
+ tool_chain TEXT NOT NULL DEFAULT '[]',
21
+ tool_count INTEGER DEFAULT 0,
22
+ final_response TEXT,
23
+ outcome_score REAL,
24
+ outcome_source TEXT,
25
+ complexity_level TEXT DEFAULT 'simple',
26
+ synthesized_skill TEXT,
27
+ created_at TEXT DEFAULT (datetime('now')),
28
+ completed_at TEXT
29
+ )
30
+ `);
31
+
32
+ db.exec(`
33
+ CREATE INDEX IF NOT EXISTS idx_traj_session
34
+ ON learning_trajectories(session_id)
35
+ `);
36
+ db.exec(`
37
+ CREATE INDEX IF NOT EXISTS idx_traj_score
38
+ ON learning_trajectories(outcome_score)
39
+ `);
40
+ db.exec(`
41
+ CREATE INDEX IF NOT EXISTS idx_traj_complexity
42
+ ON learning_trajectories(complexity_level)
43
+ `);
44
+
45
+ db.exec(`
46
+ CREATE TABLE IF NOT EXISTS learning_trajectory_tags (
47
+ trajectory_id TEXT NOT NULL,
48
+ tag TEXT NOT NULL,
49
+ UNIQUE(trajectory_id, tag)
50
+ )
51
+ `);
52
+
53
+ db.exec(`
54
+ CREATE INDEX IF NOT EXISTS idx_traj_tags_tid
55
+ ON learning_trajectory_tags(trajectory_id)
56
+ `);
57
+
58
+ db.exec(`
59
+ CREATE TABLE IF NOT EXISTS skill_improvement_log (
60
+ skill_name TEXT NOT NULL,
61
+ trigger_type TEXT NOT NULL,
62
+ detail TEXT,
63
+ created_at TEXT DEFAULT (datetime('now'))
64
+ )
65
+ `);
66
+ }