chainlesschain 0.42.3 → 0.43.0
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/package.json +1 -1
- package/src/commands/init.js +1 -1
- package/src/lib/agent-coordinator.js +111 -0
- package/src/lib/agent-core.js +167 -2
- package/src/lib/cli-context-engineering.js +48 -15
- package/src/lib/cowork/debate-review-cli.js +12 -2
- package/src/lib/hierarchical-memory.js +186 -68
- package/src/lib/sub-agent-context.js +296 -0
- package/src/lib/sub-agent-registry.js +186 -0
- package/src/lib/ws-session-manager.js +8 -0
- package/src/repl/agent-repl.js +45 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-Agent Registry — lifecycle tracking for sub-agents.
|
|
3
|
+
*
|
|
4
|
+
* Tracks active sub-agents, maintains completion history (ring buffer),
|
|
5
|
+
* and provides statistics. Singleton pattern for process-wide access.
|
|
6
|
+
*
|
|
7
|
+
* @module sub-agent-registry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Ring Buffer ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
class RingBuffer {
|
|
13
|
+
constructor(capacity = 100) {
|
|
14
|
+
this._buffer = new Array(capacity);
|
|
15
|
+
this._capacity = capacity;
|
|
16
|
+
this._head = 0;
|
|
17
|
+
this._size = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
push(item) {
|
|
21
|
+
this._buffer[this._head] = item;
|
|
22
|
+
this._head = (this._head + 1) % this._capacity;
|
|
23
|
+
if (this._size < this._capacity) this._size++;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toArray() {
|
|
27
|
+
if (this._size === 0) return [];
|
|
28
|
+
if (this._size < this._capacity) {
|
|
29
|
+
return this._buffer.slice(0, this._size);
|
|
30
|
+
}
|
|
31
|
+
// Wrap around — oldest first
|
|
32
|
+
return [
|
|
33
|
+
...this._buffer.slice(this._head),
|
|
34
|
+
...this._buffer.slice(0, this._head),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get size() {
|
|
39
|
+
return this._size;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
clear() {
|
|
43
|
+
this._buffer = new Array(this._capacity);
|
|
44
|
+
this._head = 0;
|
|
45
|
+
this._size = 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── SubAgentRegistry ───────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export class SubAgentRegistry {
|
|
52
|
+
static _instance = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get or create the singleton instance.
|
|
56
|
+
* @returns {SubAgentRegistry}
|
|
57
|
+
*/
|
|
58
|
+
static getInstance() {
|
|
59
|
+
if (!SubAgentRegistry._instance) {
|
|
60
|
+
SubAgentRegistry._instance = new SubAgentRegistry();
|
|
61
|
+
}
|
|
62
|
+
return SubAgentRegistry._instance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reset singleton (for testing).
|
|
67
|
+
*/
|
|
68
|
+
static resetInstance() {
|
|
69
|
+
SubAgentRegistry._instance = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
/** @type {Map<string, import("./sub-agent-context.js").SubAgentContext>} */
|
|
74
|
+
this._active = new Map();
|
|
75
|
+
|
|
76
|
+
/** @type {RingBuffer} */
|
|
77
|
+
this._completed = new RingBuffer(100);
|
|
78
|
+
|
|
79
|
+
this._totalTokens = 0;
|
|
80
|
+
this._totalDurationMs = 0;
|
|
81
|
+
this._completedCount = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register an active sub-agent.
|
|
86
|
+
* @param {import("./sub-agent-context.js").SubAgentContext} subCtx
|
|
87
|
+
*/
|
|
88
|
+
register(subCtx) {
|
|
89
|
+
this._active.set(subCtx.id, subCtx);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Mark a sub-agent as completed and move to history.
|
|
94
|
+
* @param {string} id - Sub-agent ID
|
|
95
|
+
* @param {object} result - { summary, artifacts, tokenCount, toolsUsed, iterationCount }
|
|
96
|
+
*/
|
|
97
|
+
complete(id, result) {
|
|
98
|
+
const subCtx = this._active.get(id);
|
|
99
|
+
if (!subCtx) return;
|
|
100
|
+
|
|
101
|
+
this._active.delete(id);
|
|
102
|
+
|
|
103
|
+
const record = {
|
|
104
|
+
id: subCtx.id,
|
|
105
|
+
parentId: subCtx.parentId,
|
|
106
|
+
role: subCtx.role,
|
|
107
|
+
task: subCtx.task,
|
|
108
|
+
status: subCtx.status,
|
|
109
|
+
summary: result?.summary || "(no summary)",
|
|
110
|
+
toolsUsed: result?.toolsUsed || [],
|
|
111
|
+
tokenCount: result?.tokenCount || 0,
|
|
112
|
+
iterationCount: result?.iterationCount || 0,
|
|
113
|
+
createdAt: subCtx.createdAt,
|
|
114
|
+
completedAt: subCtx.completedAt || new Date().toISOString(),
|
|
115
|
+
durationMs: subCtx.completedAt
|
|
116
|
+
? new Date(subCtx.completedAt) - new Date(subCtx.createdAt)
|
|
117
|
+
: Date.now() - new Date(subCtx.createdAt).getTime(),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this._completed.push(record);
|
|
121
|
+
this._totalTokens += record.tokenCount;
|
|
122
|
+
this._totalDurationMs += record.durationMs;
|
|
123
|
+
this._completedCount++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all active sub-agents.
|
|
128
|
+
* @returns {Array<object>}
|
|
129
|
+
*/
|
|
130
|
+
getActive() {
|
|
131
|
+
return [...this._active.values()].map((ctx) => ctx.toJSON());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get completion history (most recent last).
|
|
136
|
+
* @returns {Array<object>}
|
|
137
|
+
*/
|
|
138
|
+
getHistory() {
|
|
139
|
+
return this._completed.toArray();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Force-complete all sub-agents belonging to a session.
|
|
144
|
+
* Used by ws-session-manager on session close.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} [sessionId] - If provided, only force-complete agents whose parentId matches
|
|
147
|
+
*/
|
|
148
|
+
forceCompleteAll(sessionId) {
|
|
149
|
+
for (const [id, subCtx] of this._active) {
|
|
150
|
+
if (!sessionId || subCtx.parentId === sessionId) {
|
|
151
|
+
subCtx.forceComplete("session-closed");
|
|
152
|
+
this.complete(id, subCtx.result);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Clean up stale entries older than maxAgeMs.
|
|
159
|
+
* @param {number} [maxAgeMs=600000] - Max age in ms (default: 10 minutes)
|
|
160
|
+
*/
|
|
161
|
+
cleanup(maxAgeMs = 600000) {
|
|
162
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
163
|
+
for (const [id, subCtx] of this._active) {
|
|
164
|
+
if (new Date(subCtx.createdAt).getTime() < cutoff) {
|
|
165
|
+
subCtx.forceComplete("timeout");
|
|
166
|
+
this.complete(id, subCtx.result);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get registry statistics.
|
|
173
|
+
*/
|
|
174
|
+
getStats() {
|
|
175
|
+
return {
|
|
176
|
+
active: this._active.size,
|
|
177
|
+
completed: this._completedCount,
|
|
178
|
+
historySize: this._completed.size,
|
|
179
|
+
totalTokens: this._totalTokens,
|
|
180
|
+
avgDurationMs:
|
|
181
|
+
this._completedCount > 0
|
|
182
|
+
? Math.round(this._totalDurationMs / this._completedCount)
|
|
183
|
+
: 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
listSessions as dbListSessions,
|
|
20
20
|
} from "./session-manager.js";
|
|
21
21
|
import { buildSystemPrompt } from "./agent-core.js";
|
|
22
|
+
import { SubAgentRegistry } from "./sub-agent-registry.js";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* @typedef {object} Session
|
|
@@ -266,6 +267,13 @@ export class WSSessionManager {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// Force-complete any active sub-agents for this session
|
|
271
|
+
try {
|
|
272
|
+
SubAgentRegistry.getInstance().forceCompleteAll(sessionId);
|
|
273
|
+
} catch (_err) {
|
|
274
|
+
// Non-critical
|
|
275
|
+
}
|
|
276
|
+
|
|
269
277
|
// Clean up plan manager listeners
|
|
270
278
|
if (session.planManager) {
|
|
271
279
|
session.planManager.removeAllListeners();
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -262,6 +262,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
262
262
|
` ${chalk.cyan("/plan approve")} Approve and execute the plan`,
|
|
263
263
|
);
|
|
264
264
|
logger.log(` ${chalk.cyan("/plan reject")} Reject the plan`);
|
|
265
|
+
logger.log(
|
|
266
|
+
` ${chalk.cyan("/sub-agents")} Show active/completed sub-agents`,
|
|
267
|
+
);
|
|
265
268
|
logger.log(chalk.bold("\nCapabilities:"));
|
|
266
269
|
logger.log(" Read, write, and edit files");
|
|
267
270
|
logger.log(" Run shell commands (git, npm, etc.)");
|
|
@@ -275,6 +278,48 @@ export async function startAgentRepl(options = {}) {
|
|
|
275
278
|
return;
|
|
276
279
|
}
|
|
277
280
|
|
|
281
|
+
if (trimmed === "/sub-agents" || trimmed === "/subagents") {
|
|
282
|
+
try {
|
|
283
|
+
const { SubAgentRegistry } =
|
|
284
|
+
await import("../lib/sub-agent-registry.js");
|
|
285
|
+
const registry = SubAgentRegistry.getInstance();
|
|
286
|
+
const active = registry.getActive();
|
|
287
|
+
const history = registry.getHistory();
|
|
288
|
+
const stats = registry.getStats();
|
|
289
|
+
|
|
290
|
+
logger.log(chalk.bold("\nSub-Agent Registry:"));
|
|
291
|
+
logger.log(
|
|
292
|
+
` Active: ${chalk.yellow(active.length)} Completed: ${chalk.green(stats.completed)} Tokens: ${stats.totalTokens} Avg Duration: ${stats.avgDurationMs}ms`,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (active.length > 0) {
|
|
296
|
+
logger.log(chalk.bold("\n Active Sub-Agents:"));
|
|
297
|
+
for (const a of active) {
|
|
298
|
+
logger.log(
|
|
299
|
+
` ${chalk.cyan(a.id)} [${a.role}] ${a.task.substring(0, 50)} (iter: ${a.iterationCount})`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (history.length > 0) {
|
|
305
|
+
logger.log(chalk.bold("\n Recent History (last 10):"));
|
|
306
|
+
for (const h of history.slice(-10)) {
|
|
307
|
+
const status =
|
|
308
|
+
h.status === "completed" ? chalk.green("✓") : chalk.red("✗");
|
|
309
|
+
logger.log(
|
|
310
|
+
` ${status} ${chalk.dim(h.id)} [${h.role}] ${(h.summary || "").substring(0, 60)}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
logger.log("");
|
|
316
|
+
} catch (_err) {
|
|
317
|
+
logger.log(chalk.dim("Sub-agent registry not available."));
|
|
318
|
+
}
|
|
319
|
+
prompt();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
278
323
|
if (trimmed.startsWith("/model")) {
|
|
279
324
|
const arg = trimmed.slice(6).trim();
|
|
280
325
|
if (arg) {
|