iranti 0.3.21 → 0.3.23

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.
@@ -7,8 +7,10 @@ exports.extractRuleTriggers = extractRuleTriggers;
7
7
  exports.matchesRuleTriggers = matchesRuleTriggers;
8
8
  exports.formatMatchedUserRules = formatMatchedUserRules;
9
9
  exports.extractFilePathEntityHints = extractFilePathEntityHints;
10
+ exports.derivePendingToolCallEntityHints = derivePendingToolCallEntityHints;
10
11
  exports.readPersistedSessionState = readPersistedSessionState;
11
12
  exports.summarizeSessionState = summarizeSessionState;
13
+ exports.injectedFactsAreTaskRelevant = injectedFactsAreTaskRelevant;
12
14
  const crypto_1 = require("crypto");
13
15
  const router_1 = require("../lib/router");
14
16
  const staffEventRegistry_1 = require("../lib/staffEventRegistry");
@@ -59,6 +61,11 @@ const SESSION_INTERRUPTION_TTL_MS = 5 * 60 * 1000;
59
61
  const PERSISTENCE_WARNING_THRESHOLD = 3;
60
62
  const PERSISTENCE_NON_COMPLIANT_THRESHOLD = 5;
61
63
  const ENTITY_DETECTION_WINDOW_CHARS = 1500;
64
+ // A1: mid-turn attends default to a smaller fact budget than pre-response.
65
+ // The agent is already deep in the task with a working memory frame; the point
66
+ // of a mid-turn attend is to surface 1-3 NEW facts on a topic shift, not to
67
+ // re-dump the whole briefing.
68
+ const MID_TURN_DEFAULT_MAX_FACTS = 3;
62
69
  const MIN_ENTITY_CONFIDENCE = 0.75;
63
70
  const MEMORY_DECISION_CONTEXT_WINDOW_CHARS = 2000;
64
71
  const LEDGER_WORKING_MEMORY_PREFIX = 'system/session_ledger/recent_learning_';
@@ -446,6 +453,174 @@ function extractFilePathEntityHints(text, projectEntity) {
446
453
  }
447
454
  return hints;
448
455
  }
