@element47/ag 4.4.5 → 4.5.2

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.
Files changed (54) hide show
  1. package/README.md +106 -4
  2. package/dist/cli/repl.d.ts +7 -2
  3. package/dist/cli/repl.d.ts.map +1 -1
  4. package/dist/cli/repl.js +390 -95
  5. package/dist/cli/repl.js.map +1 -1
  6. package/dist/cli.js +18 -8
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/__tests__/agent.test.js +42 -1
  9. package/dist/core/__tests__/agent.test.js.map +1 -1
  10. package/dist/core/__tests__/context.test.js +37 -0
  11. package/dist/core/__tests__/context.test.js.map +1 -1
  12. package/dist/core/__tests__/guardrails.test.d.ts +2 -0
  13. package/dist/core/__tests__/guardrails.test.d.ts.map +1 -0
  14. package/dist/core/__tests__/guardrails.test.js +400 -0
  15. package/dist/core/__tests__/guardrails.test.js.map +1 -0
  16. package/dist/core/__tests__/permission-manager.test.d.ts +2 -0
  17. package/dist/core/__tests__/permission-manager.test.d.ts.map +1 -0
  18. package/dist/core/__tests__/permission-manager.test.js +246 -0
  19. package/dist/core/__tests__/permission-manager.test.js.map +1 -0
  20. package/dist/core/__tests__/streaming.test.js +8 -1
  21. package/dist/core/__tests__/streaming.test.js.map +1 -1
  22. package/dist/core/agent.d.ts +20 -2
  23. package/dist/core/agent.d.ts.map +1 -1
  24. package/dist/core/agent.js +259 -82
  25. package/dist/core/agent.js.map +1 -1
  26. package/dist/core/context.d.ts.map +1 -1
  27. package/dist/core/context.js +17 -2
  28. package/dist/core/context.js.map +1 -1
  29. package/dist/core/guardrails.d.ts +32 -0
  30. package/dist/core/guardrails.d.ts.map +1 -0
  31. package/dist/core/guardrails.js +149 -0
  32. package/dist/core/guardrails.js.map +1 -0
  33. package/dist/core/loader.d.ts +6 -2
  34. package/dist/core/loader.d.ts.map +1 -1
  35. package/dist/core/loader.js +23 -9
  36. package/dist/core/loader.js.map +1 -1
  37. package/dist/core/permissions.d.ts +60 -0
  38. package/dist/core/permissions.d.ts.map +1 -0
  39. package/dist/core/permissions.js +252 -0
  40. package/dist/core/permissions.js.map +1 -0
  41. package/dist/core/registry.d.ts.map +1 -1
  42. package/dist/core/registry.js +16 -0
  43. package/dist/core/registry.js.map +1 -1
  44. package/dist/core/skills.d.ts.map +1 -1
  45. package/dist/core/skills.js +26 -3
  46. package/dist/core/skills.js.map +1 -1
  47. package/dist/core/types.d.ts +20 -2
  48. package/dist/core/types.d.ts.map +1 -1
  49. package/dist/memory/__tests__/memory.test.js +33 -3
  50. package/dist/memory/__tests__/memory.test.js.map +1 -1
  51. package/dist/memory/memory.d.ts.map +1 -1
  52. package/dist/memory/memory.js +25 -3
  53. package/dist/memory/memory.js.map +1 -1
  54. package/package.json +2 -2
@@ -118,6 +118,16 @@ export function truncateToolResult(result) {
118
118
  const omitted = lines.length - TRUNCATION_HEAD_LINES - TRUNCATION_TAIL_LINES;
119
119
  return [...head, `\n... [${omitted} lines truncated] ...\n`, ...tail].join('\n');
120
120
  }
