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
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import crypto from "crypto";
7
+ import fs from "fs";
8
+ import path from "path";
7
9
 
8
10
  /** @type {Map<string, object>} In-memory app cache */
9
11
  const _apps = new Map();
@@ -220,14 +222,12 @@ export function saveDesign(db, appId, design) {
220
222
  ).run(versionId, appId, newVersion, JSON.stringify(design));
221
223
 
222
224
  if (!_versions.has(appId)) _versions.set(appId, []);
223
- _versions
224
- .get(appId)
225
- .push({
226
- id: versionId,
227
- app_id: appId,
228
- version: newVersion,
229
- snapshot: design,
230
- });
225
+ _versions.get(appId).push({
226
+ id: versionId,
227
+ app_id: appId,
228
+ version: newVersion,
229
+ snapshot: design,
230
+ });
231
231
 
232
232
  if (_apps.has(appId)) {
233
233
  _apps.get(appId).design = design;
@@ -375,3 +375,131 @@ export function listApps(db) {
375
375
  updated_at: r.updated_at,
376
376
  }));
377
377
  }
378
+
379
+ /**
380
+ * Get a single application by ID from the database.
381
+ *
382
+ * @param {object} db
383
+ * @param {string} appId
384
+ * @returns {object|null}
385
+ */
386
+ export function getApp(db, appId) {
387
+ const row = db.prepare(`SELECT * FROM lowcode_apps WHERE id = ?`).get(appId);
388
+ if (!row) return null;
389
+ return {
390
+ id: row.id,
391
+ name: row.name,
392
+ description: row.description,
393
+ design: row.design
394
+ ? JSON.parse(row.design)
395
+ : { components: [], layout: {} },
396
+ status: row.status,
397
+ version: row.version,
398
+ platform: row.platform,
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Generate a static HTML bundle string for an app's design.
404
+ *
405
+ * @param {object} app - App object with design
406
+ * @returns {{ "index.html": string, "app.js": string, "style.css": string }}
407
+ */
408
+ function generateStaticBundle(app) {
409
+ const design = app.design || { components: [], layout: {} };
410
+ const components = design.components || [];
411
+
412
+ const componentHtml = components
413
+ .map(
414
+ (c, i) =>
415
+ ` <div class="lc-component lc-${(c.type || c.name || "widget").toLowerCase()}" id="comp-${i}">${c.label || c.type || c.name || "Component"}</div>`,
416
+ )
417
+ .join("\n");
418
+
419
+ const html = `<!DOCTYPE html>
420
+ <html lang="en">
421
+ <head>
422
+ <meta charset="UTF-8">
423
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
424
+ <title>${app.name || "Low-Code App"}</title>
425
+ <link rel="stylesheet" href="style.css">
426
+ </head>
427
+ <body>
428
+ <div id="app">
429
+ <h1>${app.name || "Low-Code App"}</h1>
430
+ <p>${app.description || ""}</p>
431
+ <div class="lc-container">
432
+ ${componentHtml || " <p>No components defined</p>"}
433
+ </div>
434
+ </div>
435
+ <script src="app.js"><\/script>
436
+ </body>
437
+ </html>`;
438
+
439
+ const css = `/* Generated by ChainlessChain Low-Code Platform */
440
+ * { margin: 0; padding: 0; box-sizing: border-box; }
441
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 24px; background: #f5f5f5; }
442
+ #app { max-width: 1200px; margin: 0 auto; }
443
+ h1 { margin-bottom: 8px; color: #1a1a1a; }
444
+ p { margin-bottom: 16px; color: #666; }
445
+ .lc-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
446
+ .lc-component { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; }
447
+ `;
448
+
449
+ const js = `// Generated by ChainlessChain Low-Code Platform
450
+ // App: ${app.name || "Untitled"} v${app.version || 1}
451
+ (function() {
452
+ console.log("Low-Code App initialized: ${app.name || "Untitled"}");
453
+ var config = ${JSON.stringify({ id: app.id, name: app.name, version: app.version, platform: app.platform })};
454
+ document.querySelectorAll(".lc-component").forEach(function(el) {
455
+ el.addEventListener("click", function() { el.classList.toggle("active"); });
456
+ });
457
+ })();
458
+ `;
459
+
460
+ return { "index.html": html, "app.js": js, "style.css": css };
461
+ }
462
+
463
+ /**
464
+ * Deploy an application by generating a static bundle and writing it to disk.
465
+ *
466
+ * @param {object} db
467
+ * @param {string} appId
468
+ * @param {{ outputDir?: string }} options
469
+ * @returns {{ appId: string, outputDir: string, files: string[], deployedAt: string }}
470
+ */
471
+ export function deployApp(db, appId, options = {}) {
472
+ const app = getApp(db, appId);
473
+ if (!app) {
474
+ throw new Error(`App '${appId}' not found`);
475
+ }
476
+ if (app.status !== "published") {
477
+ throw new Error(
478
+ `App '${appId}' must be published before deploying (current status: ${app.status})`,
479
+ );
480
+ }
481
+
482
+ const outputDir =
483
+ options.outputDir ||
484
+ path.join(process.cwd(), ".chainlesschain", "deploys", appId);
485
+ fs.mkdirSync(outputDir, { recursive: true });
486
+
487
+ const bundle = generateStaticBundle(app);
488
+ const fileNames = [];
489
+ for (const [fileName, content] of Object.entries(bundle)) {
490
+ fs.writeFileSync(path.join(outputDir, fileName), content, "utf-8");
491
+ fileNames.push(fileName);
492
+ }
493
+
494
+ // Update app status to deployed
495
+ db.prepare(
496
+ `UPDATE lowcode_apps SET status = ?, updated_at = datetime('now') WHERE id = ?`,
497
+ ).run("deployed", appId);
498
+
499
+ if (_apps.has(appId)) {
500
+ _apps.get(appId).status = "deployed";
501
+ }
502
+
503
+ const deployedAt = new Date().toISOString();
504
+ return { appId, outputDir, files: fileNames, deployedAt };
505
+ }
@@ -52,11 +52,18 @@ export class CLIAutonomousAgent extends EventEmitter {
52
52
  /**
53
53
  * Initialize with required dependencies.
54
54
  */
55
- initialize({ llmChat, toolExecutor, hookManager, maxIterations } = {}) {
55
+ initialize({
56
+ llmChat,
57
+ toolExecutor,
58
+ hookManager,
59
+ maxIterations,
60
+ iterationBudget,
61
+ } = {}) {
56
62
  this._llmChat = llmChat || null;
57
63
  this._toolExecutor = toolExecutor || null;
58
64
  this._hookManager = hookManager || null;
59
65
  if (maxIterations) this._maxIterations = maxIterations;
66
+ this._iterationBudget = iterationBudget || null; // shared budget from caller
60
67
  this._initialized = true;
61
68
  }
62
69
 
@@ -12,6 +12,7 @@ import { generateInstinctPrompt } from "./instinct-manager.js";
12
12
  import { recallMemory } from "./hierarchical-memory.js";
13
13
  import { BM25Search } from "./bm25-search.js";
14
14
  import { createHash } from "crypto";
15
+ import { readUserProfile } from "./user-profile.js";
15
16
 
16
17
  // Exported for test injection
17
18
  export const _deps = {
@@ -19,6 +20,7 @@ export const _deps = {
19
20
  recallMemory,
20
21
  BM25Search,
21
22
  createHash,
23
+ readUserProfile,
22
24
  };
23
25
 
24
26
  // ─── System prompt cleaning regexes (match desktop KV-Cache optimization) ───
@@ -105,6 +107,19 @@ export class CLIContextEngineering {
105
107
  }
106
108
  }
107
109
 
110
+ // 2b. User profile injection (USER.md)
111
+ try {
112
+ const profile = _deps.readUserProfile();
113
+ if (profile) {
114
+ result.push({
115
+ role: "system",
116
+ content: `## User Profile\n${profile}`,
117
+ });
118
+ }
119
+ } catch (_err) {
120
+ // User profile injection failed — skip silently
121
+ }
122
+
108
123
  // 3. Memory injection (scoped: higher threshold, namespace-aware)
109
124
  if (this.db && userQuery) {
110
125
  try {
@@ -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;