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.
- package/README.md +52 -15
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-BhJ3YFWt.js → AppLayout-2RCrdXxl.js} +1 -1
- package/src/assets/web-panel/assets/AppLayout-D9pBLPC3.css +1 -0
- package/src/assets/web-panel/assets/{Chat-DaxTP3x8.js → Chat-B2nB8o_F.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-CjlX4CrX.js → Dashboard-DanoHPSI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BCvgBkD3.js → Skills-CLlblJcG.js} +1 -1
- package/src/assets/web-panel/assets/chat-DWBA4-cl.js +1 -0
- package/src/assets/web-panel/assets/{index-DrmEk9S3.js → index-CyGtHm63.js} +2 -2
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/learning.js +273 -0
- package/src/commands/lowcode.js +23 -8
- package/src/gateways/discord/discord-formatter.js +89 -0
- package/src/gateways/gateway-base.js +189 -0
- package/src/gateways/telegram/telegram-formatter.js +93 -0
- package/src/index.js +2 -0
- package/src/lib/app-builder.js +136 -8
- package/src/lib/autonomous-agent.js +8 -1
- package/src/lib/cli-context-engineering.js +15 -0
- package/src/lib/execution-backend.js +239 -0
- package/src/lib/hook-manager.js +2 -0
- package/src/lib/iteration-budget.js +175 -0
- package/src/lib/learning/learning-hooks.js +117 -0
- package/src/lib/learning/learning-tables.js +66 -0
- package/src/lib/learning/outcome-feedback.js +243 -0
- package/src/lib/learning/reflection-engine.js +323 -0
- package/src/lib/learning/skill-improver.js +536 -0
- package/src/lib/learning/skill-synthesizer.js +315 -0
- package/src/lib/learning/trajectory-store.js +409 -0
- package/src/lib/plugin-autodiscovery.js +224 -0
- package/src/lib/session-search.js +193 -0
- package/src/lib/sub-agent-context.js +7 -2
- package/src/lib/user-profile.js +172 -0
- package/src/lib/web-ui-server.js +1 -1
- package/src/repl/agent-repl.js +109 -0
- package/src/runtime/agent-core.js +75 -4
- package/src/runtime/coding-agent-contract-shared.cjs +35 -0
- package/src/runtime/coding-agent-policy.cjs +10 -0
- package/src/assets/web-panel/assets/AppLayout-Cr2lWhF-.css +0 -1
- package/src/assets/web-panel/assets/chat-BmwHBi9M.js +0 -1
package/src/lib/app-builder.js
CHANGED
|
@@ -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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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({
|
|
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
|
+
}
|
package/src/lib/hook-manager.js
CHANGED
|
@@ -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;
|