121
+ /** Yield promise results as they resolve (like Promise.all but streaming) */
122
+ export async function* raceAll(promises) {
123
+ const wrapped = promises.map((p, i) => p.then(v => ({ i, v })));
124
+ const settled = new Set();
125
+ while (settled.size < promises.length) {
126
+ const result = await Promise.race(wrapped.filter((_, idx) => !settled.has(idx)));
127
+ settled.add(result.i);
128
+ yield result.v;
129
+ }
130
+ }
121
131
  export class Agent {
122
132
  apiKey;
123
133
  model;
@@ -133,6 +143,7 @@ export class Agent {
133
143
  cachedCatalog = '';
134
144
  cachedAlwaysOn = '';
135
145
  builtinToolNames = new Set();
146
+ toolFailures;
136
147
  contextTracker;
137
148
  compactionInProgress = false;
138
149
  confirmToolCall;
@@ -178,6 +189,7 @@ export class Agent {
178
189
  - Be concise. Short responses, no filler, no trailing summaries of what you just did.
179
190
  - When referencing code, include the file path and relevant context.
180
191
  - Only use markdown formatting when it aids clarity.
192
+ - Always follow instructions in <global-memory> — these are persistent user preferences that override defaults.
181
193
 
182
194
  # Verification
183
195
  - After making changes, verify they work: run tests, check for syntax errors, or start the dev server as appropriate.
@@ -211,6 +223,7 @@ export class Agent {
211
223
  this.builtinToolNames = new Set(this.tools.keys());
212
224
  for (const t of config.extraTools ?? [])
213
225
  this.addTool(t);
226
+ this.toolFailures = config.toolFailures ?? [];
214
227
  // Context window tracking
215
228
  this.contextTracker = new ContextTracker(this.model);
216
229
  // Cache context and skill catalog (invalidated on skill activation/refresh)
@@ -282,6 +295,24 @@ export class Agent {
282
295
  }
283
296
  return parts.join('\n\n');
284
297
  }
298
+ /** Build the JSON request body for chat completions, with prompt caching for supported models */
299
+ buildRequestBody(stream) {
300
+ const body = {
301
+ model: this.model,
302
+ messages: [{ role: 'system', content: this.systemPrompt }, ...this.messages],
303
+ tools: Array.from(this.tools.values()).map(t => ({ type: t.type, function: t.function })),
304
+ tool_choice: 'auto',
305
+ };
306
+ if (stream) {
307
+ body.stream = true;
308
+ body.stream_options = { include_usage: true };
309
+ }
310
+ // Enable prompt caching for Anthropic models (top-level cache_control)
311
+ if (this.model.startsWith('anthropic/') || this.model.includes('claude')) {
312
+ body.cache_control = { type: 'ephemeral' };
313
+ }
314
+ return body;
315
+ }
285
316
  async activateSkill(name) {
286
317
  const skill = this.allSkills.find(s => s.name === name);
287
318
  if (!skill)
@@ -368,7 +399,7 @@ export class Agent {
368
399
  stopSpinner();
369
400
  }
370
401
  }
371
- async chat(content) {
402
+ async chat(content, signal) {
372
403
  const userMessage = { role: 'user', content };
373
404
  this.messages.push(userMessage);
374
405
  appendHistory(userMessage, this.cwd);
@@ -377,6 +408,10 @@ export class Agent {
377
408
  this.messages = this.messages.slice(-MAX_MESSAGES);
378
409
  }
379
410
  for (let i = 0; i < this.maxIterations; i++) {
411
+ if (signal?.aborted) {
412
+ this.messages.push({ role: 'assistant', content: '[interrupted by user]' });
413
+ return '[interrupted by user]';
414
+ }
380
415
  const iterLabel = this.maxIterations > 1 ? ` [${i + 1}/${this.maxIterations}]` : '';
381
416
  const stopSpinner = startSpinner(`thinking${iterLabel}`);
382
417
  let msg;
@@ -384,12 +419,8 @@ export class Agent {
384
419
  const res = await fetch(`${this.baseURL}/chat/completions`, {
385
420
  method: 'POST',
386
421
  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
387
- body: JSON.stringify({
388
- model: this.model,
389
- messages: [{ role: 'system', content: this.systemPrompt }, ...this.messages],
390
- tools: Array.from(this.tools.values()).map(t => ({ type: t.type, function: t.function })),
391
- tool_choice: 'auto'
392
- })
422
+ body: JSON.stringify(this.buildRequestBody(false)),
423
+ signal
393
424
  });
394
425
  if (!res.ok)
395
426
  throw new Error(`API ${res.status}: ${await res.text()}`);
@@ -431,11 +462,28 @@ export class Agent {
431
462
  }
432
463
  }
433
464
  this.messages.push(msg);
465
+ appendHistory(msg, this.cwd);
434
466
  if (!msg.tool_calls?.length) {
435
- appendHistory({ role: 'assistant', content: msg.content || '' }, this.cwd);
436
467
  return msg.content || '';
437
468
  }
438
- // Execute tool calls in parallel
469
+ // Permission checks run sequentially so prompts don't overlap
470
+ const permissionDecisions = new Map();
471
+ for (const call of msg.tool_calls) {
472
+ const tool = this.tools.get(call.function.name);
473
+ if (!tool)
474
+ continue;
475
+ let args;
476
+ try {
477
+ args = JSON.parse(call.function.arguments || '{}');
478
+ }
479
+ catch {
480
+ continue;
481
+ }
482
+ if (this.confirmToolCall && !isReadOnlyToolCall(call.function.name, args)) {
483
+ permissionDecisions.set(call.id, await this.confirmToolCall(call.function.name, args, tool.permissionKey));
484
+ }
485
+ }
486
+ // Execute tool calls in parallel (permissions already resolved)
439
487
  const toolPromises = msg.tool_calls.map(async (call) => {
440
488
  const tool = this.tools.get(call.function.name);
441
489
  if (!tool) {
@@ -448,12 +496,8 @@ export class Agent {
448
496
  catch {
449
497
  return { call, content: 'Error: malformed tool arguments' };
450
498
  }
451
- // Permission check
452
- if (this.confirmToolCall && !isReadOnlyToolCall(call.function.name, args)) {
453
- const decision = await this.confirmToolCall(call.function.name, args);
454
- if (decision === 'deny') {
455
- return { call, content: 'Tool call denied by user.' };
456
- }
499
+ if (permissionDecisions.get(call.id) === 'deny') {
500
+ return { call, content: 'Tool call denied by user.' };
457
501
  }
458
502
  const summary = args.command ?? args.action ?? JSON.stringify(args).slice(0, 80);
459
503
  const stopToolSpinner = startSpinner(`[${call.function.name}] ${summary}`);
@@ -477,11 +521,12 @@ export class Agent {
477
521
  const results = await Promise.all(toolPromises);
478
522
  for (const { call, content } of results) {
479
523
  this.messages.push({ role: 'tool', tool_call_id: call.id, content });
524
+ appendHistory({ role: 'tool', tool_call_id: call.id, content }, this.cwd);
480
525
  }
481
526
  }
482
527
  return MAX_ITERATIONS_REACHED;
483
528
  }
484
- async *chatStream(content) {
529
+ async *chatStream(content, signal) {
485
530
  const userMessage = { role: 'user', content };
486
531
  this.messages.push(userMessage);
487
532
  appendHistory(userMessage, this.cwd);
@@ -489,6 +534,12 @@ export class Agent {
489
534
  this.messages = this.messages.slice(-MAX_MESSAGES);
490
535
  }
491
536
  for (let i = 0; i < this.maxIterations; i++) {
537
+ // ── Abort check: top of iteration ──
538
+ if (signal?.aborted) {
539
+ this.messages.push({ role: 'assistant', content: '[interrupted by user]' });
540
+ yield { type: 'interrupted' };
541
+ return;
542
+ }
492
543
  const iterLabel = this.maxIterations > 1 ? ` [${i + 1}/${this.maxIterations}]` : '';
493
544
  yield { type: 'thinking', content: `thinking${iterLabel}` };
494
545
  // Compact if needed
@@ -513,17 +564,24 @@ export class Agent {
513
564
  this.compactionInProgress = false;
514
565
  }
515
566
  }
516
- const res = await fetch(`${this.baseURL}/chat/completions`, {
517
- method: 'POST',
518
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
519
- body: JSON.stringify({
520
- model: this.model,
521
- messages: [{ role: 'system', content: this.systemPrompt }, ...this.messages],
522
- tools: Array.from(this.tools.values()).map(t => ({ type: t.type, function: t.function })),
523
- tool_choice: 'auto',
524
- stream: true
525
- })
526
- });
567
+ // ── API call with abort signal ──
568
+ let res;
569
+ try {
570
+ res = await fetch(`${this.baseURL}/chat/completions`, {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` },
573
+ body: JSON.stringify(this.buildRequestBody(true)),
574
+ signal
575
+ });
576
+ }
577
+ catch (e) {
578
+ if (e instanceof DOMException && e.name === 'AbortError') {
579
+ this.messages.push({ role: 'assistant', content: '[interrupted by user]' });
580
+ yield { type: 'interrupted' };
581
+ return;
582
+ }
583
+ throw e;
584
+ }
527
585
  if (!res.ok)
528
586
  throw new Error(`API ${res.status}: ${await res.text()}`);
529
587
  if (!res.body)
@@ -532,62 +590,89 @@ export class Agent {
532
590
  let assistantContent = '';
533
591
  const toolCalls = new Map();
534
592
  let usage = null;
593
+ let streamAborted = false;
535
594
  const reader = res.body.getReader();
536
595
  const decoder = new TextDecoder();
537
596
  let buffer = '';
538
- while (true) {
539
- const { done, value } = await reader.read();
540
- if (done)
541
- break;
542
- buffer += decoder.decode(value, { stream: true });
543
- const lines = buffer.split('\n');
544
- buffer = lines.pop() || '';
545
- for (const line of lines) {
546
- if (!line.startsWith('data: '))
547
- continue;
548
- const data = line.slice(6).trim();
549
- if (data === '[DONE]')
550
- continue;
551
- let parsed;
552
- try {
553
- parsed = JSON.parse(data);
554
- }
555
- catch {
556
- continue;
597
+ try {
598
+ while (true) {
599
+ if (signal?.aborted) {
600
+ streamAborted = true;
601
+ break;
557
602
  }
558
- const delta = parsed.choices?.[0]?.delta;
559
- if (!delta) {
603
+ const { done, value } = await reader.read();
604
+ if (done)
605
+ break;
606
+ buffer += decoder.decode(value, { stream: true });
607
+ const lines = buffer.split('\n');
608
+ buffer = lines.pop() || '';
609
+ for (const line of lines) {
610
+ if (!line.startsWith('data: '))
611
+ continue;
612
+ const data = line.slice(6).trim();
613
+ if (data === '[DONE]')
614
+ continue;
615
+ let parsed;
616
+ try {
617
+ parsed = JSON.parse(data);
618
+ }
619
+ catch {
620
+ continue;
621
+ }
622
+ // Capture usage from any chunk that has it (may coexist with delta)
560
623
  if (parsed.usage)
561
624
  usage = parsed.usage;
562
- continue;
563
- }
564
- // Text content
565
- if (delta.content) {
566
- assistantContent += delta.content;
567
- yield { type: 'text', content: delta.content };
568
- }
569
- // Tool calls (streamed incrementally)
570
- if (delta.tool_calls) {
571
- // Signal spinner on first tool_call delta so the user sees activity
572
- if (toolCalls.size === 0) {
573
- yield { type: 'thinking', content: 'running tools' };
625
+ const delta = parsed.choices?.[0]?.delta;
626
+ if (!delta)
627
+ continue;
628
+ // Text content
629
+ if (delta.content) {
630
+ assistantContent += delta.content;
631
+ yield { type: 'text', content: delta.content };
574
632
  }
575
- for (const tc of delta.tool_calls) {
576
- const idx = tc.index ?? 0;
577
- if (!toolCalls.has(idx)) {
578
- toolCalls.set(idx, { id: tc.id || '', name: tc.function?.name || '', arguments: '' });
633
+ // Tool calls (streamed incrementally)
634
+ if (delta.tool_calls) {
635
+ for (const tc of delta.tool_calls) {
636
+ const idx = tc.index ?? 0;
637
+ if (!toolCalls.has(idx)) {
638
+ toolCalls.set(idx, { id: tc.id || '', name: tc.function?.name || '', arguments: '' });
639
+ }
640
+ const entry = toolCalls.get(idx);
641
+ if (tc.id)
642
+ entry.id = tc.id;
643
+ if (tc.function?.name) {
644
+ entry.name = tc.function.name;
645
+ // Show tool names as they arrive (replaces spinner in real-time)
646
+ const names = [...toolCalls.values()].map(t => t.name).filter(Boolean);
647
+ yield { type: 'thinking', content: `preparing ${names.join(', ')}` };
648
+ }
649
+ if (tc.function?.arguments)
650
+ entry.arguments += tc.function.arguments;
579
651
  }
580
- const entry = toolCalls.get(idx);
581
- if (tc.id)
582
- entry.id = tc.id;
583
- if (tc.function?.name)
584
- entry.name = tc.function.name;
585
- if (tc.function?.arguments)
586
- entry.arguments += tc.function.arguments;
587
652
  }
588
653
  }
589
654
  }
590
655
  }
656
+ catch (e) {
657
+ if (e instanceof DOMException && e.name === 'AbortError') {
658
+ streamAborted = true;
659
+ }
660
+ else {
661
+ throw e;
662
+ }
663
+ }
664
+ finally {
665
+ try {
666
+ reader.cancel();
667
+ }
668
+ catch { /* already closed */ }
669
+ }
670
+ // If aborted during streaming, discard partial message
671
+ if (streamAborted) {
672
+ this.messages.push({ role: 'assistant', content: '[interrupted by user]' });
673
+ yield { type: 'interrupted' };
674
+ return;
675
+ }
591
676
  if (usage)
592
677
  this.contextTracker.update(usage);
593
678
  // Build assistant message
@@ -600,8 +685,8 @@ export class Agent {
600
685
  }));
601
686
  }
602
687
  this.messages.push(msg);
688
+ appendHistory(msg, this.cwd);
603
689
  if (!msg.tool_calls?.length) {
604
- appendHistory({ role: 'assistant', content: assistantContent }, this.cwd);
605
690
  yield { type: 'done', content: assistantContent };
606
691
  return;
607
692
  }
@@ -616,8 +701,29 @@ export class Agent {
616
701
  const summary = args.command ?? args.action ?? call.function.name;
617
702
  yield { type: 'tool_start', toolName: call.function.name, toolCallId: call.id, content: summary };
618
703
  }
619
- // Execute tool calls in parallel
704
+ // Permission checks run sequentially so prompts don't overlap
705
+ const permissionDecisions = new Map();
706
+ for (const call of msg.tool_calls) {
707
+ if (signal?.aborted)
708
+ break;
709
+ const tool = this.tools.get(call.function.name);
710
+ if (!tool)
711
+ continue;
712
+ let args;
713
+ try {
714
+ args = JSON.parse(call.function.arguments || '{}');
715
+ }
716
+ catch {
717
+ continue;
718
+ }
719
+ if (this.confirmToolCall && !isReadOnlyToolCall(call.function.name, args)) {
720
+ permissionDecisions.set(call.id, await this.confirmToolCall(call.function.name, args, tool.permissionKey));
721
+ }
722
+ }
723
+ // Execute tool calls — yield results as they complete (not Promise.all)
620
724
  const execPromises = msg.tool_calls.map(async (call) => {
725
+ if (signal?.aborted)
726
+ return { call, content: '[cancelled by user]', isError: true };
621
727
  const tool = this.tools.get(call.function.name);
622
728
  if (!tool)
623
729
  return { call, content: `Error: unknown tool "${call.function.name}"`, isError: true };
@@ -628,12 +734,8 @@ export class Agent {
628
734
  catch {
629
735
  return { call, content: 'Error: malformed tool arguments', isError: true };
630
736
  }
631
- // Permission check
632
- if (this.confirmToolCall && !isReadOnlyToolCall(call.function.name, args)) {
633
- const decision = await this.confirmToolCall(call.function.name, args);
634
- if (decision === 'deny') {
635
- return { call, content: 'Tool call denied by user.', isError: true };
636
- }
737
+ if (permissionDecisions.get(call.id) === 'deny') {
738
+ return { call, content: 'Tool call denied by user.', isError: true };
637
739
  }
638
740
  try {
639
741
  const rawResult = await tool.execute(args);
@@ -645,18 +747,89 @@ export class Agent {
645
747
  return { call, content: `Tool error: ${error}`, isError: true };
646
748
  }
647
749
  });
648
- const execResults = await Promise.all(execPromises);
649
- for (const r of execResults) {
750
+ // Yield results as they resolve (not waiting for all)
751
+ const completedCallIds = new Set();
752
+ for await (const r of raceAll(execPromises)) {
753
+ completedCallIds.add(r.call.id);
650
754
  this.messages.push({ role: 'tool', tool_call_id: r.call.id, content: r.content });
755
+ appendHistory({ role: 'tool', tool_call_id: r.call.id, content: r.content }, this.cwd);
651
756
  yield { type: 'tool_end', toolName: r.call.function.name, toolCallId: r.call.id, content: r.content, success: !r.isError };
757
+ // Check abort after each tool completes
758
+ if (signal?.aborted)
759
+ break;
760
+ }
761
+ // Fill placeholders for any tool calls that didn't complete (API requires all tool_call_ids)
762
+ if (signal?.aborted && msg.tool_calls) {
763
+ for (const call of msg.tool_calls) {
764
+ if (!completedCallIds.has(call.id)) {
765
+ const placeholder = { role: 'tool', tool_call_id: call.id, content: '[cancelled by user]' };
766
+ this.messages.push(placeholder);
767
+ appendHistory(placeholder, this.cwd);
768
+ }
769
+ }
770
+ this.messages.push({ role: 'assistant', content: '[interrupted by user]' });
771
+ yield { type: 'interrupted', content: `${completedCallIds.size} completed, ${msg.tool_calls.length - completedCallIds.size} cancelled` };
772
+ return;
652
773
  }
653
774
  }
654
775
  yield { type: 'max_iterations' };
655
776
  }
656
777
  // ── Public API ─────────────────────────────────────────────────────────────
778
+ /** Return the char-size breakdown of every context component */
779
+ getContextBreakdown() {
780
+ const parts = [];
781
+ parts.push({ label: 'System prompt', chars: this.baseSystemPrompt.length });
782
+ parts.push({ label: 'Environment', chars: getEnvironmentContext(this.cwd).length });
783
+ const listing = this.getProjectListing();
784
+ if (listing)
785
+ parts.push({ label: 'Project files', chars: listing.length });
786
+ // cachedContext contains global memory + project memory + plan, but we want them separate
787
+ const globalMem = loadGlobalMemory(this.cwd);
788
+ const projectMem = loadProjectMemory(this.cwd);
789
+ const plan = loadPlan(this.cwd);
790
+ if (globalMem)
791
+ parts.push({ label: 'Global memory', chars: globalMem.length + '<global-memory>\n\n</global-memory>'.length });
792
+ if (projectMem)
793
+ parts.push({ label: 'Project memory', chars: projectMem.length + '<project-memory>\n\n</project-memory>'.length });
794
+ if (plan)
795
+ parts.push({ label: 'Plan', chars: plan.length + 50 /* tags + header */ });
796
+ if (this.cachedCatalog)
797
+ parts.push({ label: 'Skill catalog', chars: this.cachedCatalog.length });
798
+ if (this.cachedAlwaysOn)
799
+ parts.push({ label: 'Always-on skills', chars: this.cachedAlwaysOn.length });
800
+ if (this.activeSkillContent.length > 0) {
801
+ parts.push({ label: 'Active skills', chars: this.activeSkillContent.join('\n\n').length });
802
+ }
803
+ // Tool definitions (sent in every API call as the tools array)
804
+ const builtinChars = Array.from(this.tools.values())
805
+ .filter(t => this.builtinToolNames.has(t.function.name))
806
+ .reduce((sum, t) => sum + JSON.stringify({ type: t.type, function: t.function }).length, 0);
807
+ const customChars = Array.from(this.tools.values())
808
+ .filter(t => !this.builtinToolNames.has(t.function.name))
809
+ .reduce((sum, t) => sum + JSON.stringify({ type: t.type, function: t.function }).length, 0);
810
+ if (builtinChars > 0)
811
+ parts.push({ label: 'Tool definitions', chars: builtinChars });
812
+ if (customChars > 0)
813
+ parts.push({ label: 'Custom tools', chars: customChars });
814
+ // Messages (conversation history)
815
+ const msgChars = this.messages.reduce((sum, m) => {
816
+ let size = (m.content || '').length;
817
+ if (m.tool_calls)
818
+ size += JSON.stringify(m.tool_calls).length;
819
+ return sum + size;
820
+ }, 0);
821
+ if (msgChars > 0)
822
+ parts.push({ label: 'Messages', chars: msgChars });
823
+ return parts;
824
+ }
657
825
  getTools() {
658
- return Array.from(this.tools.values()).map(t => ({ name: t.function.name, description: t.function.description }));
826
+ return Array.from(this.tools.values()).map(t => ({
827
+ name: t.function.name,
828
+ description: t.function.description,
829
+ isBuiltin: this.builtinToolNames.has(t.function.name),
830
+ }));
659
831
  }
832
+ getToolFailures() { return this.toolFailures; }
660
833
  getModel() { return this.model; }
661
834
  getBaseURL() { return this.baseURL; }
662
835
  setModel(model) { this.model = model; this.contextTracker = new ContextTracker(model); }
@@ -708,5 +881,9 @@ export class Agent {
708
881
  getContextDetails() { return this.contextTracker.formatDetailed(); }
709
882
  getContextTracker() { return this.contextTracker; }
710
883
  getSystemPromptSize() { return this.systemPrompt.length; }
884
+ /** Total estimated chars across all context components (system prompt + tools + messages) */
885
+ getTotalContextChars() {
886
+ return this.getContextBreakdown().reduce((sum, p) => sum + p.chars, 0);
887
+ }
711
888
  }
712
889
  //# sourceMappingURL=agent.js.map