comisai 1.0.15 → 1.0.16
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/node_modules/@comis/agent/dist/bootstrap/types.d.ts +1 -1
- package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +12 -11
- package/node_modules/@comis/agent/dist/context-engine/constants.d.ts +11 -0
- package/node_modules/@comis/agent/dist/context-engine/constants.js +11 -0
- package/node_modules/@comis/agent/dist/context-engine/index.d.ts +1 -1
- package/node_modules/@comis/agent/dist/context-engine/index.js +1 -1
- package/node_modules/@comis/agent/dist/context-engine/llm-compaction.js +32 -18
- package/node_modules/@comis/agent/dist/executor/cache-break-diff-writer.d.ts +2 -0
- package/node_modules/@comis/agent/dist/executor/cache-break-diff-writer.js +36 -0
- package/node_modules/@comis/agent/dist/executor/executor-post-execution.d.ts +12 -0
- package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +117 -27
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +1 -0
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +16 -4
- package/node_modules/@comis/agent/dist/executor/executor-stream-setup.js +17 -1
- package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +27 -1
- package/node_modules/@comis/agent/dist/executor/session-snapshot-cleanup.js +3 -1
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/index.d.ts +2 -0
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/index.js +2 -0
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.d.ts +9 -0
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +65 -3
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/stub-filter-injector.d.ts +28 -0
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/stub-filter-injector.js +63 -0
- package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +18 -1
- package/node_modules/@comis/agent/dist/executor/tool-deferral.js +275 -133
- package/node_modules/@comis/agent/dist/executor/ttl-guard.d.ts +6 -0
- package/node_modules/@comis/agent/dist/executor/ttl-guard.js +8 -0
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/dist/shared/typing-controller.d.ts +7 -0
- package/node_modules/@comis/channels/dist/shared/typing-controller.js +33 -0
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/dist/commands/daemon.js +116 -28
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/dist/config/schema-agent.d.ts +10 -0
- package/node_modules/@comis/core/dist/config/schema-agent.js +8 -0
- package/node_modules/@comis/core/dist/config/schema-skills.d.ts +16 -2
- package/node_modules/@comis/core/dist/config/schema-skills.js +9 -2
- package/node_modules/@comis/core/dist/config/schema.d.ts +6 -3
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +1 -1
- package/node_modules/@comis/skills/package.json +1 -1
- package/package.json +12 -12
|
@@ -95,7 +95,7 @@ export interface BootstrapContextFile {
|
|
|
95
95
|
* Workspace file names allowed in sub-agent bootstrap context.
|
|
96
96
|
* Sub-agents only receive AGENTS.md (instructions) and TOOLS.md (tool notes).
|
|
97
97
|
*/
|
|
98
|
-
export declare const SUBAGENT_BOOTSTRAP_ALLOWLIST: Set<"
|
|
98
|
+
export declare const SUBAGENT_BOOTSTRAP_ALLOWLIST: Set<"AGENTS.md" | "SOUL.md" | "IDENTITY.md" | "USER.md" | "ROLE.md" | "TOOLS.md" | "HEARTBEAT.md" | "BOOTSTRAP.md" | "BOOT.md">;
|
|
99
99
|
/** Head portion ratio for truncation (first 70% of maxChars) */
|
|
100
100
|
export declare const BOOTSTRAP_HEAD_RATIO = 0.7;
|
|
101
101
|
/** Tail portion ratio for truncation (last 20% of maxChars) */
|
|
@@ -49,9 +49,14 @@ export function createPiEventBridge(deps) {
|
|
|
49
49
|
// -----------------------------------------------------------------
|
|
50
50
|
case "message_update": {
|
|
51
51
|
const ame = event.assistantMessageEvent;
|
|
52
|
-
if (ame && ame.type === "text_delta") {
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (ame && (ame.type === "text_delta" || ame.type === "thinking_delta")) {
|
|
53
|
+
if (ame.type === "text_delta") {
|
|
54
|
+
// Track that visible text was produced in some turn.
|
|
55
|
+
// thinking_delta intentionally excluded — empty-final-turn detection
|
|
56
|
+
// depends on this flag reflecting user-visible text only.
|
|
57
|
+
m.textEmitted = true;
|
|
58
|
+
}
|
|
59
|
+
if (deps.onDelta && typeof ame.delta === "string") {
|
|
55
60
|
try {
|
|
56
61
|
deps.onDelta(ame.delta);
|
|
57
62
|
}
|
|
@@ -394,7 +399,7 @@ export function createPiEventBridge(deps) {
|
|
|
394
399
|
totalTokens: usage.totalTokens,
|
|
395
400
|
cost,
|
|
396
401
|
provider: deps.provider,
|
|
397
|
-
model: deps.model,
|
|
402
|
+
model: deps.getCurrentModel?.() ?? deps.model,
|
|
398
403
|
sessionKey: formatSessionKey(deps.sessionKey),
|
|
399
404
|
operationType: deps.operationType,
|
|
400
405
|
});
|
|
@@ -457,7 +462,7 @@ export function createPiEventBridge(deps) {
|
|
|
457
462
|
channelId: deps.channelId,
|
|
458
463
|
executionId: deps.executionId,
|
|
459
464
|
provider: deps.provider,
|
|
460
|
-
model: deps.model,
|
|
465
|
+
model: deps.getCurrentModel?.() ?? deps.model,
|
|
461
466
|
tokens: {
|
|
462
467
|
prompt: usage.input,
|
|
463
468
|
completion: usage.output,
|
|
@@ -475,7 +480,7 @@ export function createPiEventBridge(deps) {
|
|
|
475
480
|
cacheWriteTokens,
|
|
476
481
|
sessionKey: formatSessionKey(deps.sessionKey),
|
|
477
482
|
savedVsUncached,
|
|
478
|
-
cacheEligible: getCacheProviderInfo(deps.provider, deps.model).cacheEligible,
|
|
483
|
+
cacheEligible: getCacheProviderInfo(deps.provider, deps.getCurrentModel?.() ?? deps.model).cacheEligible,
|
|
479
484
|
responseId,
|
|
480
485
|
cacheCreation: effectiveCacheCreation,
|
|
481
486
|
});
|
|
@@ -561,17 +566,13 @@ export function createPiEventBridge(deps) {
|
|
|
561
566
|
// SEP: Extract plan from first LLM turn that has tool calls + assistant text.
|
|
562
567
|
// This runs inside the agentic loop so subsequent turns can track against the plan.
|
|
563
568
|
if (deps.executionPlan && deps.sepConfig && !deps.executionPlan.current) {
|
|
564
|
-
const hasToolCalls = Array.isArray(assistantMsg?.content) && assistantMsg.content.some((c) => {
|
|
565
|
-
const block = c;
|
|
566
|
-
return block.type === "toolCall" || block.type === "tool_use";
|
|
567
|
-
});
|
|
568
569
|
const assistantTextForPlan = Array.isArray(assistantMsg?.content)
|
|
569
570
|
? assistantMsg.content
|
|
570
571
|
.filter((c) => c?.type === "text")
|
|
571
572
|
.map((c) => c.text ?? "")
|
|
572
573
|
.join(" ")
|
|
573
574
|
: "";
|
|
574
|
-
if (
|
|
575
|
+
if (assistantTextForPlan.length > 0) {
|
|
575
576
|
const steps = extractPlanFromResponse(assistantTextForPlan, deps.sepConfig.maxSteps);
|
|
576
577
|
if (steps && steps.length >= deps.sepConfig.minSteps) {
|
|
577
578
|
const plan = {
|
|
@@ -96,6 +96,17 @@ export declare const MCP_DEFERRAL_THRESHOLD = 0.1;
|
|
|
96
96
|
* this many blocks apart cannot see each other for prefix matching.
|
|
97
97
|
* Used by: lookback window enforcement in stream-wrappers.ts. */
|
|
98
98
|
export declare const CACHE_LOOKBACK_WINDOW = 20;
|
|
99
|
+
/** Maximum message blocks before cache-aware compaction trigger.
|
|
100
|
+
* With 4 breakpoints (3 Comis + 1 SDK) and a 20-block lookback window,
|
|
101
|
+
* optimal coverage spans 4 × 20 = 80 blocks. Trigger when the count
|
|
102
|
+
* *exceeds* 60 (i.e. first fires at 61 blocks — 75% of theoretical max)
|
|
103
|
+
* to leave headroom for multi-call turns.
|
|
104
|
+
*
|
|
105
|
+
* APPROXIMATION NOTE: `messages.length` (AgentMessage[]) approximates
|
|
106
|
+
* Anthropic's request-body `messages[]` block count but is not strictly 1:1.
|
|
107
|
+
* Treat 60 as a defensive setpoint, not a calibrated threshold.
|
|
108
|
+
* Used by: llm-compaction layer cache-aware trigger. */
|
|
109
|
+
export declare const CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD = 60;
|
|
99
110
|
/** Context utilization percentage that triggers LLM compaction. Used by: llm-compaction layer. */
|
|
100
111
|
export declare const COMPACTION_TRIGGER_PERCENT = 85;
|
|
101
112
|
/** Default turns to wait before re-triggering compaction. Used by: llm-compaction layer. */
|
|
@@ -155,6 +155,17 @@ export const MCP_DEFERRAL_THRESHOLD = 0.10;
|
|
|
155
155
|
* this many blocks apart cannot see each other for prefix matching.
|
|
156
156
|
* Used by: lookback window enforcement in stream-wrappers.ts. */
|
|
157
157
|
export const CACHE_LOOKBACK_WINDOW = 20;
|
|
158
|
+
/** Maximum message blocks before cache-aware compaction trigger.
|
|
159
|
+
* With 4 breakpoints (3 Comis + 1 SDK) and a 20-block lookback window,
|
|
160
|
+
* optimal coverage spans 4 × 20 = 80 blocks. Trigger when the count
|
|
161
|
+
* *exceeds* 60 (i.e. first fires at 61 blocks — 75% of theoretical max)
|
|
162
|
+
* to leave headroom for multi-call turns.
|
|
163
|
+
*
|
|
164
|
+
* APPROXIMATION NOTE: `messages.length` (AgentMessage[]) approximates
|
|
165
|
+
* Anthropic's request-body `messages[]` block count but is not strictly 1:1.
|
|
166
|
+
* Treat 60 as a defensive setpoint, not a calibrated threshold.
|
|
167
|
+
* Used by: llm-compaction layer cache-aware trigger. */
|
|
168
|
+
export const CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD = 60;
|
|
158
169
|
// ---------------------------------------------------------------------------
|
|
159
170
|
// LLM Compaction (Layer 5: )
|
|
160
171
|
// ---------------------------------------------------------------------------
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export type { TokenBudget, TokenAnchor, ContextLayer, ContextEngine, ContextEngineDeps, ContextEngineMetrics, AssembledContext, LayerCircuitBreaker, MicrocompactionGuard, ObservationMasker, CacheOptimizationMetrics, CacheSessionStats, EvictionStats, } from "./types.js";
|
|
10
10
|
export type { ToolMaskingTier } from "./constants.js";
|
|
11
|
-
export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
|
|
11
|
+
export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
|
|
12
12
|
export { computeTokenBudget } from "./token-budget.js";
|
|
13
13
|
export { createContextEngine } from "./context-engine.js";
|
|
14
14
|
export { createThinkingBlockCleaner } from "./thinking-block-cleaner.js";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
// Constants
|
|
11
|
-
export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
|
|
11
|
+
export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
|
|
12
12
|
// Token budget algebra
|
|
13
13
|
export { computeTokenBudget } from "./token-budget.js";
|
|
14
14
|
// Context engine factory
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* @module
|
|
26
26
|
*/
|
|
27
27
|
import { generateSummary } from "@mariozechner/pi-coding-agent";
|
|
28
|
-
import { COMPACTION_TRIGGER_PERCENT, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, CHARS_PER_TOKEN_RATIO, MIN_MIDDLE_MESSAGES_FOR_COMPACTION, } from "./constants.js";
|
|
28
|
+
import { COMPACTION_TRIGGER_PERCENT, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, CHARS_PER_TOKEN_RATIO, MIN_MIDDLE_MESSAGES_FOR_COMPACTION, CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD, } from "./constants.js";
|
|
29
29
|
import { estimateContextCharsWithDualRatio, estimateMessageChars, estimateWithAnchor, } from "../safety/token-estimator.js";
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Structured output instructions
|
|
@@ -258,27 +258,41 @@ export function createLlmCompactionLayer(config, deps) {
|
|
|
258
258
|
if (turnsSinceLastCompaction < config.compactionCooldownTurns) {
|
|
259
259
|
return messages;
|
|
260
260
|
}
|
|
261
|
-
// Step
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
261
|
+
// Step 2b: Cache-aware block count trigger.
|
|
262
|
+
// Fires BEFORE the token-based threshold because lookback overflow
|
|
263
|
+
// causes cache breaks regardless of how few tokens the messages contain.
|
|
264
|
+
const messageCount = messages.length;
|
|
265
|
+
const blockThreshold = CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD;
|
|
266
|
+
const blockCountExceeded = messageCount > blockThreshold;
|
|
267
|
+
// Step 3: Token threshold check (only when block-count trigger didn't fire)
|
|
268
|
+
let contextTokens;
|
|
269
|
+
let thresholdTokens;
|
|
270
|
+
if (!blockCountExceeded) {
|
|
271
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
272
|
+
const contextChars = estimateContextCharsWithDualRatio(messages);
|
|
273
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
274
|
+
const charBasedTokens = Math.ceil(contextChars / CHARS_PER_TOKEN_RATIO);
|
|
275
|
+
const anchor = deps.getTokenAnchor?.() ?? null;
|
|
276
|
+
contextTokens = estimateWithAnchor(anchor, messages, charBasedTokens);
|
|
277
|
+
thresholdTokens = Math.floor(budget.windowTokens * COMPACTION_TRIGGER_PERCENT / 100);
|
|
278
|
+
if (contextTokens <= thresholdTokens) {
|
|
279
|
+
return messages;
|
|
280
|
+
}
|
|
272
281
|
}
|
|
273
|
-
// Step 4:
|
|
282
|
+
// Step 4: Unified log (conditional spread keeps JSON shape clean).
|
|
274
283
|
deps.logger.warn({
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
messageCount,
|
|
285
|
+
...(blockCountExceeded
|
|
286
|
+
? { blockThreshold, trigger: "block_count" }
|
|
287
|
+
: { contextTokens, thresholdTokens, trigger: "token_threshold" }),
|
|
277
288
|
windowTokens: budget.windowTokens,
|
|
278
|
-
messageCount: messages.length,
|
|
279
289
|
errorKind: "resource",
|
|
280
|
-
hint:
|
|
281
|
-
|
|
290
|
+
hint: blockCountExceeded
|
|
291
|
+
? "Message count approaching breakpoint lookback limit; compacting to prevent cache fragmentation"
|
|
292
|
+
: "Context approaching capacity; LLM compaction will summarize older messages to free space",
|
|
293
|
+
}, blockCountExceeded
|
|
294
|
+
? "LLM compaction triggered: message count exceeds cache lookback threshold"
|
|
295
|
+
: "LLM compaction triggered: context exceeds 85% threshold");
|
|
282
296
|
// Step 5: Resolve model
|
|
283
297
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
284
298
|
let model;
|
|
@@ -60,6 +60,8 @@ export interface CacheBreakDiffPayload {
|
|
|
60
60
|
message: number;
|
|
61
61
|
sdkAuto: number;
|
|
62
62
|
};
|
|
63
|
+
/** Model ID for per-model cost attribution. Populated by pi-event-bridge.ts. */
|
|
64
|
+
model?: string;
|
|
63
65
|
}
|
|
64
66
|
export interface CacheBreakDiffWriterConfig {
|
|
65
67
|
/** Directory for diff files (e.g., ~/.comis/cache-breaks) */
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { writeFileSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
|
|
13
13
|
import { createPatch } from "diff";
|
|
14
14
|
import { safePath } from "@comis/core";
|
|
15
|
+
import { resolveModelPricing, ZERO_COST } from "../model/model-catalog.js";
|
|
15
16
|
const MAX_DIFF_FILES = 50;
|
|
16
17
|
/** DIFF-CONTENT: Maximum chars per category (system, tools) for snapshot content before diffing. */
|
|
17
18
|
const MAX_SNAPSHOT_CHARS = 50_000;
|
|
@@ -40,6 +41,7 @@ export function buildDiffableContent(system, tools, model) {
|
|
|
40
41
|
*/
|
|
41
42
|
export function createCacheBreakDiffWriter(config) {
|
|
42
43
|
let dirEnsured = false;
|
|
44
|
+
const unknownModelWarnLatch = new Set();
|
|
43
45
|
return (event) => {
|
|
44
46
|
try {
|
|
45
47
|
if (!dirEnsured) {
|
|
@@ -50,6 +52,24 @@ export function createCacheBreakDiffWriter(config) {
|
|
|
50
52
|
const ts = new Date(event.timestamp).toISOString().replace(/[:.]/g, "-");
|
|
51
53
|
const filename = `${ts}_${event.agentId}_${event.reason}.json`;
|
|
52
54
|
const filePath = safePath(config.outputDir, filename);
|
|
55
|
+
// Cost attribution: compute estimated USD impact using per-model pricing.
|
|
56
|
+
const modelId = event.model ?? "";
|
|
57
|
+
const pricing = resolveModelPricing(event.provider, modelId);
|
|
58
|
+
const pricingKnown = pricing !== ZERO_COST && pricing.input > 0;
|
|
59
|
+
const retentionDisabled = event.ttlCategory === "none";
|
|
60
|
+
const writeRatePerToken = event.ttlCategory === "long" ? pricing.cacheWrite1h : pricing.cacheWrite;
|
|
61
|
+
const readRatePerToken = pricing.cacheRead;
|
|
62
|
+
const perTokenDelta = Math.max(0, writeRatePerToken - readRatePerToken);
|
|
63
|
+
const estimatedCostUsd = retentionDisabled
|
|
64
|
+
? 0
|
|
65
|
+
: (pricingKnown ? event.tokenDrop * perTokenDelta : null);
|
|
66
|
+
if (!pricingKnown && !retentionDisabled && modelId) {
|
|
67
|
+
const latchKey = `${event.provider}:${modelId}`;
|
|
68
|
+
if (!unknownModelWarnLatch.has(latchKey)) {
|
|
69
|
+
unknownModelWarnLatch.add(latchKey);
|
|
70
|
+
config.logger.warn({ provider: event.provider, model: modelId, hint: "Cache break cost attribution unavailable for this model", errorKind: "config" }, "Unknown model pricing for cache break cost attribution");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
53
73
|
const diff = {
|
|
54
74
|
timestamp: new Date(event.timestamp).toISOString(),
|
|
55
75
|
agentId: event.agentId,
|
|
@@ -85,6 +105,22 @@ export function createCacheBreakDiffWriter(config) {
|
|
|
85
105
|
effortValue: event.effortValue,
|
|
86
106
|
// Breakpoint budget context
|
|
87
107
|
...(event.breakpointBudget && { breakpointBudget: event.breakpointBudget }),
|
|
108
|
+
// Cost attribution
|
|
109
|
+
estimatedCostUsd: estimatedCostUsd === null
|
|
110
|
+
? null
|
|
111
|
+
: Math.round(estimatedCostUsd * 100_000) / 100_000,
|
|
112
|
+
costBreakdown: {
|
|
113
|
+
writeRatePerMTok: pricingKnown
|
|
114
|
+
? Math.round(writeRatePerToken * 1_000_000 * 100) / 100
|
|
115
|
+
: null,
|
|
116
|
+
readRatePerMTok: pricingKnown
|
|
117
|
+
? Math.round(readRatePerToken * 1_000_000 * 100) / 100
|
|
118
|
+
: null,
|
|
119
|
+
tokenDrop: event.tokenDrop,
|
|
120
|
+
model: event.model ?? null,
|
|
121
|
+
ttlCategory: event.ttlCategory,
|
|
122
|
+
pricingKnown,
|
|
123
|
+
},
|
|
88
124
|
};
|
|
89
125
|
writeFileSync(filePath, JSON.stringify(diff, null, 2) + "\n");
|
|
90
126
|
// DIFF-CONTENT: Generate unified diff file alongside JSON
|
|
@@ -140,6 +140,18 @@ export interface PostExecutionParams {
|
|
|
140
140
|
* @returns true if the turn qualifies for memory storage
|
|
141
141
|
*/
|
|
142
142
|
export declare function shouldStorePairedMemory(userText: string, agentResponse: string): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Check whether paired memory content was stored recently for this agent.
|
|
145
|
+
*
|
|
146
|
+
* Exact-content hashing (not semantic similarity) -- targets the cron pattern
|
|
147
|
+
* of identical prompt + identical NO_REPLY response. Lazy eviction keeps the
|
|
148
|
+
* cache bounded without a timer.
|
|
149
|
+
*
|
|
150
|
+
* Exported for unit tests.
|
|
151
|
+
*/
|
|
152
|
+
export declare function isDuplicatePairedMemory(content: string, agentId: string): boolean;
|
|
153
|
+
/** Reset the paired-memory dedup cache. Exported for unit tests. */
|
|
154
|
+
export declare function resetPairedMemoryDedupForTests(): void;
|
|
143
155
|
/**
|
|
144
156
|
* Run post-execution cleanup for a PiExecutor turn.
|
|
145
157
|
*
|
|
@@ -19,7 +19,7 @@ import { mergeSessionStats } from "./pi-executor.js";
|
|
|
19
19
|
import { recordLastResponseTs } from "./ttl-guard.js";
|
|
20
20
|
import { stripDiscoverySchemas } from "./schema-stripping.js";
|
|
21
21
|
import { getWorkspaceStatus } from "../workspace/index.js";
|
|
22
|
-
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// Helpers
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
@@ -61,6 +61,69 @@ export function shouldStorePairedMemory(userText, agentResponse) {
|
|
|
61
61
|
return false;
|
|
62
62
|
return true;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Operation types that must NOT create paired memories.
|
|
66
|
+
*
|
|
67
|
+
* Cron and heartbeat executions are stateless and repetitive -- storing their
|
|
68
|
+
* prompts pollutes the vector space and degrades RAG recall for interactive
|
|
69
|
+
* conversations. Compaction / taskExtraction / condensation are system-internal
|
|
70
|
+
* operations that have their own dedicated memory paths (compaction summaries,
|
|
71
|
+
* extracted tasks) and should not additionally create paired conversation
|
|
72
|
+
* entries. Interactive and subagent are deliberately excluded.
|
|
73
|
+
*/
|
|
74
|
+
const MEMORY_SKIP_OPERATIONS = new Set([
|
|
75
|
+
"cron",
|
|
76
|
+
"heartbeat",
|
|
77
|
+
"compaction",
|
|
78
|
+
"taskExtraction",
|
|
79
|
+
"condensation",
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* In-memory dedup cache for paired memory content.
|
|
83
|
+
*
|
|
84
|
+
* Defense-in-depth for interactive conversations where the user sends the same
|
|
85
|
+
* message multiple times (retries, reconnects). The Layer-1 operationType gate
|
|
86
|
+
* is the primary defense against cron/heartbeat duplication; this Layer-2 hash
|
|
87
|
+
* dedup catches residual duplicates that slip through.
|
|
88
|
+
*
|
|
89
|
+
* Keyed by a 64-bit truncation of sha256(agentId || content). Single-process
|
|
90
|
+
* daemon deployment (pm2, no cluster) means this cache is always complete.
|
|
91
|
+
*/
|
|
92
|
+
const DEDUP_TTL_MS = 10 * 60 * 1000;
|
|
93
|
+
const DEDUP_MAX_ENTRIES = 500;
|
|
94
|
+
const pairedMemoryDedup = new Map();
|
|
95
|
+
/**
|
|
96
|
+
* Check whether paired memory content was stored recently for this agent.
|
|
97
|
+
*
|
|
98
|
+
* Exact-content hashing (not semantic similarity) -- targets the cron pattern
|
|
99
|
+
* of identical prompt + identical NO_REPLY response. Lazy eviction keeps the
|
|
100
|
+
* cache bounded without a timer.
|
|
101
|
+
*
|
|
102
|
+
* Exported for unit tests.
|
|
103
|
+
*/
|
|
104
|
+
export function isDuplicatePairedMemory(content, agentId) {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (pairedMemoryDedup.size > DEDUP_MAX_ENTRIES) {
|
|
107
|
+
for (const [key, ts] of pairedMemoryDedup) {
|
|
108
|
+
if (now - ts > DEDUP_TTL_MS)
|
|
109
|
+
pairedMemoryDedup.delete(key);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const hash = createHash("sha256")
|
|
113
|
+
.update(agentId)
|
|
114
|
+
.update(content)
|
|
115
|
+
.digest("hex")
|
|
116
|
+
.slice(0, 16);
|
|
117
|
+
const existing = pairedMemoryDedup.get(hash);
|
|
118
|
+
if (existing != null && now - existing <= DEDUP_TTL_MS)
|
|
119
|
+
return true;
|
|
120
|
+
pairedMemoryDedup.set(hash, now);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/** Reset the paired-memory dedup cache. Exported for unit tests. */
|
|
124
|
+
export function resetPairedMemoryDedupForTests() {
|
|
125
|
+
pairedMemoryDedup.clear();
|
|
126
|
+
}
|
|
64
127
|
// ---------------------------------------------------------------------------
|
|
65
128
|
// Implementation
|
|
66
129
|
// ---------------------------------------------------------------------------
|
|
@@ -292,38 +355,65 @@ export async function postExecution(params) {
|
|
|
292
355
|
// Pairing user message with agent response creates entries that carry enough
|
|
293
356
|
// context for meaningful RAG retrieval. Standalone user messages like "Hello"
|
|
294
357
|
// or "you choose" have no semantic value without the agent's response.
|
|
295
|
-
//
|
|
358
|
+
//
|
|
359
|
+
// Two-layer dedup defense:
|
|
360
|
+
// Layer 1: operationType gate -- skip cron/heartbeat/system-internal ops
|
|
361
|
+
// which are stateless/repetitive and would pollute the vector
|
|
362
|
+
// space with near-identical entries.
|
|
363
|
+
// Layer 2: content-hash dedup -- safety net for interactive retries.
|
|
364
|
+
//
|
|
296
365
|
// Non-blocking, non-fatal -- execution never fails due to memory store errors.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
366
|
+
const operationType = params.executionOverrides?.operationType;
|
|
367
|
+
const skipMemoryForOperation = operationType != null && MEMORY_SKIP_OPERATIONS.has(operationType);
|
|
368
|
+
if (deps.memoryPort &&
|
|
369
|
+
result.response &&
|
|
370
|
+
msg.text &&
|
|
371
|
+
!skipMemoryForOperation &&
|
|
372
|
+
shouldStorePairedMemory(msg.text, result.response)) {
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
const pairedContent = buildPairedMemoryContent(msg.text, result.response);
|
|
375
|
+
const effectiveAgentId = agentId ?? "default";
|
|
376
|
+
if (isDuplicatePairedMemory(pairedContent, effectiveAgentId)) {
|
|
377
|
+
deps.logger.debug({ agentId: effectiveAgentId, sessionKey: formattedKey }, "Paired memory skipped: duplicate content within dedup window");
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
try {
|
|
381
|
+
const userEntryId = randomUUID();
|
|
382
|
+
const userStoreResult = await deps.memoryPort.store({
|
|
383
|
+
id: userEntryId,
|
|
384
|
+
tenantId: sessionKey.tenantId,
|
|
385
|
+
agentId: effectiveAgentId,
|
|
386
|
+
userId: sessionKey.userId,
|
|
387
|
+
content: pairedContent,
|
|
388
|
+
trustLevel: "learned",
|
|
389
|
+
source: {
|
|
390
|
+
who: sessionKey.userId,
|
|
391
|
+
channel: msg.channelType ?? "unknown",
|
|
392
|
+
sessionKey: formattedKey,
|
|
393
|
+
},
|
|
394
|
+
tags: ["conversation", "paired"],
|
|
395
|
+
createdAt: now,
|
|
396
|
+
});
|
|
397
|
+
if (!userStoreResult.ok) {
|
|
398
|
+
deps.logger.warn({ err: userStoreResult.error.message, hint: "Check database connectivity and disk space", errorKind: "dependency" }, "Memory store failed for user message");
|
|
399
|
+
}
|
|
400
|
+
else if (deps.embeddingEnqueue) {
|
|
401
|
+
deps.embeddingEnqueue(userEntryId, pairedContent);
|
|
402
|
+
}
|
|
315
403
|
}
|
|
316
|
-
|
|
317
|
-
|
|
404
|
+
catch {
|
|
405
|
+
// Memory storage failure is non-fatal -- errors already logged per-entry
|
|
318
406
|
}
|
|
319
407
|
}
|
|
320
|
-
catch {
|
|
321
|
-
// Memory storage failure is non-fatal -- errors already logged per-entry
|
|
322
|
-
}
|
|
323
408
|
}
|
|
324
409
|
else if (deps.memoryPort && result.response && msg.text) {
|
|
325
|
-
//
|
|
326
|
-
|
|
410
|
+
// Memory not stored -- distinguish the two skip reasons for observability.
|
|
411
|
+
if (skipMemoryForOperation) {
|
|
412
|
+
deps.logger.debug({ operationType, sessionKey: formattedKey }, "Paired memory skipped: non-interactive operation type");
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
deps.logger.debug({ userLen: msg.text.trim().length, minUserChars: PAIRED_MIN_USER_CHARS, minCombinedChars: PAIRED_MIN_COMBINED_CHARS }, "Paired memory skipped: content below quality threshold");
|
|
416
|
+
}
|
|
327
417
|
}
|
|
328
418
|
// Deregister active run before dispose
|
|
329
419
|
if (deps.activeRunRegistry) {
|
|
@@ -515,6 +515,17 @@ export async function runPrompt(params) {
|
|
|
515
515
|
if (plan) {
|
|
516
516
|
executionPlanRef.current = plan;
|
|
517
517
|
deps.logger.debug({ agentId }, "SEP plan extracted (post-loop fallback)");
|
|
518
|
+
// Inline backfill: post-loop extraction means no mid-loop step tracking
|
|
519
|
+
// ran, so completedCount is stuck at 0 and the nudge cannot fire. Use
|
|
520
|
+
// the bridge's recorded tool history as a proxy for work done and mark
|
|
521
|
+
// the first N steps as "done" (N = min(toolHistoryLen, stepCount)).
|
|
522
|
+
// Tool-to-step attribution is advisory/observability only; over-counting
|
|
523
|
+
// is strictly better than the 0/N deadlock.
|
|
524
|
+
const toolHistoryLen = bridge.getResult().toolCallHistory?.length ?? 0;
|
|
525
|
+
const doneCount = Math.min(toolHistoryLen, plan.steps.length);
|
|
526
|
+
for (let i = 0; i < doneCount; i++)
|
|
527
|
+
plan.steps[i].status = "done";
|
|
528
|
+
plan.completedCount = doneCount;
|
|
518
529
|
}
|
|
519
530
|
}
|
|
520
531
|
if (sepEnabled && !executionPlanRef.current && extractedResponse && toolCallCount === 0) {
|
|
@@ -676,11 +687,12 @@ export async function runPrompt(params) {
|
|
|
676
687
|
// Estimated cache write cost for the system prompt portion.
|
|
677
688
|
// System prompt is sent as cacheable prefix; on first request it incurs cache write cost.
|
|
678
689
|
const estimatedCacheWriteTokens = Math.ceil(sysPromptChars / CHARS_PER_TOKEN_RATIO);
|
|
679
|
-
const
|
|
690
|
+
const effectiveModelId = resolvedModel?.id ?? config.model;
|
|
691
|
+
const pricing = resolveModelPricing(config.provider, effectiveModelId);
|
|
680
692
|
if (pricing.input === 0) {
|
|
681
693
|
deps.logger.warn({
|
|
682
694
|
provider: config.provider,
|
|
683
|
-
model:
|
|
695
|
+
model: effectiveModelId,
|
|
684
696
|
hint: "Model not found in pricing catalog; timeout cost estimate is $0 -- actual provider billing may differ",
|
|
685
697
|
errorKind: "config",
|
|
686
698
|
}, "Unknown model for timeout cost estimation");
|
|
@@ -699,7 +711,7 @@ export async function runPrompt(params) {
|
|
|
699
711
|
channelId: msg.channelId,
|
|
700
712
|
executionId,
|
|
701
713
|
provider: config.provider,
|
|
702
|
-
model:
|
|
714
|
+
model: effectiveModelId,
|
|
703
715
|
tokens: {
|
|
704
716
|
prompt: estimatedPromptTokens,
|
|
705
717
|
completion: 0,
|
|
@@ -717,7 +729,7 @@ export async function runPrompt(params) {
|
|
|
717
729
|
cacheWriteTokens: estimatedCacheWriteTokens,
|
|
718
730
|
sessionKey: formatSessionKey(sessionKey),
|
|
719
731
|
savedVsUncached: 0,
|
|
720
|
-
cacheEligible: getCacheProviderInfo(config.provider).cacheEligible,
|
|
732
|
+
cacheEligible: getCacheProviderInfo(config.provider, effectiveModelId).cacheEligible,
|
|
721
733
|
});
|
|
722
734
|
// Include ghost cost estimate in result for bridge accumulation
|
|
723
735
|
ghostCost = pricing.input > 0 ? {
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
import { safePath, } from "@comis/core";
|
|
30
30
|
import { createToolResultSizeBouncer, createTurnResultBudgetWrapper, createConfigResolver, createCacheTraceWriter, createApiPayloadTraceWriter, createRequestBodyInjector, createValidationErrorFormatter, } from "./stream-wrappers/index.js";
|
|
31
31
|
import { resolveToolCallingTemperature } from "./tool-deferral.js";
|
|
32
|
+
import { createStubFilterInjector } from "./stream-wrappers/stub-filter-injector.js";
|
|
32
33
|
import { computeFeatureFlagHash } from "./prompt-assembly.js";
|
|
33
|
-
import { createTtlGuard, getElapsedSinceLastResponse } from "./ttl-guard.js";
|
|
34
|
+
import { createTtlGuard, getElapsedSinceLastResponse, getLastResponseTs } from "./ttl-guard.js";
|
|
34
35
|
import { isAnthropicFamily, isGoogleFamily } from "../provider/capabilities.js";
|
|
35
36
|
import { createGeminiCacheInjector } from "./gemini-cache-injector.js";
|
|
36
37
|
import { extractAnthropicPromptState, extractGeminiPromptState } from "./cache-break-detection.js";
|
|
@@ -142,6 +143,8 @@ export function setupStreamWrappers(params) {
|
|
|
142
143
|
parentCacheRetention: executionOverrides?.spawnPacket?.cacheSafeParams?.cacheRetention,
|
|
143
144
|
getCacheFenceIndex: () => getBreakpointIndex(formattedKey) ?? -1,
|
|
144
145
|
getElapsedSinceLastResponse: () => getElapsedSinceLastResponse(formattedKey),
|
|
146
|
+
getLastResponseTs: () => getLastResponseTs(formattedKey),
|
|
147
|
+
promoteRecentZoneOnSlowCadence: config.advancedCacheOptimization?.enableRecentZonePromotion ?? true,
|
|
145
148
|
observationKeepWindow: 25,
|
|
146
149
|
microcompactTokenCeiling: 180_000,
|
|
147
150
|
onContentModification: () => cacheBreakDetector.notifyContentModification(formattedKey),
|
|
@@ -231,6 +234,19 @@ export function setupStreamWrappers(params) {
|
|
|
231
234
|
deps.logger.info({ outputDir, cacheTracePath, apiPayloadPath }, "JSONL tracing enabled");
|
|
232
235
|
}
|
|
233
236
|
}
|
|
237
|
+
// MUST be the last wrapper pushed (innermost). Runs its onPayload FIRST in
|
|
238
|
+
// the chain so all downstream wrappers operate on stub-free tools. Violating
|
|
239
|
+
// this ordering would (a) include stub schemas in the Anthropic rendered-tool
|
|
240
|
+
// cache hash, (b) persist stubs into the Gemini CachedContent entry for its
|
|
241
|
+
// whole lifetime, and (c) cause deferCount > 0 in the DEFER-TOOL block,
|
|
242
|
+
// unintentionally flipping Anthropic sessions to server-side tool_search.
|
|
243
|
+
wrappers.push(createStubFilterInjector({
|
|
244
|
+
getStubToolNames: () => {
|
|
245
|
+
if (!deferralResult?.deferredEntries.length)
|
|
246
|
+
return new Set();
|
|
247
|
+
return new Set(deferralResult.deferredEntries.map(e => e.name));
|
|
248
|
+
},
|
|
249
|
+
}, deps.logger));
|
|
234
250
|
return {
|
|
235
251
|
wrappers,
|
|
236
252
|
contextEngineRef,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
17
17
|
import { formatSessionKey, } from "@comis/core";
|
|
18
|
-
import { applyToolDeferral, buildDeferredToolsContext, extractRecentlyUsedToolNames, resolveModelTier, CORE_TOOLS } from "./tool-deferral.js";
|
|
18
|
+
import { applyToolDeferral, buildDeferredToolsContext, createDiscoverTool, createAutoDiscoveryStubs, extractRecentlyUsedToolNames, resolveModelTier, CORE_TOOLS } from "./tool-deferral.js";
|
|
19
19
|
import { getOrCreateDiscoveryTracker } from "./discovery-tracker.js";
|
|
20
20
|
import { getOrCreateTracker, DEFAULT_LIFECYCLE_CONFIG } from "./tool-lifecycle.js";
|
|
21
21
|
import { isAnthropicFamily, isGoogleFamily } from "../provider/capabilities.js";
|
|
@@ -289,10 +289,36 @@ export async function assembleTools(params) {
|
|
|
289
289
|
: undefined,
|
|
290
290
|
};
|
|
291
291
|
const deferralResult = applyToolDeferral(mergedCustomTools, contextWindow, deferralCtx, deps.logger, deps.embeddingPort, config.skills?.toolDiscovery);
|
|
292
|
+
// Rebuild discover_tools with the now-known active set so it can answer
|
|
293
|
+
// "already active" queries correctly. Post-deferral set (active +
|
|
294
|
+
// discovered) is the only factually-accurate set -- using mergedCustomTools
|
|
295
|
+
// (pre-deferral) would false-positive on currently-deferred tools and tell
|
|
296
|
+
// the agent "call directly, no discovery needed" for tools that aren't
|
|
297
|
+
// actually loaded. See design §4.2, §5.5, decision log row 4:
|
|
298
|
+
// .planning/design/discover-tools-bm25-fallback-fix.md
|
|
299
|
+
if (deferralResult.discoverTool) {
|
|
300
|
+
const activeAfterDeferral = new Set([
|
|
301
|
+
...deferralResult.activeTools.map(t => t.name),
|
|
302
|
+
...deferralResult.discoveredTools.map(t => t.name),
|
|
303
|
+
]);
|
|
304
|
+
deferralResult.discoverTool = createDiscoverTool(deferralResult.deferredEntries, deps.logger, deps.embeddingPort, config.skills?.toolDiscovery, activeAfterDeferral);
|
|
305
|
+
}
|
|
292
306
|
mergedCustomTools = [...deferralResult.activeTools, ...deferralResult.discoveredTools];
|
|
293
307
|
if (deferralResult.discoverTool) {
|
|
294
308
|
mergedCustomTools.push(deferralResult.discoverTool);
|
|
295
309
|
}
|
|
310
|
+
// 7b. Auto-discovery stubs for deferred tools.
|
|
311
|
+
// Lightweight stubs so the SDK's agent-loop finds deferred tools during
|
|
312
|
+
// tool resolution. Prevents "Tool X not found" errors when skills
|
|
313
|
+
// reference deferred MCP tools directly. Stubs are filtered from the
|
|
314
|
+
// API request by createStubFilterInjector (see stream-wrappers/
|
|
315
|
+
// stub-filter-injector.ts) -- zero token cost guarantee.
|
|
316
|
+
// Position: BEFORE JIT guide wrapping / schema pruning / sideEffects
|
|
317
|
+
// wrapping -- stubs receive all downstream wrappers automatically.
|
|
318
|
+
if (deferralResult.deferredEntries.length > 0) {
|
|
319
|
+
const stubs = createAutoDiscoveryStubs(deferralResult.deferredEntries, discoveryTracker, deps.logger);
|
|
320
|
+
mergedCustomTools.push(...stubs);
|
|
321
|
+
}
|
|
296
322
|
// Build deferred context for dynamic preamble injection
|
|
297
323
|
let deferredContext = "";
|
|
298
324
|
if (deferralResult.deferredEntries.length > 0) {
|