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.
@@ -15,9 +15,83 @@ export const MEMORY_CONFIG = {
15
15
  };
16
16
 
17
17
  // ─── In-memory layers ────────────────────────────────────────────
18
- // Map<id, { id, content, type, importance, accessCount, createdAt, lastAccessed }>
19
- export const _working = new Map();
20
- export const _shortTerm = new Map();
18
+ // Internal storage: Map<namespace, Map<id, entry>>
19
+ // Default namespace "global" preserves backward compatibility.
20
+ const _workingNS = new Map();
21
+ const _shortTermNS = new Map();
22
+ const DEFAULT_NS = "global";
23
+
24
+ function _getWorkingNS(namespace) {
25
+ const ns = namespace || DEFAULT_NS;
26
+ if (!_workingNS.has(ns)) _workingNS.set(ns, new Map());
27
+ return _workingNS.get(ns);
28
+ }
29
+
30
+ function _getShortTermNS(namespace) {
31
+ const ns = namespace || DEFAULT_NS;
32
+ if (!_shortTermNS.has(ns)) _shortTermNS.set(ns, new Map());
33
+ return _shortTermNS.get(ns);
34
+ }
35
+
36
+ // ─── Backward-compatible proxy ──────────────────────────────────
37
+ // Existing code (and tests) access _working/_shortTerm as flat Maps:
38
+ // _working.size, _working.get(id), _working.clear(), _working.delete(id)
39
+ // We proxy these to the default namespace while exposing namespaced internals.
40
+ function _createCompatProxy(nsMap, getNS) {
41
+ return {
42
+ // Flat access — routes to default namespace
43
+ get size() {
44
+ const ns = getNS(DEFAULT_NS);
45
+ return ns.size;
46
+ },
47
+ get(key) {
48
+ const ns = getNS(DEFAULT_NS);
49
+ return ns.get(key);
50
+ },
51
+ set(key, value) {
52
+ const ns = getNS(DEFAULT_NS);
53
+ return ns.set(key, value);
54
+ },
55
+ has(key) {
56
+ const ns = getNS(DEFAULT_NS);
57
+ return ns.has(key);
58
+ },
59
+ delete(key) {
60
+ const ns = getNS(DEFAULT_NS);
61
+ return ns.delete(key);
62
+ },
63
+ values() {
64
+ const ns = getNS(DEFAULT_NS);
65
+ return ns.values();
66
+ },
67
+ entries() {
68
+ const ns = getNS(DEFAULT_NS);
69
+ return ns.entries();
70
+ },
71
+ keys() {
72
+ const ns = getNS(DEFAULT_NS);
73
+ return ns.keys();
74
+ },
75
+ forEach(callback) {
76
+ const ns = getNS(DEFAULT_NS);
77
+ return ns.forEach(callback);
78
+ },
79
+ [Symbol.iterator]() {
80
+ const ns = getNS(DEFAULT_NS);
81
+ return ns[Symbol.iterator]();
82
+ },
83
+ // Clear ALL namespaces (for test cleanup)
84
+ clear() {
85
+ nsMap.clear();
86
+ },
87
+ // ─── Namespace-aware internals (used by this module) ──────
88
+ _nsMap: nsMap,
89
+ _getNS: getNS,
90
+ };
91
+ }
92
+
93
+ export const _working = _createCompatProxy(_workingNS, _getWorkingNS);
94
+ export const _shortTerm = _createCompatProxy(_shortTermNS, _getShortTermNS);
21
95
 
22
96
  // ─── Helpers ─────────────────────────────────────────────────────