456
+ // ─── A2: Pending Tool-Call Entity Hints ─────────────────────────────────────
457
+ //
458
+ // Derive entity hints from a structured tool call that the agent is about to
459
+ // make. This lets attend() preempt redundant read-only tool calls by surfacing
460
+ // stored facts keyed to the target of the tool call (a file, a URL, a query).
461
+ //
462
+ // We deliberately keep this a PURE function so it is trivially unit-testable
463
+ // and so the caller can exercise different tool shapes without spinning up an
464
+ // attendant. It returns entity hints in `entityType/entityId` form.
465
+ function normalizeWebToken(value, maxLen = 48) {
466
+ return value
467
+ .toLowerCase()
468
+ .replace(/[^a-z0-9]+/g, '_')
469
+ .replace(/^_+|_+$/g, '')
470
+ .slice(0, maxLen);
471
+ }
472
+ function deriveFileEntityFromPath(rawPath, projectId, seen, hints) {
473
+ const trimmed = rawPath.trim();
474
+ if (!trimmed)
475
+ return;
476
+ const basename = trimmed.replace(/\\/g, '/').split('/').pop() ?? '';
477
+ // Strip trailing args like "foo.ts:42" or "foo.ts,bar.ts"
478
+ const stripped = basename.split(/[,:()]/)[0] ?? basename;
479
+ const nameWithoutExt = stripped
480
+ .replace(/\.\w+$/, '')
481
+ .replace(/[^a-zA-Z0-9]/g, '_')
482
+ .toLowerCase();
483
+ if (!nameWithoutExt || nameWithoutExt.length < 2)
484
+ return;
485
+ const hint = `project/${projectId}/file/${nameWithoutExt}`;
486
+ if (!seen.has(hint)) {
487
+ seen.add(hint);
488
+ hints.push(hint);
489
+ }
490
+ }
491
+ function scanStringForPathEntities(text, projectId, seen, hints) {
492
+ if (!text)
493
+ return;
494
+ FILE_PATH_PATTERN.lastIndex = 0;
495
+ let match;
496
+ while ((match = FILE_PATH_PATTERN.exec(text)) !== null) {
497
+ deriveFileEntityFromPath(match[1], projectId, seen, hints);
498
+ }
499
+ }
500
+ function extractBasenameFromGlobPattern(pattern) {
501
+ // A glob like "src/**/*.ts" has no meaningful basename; one like
502
+ // "tests/attendant/run_mid_turn_attend_tests.ts" does. We only derive a
503
+ // file entity from the last segment if it looks like a literal filename.
504
+ const segments = pattern.replace(/\\/g, '/').split('/');
505
+ for (let i = segments.length - 1; i >= 0; i--) {
506
+ const seg = segments[i];
507
+ if (!seg || seg.includes('*') || seg.includes('?') || seg === '.' || seg === '..')
508
+ continue;
509
+ if (/\.\w+$/.test(seg))
510
+ return seg;
511
+ }
512
+ return null;
513
+ }
514
+ /**
515
+ * Derive entity hints from a pending tool call. Pure, stateless, safe to call
516
+ * from tests. Callers should merge the result into `effectiveEntityHints`
517
+ * *after* text-derived hints so text signals still take precedence when a hint
518
+ * appears in both places.
519
+ */
520
+ function derivePendingToolCallEntityHints(toolCall, projectEntity) {
521
+ if (!toolCall || !projectEntity)
522
+ return [];
523
+ const parsed = (0, entity_resolution_1.parseEntityString)(projectEntity);
524
+ const projectId = parsed.entityId;
525
+ if (!projectId)
526
+ return [];
527
+ const args = (toolCall.args ?? {});
528
+ const seen = new Set();
529
+ const hints = [];
530
+ switch (toolCall.name) {
531
+ case 'Read': {
532
+ const filePath = typeof args.file_path === 'string' ? args.file_path : '';
533
+ if (filePath) {
534
+ deriveFileEntityFromPath(filePath, projectId, seen, hints);
535
+ }
536
+ break;
537
+ }
538
+ case 'Grep': {
539
+ // Grep has pattern + optional path + optional glob. Only path/glob
540
+ // can yield file-level entities; the regex pattern itself is
541
+ // content-level and gets surfaced via the existing text hints from
542
+ // latestMessage/currentContext.
543
+ if (typeof args.path === 'string') {
544
+ deriveFileEntityFromPath(args.path, projectId, seen, hints);
545
+ }
546
+ if (typeof args.glob === 'string') {
547
+ const base = extractBasenameFromGlobPattern(args.glob);
548
+ if (base)
549
+ deriveFileEntityFromPath(base, projectId, seen, hints);
550
+ }
551
+ break;
552
+ }
553
+ case 'Glob': {
554
+ if (typeof args.pattern === 'string') {
555
+ const base = extractBasenameFromGlobPattern(args.pattern);
556
+ if (base)
557
+ deriveFileEntityFromPath(base, projectId, seen, hints);
558
+ // If the pattern has no literal basename, fall through to the
559
+ // project entity so stored project-level facts still surface.
560
+ }
561
+ if (typeof args.path === 'string') {
562
+ deriveFileEntityFromPath(args.path, projectId, seen, hints);
563
+ }
564
+ break;
565
+ }
566
+ case 'Bash': {
567
+ // Scan the command string for embedded paths. This covers
568
+ // `cat src/foo.ts`, `rm ./dist/bar.js`, `node scripts/baz.ts`, etc.
569
+ const command = typeof args.command === 'string' ? args.command : '';
570
+ scanStringForPathEntities(command, projectId, seen, hints);
571
+ break;
572
+ }
573
+ case 'WebSearch': {
574
+ const query = typeof args.query === 'string' ? args.query : '';
575
+ const token = normalizeWebToken(query);
576
+ if (token) {
577
+ const hint = `web/search_${token}`;
578
+ if (!seen.has(hint)) {
579
+ seen.add(hint);
580
+ hints.push(hint);
581
+ }
582
+ }
583
+ break;
584
+ }
585
+ case 'WebFetch': {
586
+ const url = typeof args.url === 'string' ? args.url : '';
587
+ if (url) {
588
+ try {
589
+ const parsedUrl = new URL(url);
590
+ const host = normalizeWebToken(parsedUrl.hostname);
591
+ if (host) {
592
+ const hostHint = `web/${host}`;
593
+ if (!seen.has(hostHint)) {
594
+ seen.add(hostHint);
595
+ hints.push(hostHint);
596
+ }
597
+ }
598
+ const path = normalizeWebToken(parsedUrl.pathname);
599
+ if (host && path && path.length >= 2) {
600
+ const pathHint = `web/${host}_${path}`;
601
+ if (!seen.has(pathHint)) {
602
+ seen.add(pathHint);
603
+ hints.push(pathHint);
604
+ }
605
+ }
606
+ }
607
+ catch {
608
+ // Non-URL string — fall back to a normalized token entity.
609
+ const token = normalizeWebToken(url);
610
+ if (token) {
611
+ const hint = `web/${token}`;
612
+ if (!seen.has(hint)) {
613
+ seen.add(hint);
614
+ hints.push(hint);
615
+ }
616
+ }
617
+ }
618
+ }
619
+ break;
620
+ }
621
+ }
622
+ return hints;
623
+ }
449
624
  function advisoryTaskTokens(taskType) {
450
625
  if (!taskType)
451
626
  return [];
@@ -931,6 +1106,38 @@ function tokenize(text) {
931
1106
  .map((part) => part.trim())
932
1107
  .filter((part) => part.length > 2);
933
1108
  }
1109
+ /**
1110
+ * Determine whether injected facts are relevant to the current task context.
1111
+ * Uses token overlap between the task description and fact keys/summaries.
1112
+ * Exported for unit testing.
1113
+ */
1114
+ function injectedFactsAreTaskRelevant(taskContext, injectedKeys, injectedSummaries) {
1115
+ if (!taskContext)
1116
+ return true;
1117
+ const taskTokens = new Set(tokenize(taskContext));
1118
+ if (taskTokens.size === 0)
1119
+ return true;
1120
+ for (const entityKey of injectedKeys) {
1121
+ const key = entityKey.split('/').slice(2).join('/');
1122
+ for (const token of tokenize(key.replace(/[_/.-]+/g, ' '))) {
1123
+ if (taskTokens.has(token))
1124
+ return true;
1125
+ }
1126
+ }
1127
+ if (injectedSummaries && injectedSummaries.length > 0) {
1128
+ const contentTokens = injectedSummaries
1129
+ .flatMap((s) => tokenize(s).filter((t) => t.length > 5));
1130
+ let contentMatches = 0;
1131
+ for (const token of contentTokens) {
1132
+ if (taskTokens.has(token)) {
1133
+ contentMatches++;
1134
+ if (contentMatches >= 2)
1135
+ return true;
1136
+ }
1137
+ }
1138
+ }
1139
+ return false;
1140
+ }
934
1141
  function tokenizeForSearch(text) {
935
1142
  return tokenize(text).filter((token) => !SEARCH_SUGGESTION_STOPWORDS.has(token));
936
1143
  }
@@ -1512,6 +1719,7 @@ class AttendantInstance {
1512
1719
  injectedEntryIds: [...input.injectedEntryIds],
1513
1720
  injectedSummaries: input.injectedSummaries ? [...input.injectedSummaries] : undefined,
1514
1721
  evidenceKinds: [],
1722
+ taskContext: input.taskContext,
1515
1723
  };
1516
1724
  this.pendingMemoryAttributions.push(attribution);
1517
1725
  this.updateBriefPendingMemoryAttributions();
@@ -1582,6 +1790,9 @@ class AttendantInstance {
1582
1790
  && /\b(next_step|current_step|open_risks|status|checkpoint_summary|recent_file_changes|recent_actions|implementation_status|blockers?)\b/i.test(key));
1583
1791
  });
