@ch4p/cli 0.1.5 → 0.2.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.
@@ -0,0 +1,1854 @@
1
+ import {
2
+ EngineError,
3
+ ToolError,
4
+ abortableSleep,
5
+ backoffDelay
6
+ } from "./chunk-YSCX2QQQ.js";
7
+
8
+ // ../../packages/agent/dist/index.js
9
+ import { Worker } from "worker_threads";
10
+ import { EventEmitter } from "events";
11
+ import { setMaxListeners } from "events";
12
+ import { homedir } from "os";
13
+ var SteeringQueue = class {
14
+ queue = [];
15
+ /**
16
+ * Push a message into the queue. The queue is re-sorted on every push
17
+ * so that drain() always returns messages in priority-then-timestamp order.
18
+ */
19
+ push(msg) {
20
+ this.queue.push(msg);
21
+ this.queue.sort((a, b) => {
22
+ const pa = a.priority ?? 0;
23
+ const pb = b.priority ?? 0;
24
+ if (pa !== pb) return pb - pa;
25
+ return a.timestamp.getTime() - b.timestamp.getTime();
26
+ });
27
+ }
28
+ /**
29
+ * Drain all messages from the queue, returning them in priority order.
30
+ * After this call the queue is empty.
31
+ */
32
+ drain() {
33
+ const messages = this.queue;
34
+ this.queue = [];
35
+ return messages;
36
+ }
37
+ /** Peek at the highest-priority message without removing it. */
38
+ peek() {
39
+ return this.queue[0];
40
+ }
41
+ /** Returns true if any pending message is an abort request. */
42
+ hasAbort() {
43
+ return this.queue.some((m) => m.type === "abort");
44
+ }
45
+ /** Returns true if the queue has any pending messages. */
46
+ hasMessages() {
47
+ return this.queue.length > 0;
48
+ }
49
+ /** Discard all pending messages. */
50
+ clear() {
51
+ this.queue = [];
52
+ }
53
+ /** Number of messages currently in the queue. */
54
+ get length() {
55
+ return this.queue.length;
56
+ }
57
+ };
58
+ var NAMED_STRATEGIES = {
59
+ /** Aggressive sliding window — keeps only the 3 most recent exchanges. */
60
+ sliding_window_3: {
61
+ name: "sliding_window_3",
62
+ type: "sliding",
63
+ compactionTarget: 0.4,
64
+ keepRatio: 0.2,
65
+ preserveRecentToolPairs: 3,
66
+ preserveTaskDescription: true,
67
+ description: "Aggressive sliding window. Best for long multi-step tasks where older context is less relevant."
68
+ },
69
+ /** Conservative sliding — preserves more history for tasks that need it. */
70
+ sliding_conservative: {
71
+ name: "sliding_conservative",
72
+ type: "sliding",
73
+ compactionTarget: 0.7,
74
+ keepRatio: 0.5,
75
+ preserveRecentToolPairs: 5,
76
+ preserveTaskDescription: true,
77
+ description: "Conservative sliding window. Best for tasks that reference earlier context frequently."
78
+ },
79
+ /** Summarize with high keep ratio — good for coding tasks. */
80
+ summarize_coding: {
81
+ name: "summarize_coding",
82
+ type: "summarize",
83
+ compactionTarget: 0.6,
84
+ keepRatio: 0.4,
85
+ preserveRecentToolPairs: 4,
86
+ preserveTaskDescription: true,
87
+ description: "Summarize old context while keeping recent code-related tool calls. Best for coding tasks."
88
+ },
89
+ /** Drop oldest with task description pinning. */
90
+ drop_oldest_pinned: {
91
+ name: "drop_oldest_pinned",
92
+ type: "drop_oldest",
93
+ compactionTarget: 0.5,
94
+ preserveRecentToolPairs: 2,
95
+ preserveTaskDescription: true,
96
+ description: "Drop oldest messages but always preserve the original task and recent tool calls."
97
+ }
98
+ };
99
+ function estimateTokens(msg) {
100
+ let chars = 0;
101
+ if (typeof msg.content === "string") {
102
+ chars += msg.content.length;
103
+ } else {
104
+ for (const block of msg.content) {
105
+ if (block.text) chars += block.text.length;
106
+ if (block.toolOutput) chars += block.toolOutput.length;
107
+ if (block.toolInput) chars += JSON.stringify(block.toolInput).length;
108
+ }
109
+ }
110
+ if (Array.isArray(msg.toolCalls)) {
111
+ for (const tc of msg.toolCalls) {
112
+ if (tc.name) chars += tc.name.length;
113
+ if (tc.args != null) chars += JSON.stringify(tc.args).length;
114
+ }
115
+ }
116
+ return Math.ceil(chars / 4);
117
+ }
118
+ function isToolResultMessage(msg) {
119
+ return msg.role === "tool" || msg.toolCallId !== void 0 && msg.toolCallId !== "";
120
+ }
121
+ function hasToolCalls(msg) {
122
+ return msg.role === "assistant" && Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0;
123
+ }
124
+ var ContextManager = class {
125
+ messages = [];
126
+ systemPrompt = null;
127
+ tokenEstimate = 0;
128
+ maxTokens;
129
+ compactionThreshold;
130
+ maxMessages;
131
+ strategyType;
132
+ namedStrategy;
133
+ summarizer;
134
+ constructor(opts = {}) {
135
+ this.maxTokens = opts.maxTokens ?? 1e5;
136
+ this.compactionThreshold = opts.compactionThreshold ?? 0.85;
137
+ this.maxMessages = opts.maxMessages ?? 500;
138
+ this.summarizer = opts.summarizer;
139
+ if (typeof opts.strategy === "object" && opts.strategy !== null) {
140
+ this.namedStrategy = opts.strategy;
141
+ this.strategyType = opts.strategy.type;
142
+ } else {
143
+ this.namedStrategy = null;
144
+ this.strategyType = opts.strategy ?? "sliding";
145
+ }
146
+ }
147
+ // -----------------------------------------------------------------------
148
+ // Public API
149
+ // -----------------------------------------------------------------------
150
+ /** Set or replace the system prompt (always position 0). */
151
+ setSystemPrompt(prompt) {
152
+ const msg = { role: "system", content: prompt };
153
+ if (this.systemPrompt) {
154
+ this.tokenEstimate -= estimateTokens(this.systemPrompt);
155
+ }
156
+ this.systemPrompt = msg;
157
+ this.tokenEstimate += estimateTokens(msg);
158
+ }
159
+ /** Append a message to the context. Triggers compaction if over threshold. */
160
+ async addMessage(msg) {
161
+ const tokens = estimateTokens(msg);
162
+ this.messages.push(msg);
163
+ this.tokenEstimate += tokens;
164
+ if (this.tokenEstimate > this.maxTokens * this.compactionThreshold || this.messages.length > this.maxMessages) {
165
+ await this.compact();
166
+ }
167
+ }
168
+ /** Return the full message array (system prompt + conversation). */
169
+ getMessages() {
170
+ if (this.systemPrompt) {
171
+ return [this.systemPrompt, ...this.messages];
172
+ }
173
+ return [...this.messages];
174
+ }
175
+ /** Return the current approximate token usage. */
176
+ getTokenEstimate() {
177
+ return this.tokenEstimate;
178
+ }
179
+ /** Return the configured maximum token budget. */
180
+ getMaxTokens() {
181
+ return this.maxTokens;
182
+ }
183
+ /** Return the configured maximum message count before compaction is forced. */
184
+ getMaxMessages() {
185
+ return this.maxMessages;
186
+ }
187
+ /** Return the active strategy name. */
188
+ getStrategyName() {
189
+ return this.namedStrategy?.name ?? this.strategyType;
190
+ }
191
+ /** Return the full named strategy config if one is active. */
192
+ getNamedStrategy() {
193
+ return this.namedStrategy;
194
+ }
195
+ /** Remove all conversation messages (keeps system prompt). */
196
+ clear() {
197
+ this.messages = [];
198
+ this.tokenEstimate = this.systemPrompt ? estimateTokens(this.systemPrompt) : 0;
199
+ }
200
+ // -----------------------------------------------------------------------
201
+ // Compaction
202
+ // -----------------------------------------------------------------------
203
+ /**
204
+ * Compact the context to fit within the token budget.
205
+ *
206
+ * This is invoked automatically when `addMessage` pushes the estimate past
207
+ * the compaction threshold, but can also be called manually.
208
+ */
209
+ async compact() {
210
+ switch (this.strategyType) {
211
+ case "drop_oldest":
212
+ this.compactDropOldest();
213
+ break;
214
+ case "summarize":
215
+ await this.compactSummarize();
216
+ break;
217
+ case "sliding":
218
+ await this.compactSliding();
219
+ break;
220
+ }
221
+ }
222
+ // -----------------------------------------------------------------------
223
+ // Strategy 1: drop_oldest — remove oldest messages, preserving tool pairs
224
+ // -----------------------------------------------------------------------
225
+ compactDropOldest() {
226
+ const target = this.maxTokens * (this.namedStrategy?.compactionTarget ?? 0.6);
227
+ let idx = 0;
228
+ const protectedIndices = this.getProtectedIndices();
229
+ while (this.tokenEstimate > target && idx < this.messages.length - 1) {
230
+ const msg = this.messages[idx];
231
+ if (protectedIndices.has(idx)) {
232
+ idx++;
233
+ continue;
234
+ }
235
+ if (hasToolCalls(msg)) {
236
+ const groupEnd = this.findToolGroupEnd(idx);
237
+ let groupProtected = false;
238
+ for (let i = idx; i <= groupEnd; i++) {
239
+ if (protectedIndices.has(i)) {
240
+ groupProtected = true;
241
+ break;
242
+ }
243
+ }
244
+ if (groupProtected) {
245
+ idx = groupEnd + 1;
246
+ continue;
247
+ }
248
+ const dropped = this.messages.splice(idx, groupEnd - idx + 1);
249
+ for (const d of dropped) this.tokenEstimate -= estimateTokens(d);
250
+ continue;
251
+ }
252
+ if (isToolResultMessage(msg)) {
253
+ idx++;
254
+ continue;
255
+ }
256
+ this.messages.splice(idx, 1);
257
+ this.tokenEstimate -= estimateTokens(msg);
258
+ }
259
+ }
260
+ // -----------------------------------------------------------------------
261
+ // Strategy 2: summarize — collapse old messages into one summary message
262
+ // -----------------------------------------------------------------------
263
+ async compactSummarize() {
264
+ if (!this.summarizer) {
265
+ this.compactDropOldest();
266
+ return;
267
+ }
268
+ const keepRatio = this.namedStrategy?.keepRatio ?? 0.3;
269
+ const keepCount = Math.max(2, Math.floor(this.messages.length * keepRatio));
270
+ const splitIdx = this.messages.length - keepCount;
271
+ if (this.namedStrategy?.preserveTaskDescription) {
272
+ const firstUserIdx = this.messages.findIndex((m) => m.role === "user");
273
+ if (firstUserIdx >= 0 && firstUserIdx < splitIdx) {
274
+ }
275
+ }
276
+ const toSummarize = this.messages.slice(0, splitIdx);
277
+ const toKeep = this.messages.slice(splitIdx);
278
+ if (toSummarize.length === 0) return;
279
+ let taskDescription;
280
+ if (this.namedStrategy?.preserveTaskDescription) {
281
+ const firstUser = toSummarize.find((m) => m.role === "user");
282
+ if (firstUser) {
283
+ taskDescription = firstUser;
284
+ }
285
+ }
286
+ const summary = await this.summarizer(toSummarize);
287
+ const summaryMsg = {
288
+ role: "system",
289
+ content: `[Conversation summary]
290
+ ${summary}`
291
+ };
292
+ if (taskDescription) {
293
+ this.messages = [taskDescription, summaryMsg, ...toKeep];
294
+ } else {
295
+ this.messages = [summaryMsg, ...toKeep];
296
+ }
297
+ this.recalculateTokens();
298
+ }
299
+ // -----------------------------------------------------------------------
300
+ // Strategy 3: sliding — sliding window with summary prefix
301
+ // -----------------------------------------------------------------------
302
+ async compactSliding() {
303
+ if (!this.summarizer) {
304
+ this.compactDropOldest();
305
+ return;
306
+ }
307
+ const compactionTarget = this.namedStrategy?.compactionTarget ?? 0.6;
308
+ const targetTokens = this.maxTokens * compactionTarget;
309
+ let windowTokens = 0;
310
+ let windowStart = this.messages.length;
311
+ const preserveToolPairs = this.namedStrategy?.preserveRecentToolPairs ?? 3;
312
+ let toolPairsFound = 0;
313
+ for (let i = this.messages.length - 1; i >= 0; i--) {
314
+ const msgTokens = estimateTokens(this.messages[i]);
315
+ if (windowTokens + msgTokens > targetTokens && toolPairsFound >= preserveToolPairs) break;
316
+ if (isToolResultMessage(this.messages[i]) && i > 0 && hasToolCalls(this.messages[i - 1])) {
317
+ windowTokens += msgTokens + estimateTokens(this.messages[i - 1]);
318
+ windowStart = i - 1;
319
+ toolPairsFound++;
320
+ i--;
321
+ } else {
322
+ windowTokens += msgTokens;
323
+ windowStart = i;
324
+ if (hasToolCalls(this.messages[i])) {
325
+ toolPairsFound++;
326
+ }
327
+ }
328
+ }
329
+ if (windowStart <= 0) return;
330
+ const toSummarize = this.messages.slice(0, windowStart);
331
+ const window = this.messages.slice(windowStart);
332
+ if (toSummarize.length === 0) return;
333
+ let taskDescription;
334
+ if (this.namedStrategy?.preserveTaskDescription) {
335
+ const firstUser = toSummarize.find((m) => m.role === "user");
336
+ if (firstUser) {
337
+ taskDescription = firstUser;
338
+ }
339
+ }
340
+ const summary = await this.summarizer(toSummarize);
341
+ const summaryMsg = {
342
+ role: "system",
343
+ content: `[Conversation summary]
344
+ ${summary}`
345
+ };
346
+ if (taskDescription) {
347
+ this.messages = [taskDescription, summaryMsg, ...window];
348
+ } else {
349
+ this.messages = [summaryMsg, ...window];
350
+ }
351
+ this.recalculateTokens();
352
+ }
353
+ // -----------------------------------------------------------------------
354
+ // AWM: Protected message index computation
355
+ // -----------------------------------------------------------------------
356
+ /**
357
+ * Compute the set of message indices that should never be compacted away.
358
+ * This implements the AWM insight that preserving task description and
359
+ * recent tool interactions dramatically improves task success rates.
360
+ */
361
+ getProtectedIndices() {
362
+ const protected_ = /* @__PURE__ */ new Set();
363
+ if (this.namedStrategy?.preserveTaskDescription !== false) {
364
+ const firstUserIdx = this.messages.findIndex((m) => m.role === "user");
365
+ if (firstUserIdx >= 0) {
366
+ protected_.add(firstUserIdx);
367
+ }
368
+ }
369
+ if (this.namedStrategy?.pinnedRoles) {
370
+ for (let i = 0; i < this.messages.length; i++) {
371
+ if (this.namedStrategy.pinnedRoles.includes(this.messages[i].role)) {
372
+ protected_.add(i);
373
+ }
374
+ }
375
+ }
376
+ const preserveCount = this.namedStrategy?.preserveRecentToolPairs ?? 0;
377
+ if (preserveCount > 0) {
378
+ let pairsFound = 0;
379
+ for (let i = this.messages.length - 1; i >= 0 && pairsFound < preserveCount; i--) {
380
+ if (hasToolCalls(this.messages[i])) {
381
+ protected_.add(i);
382
+ const groupEnd = this.findToolGroupEnd(i);
383
+ for (let j = i; j <= groupEnd; j++) {
384
+ protected_.add(j);
385
+ }
386
+ pairsFound++;
387
+ }
388
+ }
389
+ }
390
+ return protected_;
391
+ }
392
+ // -----------------------------------------------------------------------
393
+ // Internal helpers
394
+ // -----------------------------------------------------------------------
395
+ /**
396
+ * Given the index of an assistant message with tool calls, find the index
397
+ * of the last related tool-result message that immediately follows it.
398
+ */
399
+ findToolGroupEnd(assistantIdx) {
400
+ let end = assistantIdx;
401
+ for (let i = assistantIdx + 1; i < this.messages.length; i++) {
402
+ if (isToolResultMessage(this.messages[i])) {
403
+ end = i;
404
+ } else {
405
+ break;
406
+ }
407
+ }
408
+ return end;
409
+ }
410
+ /** Recompute tokenEstimate from scratch. */
411
+ recalculateTokens() {
412
+ this.tokenEstimate = this.systemPrompt ? estimateTokens(this.systemPrompt) : 0;
413
+ for (const msg of this.messages) {
414
+ this.tokenEstimate += estimateTokens(msg);
415
+ }
416
+ }
417
+ };
418
+ var DEFAULT_WORKER_SCRIPT = `
419
+ const { parentPort } = require('node:worker_threads');
420
+
421
+ parentPort.on('message', async (msg) => {
422
+ if (msg.type === 'execute') {
423
+ try {
424
+ // In production the worker would look up the tool in a registry.
425
+ // Here we simply return an error indicating the tool is not loaded.
426
+ parentPort.postMessage({
427
+ type: 'result',
428
+ result: {
429
+ success: false,
430
+ output: '',
431
+ error: 'Worker has no tool registry \u2014 provide a workerScript.',
432
+ },
433
+ });
434
+ } catch (err) {
435
+ parentPort.postMessage({
436
+ type: 'error',
437
+ message: err instanceof Error ? err.message : String(err),
438
+ });
439
+ }
440
+ }
441
+ });
442
+ `;
443
+ var ToolWorkerPool = class extends EventEmitter {
444
+ workers = [];
445
+ taskQueue = [];
446
+ shuttingDown = false;
447
+ maxWorkers;
448
+ taskTimeoutMs;
449
+ workerScript;
450
+ // Stats
451
+ totalTasks = 0;
452
+ completedTasks = 0;
453
+ failedTasks = 0;
454
+ totalDurationMs = 0;
455
+ stubWarned = false;
456
+ constructor(opts = {}) {
457
+ super();
458
+ this.maxWorkers = opts.maxWorkers ?? 4;
459
+ this.taskTimeoutMs = opts.taskTimeoutMs ?? 6e4;
460
+ this.workerScript = opts.workerScript;
461
+ }
462
+ // -----------------------------------------------------------------------
463
+ // Public API
464
+ // -----------------------------------------------------------------------
465
+ /**
466
+ * Execute a tool task in a worker thread. Returns a promise that resolves
467
+ * with the ToolResult. If `signal` is already aborted the task is rejected
468
+ * immediately.
469
+ */
470
+ execute(task, signal, onProgress = () => {
471
+ }) {
472
+ if (this.shuttingDown) {
473
+ return Promise.reject(new Error("Worker pool is shutting down"));
474
+ }
475
+ if (signal?.aborted) {
476
+ return Promise.reject(new Error("Task aborted before execution"));
477
+ }
478
+ this.totalTasks++;
479
+ return new Promise((resolve, reject) => {
480
+ const queued = { task, signal, onProgress, resolve, reject };
481
+ if (signal) {
482
+ const onAbort = () => {
483
+ const idx = this.taskQueue.indexOf(queued);
484
+ if (idx !== -1) {
485
+ this.taskQueue.splice(idx, 1);
486
+ this.failedTasks++;
487
+ reject(new Error("Task aborted while queued"));
488
+ }
489
+ };
490
+ signal.addEventListener("abort", onAbort, { once: true });
491
+ }
492
+ const idle = this.getIdleWorker();
493
+ if (idle) {
494
+ this.dispatch(idle, queued);
495
+ } else if (this.workers.length < this.maxWorkers) {
496
+ const managed = this.spawnWorker();
497
+ this.dispatch(managed, queued);
498
+ } else {
499
+ this.taskQueue.push(queued);
500
+ }
501
+ });
502
+ }
503
+ /** Gracefully shut down the pool: finish in-flight tasks, then terminate. */
504
+ async shutdown() {
505
+ this.shuttingDown = true;
506
+ for (const q of this.taskQueue) {
507
+ this.failedTasks++;
508
+ q.reject(new Error("Worker pool shutting down"));
509
+ }
510
+ this.taskQueue = [];
511
+ const terminatePromises = this.workers.map((mw) => {
512
+ if (mw.currentTimer) clearTimeout(mw.currentTimer);
513
+ return mw.worker.terminate();
514
+ });
515
+ await Promise.allSettled(terminatePromises);
516
+ this.workers = [];
517
+ }
518
+ /** Check whether a real worker script is configured (not the default stub). */
519
+ hasWorkerScript() {
520
+ return this.workerScript !== void 0;
521
+ }
522
+ /** Return current pool statistics. */
523
+ getStats() {
524
+ const activeWorkers = this.workers.filter((w) => w.busy).length;
525
+ return {
526
+ totalTasks: this.totalTasks,
527
+ completedTasks: this.completedTasks,
528
+ failedTasks: this.failedTasks,
529
+ activeTasks: activeWorkers,
530
+ queuedTasks: this.taskQueue.length,
531
+ avgDurationMs: this.completedTasks > 0 ? Math.round(this.totalDurationMs / this.completedTasks) : 0,
532
+ workerCount: this.workers.length,
533
+ idleWorkers: this.workers.filter((w) => !w.busy).length
534
+ };
535
+ }
536
+ // -----------------------------------------------------------------------
537
+ // Internal
538
+ // -----------------------------------------------------------------------
539
+ spawnWorker() {
540
+ let worker;
541
+ if (this.workerScript) {
542
+ worker = new Worker(this.workerScript);
543
+ } else {
544
+ if (!this.stubWarned) {
545
+ this.stubWarned = true;
546
+ this.emit("worker_stub", "Using default worker stub \u2014 heavyweight tools will fail. Build the worker script or provide workerScript option.");
547
+ }
548
+ worker = new Worker(DEFAULT_WORKER_SCRIPT, { eval: true });
549
+ }
550
+ const managed = {
551
+ worker,
552
+ busy: false,
553
+ taskCount: 0
554
+ };
555
+ worker.on("error", (err) => {
556
+ this.emit("worker_error", err);
557
+ this.handleWorkerCrash(managed, err);
558
+ });
559
+ worker.on("exit", (code) => {
560
+ if (code !== 0 && !this.shuttingDown) {
561
+ this.emit("worker_exit", code);
562
+ this.removeWorker(managed);
563
+ }
564
+ });
565
+ this.workers.push(managed);
566
+ return managed;
567
+ }
568
+ getIdleWorker() {
569
+ return this.workers.find((w) => !w.busy);
570
+ }
571
+ dispatch(managed, queued) {
572
+ managed.busy = true;
573
+ managed.taskCount++;
574
+ const startTime = Date.now();
575
+ const { task, signal, onProgress, resolve, reject } = queued;
576
+ const handler = (msg) => {
577
+ if (msg.type === "progress" && msg.update) {
578
+ onProgress(msg.update);
579
+ return;
580
+ }
581
+ if (msg.type === "result" && msg.result) {
582
+ cleanup();
583
+ const duration = Date.now() - startTime;
584
+ this.completedTasks++;
585
+ this.totalDurationMs += duration;
586
+ managed.worker.removeListener("message", handler);
587
+ resolve(msg.result);
588
+ this.dispatchNext();
589
+ return;
590
+ }
591
+ if (msg.type === "error") {
592
+ cleanup();
593
+ this.failedTasks++;
594
+ managed.worker.removeListener("message", handler);
595
+ resolve({
596
+ success: false,
597
+ output: "",
598
+ error: msg.message ?? "Unknown worker error"
599
+ });
600
+ this.dispatchNext();
601
+ return;
602
+ }
603
+ };
604
+ managed.currentTimer = setTimeout(() => {
605
+ this.failedTasks++;
606
+ managed.busy = false;
607
+ if (managed.currentTimer) clearTimeout(managed.currentTimer);
608
+ managed.currentTimer = void 0;
609
+ managed.worker.removeListener("message", handler);
610
+ try {
611
+ managed.worker.postMessage({ type: "abort" });
612
+ } catch {
613
+ }
614
+ managed.worker.terminate().catch(() => {
615
+ });
616
+ this.removeWorker(managed);
617
+ reject(new Error(`Tool "${task.tool}" timed out after ${this.taskTimeoutMs}ms`));
618
+ this.dispatchNext();
619
+ }, this.taskTimeoutMs);
620
+ let abortListener;
621
+ if (signal) {
622
+ abortListener = () => {
623
+ if (managed.currentTimer) clearTimeout(managed.currentTimer);
624
+ managed.currentTimer = void 0;
625
+ managed.busy = false;
626
+ this.failedTasks++;
627
+ managed.worker.removeListener("message", handler);
628
+ try {
629
+ managed.worker.postMessage({ type: "abort" });
630
+ } catch {
631
+ }
632
+ managed.worker.terminate().catch(() => {
633
+ });
634
+ this.removeWorker(managed);
635
+ reject(new Error("Task aborted during execution"));
636
+ this.dispatchNext();
637
+ };
638
+ signal.addEventListener("abort", abortListener, { once: true });
639
+ }
640
+ const cleanup = () => {
641
+ if (managed.currentTimer) clearTimeout(managed.currentTimer);
642
+ managed.currentTimer = void 0;
643
+ managed.busy = false;
644
+ if (abortListener && signal) {
645
+ signal.removeEventListener("abort", abortListener);
646
+ }
647
+ };
648
+ managed.worker.on("message", handler);
649
+ managed.worker.postMessage({
650
+ type: "execute",
651
+ tool: task.tool,
652
+ args: task.args,
653
+ context: task.context
654
+ });
655
+ }
656
+ dispatchNext() {
657
+ if (this.taskQueue.length === 0) return;
658
+ if (this.shuttingDown) return;
659
+ const idle = this.getIdleWorker();
660
+ if (idle) {
661
+ const next = this.taskQueue.shift();
662
+ if (next.signal?.aborted) {
663
+ this.failedTasks++;
664
+ next.reject(new Error("Task aborted while queued"));
665
+ this.dispatchNext();
666
+ return;
667
+ }
668
+ this.dispatch(idle, next);
669
+ } else if (this.workers.length < this.maxWorkers) {
670
+ const managed = this.spawnWorker();
671
+ const next = this.taskQueue.shift();
672
+ if (next.signal?.aborted) {
673
+ this.failedTasks++;
674
+ next.reject(new Error("Task aborted while queued"));
675
+ this.dispatchNext();
676
+ return;
677
+ }
678
+ this.dispatch(managed, next);
679
+ }
680
+ }
681
+ handleWorkerCrash(managed, _err) {
682
+ if (managed.currentTimer) clearTimeout(managed.currentTimer);
683
+ managed.currentTimer = void 0;
684
+ this.removeWorker(managed);
685
+ this.dispatchNext();
686
+ }
687
+ removeWorker(managed) {
688
+ const idx = this.workers.indexOf(managed);
689
+ if (idx !== -1) {
690
+ this.workers.splice(idx, 1);
691
+ }
692
+ }
693
+ };
694
+ var Session = class {
695
+ config;
696
+ context;
697
+ steering;
698
+ metadata;
699
+ maxErrors;
700
+ state;
701
+ constructor(config, opts = {}) {
702
+ this.config = config;
703
+ this.state = "created";
704
+ this.maxErrors = opts.maxErrors ?? 20;
705
+ this.context = opts.sharedContext ?? new ContextManager(opts.contextOpts);
706
+ this.steering = new SteeringQueue();
707
+ this.metadata = {
708
+ id: config.sessionId,
709
+ channelId: config.channelId,
710
+ userId: config.userId,
711
+ engineId: config.engineId,
712
+ startedAt: /* @__PURE__ */ new Date(),
713
+ state: this.state,
714
+ loopIterations: 0,
715
+ toolInvocations: 0,
716
+ llmCalls: 0,
717
+ errors: []
718
+ };
719
+ if (config.systemPrompt) {
720
+ this.context.setSystemPrompt(config.systemPrompt);
721
+ }
722
+ }
723
+ // -----------------------------------------------------------------------
724
+ // Accessors
725
+ // -----------------------------------------------------------------------
726
+ getId() {
727
+ return this.config.sessionId;
728
+ }
729
+ getConfig() {
730
+ return this.config;
731
+ }
732
+ getContext() {
733
+ return this.context;
734
+ }
735
+ getSteering() {
736
+ return this.steering;
737
+ }
738
+ getState() {
739
+ return this.state;
740
+ }
741
+ getMetadata() {
742
+ return { ...this.metadata, state: this.state };
743
+ }
744
+ // -----------------------------------------------------------------------
745
+ // Lifecycle transitions
746
+ // -----------------------------------------------------------------------
747
+ /** Transition to active. Only valid from created or paused. */
748
+ activate() {
749
+ if (this.state !== "created" && this.state !== "paused") {
750
+ throw new Error(`Cannot activate session in state "${this.state}"`);
751
+ }
752
+ this.state = "active";
753
+ this.metadata.state = this.state;
754
+ }
755
+ /** Pause the session. Only valid from active. */
756
+ pause() {
757
+ if (this.state !== "active") {
758
+ throw new Error(`Cannot pause session in state "${this.state}"`);
759
+ }
760
+ this.state = "paused";
761
+ this.metadata.state = this.state;
762
+ }
763
+ /** Resume from paused back to active. */
764
+ resume() {
765
+ if (this.state !== "paused") {
766
+ throw new Error(`Cannot resume session in state "${this.state}"`);
767
+ }
768
+ this.state = "active";
769
+ this.metadata.state = this.state;
770
+ }
771
+ /** Mark the session as successfully completed. */
772
+ complete() {
773
+ if (this.state !== "active" && this.state !== "paused") {
774
+ throw new Error(`Cannot complete session in state "${this.state}"`);
775
+ }
776
+ this.state = "completed";
777
+ this.metadata.state = this.state;
778
+ this.metadata.endedAt = /* @__PURE__ */ new Date();
779
+ this.steering.clear();
780
+ }
781
+ /** Mark the session as failed with an error. */
782
+ fail(error) {
783
+ this.metadata.errors.push(error);
784
+ if (this.metadata.errors.length > this.maxErrors) {
785
+ this.metadata.errors.shift();
786
+ }
787
+ this.state = "failed";
788
+ this.metadata.state = this.state;
789
+ this.metadata.endedAt = /* @__PURE__ */ new Date();
790
+ this.steering.clear();
791
+ }
792
+ // -----------------------------------------------------------------------
793
+ // Stats tracking
794
+ // -----------------------------------------------------------------------
795
+ /** Increment loop iteration counter. */
796
+ recordIteration() {
797
+ this.metadata.loopIterations++;
798
+ }
799
+ /** Increment tool invocation counter. */
800
+ recordToolInvocation() {
801
+ this.metadata.toolInvocations++;
802
+ }
803
+ /** Increment LLM call counter. */
804
+ recordLLMCall() {
805
+ this.metadata.llmCalls++;
806
+ }
807
+ /** Record an error without failing the session (capped to prevent unbounded growth). */
808
+ recordError(error) {
809
+ this.metadata.errors.push(error);
810
+ if (this.metadata.errors.length > this.maxErrors) {
811
+ this.metadata.errors.shift();
812
+ }
813
+ }
814
+ // -----------------------------------------------------------------------
815
+ // Cleanup
816
+ // -----------------------------------------------------------------------
817
+ /** Clear all session resources. */
818
+ dispose() {
819
+ this.context.clear();
820
+ this.steering.clear();
821
+ }
822
+ };
823
+ function sanitizeWorkspacePath(cwd) {
824
+ const home = homedir();
825
+ if (cwd === home) return ".";
826
+ if (cwd.startsWith(home + "/")) {
827
+ return "./" + cwd.slice(home.length + 1);
828
+ }
829
+ return cwd;
830
+ }
831
+ var DEFAULT_MAX_TOOL_RESULTS = 30;
832
+ var DEFAULT_MAX_TOOL_OUTPUT_LEN = 65536;
833
+ var DEFAULT_MAX_STATE_RECORDS = 20;
834
+ var PERMISSIVE_POLICY = {
835
+ autonomyLevel: "full",
836
+ validatePath: (_path, _op) => ({ allowed: true }),
837
+ validateCommand: (_cmd, _args) => ({ allowed: true }),
838
+ requiresConfirmation: () => false,
839
+ audit: () => [],
840
+ sanitizeOutput: (text) => ({ clean: text, redacted: false }),
841
+ validateInput: () => ({ safe: true, threats: [] })
842
+ };
843
+ function toolDefinitionsFrom(tools) {
844
+ return tools.map((t) => ({
845
+ name: t.name,
846
+ description: t.description,
847
+ parameters: t.parameters
848
+ }));
849
+ }
850
+ var AgentLoop = class {
851
+ session;
852
+ engine;
853
+ tools;
854
+ toolDefs;
855
+ observer;
856
+ opts;
857
+ abortController = null;
858
+ currentHandle = null;
859
+ workerPool;
860
+ ownsWorkerPool;
861
+ /** Accumulated state snapshots for verification (AWM). */
862
+ stateRecords = [];
863
+ /** Accumulated tool results for verification (AWM). */
864
+ allToolResults = [];
865
+ /** Cumulative token usage across all iterations. */
866
+ cumulativeTokens = { inputTokens: 0, outputTokens: 0 };
867
+ // Resolved memory safety caps (configurable via opts / Ch4pConfig.agent).
868
+ maxToolResults;
869
+ maxToolOutputLen;
870
+ maxStateRecords;
871
+ constructor(session, engine, tools, observer, opts = {}) {
872
+ this.session = session;
873
+ this.engine = engine;
874
+ this.observer = observer;
875
+ this.tools = new Map(tools.map((t) => [t.name, t]));
876
+ this.toolDefs = toolDefinitionsFrom(tools);
877
+ if (opts.workerPool) {
878
+ this.workerPool = opts.workerPool;
879
+ this.ownsWorkerPool = false;
880
+ } else {
881
+ this.workerPool = new ToolWorkerPool();
882
+ this.ownsWorkerPool = true;
883
+ }
884
+ this.opts = {
885
+ maxIterations: opts.maxIterations ?? 50,
886
+ maxRetries: opts.maxRetries ?? 3,
887
+ workerPool: this.workerPool,
888
+ verifier: opts.verifier,
889
+ enableStateSnapshots: opts.enableStateSnapshots ?? true,
890
+ memoryBackend: opts.memoryBackend,
891
+ securityPolicy: opts.securityPolicy,
892
+ toolContextExtensions: opts.toolContextExtensions,
893
+ onBeforeFirstRun: opts.onBeforeFirstRun,
894
+ onAfterComplete: opts.onAfterComplete
895
+ };
896
+ this.maxToolResults = opts.maxToolResults ?? DEFAULT_MAX_TOOL_RESULTS;
897
+ this.maxToolOutputLen = opts.maxToolOutputLen ?? DEFAULT_MAX_TOOL_OUTPUT_LEN;
898
+ this.maxStateRecords = opts.maxStateRecords ?? DEFAULT_MAX_STATE_RECORDS;
899
+ }
900
+ // -----------------------------------------------------------------------
901
+ // Public API
902
+ // -----------------------------------------------------------------------
903
+ /** Return the session ID for this agent loop. */
904
+ getSessionId() {
905
+ return this.session.getId();
906
+ }
907
+ /**
908
+ * Run the agent loop, returning an async iterable of AgentEvents.
909
+ * The loop continues until the engine signals completion, the iteration
910
+ * limit is reached, or the run is aborted.
911
+ */
912
+ async *run(initialMessage) {
913
+ this.abortController = new AbortController();
914
+ const signal = this.abortController.signal;
915
+ try {
916
+ setMaxListeners(this.opts.maxIterations + 5, signal);
917
+ } catch {
918
+ }
919
+ this.stateRecords = [];
920
+ this.allToolResults = [];
921
+ this.session.activate();
922
+ this.observer.onSessionStart({
923
+ sessionId: this.session.getId(),
924
+ channelId: this.session.getConfig().channelId,
925
+ userId: this.session.getConfig().userId,
926
+ engineId: this.session.getConfig().engineId,
927
+ startedAt: /* @__PURE__ */ new Date()
928
+ });
929
+ await this.session.getContext().addMessage({
930
+ role: "user",
931
+ content: initialMessage
932
+ });
933
+ if (this.opts.onBeforeFirstRun) {
934
+ try {
935
+ await this.opts.onBeforeFirstRun(this.session.getContext());
936
+ } catch {
937
+ }
938
+ }
939
+ let iterations = 0;
940
+ let consecutiveErrors = 0;
941
+ let done = false;
942
+ let finalAnswer = "";
943
+ try {
944
+ while (!done && iterations < this.opts.maxIterations) {
945
+ iterations++;
946
+ this.session.recordIteration();
947
+ const steeringResult = this.processSteering();
948
+ if (steeringResult.abort) {
949
+ yield { type: "aborted", reason: steeringResult.abortReason };
950
+ return;
951
+ }
952
+ if (signal.aborted) {
953
+ yield { type: "aborted", reason: "Signal aborted" };
954
+ return;
955
+ }
956
+ const job = {
957
+ sessionId: this.session.getId(),
958
+ messages: this.session.getContext().getMessages(),
959
+ tools: this.toolDefs.length > 0 ? this.toolDefs : void 0,
960
+ systemPrompt: this.session.getConfig().systemPrompt,
961
+ model: this.session.getConfig().model
962
+ };
963
+ let handle;
964
+ try {
965
+ handle = await this.engine.startRun(job, { signal });
966
+ this.currentHandle = handle;
967
+ this.session.recordLLMCall();
968
+ } catch (err) {
969
+ const error = err instanceof Error ? err : new Error(String(err));
970
+ consecutiveErrors++;
971
+ this.session.recordError(error);
972
+ this.observer.onError(error, { phase: "engine_start", iteration: iterations });
973
+ const nonRetryable = error instanceof EngineError && !error.retryable;
974
+ if (nonRetryable || consecutiveErrors >= this.opts.maxRetries) {
975
+ yield { type: "error", error };
976
+ done = true;
977
+ break;
978
+ }
979
+ await abortableSleep(backoffDelay(consecutiveErrors), signal);
980
+ continue;
981
+ }
982
+ consecutiveErrors = 0;
983
+ let accumulatedText = "";
984
+ const pendingToolCalls = [];
985
+ let completionAnswer;
986
+ let completionUsage;
987
+ let engineErrored = false;
988
+ let lastEngineError;
989
+ try {
990
+ for await (const event of handle.events) {
991
+ if (signal.aborted) {
992
+ await handle.cancel();
993
+ yield { type: "aborted", reason: "Signal aborted" };
994
+ return;
995
+ }
996
+ if (this.session.getSteering().hasAbort()) {
997
+ await handle.cancel();
998
+ const reason = this.drainAbortReason();
999
+ yield { type: "aborted", reason };
1000
+ return;
1001
+ }
1002
+ yield* this.handleEngineEvent(
1003
+ event,
1004
+ accumulatedText,
1005
+ pendingToolCalls
1006
+ );
1007
+ if (event.type === "text_delta") {
1008
+ accumulatedText += event.delta;
1009
+ }
1010
+ if (event.type === "completed") {
1011
+ completionAnswer = event.answer;
1012
+ completionUsage = event.usage;
1013
+ if (completionUsage) {
1014
+ this.cumulativeTokens.inputTokens += completionUsage.inputTokens;
1015
+ this.cumulativeTokens.outputTokens += completionUsage.outputTokens;
1016
+ }
1017
+ }
1018
+ if (event.type === "error") {
1019
+ engineErrored = true;
1020
+ lastEngineError = event.error;
1021
+ break;
1022
+ }
1023
+ }
1024
+ } catch (err) {
1025
+ const error = err instanceof Error ? err : new Error(String(err));
1026
+ consecutiveErrors++;
1027
+ this.session.recordError(error);
1028
+ this.observer.onError(error, { phase: "engine_stream", iteration: iterations });
1029
+ const nonRetryable = error instanceof EngineError && !error.retryable;
1030
+ if (nonRetryable || consecutiveErrors >= this.opts.maxRetries) {
1031
+ yield { type: "error", error };
1032
+ done = true;
1033
+ break;
1034
+ }
1035
+ await abortableSleep(backoffDelay(consecutiveErrors), signal);
1036
+ continue;
1037
+ }
1038
+ if (engineErrored) {
1039
+ consecutiveErrors++;
1040
+ const nonRetryable = lastEngineError instanceof EngineError && !lastEngineError.retryable;
1041
+ if (nonRetryable || consecutiveErrors >= this.opts.maxRetries) {
1042
+ yield { type: "error", error: lastEngineError ?? new EngineError("Engine returned error", this.engine.id) };
1043
+ done = true;
1044
+ break;
1045
+ }
1046
+ await abortableSleep(backoffDelay(consecutiveErrors), signal);
1047
+ continue;
1048
+ }
1049
+ if (completionAnswer !== void 0 && pendingToolCalls.length === 0) {
1050
+ await this.session.getContext().addMessage({
1051
+ role: "assistant",
1052
+ content: completionAnswer
1053
+ });
1054
+ finalAnswer = completionAnswer;
1055
+ yield { type: "complete", answer: completionAnswer, usage: completionUsage };
1056
+ done = true;
1057
+ break;
1058
+ }
1059
+ if (pendingToolCalls.length > 0) {
1060
+ await this.session.getContext().addMessage({
1061
+ role: "assistant",
1062
+ content: accumulatedText || "",
1063
+ toolCalls: pendingToolCalls
1064
+ });
1065
+ for (const toolCall of pendingToolCalls) {
1066
+ const preToolSteering = this.processSteering();
1067
+ if (preToolSteering.abort) {
1068
+ yield { type: "aborted", reason: preToolSteering.abortReason };
1069
+ return;
1070
+ }
1071
+ if (signal.aborted) {
1072
+ yield { type: "aborted", reason: "Signal aborted" };
1073
+ return;
1074
+ }
1075
+ const validationResult = this.validateToolCall(toolCall);
1076
+ if (validationResult !== null) {
1077
+ yield {
1078
+ type: "tool_validation_error",
1079
+ tool: toolCall.name,
1080
+ errors: validationResult.errors ?? ["Validation failed"]
1081
+ };
1082
+ await this.session.getContext().addMessage({
1083
+ role: "tool",
1084
+ content: `[VALIDATION ERROR] Invalid arguments for tool "${toolCall.name}": ${validationResult.errors?.join(", ") ?? "validation failed"}. Please fix the arguments and try again.`,
1085
+ toolCallId: toolCall.id
1086
+ });
1087
+ this.session.recordToolInvocation();
1088
+ continue;
1089
+ }
1090
+ yield { type: "tool_start", tool: toolCall.name, args: toolCall.args };
1091
+ const result = await this.executeTool(toolCall, signal);
1092
+ const cappedResult = { ...result };
1093
+ if (cappedResult.output && cappedResult.output.length > this.maxToolOutputLen) {
1094
+ cappedResult.output = cappedResult.output.slice(0, this.maxToolOutputLen) + "\n[truncated]";
1095
+ }
1096
+ if (cappedResult.error && cappedResult.error.length > this.maxToolOutputLen) {
1097
+ cappedResult.error = cappedResult.error.slice(0, this.maxToolOutputLen) + "\n[truncated]";
1098
+ }
1099
+ this.allToolResults.push(cappedResult);
1100
+ if (this.allToolResults.length > this.maxToolResults) {
1101
+ this.allToolResults.shift();
1102
+ }
1103
+ yield { type: "tool_end", tool: toolCall.name, result };
1104
+ const rawContent = result.output || result.error || "";
1105
+ const policy = this.opts.securityPolicy ?? PERMISSIVE_POLICY;
1106
+ const sanitized = policy.sanitizeOutput(rawContent);
1107
+ if (sanitized.redacted) {
1108
+ this.observer.onSecurityEvent({
1109
+ type: "secret_redacted",
1110
+ details: {
1111
+ source: "tool_output",
1112
+ tool: toolCall.name,
1113
+ patterns: sanitized.redactedPatterns
1114
+ },
1115
+ timestamp: /* @__PURE__ */ new Date()
1116
+ });
1117
+ }
1118
+ await this.session.getContext().addMessage({
1119
+ role: "tool",
1120
+ content: sanitized.clean,
1121
+ toolCallId: toolCall.id
1122
+ });
1123
+ this.session.recordToolInvocation();
1124
+ }
1125
+ continue;
1126
+ }
1127
+ if (accumulatedText) {
1128
+ await this.session.getContext().addMessage({
1129
+ role: "assistant",
1130
+ content: accumulatedText
1131
+ });
1132
+ finalAnswer = accumulatedText;
1133
+ yield { type: "complete", answer: accumulatedText, usage: completionUsage };
1134
+ done = true;
1135
+ }
1136
+ }
1137
+ if (!done) {
1138
+ const error = new Error(`Agent loop exceeded maximum iterations (${this.opts.maxIterations})`);
1139
+ yield { type: "error", error };
1140
+ this.session.fail(error);
1141
+ return;
1142
+ }
1143
+ if (this.opts.verifier && finalAnswer) {
1144
+ try {
1145
+ const verificationResult = await this.opts.verifier.verify({
1146
+ taskDescription: initialMessage,
1147
+ finalAnswer,
1148
+ messages: this.session.getContext().getMessages(),
1149
+ toolResults: this.allToolResults,
1150
+ stateSnapshots: this.stateRecords
1151
+ });
1152
+ yield { type: "verification", result: verificationResult };
1153
+ if (verificationResult.outcome === "partial" || verificationResult.outcome === "failure") {
1154
+ const suggestions = verificationResult.suggestions?.join("\n- ") ?? "No specific suggestions.";
1155
+ const feedback = `[VERIFICATION ${verificationResult.outcome.toUpperCase()}] ${verificationResult.reasoning}
1156
+ Suggestions:
1157
+ - ${suggestions}`;
1158
+ this.observer.onError(
1159
+ new Error(`Task verification: ${verificationResult.outcome}`),
1160
+ {
1161
+ phase: "verification",
1162
+ confidence: verificationResult.confidence,
1163
+ issues: verificationResult.issues?.length ?? 0
1164
+ }
1165
+ );
1166
+ await this.session.getContext().addMessage({
1167
+ role: "system",
1168
+ content: feedback
1169
+ });
1170
+ }
1171
+ } catch (err) {
1172
+ this.observer.onError(
1173
+ err instanceof Error ? err : new Error(String(err)),
1174
+ { phase: "verification" }
1175
+ );
1176
+ }
1177
+ }
1178
+ this.session.complete();
1179
+ if (this.opts.onAfterComplete && finalAnswer) {
1180
+ try {
1181
+ await this.opts.onAfterComplete(this.session.getContext(), finalAnswer);
1182
+ } catch {
1183
+ }
1184
+ }
1185
+ } catch (err) {
1186
+ const error = err instanceof Error ? err : new Error(String(err));
1187
+ yield { type: "error", error };
1188
+ this.session.fail(error);
1189
+ } finally {
1190
+ this.currentHandle = null;
1191
+ this.observer.onSessionEnd(
1192
+ {
1193
+ sessionId: this.session.getId(),
1194
+ channelId: this.session.getConfig().channelId,
1195
+ userId: this.session.getConfig().userId,
1196
+ engineId: this.session.getConfig().engineId,
1197
+ startedAt: this.session.getMetadata().startedAt
1198
+ },
1199
+ {
1200
+ duration: Date.now() - this.session.getMetadata().startedAt.getTime(),
1201
+ toolInvocations: this.session.getMetadata().toolInvocations,
1202
+ llmCalls: this.session.getMetadata().llmCalls,
1203
+ tokensUsed: {
1204
+ inputTokens: this.cumulativeTokens.inputTokens,
1205
+ outputTokens: this.cumulativeTokens.outputTokens
1206
+ },
1207
+ errors: this.session.getMetadata().errors.length
1208
+ }
1209
+ );
1210
+ if (this.ownsWorkerPool) {
1211
+ await this.workerPool.shutdown();
1212
+ }
1213
+ await this.observer.flush?.();
1214
+ }
1215
+ }
1216
+ /** Abort the running loop with a reason. */
1217
+ abort(reason) {
1218
+ this.session.getSteering().push({
1219
+ type: "abort",
1220
+ content: reason,
1221
+ priority: 100,
1222
+ // highest
1223
+ timestamp: /* @__PURE__ */ new Date()
1224
+ });
1225
+ this.abortController?.abort(reason);
1226
+ }
1227
+ /** Push a live steering message into the session's queue. */
1228
+ steer(message) {
1229
+ this.session.getSteering().push(message);
1230
+ }
1231
+ /**
1232
+ * Forward a raw string to the engine's stdin.
1233
+ * Used to respond to permission prompts from SubprocessEngine (e.g. claude-cli).
1234
+ */
1235
+ steerEngine(message) {
1236
+ this.currentHandle?.steer(message);
1237
+ }
1238
+ /** Get accumulated state records for external inspection. */
1239
+ getStateRecords() {
1240
+ return this.stateRecords;
1241
+ }
1242
+ /** Get accumulated tool results for external inspection. */
1243
+ getToolResults() {
1244
+ return this.allToolResults;
1245
+ }
1246
+ // -----------------------------------------------------------------------
1247
+ // Engine event handling
1248
+ // -----------------------------------------------------------------------
1249
+ *handleEngineEvent(event, accumulatedText, pendingToolCalls) {
1250
+ switch (event.type) {
1251
+ case "text_delta":
1252
+ yield {
1253
+ type: "text",
1254
+ delta: event.delta,
1255
+ partial: accumulatedText + event.delta
1256
+ };
1257
+ break;
1258
+ case "tool_start":
1259
+ pendingToolCalls.push({
1260
+ id: event.id,
1261
+ name: event.tool,
1262
+ args: event.args
1263
+ });
1264
+ break;
1265
+ case "tool_progress":
1266
+ yield { type: "tool_progress", tool: "", update: event.update };
1267
+ break;
1268
+ case "tool_end":
1269
+ yield { type: "tool_end", tool: "", result: event.result };
1270
+ break;
1271
+ case "error":
1272
+ yield { type: "error", error: event.error };
1273
+ break;
1274
+ case "completed":
1275
+ break;
1276
+ case "started":
1277
+ break;
1278
+ }
1279
+ }
1280
+ // -----------------------------------------------------------------------
1281
+ // AWM: Mandatory step-level tool call validation
1282
+ // -----------------------------------------------------------------------
1283
+ /**
1284
+ * Validate a tool call's arguments before execution.
1285
+ * Returns null if validation passes, or a ValidationResult with errors.
1286
+ *
1287
+ * This is a mandatory step — if a tool does not implement validate(),
1288
+ * we perform basic structural checks (args must be an object or undefined).
1289
+ */
1290
+ validateToolCall(toolCall) {
1291
+ const tool = this.tools.get(toolCall.name);
1292
+ if (!tool) {
1293
+ return { errors: [`Tool "${toolCall.name}" not found.`] };
1294
+ }
1295
+ if (tool.validate) {
1296
+ const result = tool.validate(toolCall.args);
1297
+ if (!result.valid) {
1298
+ return { errors: result.errors ?? ["Validation failed."] };
1299
+ }
1300
+ return null;
1301
+ }
1302
+ if (toolCall.args !== void 0 && toolCall.args !== null) {
1303
+ if (typeof toolCall.args !== "object" || Array.isArray(toolCall.args)) {
1304
+ return { errors: ["Arguments must be an object."] };
1305
+ }
1306
+ }
1307
+ return null;
1308
+ }
1309
+ // -----------------------------------------------------------------------
1310
+ // Tool execution (with AWM state snapshots)
1311
+ // -----------------------------------------------------------------------
1312
+ async executeTool(toolCall, signal) {
1313
+ const tool = this.tools.get(toolCall.name);
1314
+ if (!tool) {
1315
+ const error = new ToolError(`Unknown tool: ${toolCall.name}`, toolCall.name);
1316
+ this.observer.onError(error, { tool: toolCall.name });
1317
+ return {
1318
+ success: false,
1319
+ output: "",
1320
+ error: `Tool "${toolCall.name}" not found`
1321
+ };
1322
+ }
1323
+ const startTime = Date.now();
1324
+ const rawCwd = this.session.getConfig().cwd ?? process.cwd();
1325
+ const toolContext = {
1326
+ sessionId: this.session.getId(),
1327
+ cwd: sanitizeWorkspacePath(rawCwd),
1328
+ securityPolicy: this.opts.securityPolicy ?? PERMISSIVE_POLICY,
1329
+ abortSignal: signal,
1330
+ onProgress: (_update) => {
1331
+ },
1332
+ // Inject memory backend so memory_store / memory_recall tools can access it.
1333
+ ...this.opts.memoryBackend ? { memoryBackend: this.opts.memoryBackend } : {},
1334
+ // Spread any domain-specific extensions (e.g. canvasState for canvas tool).
1335
+ ...this.opts.toolContextExtensions ?? {}
1336
+ };
1337
+ let beforeSnapshot;
1338
+ if (this.opts.enableStateSnapshots && tool.getStateSnapshot) {
1339
+ try {
1340
+ beforeSnapshot = await tool.getStateSnapshot(toolCall.args, toolContext);
1341
+ } catch {
1342
+ }
1343
+ }
1344
+ let result;
1345
+ try {
1346
+ if (tool.weight === "heavyweight" && this.opts.workerPool?.hasWorkerScript?.()) {
1347
+ result = await this.workerPool.execute(
1348
+ {
1349
+ tool: toolCall.name,
1350
+ args: toolCall.args,
1351
+ context: {
1352
+ sessionId: this.session.getId(),
1353
+ cwd: sanitizeWorkspacePath(rawCwd)
1354
+ }
1355
+ },
1356
+ signal,
1357
+ (_update) => {
1358
+ }
1359
+ );
1360
+ } else {
1361
+ result = await tool.execute(toolCall.args, toolContext);
1362
+ }
1363
+ } catch (err) {
1364
+ const error = err instanceof Error ? err : new Error(String(err));
1365
+ result = {
1366
+ success: false,
1367
+ output: "",
1368
+ error: error.message
1369
+ };
1370
+ }
1371
+ let afterSnapshot;
1372
+ if (this.opts.enableStateSnapshots && tool.getStateSnapshot) {
1373
+ try {
1374
+ afterSnapshot = await tool.getStateSnapshot(toolCall.args, toolContext);
1375
+ result.stateSnapshot = afterSnapshot;
1376
+ } catch {
1377
+ }
1378
+ }
1379
+ if (beforeSnapshot || afterSnapshot) {
1380
+ this.stateRecords.push({
1381
+ tool: toolCall.name,
1382
+ args: toolCall.args,
1383
+ before: beforeSnapshot,
1384
+ after: afterSnapshot
1385
+ });
1386
+ if (this.stateRecords.length > this.maxStateRecords) {
1387
+ this.stateRecords.shift();
1388
+ }
1389
+ }
1390
+ const duration = Date.now() - startTime;
1391
+ this.observer.onToolInvocation({
1392
+ sessionId: this.session.getId(),
1393
+ tool: toolCall.name,
1394
+ args: toolCall.args,
1395
+ result,
1396
+ duration,
1397
+ error: result.success ? void 0 : new Error(result.error ?? "Tool failed")
1398
+ });
1399
+ return result;
1400
+ }
1401
+ // -----------------------------------------------------------------------
1402
+ // Steering
1403
+ // -----------------------------------------------------------------------
1404
+ /**
1405
+ * Drain the steering queue and process all messages. Returns an object
1406
+ * indicating whether an abort was requested.
1407
+ */
1408
+ processSteering() {
1409
+ const steering = this.session.getSteering();
1410
+ if (!steering.hasMessages()) {
1411
+ return { abort: false };
1412
+ }
1413
+ const messages = steering.drain();
1414
+ let abort = false;
1415
+ let abortReason;
1416
+ for (const msg of messages) {
1417
+ switch (msg.type) {
1418
+ case "abort":
1419
+ abort = true;
1420
+ abortReason = msg.content ?? "Abort requested";
1421
+ break;
1422
+ case "inject": {
1423
+ if (msg.content) {
1424
+ const injectMsg = {
1425
+ role: "user",
1426
+ content: msg.content
1427
+ };
1428
+ this.session.getContext().addMessage(injectMsg).catch((err) => {
1429
+ this.session.recordError(
1430
+ err instanceof Error ? err : new Error(String(err))
1431
+ );
1432
+ });
1433
+ }
1434
+ break;
1435
+ }
1436
+ case "priority":
1437
+ if (msg.content) {
1438
+ const priorityMsg = {
1439
+ role: "user",
1440
+ content: `[PRIORITY] ${msg.content}`
1441
+ };
1442
+ this.session.getContext().addMessage(priorityMsg).catch((err) => {
1443
+ this.session.recordError(
1444
+ err instanceof Error ? err : new Error(String(err))
1445
+ );
1446
+ });
1447
+ }
1448
+ break;
1449
+ case "context_update":
1450
+ if (msg.content) {
1451
+ this.session.getContext().setSystemPrompt(msg.content);
1452
+ }
1453
+ break;
1454
+ }
1455
+ }
1456
+ return { abort, abortReason };
1457
+ }
1458
+ /**
1459
+ * Extract the abort reason from a pending abort message.
1460
+ */
1461
+ drainAbortReason() {
1462
+ const messages = this.session.getSteering().drain();
1463
+ const abortMsg = messages.find((m) => m.type === "abort");
1464
+ return abortMsg?.content ?? "Abort requested";
1465
+ }
1466
+ };
1467
+ function builtinRules(opts) {
1468
+ return [
1469
+ {
1470
+ id: "non-empty-answer",
1471
+ description: "Final answer must not be empty",
1472
+ check: (ctx) => {
1473
+ if (!ctx.finalAnswer || ctx.finalAnswer.trim().length < opts.minAnswerLength) {
1474
+ return `Final answer is empty or too short (minimum ${opts.minAnswerLength} characters)`;
1475
+ }
1476
+ return null;
1477
+ }
1478
+ },
1479
+ {
1480
+ id: "tool-success-ratio",
1481
+ description: "Tool error ratio must be below threshold",
1482
+ check: (ctx) => {
1483
+ if (ctx.toolResults.length === 0) return null;
1484
+ const errors = ctx.toolResults.filter((r) => !r.success).length;
1485
+ const ratio = errors / ctx.toolResults.length;
1486
+ if (ratio > opts.maxToolErrorRatio) {
1487
+ return `${errors}/${ctx.toolResults.length} tool calls failed (${(ratio * 100).toFixed(0)}% error rate, max allowed ${(opts.maxToolErrorRatio * 100).toFixed(0)}%)`;
1488
+ }
1489
+ return null;
1490
+ }
1491
+ },
1492
+ {
1493
+ id: "no-error-only-answer",
1494
+ description: "Final answer should not consist solely of an error message",
1495
+ check: (ctx) => {
1496
+ const lower = ctx.finalAnswer.toLowerCase().trim();
1497
+ if (lower.startsWith("error:") || lower.startsWith("i encountered an error")) {
1498
+ return "Final answer appears to be an error message rather than a real response";
1499
+ }
1500
+ return null;
1501
+ },
1502
+ severity: "warning"
1503
+ },
1504
+ {
1505
+ id: "task-reference",
1506
+ description: "Final answer should reference the task",
1507
+ check: (ctx) => {
1508
+ const taskWords = ctx.taskDescription.toLowerCase().split(/\W+/).filter((w) => w.length > 4);
1509
+ if (taskWords.length === 0) return null;
1510
+ const answerLower = ctx.finalAnswer.toLowerCase();
1511
+ const matches = taskWords.filter((w) => answerLower.includes(w));
1512
+ if (matches.length === 0) {
1513
+ return "Final answer does not appear to reference any key terms from the original task";
1514
+ }
1515
+ return null;
1516
+ },
1517
+ severity: "warning"
1518
+ },
1519
+ {
1520
+ id: "state-consistency",
1521
+ description: "State snapshots should show changes for write operations",
1522
+ check: (ctx) => {
1523
+ for (const record of ctx.stateSnapshots) {
1524
+ if (record.before && record.after) {
1525
+ const beforeKeys = Object.keys(record.before.state);
1526
+ const afterKeys = Object.keys(record.after.state);
1527
+ if (beforeKeys.length > 0 && afterKeys.length > 0) {
1528
+ const identical = beforeKeys.every(
1529
+ (k) => JSON.stringify(record.before.state[k]) === JSON.stringify(record.after.state[k])
1530
+ );
1531
+ if (identical && afterKeys.every((k) => beforeKeys.includes(k))) {
1532
+ return `Tool "${record.tool}" produced identical before/after state \u2014 may not have executed correctly`;
1533
+ }
1534
+ }
1535
+ }
1536
+ }
1537
+ return null;
1538
+ },
1539
+ severity: "info"
1540
+ }
1541
+ ];
1542
+ }
1543
+ var FormatVerifier = class {
1544
+ id = "format-verifier";
1545
+ name = "Format Verifier";
1546
+ rules;
1547
+ constructor(opts = {}) {
1548
+ const resolvedOpts = {
1549
+ minAnswerLength: opts.minAnswerLength ?? 1,
1550
+ maxToolErrorRatio: opts.maxToolErrorRatio ?? 0.5
1551
+ };
1552
+ this.rules = [
1553
+ ...opts.skipBuiltinRules ? [] : builtinRules(resolvedOpts),
1554
+ ...opts.customRules ?? []
1555
+ ];
1556
+ }
1557
+ async checkFormat(context) {
1558
+ const errors = [];
1559
+ for (const rule of this.rules) {
1560
+ const result = rule.check(context);
1561
+ if (result !== null && (rule.severity ?? "error") === "error") {
1562
+ errors.push(`[${rule.id}] ${result}`);
1563
+ }
1564
+ }
1565
+ return {
1566
+ passed: errors.length === 0,
1567
+ errors: errors.length > 0 ? errors : void 0
1568
+ };
1569
+ }
1570
+ async verify(context) {
1571
+ const formatCheck = await this.checkFormat(context);
1572
+ const issues = [];
1573
+ for (const rule of this.rules) {
1574
+ const result = rule.check(context);
1575
+ if (result !== null) {
1576
+ issues.push({
1577
+ severity: rule.severity ?? "error",
1578
+ message: `[${rule.id}] ${result}`
1579
+ });
1580
+ }
1581
+ }
1582
+ const errorCount = issues.filter((i) => i.severity === "error").length;
1583
+ const warningCount = issues.filter((i) => i.severity === "warning").length;
1584
+ let outcome;
1585
+ let confidence;
1586
+ if (errorCount > 0) {
1587
+ outcome = "failure";
1588
+ confidence = Math.max(0, 1 - errorCount * 0.3);
1589
+ } else if (warningCount > 0) {
1590
+ outcome = "partial";
1591
+ confidence = Math.max(0.5, 1 - warningCount * 0.15);
1592
+ } else {
1593
+ outcome = "success";
1594
+ confidence = 1;
1595
+ }
1596
+ const suggestions = issues.filter((i) => i.severity === "error" || i.severity === "warning").map((i) => `Fix: ${i.message}`);
1597
+ return {
1598
+ outcome,
1599
+ confidence,
1600
+ reasoning: formatCheck.passed ? `All ${this.rules.length} format checks passed.` : `${errorCount} error(s) and ${warningCount} warning(s) found across ${this.rules.length} checks.`,
1601
+ issues: issues.length > 0 ? issues : void 0,
1602
+ suggestions: suggestions.length > 0 ? suggestions : void 0,
1603
+ formatCheck
1604
+ };
1605
+ }
1606
+ };
1607
+ var JUDGE_SYSTEM_PROMPT = `You are a task verification judge. Your job is to assess whether an AI agent correctly completed a user's task.
1608
+
1609
+ You will receive:
1610
+ 1. The original task the user requested
1611
+ 2. The agent's final answer
1612
+ 3. A summary of tool calls the agent made
1613
+
1614
+ Evaluate the following criteria:
1615
+ - **Completeness**: Did the agent fully address the task?
1616
+ - **Correctness**: Is the agent's answer accurate and appropriate?
1617
+ - **Quality**: Is the answer well-structured and useful?
1618
+
1619
+ Respond in EXACTLY this JSON format (no markdown, no code fences):
1620
+ {"score": <number 0-100>, "passed": <boolean>, "reasoning": "<brief explanation>", "issues": [{"severity": "<error|warning|info>", "message": "<issue description>"}]}
1621
+
1622
+ Score guide: 0-30 = failure, 31-70 = partial, 71-100 = success.
1623
+ Set "passed" to true only if score >= 71.`;
1624
+ function buildJudgePrompt(context, maxToolResults) {
1625
+ const parts = [];
1626
+ parts.push(`## Original Task
1627
+ ${context.taskDescription}`);
1628
+ parts.push(`
1629
+ ## Agent's Final Answer
1630
+ ${context.finalAnswer}`);
1631
+ if (context.toolResults.length > 0) {
1632
+ const results = context.toolResults.slice(0, maxToolResults);
1633
+ const toolSummary = results.map((r, i) => {
1634
+ const status = r.success ? "\u2713" : "\u2717";
1635
+ const output = r.output ? r.output.length > 200 ? r.output.slice(0, 200) + "..." : r.output : "(no output)";
1636
+ const error = r.error ? ` | Error: ${r.error}` : "";
1637
+ return `${i + 1}. ${status} ${output}${error}`;
1638
+ });
1639
+ parts.push(`
1640
+ ## Tool Call Results (${context.toolResults.length} total)
1641
+ ${toolSummary.join("\n")}`);
1642
+ if (context.toolResults.length > maxToolResults) {
1643
+ parts.push(`
1644
+ (${context.toolResults.length - maxToolResults} additional tool results omitted)`);
1645
+ }
1646
+ }
1647
+ if (context.stateSnapshots.length > 0) {
1648
+ const diffs = context.stateSnapshots.filter((s) => s.before || s.after).slice(0, 5).map((s) => {
1649
+ const before = s.before ? JSON.stringify(s.before.state) : "(none)";
1650
+ const after = s.after ? JSON.stringify(s.after.state) : "(none)";
1651
+ return `- ${s.tool}: before=${before.slice(0, 100)}, after=${after.slice(0, 100)}`;
1652
+ });
1653
+ if (diffs.length > 0) {
1654
+ parts.push(`
1655
+ ## State Changes
1656
+ ${diffs.join("\n")}`);
1657
+ }
1658
+ }
1659
+ return parts.join("\n");
1660
+ }
1661
+ function parseJudgeResponse(text) {
1662
+ let jsonStr = text.trim();
1663
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1664
+ if (fenceMatch) {
1665
+ jsonStr = fenceMatch[1].trim();
1666
+ }
1667
+ try {
1668
+ const parsed = JSON.parse(jsonStr);
1669
+ return {
1670
+ score: Math.max(0, Math.min(100, typeof parsed.score === "number" ? parsed.score : 0)),
1671
+ passed: typeof parsed.passed === "boolean" ? parsed.passed : parsed.score >= 71,
1672
+ reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : "No reasoning provided.",
1673
+ issues: Array.isArray(parsed.issues) ? parsed.issues : void 0
1674
+ };
1675
+ } catch {
1676
+ const scoreMatch = text.match(/score["\s:]+(\d+)/i);
1677
+ const score = scoreMatch ? parseInt(scoreMatch[1], 10) : 50;
1678
+ return {
1679
+ score,
1680
+ passed: score >= 71,
1681
+ reasoning: `Failed to parse structured judge response. Raw text: ${text.slice(0, 200)}`
1682
+ };
1683
+ }
1684
+ }
1685
+ var LLMVerifier = class {
1686
+ id = "llm-verifier";
1687
+ name = "LLM Verifier";
1688
+ provider;
1689
+ model;
1690
+ formatVerifier;
1691
+ maxJudgeTokens;
1692
+ temperature;
1693
+ skipSemanticOnFormatFailure;
1694
+ maxToolResultsInPrompt;
1695
+ constructor(opts) {
1696
+ this.provider = opts.provider;
1697
+ this.model = opts.model;
1698
+ this.formatVerifier = new FormatVerifier(opts.formatOpts);
1699
+ this.maxJudgeTokens = opts.maxJudgeTokens ?? 1024;
1700
+ this.temperature = opts.temperature ?? 0;
1701
+ this.skipSemanticOnFormatFailure = opts.skipSemanticOnFormatFailure ?? true;
1702
+ this.maxToolResultsInPrompt = opts.maxToolResultsInPrompt ?? 20;
1703
+ }
1704
+ async checkFormat(context) {
1705
+ return this.formatVerifier.checkFormat(context);
1706
+ }
1707
+ async checkSemantic(context) {
1708
+ const judgePrompt = buildJudgePrompt(context, this.maxToolResultsInPrompt);
1709
+ const messages = [
1710
+ { role: "user", content: judgePrompt }
1711
+ ];
1712
+ const result = await this.provider.complete(this.model, messages, {
1713
+ systemPrompt: JUDGE_SYSTEM_PROMPT,
1714
+ maxTokens: this.maxJudgeTokens,
1715
+ temperature: this.temperature
1716
+ });
1717
+ const responseText = typeof result.message.content === "string" ? result.message.content : "";
1718
+ const judge = parseJudgeResponse(responseText);
1719
+ return {
1720
+ passed: judge.passed,
1721
+ score: judge.score / 100,
1722
+ // Normalize to 0–1.
1723
+ reasoning: judge.reasoning
1724
+ };
1725
+ }
1726
+ async verify(context) {
1727
+ const formatCheck = await this.checkFormat(context);
1728
+ let semanticCheck;
1729
+ if (!this.skipSemanticOnFormatFailure || formatCheck.passed) {
1730
+ try {
1731
+ semanticCheck = await this.checkSemantic(context);
1732
+ } catch (err) {
1733
+ semanticCheck = {
1734
+ passed: false,
1735
+ score: 0,
1736
+ reasoning: `Semantic check failed: ${err instanceof Error ? err.message : String(err)}`
1737
+ };
1738
+ }
1739
+ }
1740
+ const issues = [];
1741
+ if (!formatCheck.passed && formatCheck.errors) {
1742
+ for (const error of formatCheck.errors) {
1743
+ issues.push({ severity: "error", message: error });
1744
+ }
1745
+ }
1746
+ if (semanticCheck && !semanticCheck.passed) {
1747
+ issues.push({
1748
+ severity: "error",
1749
+ message: `Semantic check: ${semanticCheck.reasoning}`
1750
+ });
1751
+ }
1752
+ let outcome;
1753
+ let confidence;
1754
+ if (!formatCheck.passed) {
1755
+ outcome = "failure";
1756
+ confidence = 0.2;
1757
+ } else if (semanticCheck) {
1758
+ if (semanticCheck.score >= 0.71) {
1759
+ outcome = "success";
1760
+ confidence = semanticCheck.score;
1761
+ } else if (semanticCheck.score >= 0.31) {
1762
+ outcome = "partial";
1763
+ confidence = semanticCheck.score;
1764
+ } else {
1765
+ outcome = "failure";
1766
+ confidence = semanticCheck.score;
1767
+ }
1768
+ } else {
1769
+ outcome = formatCheck.passed ? "success" : "failure";
1770
+ confidence = formatCheck.passed ? 0.7 : 0.2;
1771
+ }
1772
+ const suggestions = [];
1773
+ if (!formatCheck.passed) {
1774
+ suggestions.push("Fix format issues before attempting semantic verification.");
1775
+ }
1776
+ if (semanticCheck && !semanticCheck.passed) {
1777
+ suggestions.push(`Semantic assessment: ${semanticCheck.reasoning}`);
1778
+ }
1779
+ return {
1780
+ outcome,
1781
+ confidence,
1782
+ reasoning: semanticCheck ? `Format: ${formatCheck.passed ? "PASS" : "FAIL"} | Semantic: ${semanticCheck.passed ? "PASS" : "FAIL"} (score: ${(semanticCheck.score * 100).toFixed(0)}%)` : `Format: ${formatCheck.passed ? "PASS" : "FAIL"} | Semantic: SKIPPED`,
1783
+ issues: issues.length > 0 ? issues : void 0,
1784
+ suggestions: suggestions.length > 0 ? suggestions : void 0,
1785
+ formatCheck,
1786
+ semanticCheck
1787
+ };
1788
+ }
1789
+ };
1790
+ function createAutoRecallHook(backend, opts) {
1791
+ const maxResults = opts?.maxResults ?? 5;
1792
+ const minScore = opts?.minScore ?? 0.1;
1793
+ return async (ctx) => {
1794
+ const messages = ctx.getMessages();
1795
+ const lastUserMsg = messages.filter((m) => m.role === "user").pop();
1796
+ if (!lastUserMsg || typeof lastUserMsg.content !== "string") return;
1797
+ const query = lastUserMsg.content;
1798
+ if (!query.trim()) return;
1799
+ const results = await backend.recall(query, {
1800
+ limit: maxResults,
1801
+ minScore,
1802
+ ...opts?.recallOpts,
1803
+ ...opts?.namespace ? { keyPrefix: `${opts.namespace}:` } : {}
1804
+ });
1805
+ if (results.length === 0) return;
1806
+ const memoryLines = results.map(
1807
+ (r, i) => `${i + 1}. [${r.matchType}, score=${r.score.toFixed(2)}] ${r.content}`
1808
+ );
1809
+ const memoryText = "Relevant memories from previous conversations:\n" + memoryLines.join("\n") + "\n\nUse these memories to provide more personalized and context-aware responses.";
1810
+ await ctx.addMessage({ role: "system", content: memoryText });
1811
+ };
1812
+ }
1813
+ function createAutoSummarizeHook(backend, opts) {
1814
+ const minMessages = opts?.minMessages ?? 1;
1815
+ const maxLength = opts?.maxSummaryLength ?? 2e3;
1816
+ return async (ctx, answer) => {
1817
+ const messages = ctx.getMessages();
1818
+ const userMessages = messages.filter((m) => m.role === "user");
1819
+ if (userMessages.length < minMessages) return;
1820
+ if (!answer || answer.trim().length < 10) return;
1821
+ const userQueries = userMessages.map((m) => typeof m.content === "string" ? m.content : "").filter(Boolean);
1822
+ const summaryParts = [
1823
+ `User asked: ${userQueries.join(" \u2192 ")}`,
1824
+ `Assistant answered: ${answer}`
1825
+ ];
1826
+ let summary = summaryParts.join("\n");
1827
+ if (summary.length > maxLength) {
1828
+ summary = summary.slice(0, maxLength - 3) + "...";
1829
+ }
1830
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1831
+ const queryPrefix = userQueries[0]?.slice(0, 50).replace(/[^a-zA-Z0-9 ]/g, "") ?? "conversation";
1832
+ const nsPrefix = opts?.namespace ? `${opts.namespace}:` : "";
1833
+ const key = `${nsPrefix}conv:${timestamp}:${queryPrefix}`;
1834
+ await backend.store(key, summary, {
1835
+ type: "conversation_summary",
1836
+ timestamp,
1837
+ messageCount: messages.length,
1838
+ userMessageCount: userMessages.length
1839
+ });
1840
+ };
1841
+ }
1842
+
1843
+ export {
1844
+ SteeringQueue,
1845
+ NAMED_STRATEGIES,
1846
+ ContextManager,
1847
+ ToolWorkerPool,
1848
+ Session,
1849
+ AgentLoop,
1850
+ FormatVerifier,
1851
+ LLMVerifier,
1852
+ createAutoRecallHook,
1853
+ createAutoSummarizeHook
1854
+ };