@element47/ag 4.5.2 → 4.5.4
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 +91 -5
- package/dist/cli/__tests__/repl-rawmode.test.d.ts +2 -0
- package/dist/cli/__tests__/repl-rawmode.test.d.ts.map +1 -0
- package/dist/cli/__tests__/repl-rawmode.test.js +46 -0
- package/dist/cli/__tests__/repl-rawmode.test.js.map +1 -0
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +24 -4
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/__tests__/agent.test.js +19 -19
- package/dist/core/__tests__/agent.test.js.map +1 -1
- package/dist/core/__tests__/context.test.js +45 -1
- package/dist/core/__tests__/context.test.js.map +1 -1
- package/dist/core/__tests__/events.test.d.ts +2 -0
- package/dist/core/__tests__/events.test.d.ts.map +1 -0
- package/dist/core/__tests__/events.test.js +131 -0
- package/dist/core/__tests__/events.test.js.map +1 -0
- package/dist/core/__tests__/extensions.test.d.ts +2 -0
- package/dist/core/__tests__/extensions.test.d.ts.map +1 -0
- package/dist/core/__tests__/extensions.test.js +59 -0
- package/dist/core/__tests__/extensions.test.js.map +1 -0
- package/dist/core/__tests__/guardrails.test.js +18 -0
- package/dist/core/__tests__/guardrails.test.js.map +1 -1
- package/dist/core/agent.d.ts +18 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +217 -110
- package/dist/core/agent.js.map +1 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +14 -6
- package/dist/core/context.js.map +1 -1
- package/dist/core/events.d.ts +62 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +23 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/extensions.d.ts +10 -0
- package/dist/core/extensions.d.ts.map +1 -0
- package/dist/core/extensions.js +66 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/permissions.js +1 -1
- package/dist/core/permissions.js.map +1 -1
- package/dist/memory/memory.js +1 -1
- package/dist/memory/memory.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +64 -2
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/file.test.js +7 -1
- package/dist/tools/__tests__/file.test.js.map +1 -1
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +175 -43
- package/dist/tools/bash.js.map +1 -1
- package/package.json +1 -4
package/dist/core/agent.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { AgentEventEmitter } from './events.js';
|
|
5
|
+
import { discoverExtensions, loadExtensions } from './extensions.js';
|
|
4
6
|
import { C } from './colors.js';
|
|
5
7
|
import { loadContext, loadHistory, appendHistory, getStats, clearProject, clearAll, paths, saveGlobalMemory, saveProjectMemory, savePlan, appendPlan, setActivePlan, getActivePlanName, loadGlobalMemory, loadProjectMemory, loadPlan, loadPlanByName, listPlans } from '../memory/memory.js';
|
|
6
8
|
import { bashToolFactory } from '../tools/bash.js';
|
|
@@ -33,9 +35,9 @@ function startSpinner(label) {
|
|
|
33
35
|
}
|
|
34
36
|
const MAX_MESSAGES = 200;
|
|
35
37
|
const COMPACT_THRESHOLD = 0.9;
|
|
36
|
-
const MAX_TOOL_RESULT_CHARS =
|
|
37
|
-
const TRUNCATION_HEAD_LINES =
|
|
38
|
-
const TRUNCATION_TAIL_LINES =
|
|
38
|
+
const MAX_TOOL_RESULT_CHARS = 32768;
|
|
39
|
+
const TRUNCATION_HEAD_LINES = 100;
|
|
40
|
+
const TRUNCATION_TAIL_LINES = 100;
|
|
39
41
|
const COMPACT_HEAD_KEEP = 2;
|
|
40
42
|
const COMPACT_TAIL_KEEP = 10;
|
|
41
43
|
const COMPACT_MSG_CHARS = 500;
|
|
@@ -147,6 +149,9 @@ export class Agent {
|
|
|
147
149
|
contextTracker;
|
|
148
150
|
compactionInProgress = false;
|
|
149
151
|
confirmToolCall;
|
|
152
|
+
events = new AgentEventEmitter();
|
|
153
|
+
loadedExtensions = [];
|
|
154
|
+
spinnerControl = null;
|
|
150
155
|
constructor(config = {}) {
|
|
151
156
|
this.apiKey = config.apiKey || process.env.OPENROUTER_API_KEY || '';
|
|
152
157
|
if (!this.apiKey)
|
|
@@ -204,7 +209,7 @@ export class Agent {
|
|
|
204
209
|
- plan — create and manage multi-step task plans
|
|
205
210
|
- web(fetch/search) — fetch pages or search the web
|
|
206
211
|
- skill — activate a skill by name`;
|
|
207
|
-
this.maxIterations = config.maxIterations ||
|
|
212
|
+
this.maxIterations = config.maxIterations || 200;
|
|
208
213
|
this.cwd = config.cwd || process.cwd();
|
|
209
214
|
this.confirmToolCall = config.confirmToolCall ?? null;
|
|
210
215
|
// Discover skills
|
|
@@ -238,6 +243,34 @@ export class Agent {
|
|
|
238
243
|
}
|
|
239
244
|
this.tools.set(name, tool);
|
|
240
245
|
}
|
|
246
|
+
/** Subscribe to an agent lifecycle event. Returns an unsubscribe function. */
|
|
247
|
+
on(event, handler) {
|
|
248
|
+
return this.events.on(event, handler);
|
|
249
|
+
}
|
|
250
|
+
/** Discover and load extensions from .ag/extensions/ and ~/.ag/extensions/ */
|
|
251
|
+
async initExtensions(extraPaths) {
|
|
252
|
+
const discovered = discoverExtensions(this.cwd);
|
|
253
|
+
const all = [...discovered, ...(extraPaths ?? [])];
|
|
254
|
+
if (all.length > 0) {
|
|
255
|
+
this.loadedExtensions = await loadExtensions(this, all);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Metadata for successfully loaded extensions */
|
|
259
|
+
getLoadedExtensions() {
|
|
260
|
+
return this.loadedExtensions;
|
|
261
|
+
}
|
|
262
|
+
/** Spinner-safe log for extensions. Pauses spinner, writes, resumes. */
|
|
263
|
+
log(message) {
|
|
264
|
+
if (this.spinnerControl)
|
|
265
|
+
this.spinnerControl.pause();
|
|
266
|
+
process.stderr.write(message + '\n');
|
|
267
|
+
if (this.spinnerControl)
|
|
268
|
+
this.spinnerControl.resume();
|
|
269
|
+
}
|
|
270
|
+
/** Called by the REPL to register spinner pause/resume for safe output */
|
|
271
|
+
setSpinnerControl(control) {
|
|
272
|
+
this.spinnerControl = control;
|
|
273
|
+
}
|
|
241
274
|
refreshCache() {
|
|
242
275
|
this.cachedContext = loadContext(this.cwd);
|
|
243
276
|
this.cachedCatalog = buildSkillCatalog(this.allSkills);
|
|
@@ -296,10 +329,12 @@ export class Agent {
|
|
|
296
329
|
return parts.join('\n\n');
|
|
297
330
|
}
|
|
298
331
|
/** Build the JSON request body for chat completions, with prompt caching for supported models */
|
|
299
|
-
buildRequestBody(stream) {
|
|
332
|
+
buildRequestBody(stream, overrides) {
|
|
333
|
+
const sysPrompt = overrides?.systemPrompt ?? this.systemPrompt;
|
|
334
|
+
const msgs = overrides?.messages ?? this.messages;
|
|
300
335
|
const body = {
|
|
301
336
|
model: this.model,
|
|
302
|
-
messages: [{ role: 'system', content:
|
|
337
|
+
messages: [{ role: 'system', content: sysPrompt }, ...msgs],
|
|
303
338
|
tools: Array.from(this.tools.values()).map(t => ({ type: t.type, function: t.function })),
|
|
304
339
|
tool_choice: 'auto',
|
|
305
340
|
};
|
|
@@ -333,73 +368,86 @@ export class Agent {
|
|
|
333
368
|
}
|
|
334
369
|
return `Skill "${name}" activated. Instructions loaded.`;
|
|
335
370
|
}
|
|
336
|
-
async compactConversation() {
|
|
371
|
+
async compactConversation(customSummary) {
|
|
337
372
|
const minMessages = COMPACT_HEAD_KEEP + COMPACT_TAIL_KEEP + 4;
|
|
338
373
|
if (this.messages.length <= minMessages)
|
|
339
374
|
return;
|
|
340
375
|
const head = this.messages.slice(0, COMPACT_HEAD_KEEP);
|
|
341
376
|
const middle = this.messages.slice(COMPACT_HEAD_KEEP, -COMPACT_TAIL_KEEP);
|
|
342
377
|
const tail = this.messages.slice(-COMPACT_TAIL_KEEP);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
378
|
+
let summary;
|
|
379
|
+
if (customSummary) {
|
|
380
|
+
// Use extension-provided summary instead of LLM call
|
|
381
|
+
summary = customSummary;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// Format middle messages for summarization, capping total size
|
|
385
|
+
let totalChars = 0;
|
|
386
|
+
const formatted = [];
|
|
387
|
+
for (const m of middle) {
|
|
388
|
+
let line;
|
|
389
|
+
if (m.tool_calls?.length) {
|
|
390
|
+
const names = m.tool_calls.map(tc => tc.function.name).join(', ');
|
|
391
|
+
line = `[assistant]: (tool call: ${names})`;
|
|
392
|
+
}
|
|
393
|
+
else if (m.role === 'tool') {
|
|
394
|
+
line = `[tool result]: ${(m.content || '').slice(0, COMPACT_MSG_CHARS)}`;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
line = `[${m.role}]: ${(m.content || '').slice(0, COMPACT_MSG_CHARS)}`;
|
|
398
|
+
}
|
|
399
|
+
if (totalChars + line.length > COMPACT_TOTAL_CHARS)
|
|
400
|
+
break;
|
|
401
|
+
totalChars += line.length;
|
|
402
|
+
formatted.push(line);
|
|
351
403
|
}
|
|
352
|
-
|
|
353
|
-
|
|
404
|
+
const stopSpinner = startSpinner('compacting context');
|
|
405
|
+
let stopped = false;
|
|
406
|
+
try {
|
|
407
|
+
const res = await fetch(`${this.baseURL}/chat/completions`, {
|
|
408
|
+
method: 'POST',
|
|
409
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
model: this.model,
|
|
412
|
+
messages: [
|
|
413
|
+
{ role: 'system', content: COMPACTION_PROMPT },
|
|
414
|
+
{ role: 'user', content: formatted.join('\n\n') }
|
|
415
|
+
]
|
|
416
|
+
})
|
|
417
|
+
});
|
|
418
|
+
if (!res.ok)
|
|
419
|
+
throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
420
|
+
const body = await res.json();
|
|
421
|
+
summary = body.choices?.[0]?.message?.content;
|
|
422
|
+
if (!summary)
|
|
423
|
+
throw new Error('No summary returned');
|
|
424
|
+
stopSpinner();
|
|
425
|
+
stopped = true;
|
|
354
426
|
}
|
|
355
|
-
|
|
356
|
-
|
|
427
|
+
finally {
|
|
428
|
+
if (!stopped)
|
|
429
|
+
stopSpinner();
|
|
357
430
|
}
|
|
358
|
-
if (totalChars + line.length > COMPACT_TOTAL_CHARS)
|
|
359
|
-
break;
|
|
360
|
-
totalChars += line.length;
|
|
361
|
-
formatted.push(line);
|
|
362
|
-
}
|
|
363
|
-
const stopSpinner = startSpinner('compacting context');
|
|
364
|
-
let stopped = false;
|
|
365
|
-
try {
|
|
366
|
-
const res = await fetch(`${this.baseURL}/chat/completions`, {
|
|
367
|
-
method: 'POST',
|
|
368
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
|
|
369
|
-
body: JSON.stringify({
|
|
370
|
-
model: this.model,
|
|
371
|
-
messages: [
|
|
372
|
-
{ role: 'system', content: COMPACTION_PROMPT },
|
|
373
|
-
{ role: 'user', content: formatted.join('\n\n') }
|
|
374
|
-
]
|
|
375
|
-
})
|
|
376
|
-
});
|
|
377
|
-
if (!res.ok)
|
|
378
|
-
throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
379
|
-
const body = await res.json();
|
|
380
|
-
const summary = body.choices?.[0]?.message?.content;
|
|
381
|
-
if (!summary)
|
|
382
|
-
throw new Error('No summary returned');
|
|
383
|
-
const summaryMsg = {
|
|
384
|
-
role: 'user',
|
|
385
|
-
content: `[Conversation compacted — summary of ${middle.length} earlier messages]\n\n${summary}`
|
|
386
|
-
};
|
|
387
|
-
this.messages = [...head, summaryMsg, ...tail];
|
|
388
|
-
appendHistory(summaryMsg, this.cwd);
|
|
389
|
-
// Re-estimate context usage from the compacted messages
|
|
390
|
-
const compactedChars = this.messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0)
|
|
391
|
-
+ this.systemPrompt.length;
|
|
392
|
-
this.contextTracker.estimateFromChars(compactedChars);
|
|
393
|
-
stopSpinner();
|
|
394
|
-
stopped = true;
|
|
395
|
-
process.stderr.write(` ${C.yellow}Context compacted: ${middle.length} messages → summary${C.reset}\n`);
|
|
396
|
-
}
|
|
397
|
-
finally {
|
|
398
|
-
if (!stopped)
|
|
399
|
-
stopSpinner();
|
|
400
431
|
}
|
|
432
|
+
const summaryMsg = {
|
|
433
|
+
role: 'user',
|
|
434
|
+
content: `[Conversation compacted — summary of ${middle.length} earlier messages]\n\n${summary}`
|
|
435
|
+
};
|
|
436
|
+
this.messages = [...head, summaryMsg, ...tail];
|
|
437
|
+
appendHistory(summaryMsg, this.cwd);
|
|
438
|
+
// Re-estimate context usage from the compacted messages
|
|
439
|
+
const compactedChars = this.messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0)
|
|
440
|
+
+ this.systemPrompt.length;
|
|
441
|
+
this.contextTracker.estimateFromChars(compactedChars);
|
|
442
|
+
process.stderr.write(` ${C.yellow}Context compacted: ${middle.length} messages → summary${C.reset}\n`);
|
|
401
443
|
}
|
|
402
444
|
async chat(content, signal) {
|
|
445
|
+
// ── input event ──
|
|
446
|
+
const inputEvent = { content, skip: false };
|
|
447
|
+
await this.events.emit('input', inputEvent);
|
|
448
|
+
if (inputEvent.skip)
|
|
449
|
+
return '';
|
|
450
|
+
content = inputEvent.content;
|
|
403
451
|
const userMessage = { role: 'user', content };
|
|
404
452
|
this.messages.push(userMessage);
|
|
405
453
|
appendHistory(userMessage, this.cwd);
|
|
@@ -412,14 +460,46 @@ export class Agent {
|
|
|
412
460
|
this.messages.push({ role: 'assistant', content: '[interrupted by user]' });
|
|
413
461
|
return '[interrupted by user]';
|
|
414
462
|
}
|
|
463
|
+
// ── turn_start event ──
|
|
464
|
+
await this.events.emit('turn_start', { iteration: i, maxIterations: this.maxIterations, messageCount: this.messages.length });
|
|
415
465
|
const iterLabel = this.maxIterations > 1 ? ` [${i + 1}/${this.maxIterations}]` : '';
|
|
416
466
|
const stopSpinner = startSpinner(`thinking${iterLabel}`);
|
|
467
|
+
// ── before_compact event ──
|
|
468
|
+
if (!this.compactionInProgress && this.contextTracker.shouldCompact(COMPACT_THRESHOLD)) {
|
|
469
|
+
const compactEvent = { messageCount: this.messages.length, cancel: false, customSummary: undefined };
|
|
470
|
+
await this.events.emit('before_compact', compactEvent);
|
|
471
|
+
if (!compactEvent.cancel) {
|
|
472
|
+
this.compactionInProgress = true;
|
|
473
|
+
try {
|
|
474
|
+
await this.compactConversation(compactEvent.customSummary);
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
process.stderr.write(` ${C.dim}Compaction failed: ${e} — falling back to truncation${C.reset}\n`);
|
|
478
|
+
const keep = COMPACT_HEAD_KEEP + COMPACT_TAIL_KEEP;
|
|
479
|
+
if (this.messages.length > keep) {
|
|
480
|
+
const head = this.messages.slice(0, COMPACT_HEAD_KEEP);
|
|
481
|
+
const tail = this.messages.slice(-COMPACT_TAIL_KEEP);
|
|
482
|
+
const truncMsg = {
|
|
483
|
+
role: 'user',
|
|
484
|
+
content: `[Conversation truncated — ${this.messages.length - keep} older messages removed to stay within context limit]`
|
|
485
|
+
};
|
|
486
|
+
this.messages = [...head, truncMsg, ...tail];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
this.compactionInProgress = false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// ── before_request event ──
|
|
495
|
+
const reqEvent = { messages: this.messages, systemPrompt: this.systemPrompt, model: this.model, stream: false };
|
|
496
|
+
await this.events.emit('before_request', reqEvent);
|
|
417
497
|
let msg;
|
|
418
498
|
try {
|
|
419
499
|
const res = await fetch(`${this.baseURL}/chat/completions`, {
|
|
420
500
|
method: 'POST',
|
|
421
501
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
|
|
422
|
-
body: JSON.stringify(this.buildRequestBody(false)),
|
|
502
|
+
body: JSON.stringify(this.buildRequestBody(false, { messages: reqEvent.messages, systemPrompt: reqEvent.systemPrompt })),
|
|
423
503
|
signal
|
|
424
504
|
});
|
|
425
505
|
if (!res.ok)
|
|
@@ -437,33 +517,13 @@ export class Agent {
|
|
|
437
517
|
finally {
|
|
438
518
|
stopSpinner();
|
|
439
519
|
}
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
this.compactionInProgress = true;
|
|
443
|
-
try {
|
|
444
|
-
await this.compactConversation();
|
|
445
|
-
}
|
|
446
|
-
catch (e) {
|
|
447
|
-
process.stderr.write(` ${C.dim}Compaction failed: ${e} — falling back to truncation${C.reset}\n`);
|
|
448
|
-
// Fallback: simple truncation to prevent context overflow
|
|
449
|
-
const keep = COMPACT_HEAD_KEEP + COMPACT_TAIL_KEEP;
|
|
450
|
-
if (this.messages.length > keep) {
|
|
451
|
-
const head = this.messages.slice(0, COMPACT_HEAD_KEEP);
|
|
452
|
-
const tail = this.messages.slice(-COMPACT_TAIL_KEEP);
|
|
453
|
-
const truncMsg = {
|
|
454
|
-
role: 'user',
|
|
455
|
-
content: `[Conversation truncated — ${this.messages.length - keep} older messages removed to stay within context limit]`
|
|
456
|
-
};
|
|
457
|
-
this.messages = [...head, truncMsg, ...tail];
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
finally {
|
|
461
|
-
this.compactionInProgress = false;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
520
|
+
// ── after_response event ──
|
|
521
|
+
await this.events.emit('after_response', { message: msg, usage: undefined });
|
|
464
522
|
this.messages.push(msg);
|
|
465
523
|
appendHistory(msg, this.cwd);
|
|
466
524
|
if (!msg.tool_calls?.length) {
|
|
525
|
+
// ── turn_end event (no tools) ──
|
|
526
|
+
await this.events.emit('turn_end', { iteration: i, hadToolCalls: false, toolCallCount: 0 });
|
|
467
527
|
return msg.content || '';
|
|
468
528
|
}
|
|
469
529
|
// Permission checks — run sequentially so prompts don't overlap
|
|
@@ -499,13 +559,25 @@ export class Agent {
|
|
|
499
559
|
if (permissionDecisions.get(call.id) === 'deny') {
|
|
500
560
|
return { call, content: 'Tool call denied by user.' };
|
|
501
561
|
}
|
|
562
|
+
// ── tool_call event ──
|
|
563
|
+
const tcEvent = { toolName: call.function.name, toolCallId: call.id, args, block: false, blockReason: undefined };
|
|
564
|
+
await this.events.emit('tool_call', tcEvent);
|
|
565
|
+
if (tcEvent.block) {
|
|
566
|
+
return { call, content: tcEvent.blockReason || 'Blocked by extension' };
|
|
567
|
+
}
|
|
568
|
+
args = tcEvent.args;
|
|
502
569
|
const summary = args.command ?? args.action ?? JSON.stringify(args).slice(0, 80);
|
|
503
570
|
const stopToolSpinner = startSpinner(`[${call.function.name}] ${summary}`);
|
|
504
571
|
try {
|
|
505
572
|
const rawResult = await tool.execute(args);
|
|
506
|
-
|
|
573
|
+
let result = truncateToolResult(rawResult);
|
|
574
|
+
let isError = result.startsWith('Error:') || result.startsWith('EXIT ');
|
|
575
|
+
// ── tool_result event ──
|
|
576
|
+
const trEvent = { toolName: call.function.name, toolCallId: call.id, args, content: result, isError };
|
|
577
|
+
await this.events.emit('tool_result', trEvent);
|
|
578
|
+
result = trEvent.content;
|
|
579
|
+
isError = trEvent.isError;
|
|
507
580
|
stopToolSpinner();
|
|
508
|
-
const isError = result.startsWith('Error:') || result.startsWith('EXIT ');
|
|
509
581
|
const icon = isError ? `${C.red}✗` : `${C.green}✓`;
|
|
510
582
|
const preview = result.slice(0, 150).split('\n')[0];
|
|
511
583
|
process.stderr.write(` ${icon} ${C.dim}[${call.function.name}]${C.reset} ${C.dim}${preview}${result.length > 150 ? '...' : ''}${C.reset}\n`);
|
|
@@ -523,10 +595,18 @@ export class Agent {
|
|
|
523
595
|
this.messages.push({ role: 'tool', tool_call_id: call.id, content });
|
|
524
596
|
appendHistory({ role: 'tool', tool_call_id: call.id, content }, this.cwd);
|
|
525
597
|
}
|
|
598
|
+
// ── turn_end event ──
|
|
599
|
+
await this.events.emit('turn_end', { iteration: i, hadToolCalls: true, toolCallCount: msg.tool_calls.length });
|
|
526
600
|
}
|
|
527
601
|
return MAX_ITERATIONS_REACHED;
|
|
528
602
|
}
|
|
529
603
|
async *chatStream(content, signal) {
|
|
604
|
+
// ── input event ──
|
|
605
|
+
const inputEvent = { content, skip: false };
|
|
606
|
+
await this.events.emit('input', inputEvent);
|
|
607
|
+
if (inputEvent.skip)
|
|
608
|
+
return;
|
|
609
|
+
content = inputEvent.content;
|
|
530
610
|
const userMessage = { role: 'user', content };
|
|
531
611
|
this.messages.push(userMessage);
|
|
532
612
|
appendHistory(userMessage, this.cwd);
|
|
@@ -540,37 +620,46 @@ export class Agent {
|
|
|
540
620
|
yield { type: 'interrupted' };
|
|
541
621
|
return;
|
|
542
622
|
}
|
|
623
|
+
// ── turn_start event ──
|
|
624
|
+
await this.events.emit('turn_start', { iteration: i, maxIterations: this.maxIterations, messageCount: this.messages.length });
|
|
543
625
|
const iterLabel = this.maxIterations > 1 ? ` [${i + 1}/${this.maxIterations}]` : '';
|
|
544
626
|
yield { type: 'thinking', content: `thinking${iterLabel}` };
|
|
545
|
-
//
|
|
627
|
+
// ── before_compact event ──
|
|
546
628
|
if (!this.compactionInProgress && this.contextTracker.shouldCompact(COMPACT_THRESHOLD)) {
|
|
547
|
-
this.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
629
|
+
const compactEvent = { messageCount: this.messages.length, cancel: false, customSummary: undefined };
|
|
630
|
+
await this.events.emit('before_compact', compactEvent);
|
|
631
|
+
if (!compactEvent.cancel) {
|
|
632
|
+
this.compactionInProgress = true;
|
|
633
|
+
try {
|
|
634
|
+
await this.compactConversation(compactEvent.customSummary);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
const keep = COMPACT_HEAD_KEEP + COMPACT_TAIL_KEEP;
|
|
638
|
+
if (this.messages.length > keep) {
|
|
639
|
+
const head = this.messages.slice(0, COMPACT_HEAD_KEEP);
|
|
640
|
+
const tail = this.messages.slice(-COMPACT_TAIL_KEEP);
|
|
641
|
+
const truncMsg = {
|
|
642
|
+
role: 'user',
|
|
643
|
+
content: `[Conversation truncated — ${this.messages.length - keep} older messages removed to stay within context limit]`
|
|
644
|
+
};
|
|
645
|
+
this.messages = [...head, truncMsg, ...tail];
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
this.compactionInProgress = false;
|
|
561
650
|
}
|
|
562
|
-
}
|
|
563
|
-
finally {
|
|
564
|
-
this.compactionInProgress = false;
|
|
565
651
|
}
|
|
566
652
|
}
|
|
653
|
+
// ── before_request event ──
|
|
654
|
+
const reqEvent = { messages: this.messages, systemPrompt: this.systemPrompt, model: this.model, stream: true };
|
|
655
|
+
await this.events.emit('before_request', reqEvent);
|
|
567
656
|
// ── API call with abort signal ──
|
|
568
657
|
let res;
|
|
569
658
|
try {
|
|
570
659
|
res = await fetch(`${this.baseURL}/chat/completions`, {
|
|
571
660
|
method: 'POST',
|
|
572
661
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
|
|
573
|
-
body: JSON.stringify(this.buildRequestBody(true)),
|
|
662
|
+
body: JSON.stringify(this.buildRequestBody(true, { messages: reqEvent.messages, systemPrompt: reqEvent.systemPrompt })),
|
|
574
663
|
signal
|
|
575
664
|
});
|
|
576
665
|
}
|
|
@@ -684,10 +773,14 @@ export class Agent {
|
|
|
684
773
|
function: { name: tc.name, arguments: tc.arguments }
|
|
685
774
|
}));
|
|
686
775
|
}
|
|
776
|
+
// ── after_response event ──
|
|
777
|
+
await this.events.emit('after_response', { message: msg, usage: usage ?? undefined });
|
|
687
778
|
this.messages.push(msg);
|
|
688
779
|
appendHistory(msg, this.cwd);
|
|
689
780
|
if (!msg.tool_calls?.length) {
|
|
690
|
-
|
|
781
|
+
// ── turn_end event (no tools) ──
|
|
782
|
+
await this.events.emit('turn_end', { iteration: i, hadToolCalls: false, toolCallCount: 0 });
|
|
783
|
+
yield { type: 'done', content: msg.content || '' };
|
|
691
784
|
return;
|
|
692
785
|
}
|
|
693
786
|
// Notify consumers that tools are about to run (shows spinner immediately)
|
|
@@ -737,10 +830,22 @@ export class Agent {
|
|
|
737
830
|
if (permissionDecisions.get(call.id) === 'deny') {
|
|
738
831
|
return { call, content: 'Tool call denied by user.', isError: true };
|
|
739
832
|
}
|
|
833
|
+
// ── tool_call event ──
|
|
834
|
+
const tcEvent = { toolName: call.function.name, toolCallId: call.id, args, block: false, blockReason: undefined };
|
|
835
|
+
await this.events.emit('tool_call', tcEvent);
|
|
836
|
+
if (tcEvent.block) {
|
|
837
|
+
return { call, content: tcEvent.blockReason || 'Blocked by extension', isError: true };
|
|
838
|
+
}
|
|
839
|
+
args = tcEvent.args;
|
|
740
840
|
try {
|
|
741
841
|
const rawResult = await tool.execute(args);
|
|
742
|
-
|
|
743
|
-
|
|
842
|
+
let result = truncateToolResult(rawResult);
|
|
843
|
+
let isError = result.startsWith('Error:') || result.startsWith('EXIT ');
|
|
844
|
+
// ── tool_result event ──
|
|
845
|
+
const trEvent = { toolName: call.function.name, toolCallId: call.id, args, content: result, isError };
|
|
846
|
+
await this.events.emit('tool_result', trEvent);
|
|
847
|
+
result = trEvent.content;
|
|
848
|
+
isError = trEvent.isError;
|
|
744
849
|
return { call, content: result, isError };
|
|
745
850
|
}
|
|
746
851
|
catch (error) {
|
|
@@ -771,6 +876,8 @@ export class Agent {
|
|
|
771
876
|
yield { type: 'interrupted', content: `${completedCallIds.size} completed, ${msg.tool_calls.length - completedCallIds.size} cancelled` };
|
|
772
877
|
return;
|
|
773
878
|
}
|
|
879
|
+
// ── turn_end event ──
|
|
880
|
+
await this.events.emit('turn_end', { iteration: i, hadToolCalls: true, toolCallCount: msg.tool_calls.length });
|
|
774
881
|
}
|
|
775
882
|
yield { type: 'max_iterations' };
|
|
776
883
|
}
|