1584
1792
  }
1793
+ checkInjectedFactsTaskRelevant(attribution) {
1794
+ return injectedFactsAreTaskRelevant(attribution.taskContext, attribution.injectedKeys, attribution.injectedSummaries);
1795
+ }
1585
1796
  scorePendingMemoryAttributions(response) {
1586
1797
  if (this.pendingMemoryAttributions.length === 0) {
1587
1798
  return [];
@@ -1596,18 +1807,27 @@ class AttendantInstance {
1596
1807
  if (!rediscoveredManually && this.responseShowsRecoveryValue(response, entry) && !evidenceKinds.includes('response_recovery')) {
1597
1808
  evidenceKinds.push('response_recovery');
1598
1809
  }
1810
+ // Check task-relevance: if facts are not relevant to the current task,
1811
+ // mark as task_irrelevant so the compliance scorer does not penalize.
1812
+ const taskRelevant = this.checkInjectedFactsTaskRelevant(entry);
1813
+ if (!taskRelevant && !evidenceKinds.includes('task_irrelevant')) {
1814
+ evidenceKinds.push('task_irrelevant');
1815
+ }
1599
1816
  const used = evidenceKinds.includes('write')
1600
1817
  || evidenceKinds.includes('checkpoint')
1601
1818
  || evidenceKinds.includes('response_reference')
1602
- || evidenceKinds.includes('response_recovery');
1819
+ || evidenceKinds.includes('response_recovery')
1820
+ || evidenceKinds.includes('task_irrelevant');
1603
1821
  const helpful = evidenceKinds.includes('checkpoint')
1604
1822
  || evidenceKinds.includes('write')
1605
1823
  || evidenceKinds.includes('response_recovery');
1606
1824
  const reason = helpful
1607
1825
  ? 'response_or_action_confirmed_memory_helpfulness'
1608
- : used
1609
- ? 'response_referenced_injected_memory'
1610
- : 'memory_was_only_surfaced';
1826
+ : evidenceKinds.includes('task_irrelevant')
1827
+ ? 'injected_facts_not_relevant_to_current_task'
1828
+ : used
1829
+ ? 'response_referenced_injected_memory'
1830
+ : 'memory_was_only_surfaced';
1611
1831
  const scoredEntry = {
1612
1832
  ...entry,
1613
1833
  used,
@@ -2277,11 +2497,22 @@ class AttendantInstance {
2277
2497
  const baseEntityHints = this.resolveAttendEntityHints(input.entityHints, latestMessage);
2278
2498
  // File-change demand-driven recall: extract file path mentions and add as entity hints
2279
2499
  const filePathHints = extractFilePathEntityHints(`${latestMessage}\n${currentContext}`, (0, autoRemember_1.getProjectMemoryEntity)() ?? null);
2280
- const effectiveEntityHints = filePathHints.length > 0
2281
- ? [...new Set([...baseEntityHints, ...filePathHints])]
2282
- : baseEntityHints;
2283
- // User operating rules: load rules whose triggers match the current context
2284
- const matchedUserRules = phase !== 'post-response'
2500
+ // A2: tool-call triggered retrieval. If the caller says "I'm about to
2501
+ // run Read(file_path=X) / Grep / Bash / WebFetch / WebSearch", derive
2502
+ // structured entity hints from the tool arguments BEFORE the tool
2503
+ // runs, so stored facts can preempt the lookup.
2504
+ const toolCallHints = derivePendingToolCallEntityHints(input.pendingToolCall, (0, autoRemember_1.getProjectMemoryEntity)() ?? null);
2505
+ const allExtraHints = filePathHints.length === 0 && toolCallHints.length === 0
2506
+ ? null
2507
+ : [...filePathHints, ...toolCallHints];
2508
+ const effectiveEntityHints = allExtraHints === null
2509
+ ? baseEntityHints
2510
+ : [...new Set([...baseEntityHints, ...allExtraHints])];
2511
+ // User operating rules: load rules whose triggers match the current context.
2512
+ // Mid-turn attends skip this — rules were already surfaced at pre-response,
2513
+ // and reloading them on every mid-turn call would duplicate context and burn
2514
+ // an LLM call on the trigger match.
2515
+ const matchedUserRules = (phase !== 'post-response' && phase !== 'mid-turn')
2285
2516
  ? await this.loadMatchingUserRules(`${latestMessage}\n${currentContext}`)
2286
2517
  : [];
2287
2518
  let watchedEntitiesChanged = this.updateWatchedEntities(effectiveEntityHints);
@@ -2401,6 +2632,17 @@ class AttendantInstance {
2401
2632
  await this.persistState();
2402
2633
  }
2403
2634
  (0, metrics_1.timeEnd)('attendant.attend_ms', t0);
2635
+ // A2: even on the "memory not needed" early return, if the caller
2636
+ // passed pendingToolCall, echo the derived entities so the agent
2637
+ // can see that attend() was tool-call-aware on this call.
2638
+ const skipGuidance = input.pendingToolCall
2639
+ ? {
2640
+ toolName: input.pendingToolCall.name,
2641
+ derivedEntities: toolCallHints,
2642
+ factCount: 0,
2643
+ note: `Memory was not deemed necessary for this ${input.pendingToolCall.name} call. Proceed.`,
2644
+ }
2645
+ : undefined;
2404
2646
  return {
2405
2647
  shouldInject: matchedUserRules.length > 0,
2406
2648
  reason: 'memory_not_needed',
@@ -2426,21 +2668,28 @@ class AttendantInstance {
2426
2668
  hintsResolved: 0,
2427
2669
  dropped: [{ name: latestMessage || '(none)', reason: 'memory_not_needed' }],
2428
2670
  },
2671
+ toolCallGuidance: skipGuidance,
2429
2672
  };
2430
2673
  }
2431
2674
  // Post-compaction recovery: re-surface facts that were recently injected (likely in context
2432
2675
  // just before the compact) without blocking them on the already-in-context filter.
2433
2676
  // The flag is set by handshake(postCompaction:true) and consumed exactly once here.
2434
2677
  const postCompactionRecoveryKeys = [];
2435
- let postCompactionMaxFacts = input.maxFacts;
2678
+ let effectiveMaxFacts = input.maxFacts;
2436
2679
  if (this.postCompactionPending) {
2437
2680
  const recentInjections = this.pendingMemoryAttributions.slice(-5);
2438
2681
  for (const attr of recentInjections) {
2439
2682
  postCompactionRecoveryKeys.push(...attr.injectedKeys);
2440
2683
  }
2441
- postCompactionMaxFacts = Math.min((input.maxFacts ?? 5) * 2, 10);
2684
+ effectiveMaxFacts = Math.min((input.maxFacts ?? 5) * 2, 10);
2442
2685
  this.postCompactionPending = false;
2443
2686
  }
2687
+ else if (phase === 'mid-turn' && input.maxFacts === undefined) {
2688
+ // A1: mid-turn attends default to a smaller fact budget than pre-response.
2689
+ // The agent already has a working-memory frame from the pre-response attend;
2690
+ // mid-turn is about surfacing 1-3 NEW facts on a topic shift, not re-dumping briefs.
2691
+ effectiveMaxFacts = MID_TURN_DEFAULT_MAX_FACTS;
2692
+ }
2444
2693
  const observeEntityHints = effectiveEntityHints.length > 0 ? effectiveEntityHints : freshState.entities;
2445
2694
  const allObserveEntityHints = postCompactionRecoveryKeys.length > 0
2446
2695
  ? [...new Set([
@@ -2453,7 +2702,7 @@ class AttendantInstance {
2453
2702
  : observeEntityHints;
2454
2703
  const observed = await this.observe({
2455
2704
  currentContext: observationContext,
2456
- maxFacts: postCompactionMaxFacts,
2705
+ maxFacts: effectiveMaxFacts,
2457
2706
  entityHints: allObserveEntityHints,
2458
2707
  priorityKeys: expandContinuityPriorityKeys(Array.from(new Set([
2459
2708
  ...(mandatoryRecall.key ? [mandatoryRecall.key] : []),
@@ -2477,7 +2726,31 @@ class AttendantInstance {
2477
2726
  const remainder = slashIdx2 === -1 ? '' : fact.entityKey.slice(slashIdx2);
2478
2727
  return { ...fact, entityKey: `${canonicalPersonalType}/${canonicalPersonalId}${remainder}` };
2479
2728
  });
2480
- const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(remappedFacts);
2729
+ // A1: mid-turn dedup — drop facts whose entityKey was already injected earlier
2730
+ // in the SAME turn. pendingMemoryAttributions is reset at post-response, so any
2731
+ // entry in that list belongs to the current turn. This prevents mid-turn attends
2732
+ // from spamming the agent with facts it already has in working memory from a
2733
+ // previous pre-response or mid-turn attend call.
2734
+ const midTurnFilteredKeys = [];
2735
+ let factsAfterDedup = remappedFacts;
2736
+ if (phase === 'mid-turn' && this.pendingMemoryAttributions.length > 0) {
2737
+ const alreadyInjectedThisTurn = new Set();
2738
+ for (const attr of this.pendingMemoryAttributions) {
2739
+ for (const key of attr.injectedKeys) {
2740
+ alreadyInjectedThisTurn.add(key);
2741
+ }
2742
+ }
2743
+ if (alreadyInjectedThisTurn.size > 0) {
2744
+ factsAfterDedup = remappedFacts.filter((fact) => {
2745
+ if (alreadyInjectedThisTurn.has(fact.entityKey)) {
2746
+ midTurnFilteredKeys.push(fact.entityKey);
2747
+ return false;
2748
+ }
2749
+ return true;
2750
+ });
2751
+ }
2752
+ }
2753
+ const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(factsAfterDedup);
2481
2754
  watchedEntitiesChanged = this.updateWatchedEntities(observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? []) || watchedEntitiesChanged;
2482
2755
  this.markSharedStateObserved(observeEntityHints.length > 0 ? observeEntityHints : freshState.entities);
2483
2756
  let reason = 'memory_needed_injected';
@@ -2486,8 +2759,14 @@ class AttendantInstance {
2486
2759
  const memoryResultsConsidered = observed.totalFound;
2487
2760
  let searchSuggestion;
2488
2761
  if (!shouldInject) {
2762
+ // A1: if mid-turn dedup ate everything observe returned, treat it as
2763
+ // "already in context" — the agent has these facts from an earlier attend.
2764
+ const allDroppedByMidTurnDedup = phase === 'mid-turn'
2765
+ && midTurnFilteredKeys.length > 0
2766
+ && remappedFacts.length > 0
2767
+ && factsAfterDedup.length === 0;
2489
2768
  const allAlreadyInContext = observed.totalFound > 0 && observed.alreadyPresent >= observed.totalFound;
2490
- reason = allAlreadyInContext ? 'memory_needed_but_in_context' : 'memory_checked_no_match';
2769
+ reason = (allAlreadyInContext || allDroppedByMidTurnDedup) ? 'memory_needed_but_in_context' : 'memory_checked_no_match';
2491
2770
  if (reason === 'memory_checked_no_match') {
2492
2771
  const terms = tokenizeForSearch(latestMessage).slice(0, 6);
2493
2772
  const alternativeEntities = (observed.entitiesResolved ?? [])
@@ -2514,11 +2793,34 @@ class AttendantInstance {
2514
2793
  .map((fact) => fact.knowledgeEntryId)
2515
2794
  .filter((value) => typeof value === 'number'),
2516
2795
  injectedSummaries: structuredFacts.map((fact) => fact.summary).filter(Boolean),
2796
+ taskContext: this.brief?.inferredTaskType,
2517
2797
  }),
2518
2798
  ]
2519
2799
  : [];
2800
+ // A1: when mid-turn dedup filtered keys, surface them in debug so the
2801
+ // caller (and tests) can reason about which facts were suppressed.
2802
+ const debugWithMidTurn = (midTurnFilteredKeys.length > 0 && observed.debug)
2803
+ ? { ...observed.debug, midTurnFilteredKeys: [...midTurnFilteredKeys] }
2804
+ : observed.debug;
2805
+ // A2: if the caller supplied pendingToolCall, surface a guidance block
2806
+ // so the agent can see what entities were derived and how many stored
2807
+ // facts preempted the lookup. Only populated when a tool call was
2808
+ // actually passed — no allocation overhead for the common path.
2809
+ const toolCallGuidance = input.pendingToolCall
2810
+ ? {
2811
+ toolName: input.pendingToolCall.name,
2812
+ derivedEntities: toolCallHints,
2813
+ factCount: structuredFacts.length,
2814
+ note: toolCallHints.length === 0
2815
+ ? `No entity hints derived from ${input.pendingToolCall.name} args. Memory check ran on message/context only.`
2816
+ : structuredFacts.length > 0
2817
+ ? `Iranti surfaced ${structuredFacts.length} stored fact(s) for this ${input.pendingToolCall.name} call. Read them before running the tool — you may not need to run it.`
2818
+ : `No stored facts matched the ${input.pendingToolCall.name} target. Proceed with the tool call.`,
2819
+ }
2820
+ : undefined;
2520
2821
  const attendResult = {
2521
2822
  ...observed,
2823
+ debug: debugWithMidTurn,
2522
2824
  facts: structuredFacts,
2523
2825
  shouldInject: structuredFacts.length > 0 || matchedUserRules.length > 0,
2524
2826
  reason,
@@ -2532,6 +2834,7 @@ class AttendantInstance {
2532
2834
  memoryResultsConsidered,
2533
2835
  matchedUserRules: matchedUserRules.length > 0 ? matchedUserRules : undefined,
2534
2836
  usageGuidance: buildUsageGuidance('attend', this.turnsWithoutWrite),
2837
+ toolCallGuidance,
2535
2838
  };
2536
2839
  if (input.suppressEvents !== true) {
2537
2840
  (0, staffEventRegistry_1.getStaffEventEmitter)().emit({