clementine-agent 1.18.33 → 1.18.35
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/dist/agent/assistant.js
CHANGED
|
@@ -15,7 +15,7 @@ import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DY
|
|
|
15
15
|
import pino from 'pino';
|
|
16
16
|
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, TASK_BUDGET_TOKENS, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, normalizeClaudeSdkOptionsForOneMillionContext, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, usesOneMillionContext, envSnapshot, } from '../config.js';
|
|
17
17
|
import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
|
|
18
|
-
import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, } from '../integrations/tool-preferences.js';
|
|
18
|
+
import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, KNOWN_SERVICES, } from '../integrations/tool-preferences.js';
|
|
19
19
|
import { loadClaudeIntegrations } from './mcp-bridge.js';
|
|
20
20
|
import { detectFrustrationSignals, detectRepeatedTopics } from './insight-engine.js';
|
|
21
21
|
import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
|
|
@@ -32,7 +32,7 @@ import { PromptCache } from './prompt-cache.js';
|
|
|
32
32
|
import { searchSkills as searchSkillsSync } from './skill-extractor.js';
|
|
33
33
|
import { classifyIntent, getStrategyGuidance } from './intent-classifier.js';
|
|
34
34
|
import { getEventLog } from './session-event-log.js';
|
|
35
|
-
import { routeToolSurface, TOOL_SURFACE_HARD_LIMIT, TOOL_SURFACE_WARN_THRESHOLD } from './tool-router.js';
|
|
35
|
+
import { applyServiceDedup, routeToolSurface, TOOL_SURFACE_HARD_LIMIT, TOOL_SURFACE_WARN_THRESHOLD } from './tool-router.js';
|
|
36
36
|
import { isRestrictedToolset, toolsetAllowsLocalWrites } from './toolsets.js';
|
|
37
37
|
import { looksLikeApprovalPrompt } from './local-turn.js';
|
|
38
38
|
import { decideTurn } from './turn-policy.js';
|
|
@@ -2436,6 +2436,65 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2436
2436
|
whitelist.add(mcpTool('goal_work'));
|
|
2437
2437
|
allowedTools = allowedTools.filter(t => whitelist.has(t));
|
|
2438
2438
|
}
|
|
2439
|
+
// ── Per-service dedup (intelligent routing) ───────────────────
|
|
2440
|
+
// When a service has BOTH Composio + Claude Desktop sources
|
|
2441
|
+
// connected (e.g. Composio outlook + claude.ai Microsoft 365),
|
|
2442
|
+
// bundles in tool-router list both so either path can route to
|
|
2443
|
+
// whichever is connected. But if BOTH are connected, today's
|
|
2444
|
+
// behavior loaded both — and worse, claude.ai's auto-attach
|
|
2445
|
+
// would pull in every other connector the user authorized
|
|
2446
|
+
// (Drive, Gmail, Calendar, Slack…) via the env path. ~300+ tool
|
|
2447
|
+
// schemas leak in this way and leave Sonnet's autocompact no
|
|
2448
|
+
// room to work.
|
|
2449
|
+
//
|
|
2450
|
+
// Dedup walks each (Composio↔claude.ai) pair, picks ONE per
|
|
2451
|
+
// user preference (default Composio), drops the loser from
|
|
2452
|
+
// mcpServers + allowedTools, and turns inheritFullClaudeEnv off
|
|
2453
|
+
// when no claude.ai service survived (so SAFE_ENV is used and
|
|
2454
|
+
// the SDK can't auto-attach the other connectors).
|
|
2455
|
+
if (!toolsDisabledForCall && !isPlanStep && !toolRoute.fullSurface) {
|
|
2456
|
+
const composioConnected = new Set(Object.keys(composioMcpServers));
|
|
2457
|
+
const cdIntegrationsForDedup = loadClaudeIntegrations();
|
|
2458
|
+
const claudeDesktopActive = new Set(Object.values(cdIntegrationsForDedup).filter(i => i.connected).map(i => i.name));
|
|
2459
|
+
const prefs = loadToolPreferences();
|
|
2460
|
+
const dedupResult = applyServiceDedup(toolRoute, {
|
|
2461
|
+
composioConnected,
|
|
2462
|
+
claudeDesktopActive,
|
|
2463
|
+
preferences: prefs.preferences,
|
|
2464
|
+
knownServices: KNOWN_SERVICES,
|
|
2465
|
+
});
|
|
2466
|
+
if (dedupResult.droppedClaudeAi.length > 0 || dedupResult.droppedComposio.length > 0) {
|
|
2467
|
+
const beforeAllowed = allowedTools.length;
|
|
2468
|
+
const beforeInherit = toolRoute.inheritFullClaudeEnv;
|
|
2469
|
+
toolRoute = dedupResult.route;
|
|
2470
|
+
for (const name of dedupResult.droppedClaudeAi) {
|
|
2471
|
+
delete externalMcpServers[name];
|
|
2472
|
+
}
|
|
2473
|
+
for (const slug of dedupResult.droppedComposio) {
|
|
2474
|
+
delete composioMcpServers[slug];
|
|
2475
|
+
}
|
|
2476
|
+
const droppedServers = new Set([
|
|
2477
|
+
...dedupResult.droppedClaudeAi,
|
|
2478
|
+
...dedupResult.droppedComposio,
|
|
2479
|
+
]);
|
|
2480
|
+
allowedTools = allowedTools.filter(tool => {
|
|
2481
|
+
if (!tool.startsWith('mcp__'))
|
|
2482
|
+
return true;
|
|
2483
|
+
const serverName = tool.slice('mcp__'.length).split('__')[0];
|
|
2484
|
+
return !droppedServers.has(serverName);
|
|
2485
|
+
});
|
|
2486
|
+
logger.info({
|
|
2487
|
+
sessionKey,
|
|
2488
|
+
droppedClaudeAi: dedupResult.droppedClaudeAi,
|
|
2489
|
+
droppedComposio: dedupResult.droppedComposio,
|
|
2490
|
+
anyClaudeDesktopKept: dedupResult.anyClaudeDesktopKept,
|
|
2491
|
+
inheritFullClaudeEnvBefore: beforeInherit,
|
|
2492
|
+
inheritFullClaudeEnvAfter: toolRoute.inheritFullClaudeEnv,
|
|
2493
|
+
allowedToolCountBefore: beforeAllowed,
|
|
2494
|
+
allowedToolCountAfter: allowedTools.length,
|
|
2495
|
+
}, 'Tool route deduped per user tool-preferences');
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2439
2498
|
// Tool-surface cap. Applies to chat AND to autonomous runs (cron,
|
|
2440
2499
|
// unleashed, heartbeat). Without this cap on cron, a single job got
|
|
2441
2500
|
// 300+ MCP tool schemas in the system prompt — leaving Sonnet's SDK
|
|
@@ -5331,7 +5390,24 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5331
5390
|
}
|
|
5332
5391
|
}
|
|
5333
5392
|
catch { /* non-fatal — run without skills */ }
|
|
5393
|
+
// ── Sub-agent fan-out directive (Vision 2) ──────────────────────
|
|
5394
|
+
// Detect multi-item / broad-scope signals in the job spec and
|
|
5395
|
+
// prepend a hard-line fan-out mandate when found. This is what
|
|
5396
|
+
// keeps the parent context clean on long jobs: each slice of work
|
|
5397
|
+
// runs in an Agent sub-agent (its own context window, big tool
|
|
5398
|
+
// responses contained), and the parent only sees compact summaries.
|
|
5399
|
+
const { buildAlwaysOnParallelizationHint, buildFanoutDirectiveForText } = await import('./fanout-policy.js');
|
|
5400
|
+
const fanoutScope = `${jobName}\n${jobPrompt}\n${cronProfile?.description ?? ''}\n${cronProfile?.systemPromptBody ?? ''}`;
|
|
5401
|
+
const { directive: fanoutDirective, report: fanoutReport } = buildFanoutDirectiveForText(fanoutScope);
|
|
5402
|
+
if (fanoutReport.needsFanout) {
|
|
5403
|
+
logger.info({
|
|
5404
|
+
job: jobName,
|
|
5405
|
+
signals: fanoutReport.signals.map(s => s.pattern),
|
|
5406
|
+
}, 'Fanout policy: directive injected for cron job');
|
|
5407
|
+
}
|
|
5334
5408
|
const prompt = `[Scheduled task: ${jobName}]\n\n` +
|
|
5409
|
+
(fanoutDirective ? fanoutDirective + '\n\n' : '') +
|
|
5410
|
+
buildAlwaysOnParallelizationHint() + '\n\n' +
|
|
5335
5411
|
progressContext +
|
|
5336
5412
|
goalContext +
|
|
5337
5413
|
skillContext +
|
|
@@ -5723,22 +5799,30 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5723
5799
|
}
|
|
5724
5800
|
let prompt;
|
|
5725
5801
|
if (phase === 1) {
|
|
5802
|
+
const { buildAlwaysOnParallelizationHint, buildFanoutDirectiveForText } = await import('./fanout-policy.js');
|
|
5803
|
+
const unleashedFanoutScope = `${jobName}\n${jobPrompt}\n${unleashedProfile?.description ?? ''}\n${unleashedProfile?.systemPromptBody ?? ''}`;
|
|
5804
|
+
const { directive: unleashedFanoutDirective, report: unleashedFanoutReport } = buildFanoutDirectiveForText(unleashedFanoutScope);
|
|
5805
|
+
if (unleashedFanoutReport.needsFanout) {
|
|
5806
|
+
logger.info({
|
|
5807
|
+
job: jobName,
|
|
5808
|
+
phase,
|
|
5809
|
+
signals: unleashedFanoutReport.signals.map(s => s.pattern),
|
|
5810
|
+
}, 'Fanout policy: directive injected for unleashed phase 1');
|
|
5811
|
+
}
|
|
5726
5812
|
prompt =
|
|
5727
5813
|
`[UNLEASHED TASK: ${jobName} — Phase ${phase} — ${timestamp}]\n\n` +
|
|
5728
5814
|
`You are running in unleashed mode — a long-running autonomous task.\n` +
|
|
5729
5815
|
`Time remaining: ${remainingHours} hours. You have ${turnsPerPhase} turns per phase.\n` +
|
|
5730
5816
|
`After each phase completes, your session will be resumed with fresh context.\n\n` +
|
|
5817
|
+
(unleashedFanoutDirective ? unleashedFanoutDirective + '\n\n' : '') +
|
|
5818
|
+
buildAlwaysOnParallelizationHint() + '\n\n' +
|
|
5731
5819
|
`TASK:\n${jobPrompt}\n\n` +
|
|
5732
5820
|
unleashedSkillContext +
|
|
5733
5821
|
`${unleashedContextSafety}\n\n` +
|
|
5734
5822
|
`IMPORTANT:\n` +
|
|
5735
5823
|
`- Work methodically through the task in phases\n` +
|
|
5736
5824
|
`- At the end of this phase, output a STATUS SUMMARY of what you accomplished and what remains\n` +
|
|
5737
|
-
`- Save important intermediate results to files so they persist across phases
|
|
5738
|
-
`PARALLELIZATION: When processing multiple items (prospects, accounts, emails, analyses), ` +
|
|
5739
|
-
`use the Agent tool to spawn sub-agents that work in parallel. For example, if you need to ` +
|
|
5740
|
-
`research 10 prospects, spawn 3-5 sub-agents that each handle a batch — don't process them ` +
|
|
5741
|
-
`one at a time. Each sub-agent should receive specific items and return structured results.`;
|
|
5825
|
+
`- Save important intermediate results to files so they persist across phases`;
|
|
5742
5826
|
}
|
|
5743
5827
|
else {
|
|
5744
5828
|
// Phase 2+ — inject structured checkpoint from previous phase if available
|
|
@@ -5762,6 +5846,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5762
5846
|
}
|
|
5763
5847
|
}
|
|
5764
5848
|
catch { /* fall back to no checkpoint */ }
|
|
5849
|
+
const { buildAlwaysOnParallelizationHint: hintFn } = await import('./fanout-policy.js');
|
|
5850
|
+
const phaseParallelHint = hintFn();
|
|
5765
5851
|
if (sessionId) {
|
|
5766
5852
|
// Resuming existing session — agent has conversation history + structured checkpoint
|
|
5767
5853
|
prompt =
|
|
@@ -5770,6 +5856,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5770
5856
|
`Time remaining: ${remainingHours} hours. You have ${turnsPerPhase} turns this phase.\n` +
|
|
5771
5857
|
checkpointContext +
|
|
5772
5858
|
`\n${unleashedContextSafety}\n` +
|
|
5859
|
+
`\n${phaseParallelHint}\n` +
|
|
5773
5860
|
`\nContinue working on the task. Pick up where you left off.\n` +
|
|
5774
5861
|
`If the task is COMPLETE, output "TASK_COMPLETE:" followed by a final summary.\n\n` +
|
|
5775
5862
|
`IMPORTANT: Output a STATUS SUMMARY at the end of this phase.`;
|
|
@@ -5784,6 +5871,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5784
5871
|
`TASK:\n${jobPrompt}\n` +
|
|
5785
5872
|
checkpointContext +
|
|
5786
5873
|
`\n${unleashedContextSafety}\n` +
|
|
5874
|
+
`\n${phaseParallelHint}\n` +
|
|
5787
5875
|
`\nCheck any files or progress from prior phases, then continue the work.\n` +
|
|
5788
5876
|
`If the task is COMPLETE, output "TASK_COMPLETE:" followed by a final summary.\n\n` +
|
|
5789
5877
|
`IMPORTANT: Output a STATUS SUMMARY at the end of this phase.`;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent fan-out policy for autonomous tasks.
|
|
3
|
+
*
|
|
4
|
+
* Why: even with a small tool surface, a single agent context can fill
|
|
5
|
+
* within a few turns when tool responses are large (Outlook list dumps,
|
|
6
|
+
* web search results, file reads, multi-prospect research). The SDK's
|
|
7
|
+
* autocompact then has nothing to compact and aborts with
|
|
8
|
+
* `rapid_refill_breaker`. The fix matching how Claude Code is designed:
|
|
9
|
+
* spawn sub-agents that each handle a slice of work in their own
|
|
10
|
+
* isolated context and return only a compact summary back to the parent.
|
|
11
|
+
*
|
|
12
|
+
* The Agent tool already exists in the SDK. The problem is timing —
|
|
13
|
+
* agents tend to discover the need for fan-out only after thrashing.
|
|
14
|
+
* This module front-loads the directive: scan the task description for
|
|
15
|
+
* signals that fan-out will be needed, and inject a strong, explicit
|
|
16
|
+
* mandate at the top of the prompt.
|
|
17
|
+
*
|
|
18
|
+
* Two outputs:
|
|
19
|
+
* - buildAlwaysOnParallelizationHint()
|
|
20
|
+
* Short reminder injected into every autonomous prompt. Cheap.
|
|
21
|
+
* - buildFanoutDirective(detectFanoutSignals(text).signals)
|
|
22
|
+
* Stronger, explicit fan-out contract. Only injected when signals
|
|
23
|
+
* indicate the task is genuinely multi-item or broad-scope.
|
|
24
|
+
*/
|
|
25
|
+
export interface FanoutSignal {
|
|
26
|
+
/** Why fan-out matters for this task. Surfaced in the directive. */
|
|
27
|
+
reason: string;
|
|
28
|
+
/** The pattern that matched. Used for telemetry. */
|
|
29
|
+
pattern: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FanoutSignalReport {
|
|
32
|
+
needsFanout: boolean;
|
|
33
|
+
signals: FanoutSignal[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Detect patterns that strongly predict fan-out is needed. Conservative
|
|
37
|
+
* by design — false positives waste a few hundred tokens per turn; false
|
|
38
|
+
* negatives let the agent thrash. Tune for false positives.
|
|
39
|
+
*/
|
|
40
|
+
export declare function detectFanoutSignals(text: string): FanoutSignalReport;
|
|
41
|
+
/**
|
|
42
|
+
* Always-on parallelization reminder. Short, designed to ride along in
|
|
43
|
+
* every autonomous prompt without inflating token cost.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildAlwaysOnParallelizationHint(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Strong fan-out contract injected when detector matches. Designed to be
|
|
48
|
+
* unambiguous: failing to fan out on these patterns *will* crash the run.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildFanoutDirective(signals: FanoutSignal[]): string;
|
|
51
|
+
/**
|
|
52
|
+
* Convenience: detect signals and return the directive string in one
|
|
53
|
+
* call. Returns empty string when no fan-out is indicated.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildFanoutDirectiveForText(text: string): {
|
|
56
|
+
directive: string;
|
|
57
|
+
report: FanoutSignalReport;
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=fanout-policy.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent fan-out policy for autonomous tasks.
|
|
3
|
+
*
|
|
4
|
+
* Why: even with a small tool surface, a single agent context can fill
|
|
5
|
+
* within a few turns when tool responses are large (Outlook list dumps,
|
|
6
|
+
* web search results, file reads, multi-prospect research). The SDK's
|
|
7
|
+
* autocompact then has nothing to compact and aborts with
|
|
8
|
+
* `rapid_refill_breaker`. The fix matching how Claude Code is designed:
|
|
9
|
+
* spawn sub-agents that each handle a slice of work in their own
|
|
10
|
+
* isolated context and return only a compact summary back to the parent.
|
|
11
|
+
*
|
|
12
|
+
* The Agent tool already exists in the SDK. The problem is timing —
|
|
13
|
+
* agents tend to discover the need for fan-out only after thrashing.
|
|
14
|
+
* This module front-loads the directive: scan the task description for
|
|
15
|
+
* signals that fan-out will be needed, and inject a strong, explicit
|
|
16
|
+
* mandate at the top of the prompt.
|
|
17
|
+
*
|
|
18
|
+
* Two outputs:
|
|
19
|
+
* - buildAlwaysOnParallelizationHint()
|
|
20
|
+
* Short reminder injected into every autonomous prompt. Cheap.
|
|
21
|
+
* - buildFanoutDirective(detectFanoutSignals(text).signals)
|
|
22
|
+
* Stronger, explicit fan-out contract. Only injected when signals
|
|
23
|
+
* indicate the task is genuinely multi-item or broad-scope.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Detect patterns that strongly predict fan-out is needed. Conservative
|
|
27
|
+
* by design — false positives waste a few hundred tokens per turn; false
|
|
28
|
+
* negatives let the agent thrash. Tune for false positives.
|
|
29
|
+
*/
|
|
30
|
+
export function detectFanoutSignals(text) {
|
|
31
|
+
const signals = [];
|
|
32
|
+
const lower = text.toLowerCase();
|
|
33
|
+
const checks = [
|
|
34
|
+
{
|
|
35
|
+
pattern: 'multi_item_iteration',
|
|
36
|
+
re: /\b(for each|for every|process each|iterate over|loop through|across all|across each)\b/,
|
|
37
|
+
reason: 'task explicitly iterates over multiple items — process them in parallel sub-agents, not one at a time in this conversation',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
pattern: 'collective_with_quantifier',
|
|
41
|
+
re: /\b(all|every|each)\s+(prospects?|accounts?|leads?|contacts?|customers?|deals?|emails?|messages?|threads?|files?|records?|rows?|tasks?|items?|results?|pages?|articles?|posts?|repos?|repositories|projects?)\b/,
|
|
42
|
+
reason: 'task spans every item in a collection — fan out by batching items across sub-agents',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
pattern: 'numeric_collection',
|
|
46
|
+
re: /\b\d{2,}\s+(prospects?|accounts?|leads?|contacts?|customers?|deals?|emails?|messages?|threads?|files?|records?|rows?|items?|results?|pages?|articles?)\b/,
|
|
47
|
+
reason: 'task names a numeric count of items (10+) — split into batches of 3-5 per sub-agent',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
pattern: 'comprehensive_research',
|
|
51
|
+
re: /\b(comprehensive|exhaustive|deep[- ]dive|deep dive|full audit|competitive intel|market map|content intel|brief|landscape|panorama)\b/,
|
|
52
|
+
reason: 'broad-scope research task — each step (news, search, brand, competitor, social) should run in its own sub-agent so the parent context stays clean',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: 'broad_scan_or_crawl',
|
|
56
|
+
re: /\b(scan all|crawl|backfill|inventory|migrate|refactor)\b.{0,80}\b(all|entire|every|full)\b/s,
|
|
57
|
+
reason: 'broad scan/crawl — partition by directory, date range, or ID range and fan out per partition',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
pattern: 'long_history_pull',
|
|
61
|
+
re: /\b(last|past)\s+\d+\s+(days|weeks|months)|\bsince\s+(yesterday|last week|last month)\b/,
|
|
62
|
+
reason: 'pulling a history range that is likely to return many items — sub-agents per day/week chunk',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
pattern: 'multiple_steps',
|
|
66
|
+
re: /\b(steps?|phases?|stages?)\s*[:0-9]/,
|
|
67
|
+
reason: 'task has explicit multi-step structure — each step in its own sub-agent, parent only sees the step summaries',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
for (const check of checks) {
|
|
71
|
+
if (check.re.test(lower)) {
|
|
72
|
+
signals.push({ pattern: check.pattern, reason: check.reason });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
needsFanout: signals.length > 0,
|
|
77
|
+
signals,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Always-on parallelization reminder. Short, designed to ride along in
|
|
82
|
+
* every autonomous prompt without inflating token cost.
|
|
83
|
+
*/
|
|
84
|
+
export function buildAlwaysOnParallelizationHint() {
|
|
85
|
+
return [
|
|
86
|
+
'## Sub-agent fan-out',
|
|
87
|
+
'When you process multiple items, spawn ONE Agent sub-agent per batch of 3–5 items. Sub-agents return ONE-LINE summaries (no raw tool output). Do not iterate sequentially in this conversation — that fills your context and aborts the run.',
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Strong fan-out contract injected when detector matches. Designed to be
|
|
92
|
+
* unambiguous: failing to fan out on these patterns *will* crash the run.
|
|
93
|
+
*/
|
|
94
|
+
export function buildFanoutDirective(signals) {
|
|
95
|
+
if (signals.length === 0)
|
|
96
|
+
return '';
|
|
97
|
+
const reasonLines = signals
|
|
98
|
+
.map((s, i) => `${i + 1}. ${s.reason}`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
return [
|
|
101
|
+
'## Sub-agent fan-out is MANDATORY for this task',
|
|
102
|
+
'',
|
|
103
|
+
'Preflight detected patterns that will fill the context window if you run them sequentially in this conversation:',
|
|
104
|
+
'',
|
|
105
|
+
reasonLines,
|
|
106
|
+
'',
|
|
107
|
+
'### Required pattern',
|
|
108
|
+
'',
|
|
109
|
+
'Use the `Agent` tool to spawn parallel sub-agents. Each sub-agent runs in its own isolated context, so big tool responses live and die there — your context only sees the summary.',
|
|
110
|
+
'',
|
|
111
|
+
'- **Batch size**: 3–5 items per sub-agent (or one slice of work per sub-agent for research tasks)',
|
|
112
|
+
'- **Sub-agent prompt MUST include**: the narrow task, the exact return format (e.g. `Return ONE LINE: <id> | <status> | <next-action>`), and an explicit "do not include raw tool output" directive',
|
|
113
|
+
'- **Parent context keeps**: only the sub-agent return strings, not their tool transcripts',
|
|
114
|
+
'',
|
|
115
|
+
'If you anticipate a single tool call returning more than ~5 KB of text (full email lists, web search result pages, large database queries, file dumps), wrap THAT call in an Agent invocation too. The sub-agent runs the tool, extracts only the fields you need, and returns those.',
|
|
116
|
+
'',
|
|
117
|
+
'Failing to fan out on this task will cause the SDK to abort with `rapid_refill_breaker` and the run will be lost.',
|
|
118
|
+
].join('\n');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Convenience: detect signals and return the directive string in one
|
|
122
|
+
* call. Returns empty string when no fan-out is indicated.
|
|
123
|
+
*/
|
|
124
|
+
export function buildFanoutDirectiveForText(text) {
|
|
125
|
+
const report = detectFanoutSignals(text);
|
|
126
|
+
return {
|
|
127
|
+
directive: buildFanoutDirective(report.signals),
|
|
128
|
+
report,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=fanout-policy.js.map
|
|
@@ -31,5 +31,32 @@ export declare const TOOL_SURFACE_WARN_THRESHOLD = 150;
|
|
|
31
31
|
export declare const TOOL_SURFACE_HARD_LIMIT = 220;
|
|
32
32
|
export declare const TOOL_BUNDLES: readonly ToolBundleDefinition[];
|
|
33
33
|
export declare function routeToolSurface(text: string | undefined): ToolRouteDecision;
|
|
34
|
+
import type { ServiceDefinition, ToolSource } from '../integrations/tool-preferences.js';
|
|
35
|
+
export interface ServiceDedupOptions {
|
|
36
|
+
/** Composio toolkit slugs the user has actually connected. */
|
|
37
|
+
composioConnected: Set<string>;
|
|
38
|
+
/** Claude Desktop integration names the user has actually connected. */
|
|
39
|
+
claudeDesktopActive: Set<string>;
|
|
40
|
+
/** User-selected source per service id (from tool-preferences.json). */
|
|
41
|
+
preferences: Record<string, ToolSource>;
|
|
42
|
+
/** The KNOWN_SERVICES registry. Passed in so this module stays
|
|
43
|
+
* decoupled from tool-preferences (and tests can stub the table). */
|
|
44
|
+
knownServices: readonly ServiceDefinition[];
|
|
45
|
+
}
|
|
46
|
+
export interface ServiceDedupResult {
|
|
47
|
+
/** The route with losing sources removed from external + composio sets. */
|
|
48
|
+
route: ToolRouteDecision;
|
|
49
|
+
/** Claude Desktop integration names that were dropped. Used by the
|
|
50
|
+
* caller to add disallowedTools and decide whether the SDK subprocess
|
|
51
|
+
* needs claude.ai env inheritance at all. */
|
|
52
|
+
droppedClaudeAi: string[];
|
|
53
|
+
/** Composio toolkit slugs that were dropped. Mirror of the above. */
|
|
54
|
+
droppedComposio: string[];
|
|
55
|
+
/** True if any claude.ai integration survived dedup. When false, the
|
|
56
|
+
* caller can drop CLAUDE_CODE_OAUTH_TOKEN from the subprocess env so
|
|
57
|
+
* Claude Code doesn't auto-attach claude.ai connectors. */
|
|
58
|
+
anyClaudeDesktopKept: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare function applyServiceDedup(route: ToolRouteDecision, opts: ServiceDedupOptions): ServiceDedupResult;
|
|
34
61
|
export {};
|
|
35
62
|
//# sourceMappingURL=tool-router.d.ts.map
|
|
@@ -193,4 +193,89 @@ export function routeToolSurface(text) {
|
|
|
193
193
|
reason: bundles.size > 0 || external.size > 0 || composio.size > 0 ? 'matched' : 'empty',
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
|
+
export function applyServiceDedup(route, opts) {
|
|
197
|
+
const droppedClaudeAi = [];
|
|
198
|
+
const droppedComposio = [];
|
|
199
|
+
// fullSurface routes intentionally load everything — admin/debug paths.
|
|
200
|
+
// Skip dedup so behavior matches the user's explicit "all tools" intent.
|
|
201
|
+
if (route.fullSurface) {
|
|
202
|
+
return {
|
|
203
|
+
route,
|
|
204
|
+
droppedClaudeAi,
|
|
205
|
+
droppedComposio,
|
|
206
|
+
anyClaudeDesktopKept: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const externalSet = new Set(route.externalMcpServers ?? []);
|
|
210
|
+
const composioSet = new Set(route.composioToolkits ?? []);
|
|
211
|
+
for (const service of opts.knownServices) {
|
|
212
|
+
const cdName = service.claudeDesktopName;
|
|
213
|
+
const composioSlug = service.composioSlug;
|
|
214
|
+
if (!cdName || !composioSlug)
|
|
215
|
+
continue;
|
|
216
|
+
const routeHasCd = externalSet.has(cdName);
|
|
217
|
+
const routeHasComposio = composioSet.has(composioSlug);
|
|
218
|
+
if (!routeHasCd || !routeHasComposio)
|
|
219
|
+
continue;
|
|
220
|
+
// Both sources are in the route. Resolve based on availability + pref.
|
|
221
|
+
const cdAvailable = opts.claudeDesktopActive.has(cdName);
|
|
222
|
+
const composioAvailable = opts.composioConnected.has(composioSlug);
|
|
223
|
+
if (!cdAvailable && !composioAvailable) {
|
|
224
|
+
// Neither connected — drop both (the route lists them, but they'll
|
|
225
|
+
// fail at attach time). Cleaner to remove now.
|
|
226
|
+
externalSet.delete(cdName);
|
|
227
|
+
composioSet.delete(composioSlug);
|
|
228
|
+
droppedClaudeAi.push(cdName);
|
|
229
|
+
droppedComposio.push(composioSlug);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!cdAvailable) {
|
|
233
|
+
// Only Composio connected — drop the claude.ai entry.
|
|
234
|
+
externalSet.delete(cdName);
|
|
235
|
+
droppedClaudeAi.push(cdName);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!composioAvailable) {
|
|
239
|
+
composioSet.delete(composioSlug);
|
|
240
|
+
droppedComposio.push(composioSlug);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
// Conflict: both connected. Pick per user preference, default Composio.
|
|
244
|
+
const userPref = opts.preferences[service.id];
|
|
245
|
+
const effective = userPref === 'off'
|
|
246
|
+
? 'off'
|
|
247
|
+
: userPref ?? 'composio';
|
|
248
|
+
if (effective === 'off') {
|
|
249
|
+
externalSet.delete(cdName);
|
|
250
|
+
composioSet.delete(composioSlug);
|
|
251
|
+
droppedClaudeAi.push(cdName);
|
|
252
|
+
droppedComposio.push(composioSlug);
|
|
253
|
+
}
|
|
254
|
+
else if (effective === 'composio') {
|
|
255
|
+
externalSet.delete(cdName);
|
|
256
|
+
droppedClaudeAi.push(cdName);
|
|
257
|
+
}
|
|
258
|
+
else if (effective === 'claude-desktop') {
|
|
259
|
+
composioSet.delete(composioSlug);
|
|
260
|
+
droppedComposio.push(composioSlug);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// After dedup, the SDK subprocess needs claude.ai env inheritance ONLY
|
|
264
|
+
// if some claude.ai integration is still in the route. If everything
|
|
265
|
+
// routed to Composio, force inheritFullClaudeEnv off so Claude Code
|
|
266
|
+
// can't auto-attach the rest of the user's authorized integrations.
|
|
267
|
+
const anyClaudeDesktopKept = externalSet.size > 0
|
|
268
|
+
&& [...externalSet].some(name => opts.claudeDesktopActive.has(name));
|
|
269
|
+
return {
|
|
270
|
+
route: {
|
|
271
|
+
...route,
|
|
272
|
+
externalMcpServers: [...externalSet],
|
|
273
|
+
composioToolkits: [...composioSet],
|
|
274
|
+
inheritFullClaudeEnv: route.inheritFullClaudeEnv && anyClaudeDesktopKept,
|
|
275
|
+
},
|
|
276
|
+
droppedClaudeAi,
|
|
277
|
+
droppedComposio,
|
|
278
|
+
anyClaudeDesktopKept,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
196
281
|
//# sourceMappingURL=tool-router.js.map
|