chainlesschain 0.42.2 → 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/README.md +14 -0
- package/package.json +5 -2
- package/src/commands/agent.js +7 -6
- package/src/commands/ask.js +11 -9
- package/src/commands/chat.js +8 -7
- package/src/commands/init.js +1 -1
- package/src/commands/update.js +33 -4
- 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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-Agent Context — isolated execution context for child agents.
|
|
3
|
+
*
|
|
4
|
+
* Provides message isolation, independent context engineering, tool whitelisting,
|
|
5
|
+
* iteration limits, and result summarization. Sub-agents run in their own
|
|
6
|
+
* context and only return a summary to the parent agent.
|
|
7
|
+
*
|
|
8
|
+
* @module sub-agent-context
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import crypto from "crypto";
|
|
12
|
+
import { CLIContextEngineering } from "./cli-context-engineering.js";
|
|
13
|
+
import { agentLoop, buildSystemPrompt, AGENT_TOOLS } from "./agent-core.js";
|
|
14
|
+
|
|
15
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const DEFAULT_MAX_ITERATIONS = 8;
|
|
18
|
+
const SUMMARY_DIRECT_THRESHOLD = 500; // chars — below this, use result as-is
|
|
19
|
+
const SUMMARY_SECTION_PATTERN =
|
|
20
|
+
/^##\s*(Summary|Result|Output|Conclusion|Answer)/im;
|
|
21
|
+
const TRUNCATE_LENGTH = 500;
|
|
22
|
+
|
|
23
|
+
// ─── SubAgentContext ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export class SubAgentContext {
|
|
26
|
+
/**
|
|
27
|
+
* Factory method — creates an isolated sub-agent context.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} options
|
|
30
|
+
* @param {string} options.role - Sub-agent role (e.g. "code-review", "summarizer")
|
|
31
|
+
* @param {string} options.task - Task description for the sub-agent
|
|
32
|
+
* @param {string} [options.parentId] - Parent context ID (null for root)
|
|
33
|
+
* @param {string|null} [options.inheritedContext] - Condensed context from parent
|
|
34
|
+
* @param {string[]} [options.allowedTools] - Tool whitelist (null = all tools)
|
|
35
|
+
* @param {number} [options.maxIterations] - Iteration limit
|
|
36
|
+
* @param {number} [options.tokenBudget] - Optional token budget
|
|
37
|
+
* @param {object} [options.db] - Database instance
|
|
38
|
+
* @param {object} [options.permanentMemory] - Permanent memory instance
|
|
39
|
+
* @param {object} [options.llmOptions] - LLM provider/model/key options
|
|
40
|
+
* @param {string} [options.cwd] - Working directory
|
|
41
|
+
* @returns {SubAgentContext}
|
|
42
|
+
*/
|
|
43
|
+
static create(options = {}) {
|
|
44
|
+
return new SubAgentContext(options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.id = `sub-${crypto.randomUUID().slice(0, 12)}`;
|
|
49
|
+
this.parentId = options.parentId || null;
|
|
50
|
+
this.role = options.role || "general";
|
|
51
|
+
this.task = options.task || "";
|
|
52
|
+
this.maxIterations = options.maxIterations || DEFAULT_MAX_ITERATIONS;
|
|
53
|
+
this.tokenBudget = options.tokenBudget || null;
|
|
54
|
+
this.inheritedContext = options.inheritedContext || null;
|
|
55
|
+
this.allowedTools = options.allowedTools || null; // null = all
|
|
56
|
+
this.cwd = options.cwd || process.cwd();
|
|
57
|
+
this.status = "active";
|
|
58
|
+
this.result = null;
|
|
59
|
+
this.createdAt = new Date().toISOString();
|
|
60
|
+
this.completedAt = null;
|
|
61
|
+
|
|
62
|
+
// ── Isolated state ──────────────────────────────────────────────
|
|
63
|
+
// Independent message history — never shared with parent
|
|
64
|
+
this.messages = [];
|
|
65
|
+
|
|
66
|
+
// Independent context engine — does not inherit parent's compaction/errors
|
|
67
|
+
this.contextEngine = new CLIContextEngineering({
|
|
68
|
+
db: options.db || null,
|
|
69
|
+
permanentMemory: options.permanentMemory || null,
|
|
70
|
+
scope: {
|
|
71
|
+
taskId: this.id,
|
|
72
|
+
role: this.role,
|
|
73
|
+
parentObjective: this.task,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Track tool usage and token consumption
|
|
78
|
+
this._toolsUsed = [];
|
|
79
|
+
this._tokenCount = 0;
|
|
80
|
+
this._iterationCount = 0;
|
|
81
|
+
|
|
82
|
+
// LLM options for chatWithTools
|
|
83
|
+
this._llmOptions = options.llmOptions || {};
|
|
84
|
+
|
|
85
|
+
// Build isolated system prompt
|
|
86
|
+
const basePrompt = buildSystemPrompt(this.cwd);
|
|
87
|
+
const rolePrompt = `\n\n## Sub-Agent Role: ${this.role}\nYou are a focused sub-agent with the role "${this.role}". Your task is:\n${this.task}\n\nStay focused on this specific task. Be concise and return results directly.`;
|
|
88
|
+
const contextSection = this.inheritedContext
|
|
89
|
+
? `\n\n## Parent Context\n${this.inheritedContext}`
|
|
90
|
+
: "";
|
|
91
|
+
|
|
92
|
+
this.messages.push({
|
|
93
|
+
role: "system",
|
|
94
|
+
content: basePrompt + rolePrompt + contextSection,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run the sub-agent loop with the given user prompt.
|
|
100
|
+
* Collects events, enforces iteration limit, and returns a structured result.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} userPrompt - The task prompt for this sub-agent
|
|
103
|
+
* @param {object} [loopOptions] - Additional options for agentLoop
|
|
104
|
+
* @returns {Promise<{ summary: string, artifacts: Array, tokenCount: number, toolsUsed: string[], iterationCount: number }>}
|
|
105
|
+
*/
|
|
106
|
+
async run(userPrompt, loopOptions = {}) {
|
|
107
|
+
if (this.status !== "active") {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`SubAgentContext ${this.id} is not active (status: ${this.status})`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add user message
|
|
114
|
+
this.messages.push({ role: "user", content: userPrompt });
|
|
115
|
+
|
|
116
|
+
const artifacts = [];
|
|
117
|
+
let lastContent = "";
|
|
118
|
+
|
|
119
|
+
// Build filtered tool list
|
|
120
|
+
const tools = this._getFilteredTools();
|
|
121
|
+
|
|
122
|
+
// Merge LLM options
|
|
123
|
+
const options = {
|
|
124
|
+
...this._llmOptions,
|
|
125
|
+
contextEngine: this.contextEngine,
|
|
126
|
+
cwd: this.cwd,
|
|
127
|
+
...loopOptions,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Use a separate messages array for the agent loop
|
|
132
|
+
// The agentLoop will append to this.messages directly
|
|
133
|
+
const gen = agentLoop(this.messages, options);
|
|
134
|
+
|
|
135
|
+
for await (const event of gen) {
|
|
136
|
+
this._iterationCount++;
|
|
137
|
+
|
|
138
|
+
if (event.type === "tool-executing") {
|
|
139
|
+
this._toolsUsed.push(event.tool);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (event.type === "tool-result") {
|
|
143
|
+
// Store large tool results as artifacts
|
|
144
|
+
const resultStr = JSON.stringify(event.result);
|
|
145
|
+
// Estimate token count from tool result (~4 chars per token)
|
|
146
|
+
this._tokenCount += Math.ceil(resultStr.length / 4);
|
|
147
|
+
if (resultStr.length > 2000) {
|
|
148
|
+
artifacts.push({
|
|
149
|
+
type: "tool-output",
|
|
150
|
+
tool: event.tool,
|
|
151
|
+
content: resultStr,
|
|
152
|
+
truncated: resultStr.length > 10000,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (event.type === "response-complete") {
|
|
158
|
+
lastContent = event.content || "";
|
|
159
|
+
// Estimate token count from response content (~4 chars per token)
|
|
160
|
+
this._tokenCount += Math.ceil((lastContent.length || 0) / 4);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Enforce token budget
|
|
164
|
+
if (this.tokenBudget && this._tokenCount >= this.tokenBudget) {
|
|
165
|
+
this.forceComplete("token-budget-exceeded");
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Enforce iteration limit
|
|
170
|
+
if (this._iterationCount >= this.maxIterations * 3) {
|
|
171
|
+
// 3 events per iteration (executing + result + potential response)
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
this.status = "failed";
|
|
177
|
+
this.completedAt = new Date().toISOString();
|
|
178
|
+
this.result = {
|
|
179
|
+
summary: `Sub-agent failed: ${err.message}`,
|
|
180
|
+
artifacts: [],
|
|
181
|
+
tokenCount: this._tokenCount,
|
|
182
|
+
toolsUsed: [...new Set(this._toolsUsed)],
|
|
183
|
+
iterationCount: this._iterationCount,
|
|
184
|
+
};
|
|
185
|
+
return this.result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Summarize the result
|
|
189
|
+
const summary = this.summarize(lastContent);
|
|
190
|
+
|
|
191
|
+
this.status = "completed";
|
|
192
|
+
this.completedAt = new Date().toISOString();
|
|
193
|
+
this.result = {
|
|
194
|
+
summary,
|
|
195
|
+
artifacts,
|
|
196
|
+
tokenCount: this._tokenCount,
|
|
197
|
+
toolsUsed: [...new Set(this._toolsUsed)],
|
|
198
|
+
iterationCount: this._iterationCount,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return this.result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Three-level summarization strategy.
|
|
206
|
+
*
|
|
207
|
+
* 1. Direct use — result ≤ 500 chars → return as-is
|
|
208
|
+
* 2. Section extraction — if result contains ## Summary/Result → extract that section
|
|
209
|
+
* 3. Truncate + artifact — take first 500 chars, store full output as artifact
|
|
210
|
+
*
|
|
211
|
+
* @param {string} content - Raw result content
|
|
212
|
+
* @returns {string} Summarized content
|
|
213
|
+
*/
|
|
214
|
+
summarize(content) {
|
|
215
|
+
if (!content || content.length === 0) {
|
|
216
|
+
return "(No output from sub-agent)";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Strategy 1: Direct use for short content
|
|
220
|
+
if (content.length <= SUMMARY_DIRECT_THRESHOLD) {
|
|
221
|
+
return content;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Strategy 2: Extract structured section
|
|
225
|
+
const match = content.match(SUMMARY_SECTION_PATTERN);
|
|
226
|
+
if (match) {
|
|
227
|
+
const sectionStart = match.index;
|
|
228
|
+
// Find end of section (next ## heading or end of string)
|
|
229
|
+
const rest = content.slice(sectionStart + match[0].length);
|
|
230
|
+
const nextHeading = rest.search(/^##\s/m);
|
|
231
|
+
const section =
|
|
232
|
+
nextHeading >= 0 ? rest.slice(0, nextHeading).trim() : rest.trim();
|
|
233
|
+
if (section.length > 0 && section.length <= 1000) {
|
|
234
|
+
return section;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Strategy 3: Truncate + note
|
|
239
|
+
return (
|
|
240
|
+
content.substring(0, TRUNCATE_LENGTH) +
|
|
241
|
+
`\n...[truncated, full output: ${content.length} chars]`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get filtered tools based on allowedTools whitelist.
|
|
247
|
+
* @returns {Array} Filtered AGENT_TOOLS
|
|
248
|
+
*/
|
|
249
|
+
_getFilteredTools() {
|
|
250
|
+
if (!this.allowedTools || this.allowedTools.length === 0) {
|
|
251
|
+
return AGENT_TOOLS;
|
|
252
|
+
}
|
|
253
|
+
return AGENT_TOOLS.filter((t) =>
|
|
254
|
+
this.allowedTools.includes(t.function.name),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Force-complete this sub-agent (e.g. on timeout or parent cancellation).
|
|
260
|
+
* @param {string} [reason] - Reason for force-completion
|
|
261
|
+
*/
|
|
262
|
+
forceComplete(reason = "forced") {
|
|
263
|
+
if (this.status === "active") {
|
|
264
|
+
this.status = "completed";
|
|
265
|
+
this.completedAt = new Date().toISOString();
|
|
266
|
+
if (!this.result) {
|
|
267
|
+
this.result = {
|
|
268
|
+
summary: `(Sub-agent force-completed: ${reason})`,
|
|
269
|
+
artifacts: [],
|
|
270
|
+
tokenCount: this._tokenCount,
|
|
271
|
+
toolsUsed: [...new Set(this._toolsUsed)],
|
|
272
|
+
iterationCount: this._iterationCount,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get a serializable snapshot of this context (for debugging/logging).
|
|
280
|
+
*/
|
|
281
|
+
toJSON() {
|
|
282
|
+
return {
|
|
283
|
+
id: this.id,
|
|
284
|
+
parentId: this.parentId,
|
|
285
|
+
role: this.role,
|
|
286
|
+
task: this.task,
|
|
287
|
+
status: this.status,
|
|
288
|
+
messageCount: this.messages.length,
|
|
289
|
+
toolsUsed: [...new Set(this._toolsUsed)],
|
|
290
|
+
tokenCount: this._tokenCount,
|
|
291
|
+
iterationCount: this._iterationCount,
|
|
292
|
+
createdAt: this.createdAt,
|
|
293
|
+
completedAt: this.completedAt,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -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) {
|