23
97
  function generateId() {
@@ -91,6 +165,17 @@ export function ensureMemoryTables(db) {
91
165
  * Store a memory at the appropriate layer based on importance.
92
166
  * core >= 0.9, long-term >= 0.6, short-term >= 0.3, working < 0.3
93
167
  */
168
+ /**
169
+ * Store a memory at the appropriate layer based on importance.
170
+ * core >= 0.9, long-term >= 0.6, short-term >= 0.3, working < 0.3
171
+ *
172
+ * @param {object} db - Database instance
173
+ * @param {string} content - Memory content
174
+ * @param {object} [options]
175
+ * @param {number} [options.importance=0.5]
176
+ * @param {string} [options.type="episodic"]
177
+ * @param {string} [options.namespace] - Namespace for in-memory isolation (default: "global")
178
+ */
94
179
  export function storeMemory(db, content, options = {}) {
95
180
  if (!content || !content.trim()) {
96
181
  throw new Error("Memory content cannot be empty");
@@ -101,6 +186,7 @@ export function storeMemory(db, content, options = {}) {
101
186
  Math.min(1, parseFloat(options.importance) || 0.5),
102
187
  );
103
188
  const type = options.type || "episodic";
189
+ const namespace = options.namespace || DEFAULT_NS;
104
190
  const id = generateId();
105
191
  const now = nowISO();
106
192
 
@@ -119,14 +205,15 @@ export function storeMemory(db, content, options = {}) {
119
205
  ).run(id, content, type, importance, "long-term", now, now);
120
206
  } else if (importance >= 0.3) {
121
207
  layer = "short-term";
122
- if (_shortTerm.size >= MEMORY_CONFIG.shortTermCapacity) {
208
+ const nsMap = _getShortTermNS(namespace);
209
+ if (nsMap.size >= MEMORY_CONFIG.shortTermCapacity) {
123
210
  // Evict oldest
124
- const oldest = [..._shortTerm.entries()].sort(
211
+ const oldest = [...nsMap.entries()].sort(
125
212
  (a, b) => new Date(a[1].lastAccessed) - new Date(b[1].lastAccessed),
126
213
  )[0];
127
- if (oldest) _shortTerm.delete(oldest[0]);
214
+ if (oldest) nsMap.delete(oldest[0]);
128
215
  }
129
- _shortTerm.set(id, {
216
+ nsMap.set(id, {
130
217
  id,
131
218
  content,
132
219
  type,
@@ -137,13 +224,14 @@ export function storeMemory(db, content, options = {}) {
137
224
  });
138
225
  } else {
139
226
  layer = "working";
140
- if (_working.size >= MEMORY_CONFIG.workingCapacity) {
141
- const oldest = [..._working.entries()].sort(
227
+ const nsMap = _getWorkingNS(namespace);
228
+ if (nsMap.size >= MEMORY_CONFIG.workingCapacity) {
229
+ const oldest = [...nsMap.entries()].sort(
142
230
  (a, b) => new Date(a[1].lastAccessed) - new Date(b[1].lastAccessed),
143
231
  )[0];
144
- if (oldest) _working.delete(oldest[0]);
232
+ if (oldest) nsMap.delete(oldest[0]);
145
233
  }
146
- _working.set(id, {
234
+ nsMap.set(id, {
147
235
  id,
148
236
  content,
149
237
  type,
@@ -162,16 +250,22 @@ export function storeMemory(db, content, options = {}) {
162
250
  /**
163
251
  * Search all memory layers with Ebbinghaus forgetting curve.
164
252
  * Strengthens recalled memories (spacing effect).
253
+ *
254
+ * When options.namespace is set, searches that namespace's in-memory maps
255
+ * plus the shared long-term/core DB layers. Without namespace, searches
256
+ * the default "global" namespace (backward compatible).
165
257
  */
166
258
  export function recallMemory(db, query, options = {}) {
167
259
  if (!query || !query.trim()) return [];
168
260
 
169
261
  const limit = Math.max(1, parseInt(options.limit) || 20);
170
262
  const pattern = query.toLowerCase();
263
+ const namespace = options.namespace || DEFAULT_NS;
171
264
  const results = [];
172
265
 
173
- // Search working memory
174
- for (const mem of _working.values()) {
266
+ // Search working memory (namespace-scoped)
267
+ const workingNS = _getWorkingNS(namespace);
268
+ for (const mem of workingNS.values()) {
175
269
  if (mem.content.toLowerCase().includes(pattern)) {
176
270
  const retention = calcRetention(mem.lastAccessed);
177
271
  if (retention >= MEMORY_CONFIG.recallThreshold) {
@@ -182,8 +276,9 @@ export function recallMemory(db, query, options = {}) {
182
276
  }
183
277
  }
184
278
 
185
- // Search short-term memory
186
- for (const mem of _shortTerm.values()) {
279
+ // Search short-term memory (namespace-scoped)
280
+ const shortTermNS = _getShortTermNS(namespace);
281
+ for (const mem of shortTermNS.values()) {
187
282
  if (mem.content.toLowerCase().includes(pattern)) {
188
283
  const retention = calcRetention(mem.lastAccessed);
189
284
  if (retention >= MEMORY_CONFIG.recallThreshold) {
@@ -260,41 +355,46 @@ export function consolidateMemory(db) {
260
355
  let promoted = 0;
261
356
  let forgotten = 0;
262
357
 
263
- // Promote working → short-term
264
- for (const [id, mem] of _working) {
265
- const retention = calcRetention(mem.lastAccessed);
266
- if (retention < MEMORY_CONFIG.recallThreshold) {
267
- _working.delete(id);
268
- forgotten++;
269
- } else if (mem.accessCount >= 3) {
270
- _working.delete(id);
271
- _shortTerm.set(id, { ...mem, lastAccessed: nowISO() });
272
- promoted++;
358
+ // Promote working → short-term (across all namespaces)
359
+ for (const [ns, nsMap] of _workingNS) {
360
+ const shortTermNS = _getShortTermNS(ns);
361
+ for (const [id, mem] of nsMap) {
362
+ const retention = calcRetention(mem.lastAccessed);
363
+ if (retention < MEMORY_CONFIG.recallThreshold) {
364
+ nsMap.delete(id);
365
+ forgotten++;
366
+ } else if (mem.accessCount >= 3) {
367
+ nsMap.delete(id);
368
+ shortTermNS.set(id, { ...mem, lastAccessed: nowISO() });
369
+ promoted++;
370
+ }
273
371
  }
274
372
  }
275
373
 
276
- // Promote short-term → long-term
277
- for (const [id, mem] of _shortTerm) {
278
- const retention = calcRetention(mem.lastAccessed);
279
- if (retention < MEMORY_CONFIG.recallThreshold) {
280
- _shortTerm.delete(id);
281
- forgotten++;
282
- } else if (mem.accessCount >= 5) {
283
- _shortTerm.delete(id);
284
- const now = nowISO();
285
- db.prepare(
286
- `INSERT INTO memory_long_term (id, content, type, importance, access_count, layer, created_at, last_accessed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
287
- ).run(
288
- id,
289
- mem.content,
290
- mem.type,
291
- mem.importance,
292
- mem.accessCount,
293
- "long-term",
294
- mem.createdAt,
295
- now,
296
- );
297
- promoted++;
374
+ // Promote short-term → long-term (across all namespaces)
375
+ for (const [, nsMap] of _shortTermNS) {
376
+ for (const [id, mem] of nsMap) {
377
+ const retention = calcRetention(mem.lastAccessed);
378
+ if (retention < MEMORY_CONFIG.recallThreshold) {
379
+ nsMap.delete(id);
380
+ forgotten++;
381
+ } else if (mem.accessCount >= 5) {
382
+ nsMap.delete(id);
383
+ const now = nowISO();
384
+ db.prepare(
385
+ `INSERT INTO memory_long_term (id, content, type, importance, access_count, layer, created_at, last_accessed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
386
+ ).run(
387
+ id,
388
+ mem.content,
389
+ mem.type,
390
+ mem.importance,
391
+ mem.accessCount,
392
+ "long-term",
393
+ mem.createdAt,
394
+ now,
395
+ );
396
+ promoted++;
397
+ }
298
398
  }
299
399
  }
300
400
 
@@ -324,15 +424,19 @@ function _searchByType(db, query, type, options = {}) {
324
424
  const pattern = query.toLowerCase();
325
425
  const results = [];
326
426
 
327
- // In-memory layers
328
- for (const mem of _working.values()) {
329
- if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
330
- results.push({ ...mem, layer: "working" });
427
+ // In-memory layers (search all namespaces)
428
+ for (const [, nsMap] of _workingNS) {
429
+ for (const mem of nsMap.values()) {
430
+ if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
431
+ results.push({ ...mem, layer: "working" });
432
+ }
331
433
  }
332
434
  }
333
- for (const mem of _shortTerm.values()) {
334
- if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
335
- results.push({ ...mem, layer: "short-term" });
435
+ for (const [, nsMap] of _shortTermNS) {
436
+ for (const mem of nsMap.values()) {
437
+ if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
438
+ results.push({ ...mem, layer: "short-term" });
439
+ }
336
440
  }
337
441
  }
338
442
 
@@ -412,19 +516,23 @@ export function pruneMemory(db, options = {}) {
412
516
  const maxAgeHours = parseFloat(options.maxAge) || 720; // 30 days default
413
517
  let pruned = 0;
414
518
 
415
- // Prune in-memory layers by retention
416
- for (const [id, mem] of _working) {
417
- const retention = calcRetention(mem.lastAccessed);
418
- if (retention < MEMORY_CONFIG.recallThreshold) {
419
- _working.delete(id);
420
- pruned++;
519
+ // Prune in-memory layers by retention (across all namespaces)
520
+ for (const [, nsMap] of _workingNS) {
521
+ for (const [id, mem] of nsMap) {
522
+ const retention = calcRetention(mem.lastAccessed);
523
+ if (retention < MEMORY_CONFIG.recallThreshold) {
524
+ nsMap.delete(id);
525
+ pruned++;
526
+ }
421
527
  }
422
528
  }
423
- for (const [id, mem] of _shortTerm) {
424
- const retention = calcRetention(mem.lastAccessed);
425
- if (retention < MEMORY_CONFIG.recallThreshold) {
426
- _shortTerm.delete(id);
427
- pruned++;
529
+ for (const [, nsMap] of _shortTermNS) {
530
+ for (const [id, mem] of nsMap) {
531
+ const retention = calcRetention(mem.lastAccessed);
532
+ if (retention < MEMORY_CONFIG.recallThreshold) {
533
+ nsMap.delete(id);
534
+ pruned++;
535
+ }
428
536
  }
429
537
  }
430
538
 
@@ -460,12 +568,22 @@ export function getMemoryStats(db) {
460
568
  .prepare(`SELECT COUNT(*) as count FROM memory_sharing`)
461
569
  .get();
462
570
 
571
+ // Sum across all namespaces
572
+ let workingTotal = 0;
573
+ for (const [, nsMap] of _workingNS) workingTotal += nsMap.size;
574
+ let shortTermTotal = 0;
575
+ for (const [, nsMap] of _shortTermNS) shortTermTotal += nsMap.size;
576
+
463
577
  return {
464
- working: _working.size,
465
- shortTerm: _shortTerm.size,
578
+ working: workingTotal,
579
+ shortTerm: shortTermTotal,
466
580
  longTerm: ltCount.count,
467
581
  core: coreCount.count,
468
582
  shared: shareCount.count,
469
- total: _working.size + _shortTerm.size + ltCount.count + coreCount.count,
583
+ namespaces: {
584
+ working: [..._workingNS.keys()],
585
+ shortTerm: [..._shortTermNS.keys()],
586
+ },
587
+ total: workingTotal + shortTermTotal + ltCount.count + coreCount.count,
470
588
  };
471
589
  }
@@ -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
+ }