comisai 1.0.14 → 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/index.d.ts +1 -1
- package/node_modules/@comis/agent/dist/bootstrap/index.js +1 -1
- package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.d.ts +26 -0
- package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.js +86 -54
- package/node_modules/@comis/agent/dist/bootstrap/types.d.ts +7 -1
- package/node_modules/@comis/agent/dist/bootstrap/workspace-loader.d.ts +12 -0
- package/node_modules/@comis/agent/dist/bootstrap/workspace-loader.js +21 -0
- 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 +29 -1
- package/node_modules/@comis/agent/dist/executor/prompt-assembly.d.ts +8 -1
- package/node_modules/@comis/agent/dist/executor/prompt-assembly.js +22 -5
- 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/dist/session/comis-session-manager.js +7 -2
- 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/dist/doctor/checks/channel-health.js +2 -2
- package/node_modules/@comis/cli/dist/wizard/index.d.ts +2 -3
- package/node_modules/@comis/cli/dist/wizard/index.js +2 -3
- package/node_modules/@comis/cli/dist/wizard/steps/10-write-config.js +1 -1
- package/node_modules/@comis/cli/dist/wizard/types.d.ts +1 -2
- package/node_modules/@comis/cli/dist/wizard/types.js +1 -2
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/dist/config/schema-agent.d.ts +64 -10
- package/node_modules/@comis/core/dist/config/schema-agent.js +19 -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 +19 -11
- package/node_modules/@comis/core/dist/event-bus/events-infra.d.ts +8 -0
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/dist/daemon.js +2 -1
- package/node_modules/@comis/daemon/dist/wiring/setup-channels.js +19 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.js +10 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-memory.d.ts +2 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-memory.js +15 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-schedulers.js +1 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-shutdown.d.ts +1 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-shutdown.js +4 -0
- 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/dist/cron/cron-types.d.ts +11 -0
- package/node_modules/@comis/scheduler/dist/cron/cron-types.js +11 -0
- package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.d.ts +26 -1
- package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.js +23 -2
- package/node_modules/@comis/scheduler/dist/heartbeat/heartbeat-config.d.ts +7 -0
- package/node_modules/@comis/scheduler/dist/heartbeat/heartbeat-config.js +2 -0
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/dist/index.d.ts +1 -1
- package/node_modules/@comis/skills/dist/index.js +1 -1
- package/node_modules/@comis/skills/dist/policy/tool-policy.js +25 -0
- package/node_modules/@comis/skills/package.json +1 -1
- package/package.json +12 -12
|
@@ -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";
|
|
@@ -169,6 +169,8 @@ export async function assembleTools(params) {
|
|
|
169
169
|
resolvedModelId: resolvedModel?.id,
|
|
170
170
|
resolvedModelProvider: resolvedModel?.provider,
|
|
171
171
|
resolvedModelReasoning: resolvedModel?.reasoning,
|
|
172
|
+
// "interactive" default guards the optional overrides; mirrors pi-executor.ts:1077.
|
|
173
|
+
operationType: executionOverrides?.operationType ?? "interactive",
|
|
172
174
|
});
|
|
173
175
|
// -------------------------------------------------------------------
|
|
174
176
|
// 5. System token estimate
|
|
@@ -287,10 +289,36 @@ export async function assembleTools(params) {
|
|
|
287
289
|
: undefined,
|
|
288
290
|
};
|
|
289
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
|
+
}
|
|
290
306
|
mergedCustomTools = [...deferralResult.activeTools, ...deferralResult.discoveredTools];
|
|
291
307
|
if (deferralResult.discoverTool) {
|
|
292
308
|
mergedCustomTools.push(deferralResult.discoverTool);
|
|
293
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
|
+
}
|
|
294
322
|
// Build deferred context for dynamic preamble injection
|
|
295
323
|
let deferredContext = "";
|
|
296
324
|
if (deferralResult.deferredEntries.length > 0) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
-
import type { SessionKey, NormalizedMessage, PerAgentConfig, MemoryPort, HookRunner, SecretManager, EnvelopeConfig, WrapExternalContentOptions, TypedEventBus, SenderTrustDisplayConfig, SpawnPacket, DeliveryMirrorPort } from "@comis/core";
|
|
11
|
+
import type { SessionKey, NormalizedMessage, PerAgentConfig, MemoryPort, HookRunner, SecretManager, EnvelopeConfig, WrapExternalContentOptions, TypedEventBus, SenderTrustDisplayConfig, SpawnPacket, DeliveryMirrorPort, ModelOperationType } from "@comis/core";
|
|
12
12
|
import type { ComisLogger } from "@comis/infra";
|
|
13
13
|
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
14
14
|
import type { BootstrapContextFile } from "../bootstrap/types.js";
|
|
@@ -151,6 +151,13 @@ export interface PromptAssemblyParams {
|
|
|
151
151
|
/** Whether the resolved model has native reasoning support (e.g. encrypted thinking blocks).
|
|
152
152
|
* When true, the `<think>`/`<final>` tag hint is suppressed to avoid double-reasoning. */
|
|
153
153
|
resolvedModelReasoning?: boolean;
|
|
154
|
+
/** Operation type from ExecutionOverrides. Resolves promptMode and bootstrap filter.
|
|
155
|
+
* When omitted by callers at the TypeScript level, executor-tool-assembly supplies
|
|
156
|
+
* "interactive" as the default before invoking this function, so this is required
|
|
157
|
+
* at the call-site contract level. Values of "cron" or "heartbeat" auto-upgrade
|
|
158
|
+
* the promptMode from "full" to "operational" and dispatch operation-specific
|
|
159
|
+
* bootstrap filters. */
|
|
160
|
+
operationType: ModelOperationType;
|
|
154
161
|
}
|
|
155
162
|
/**
|
|
156
163
|
* Assemble the full system prompt for a PiExecutor execution cycle.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
13
|
import { wrapExternalContent, safePath, formatSessionKey, generateCanaryToken } from "@comis/core";
|
|
14
14
|
import { suppressError } from "@comis/shared";
|
|
15
|
-
import { loadWorkspaceBootstrapFiles, buildBootstrapContextFiles, assembleRichSystemPrompt, assembleRichSystemPromptBlocks, filterBootstrapFilesForLightContext, filterBootstrapFilesForGroupChat, resolveSenderDisplay, buildDateTimeSection, buildInboundMetadataSection, buildSenderTrustSection, buildSubagentRoleSection, // for dynamic preamble injection
|
|
15
|
+
import { loadWorkspaceBootstrapFiles, buildBootstrapContextFiles, assembleRichSystemPrompt, assembleRichSystemPromptBlocks, filterBootstrapFilesForLightContext, filterBootstrapFilesForCron, filterBootstrapFilesForGroupChat, resolveSenderDisplay, buildDateTimeSection, buildInboundMetadataSection, buildSenderTrustSection, buildSubagentRoleSection, // for dynamic preamble injection
|
|
16
16
|
buildVerbosityHintSection, resolveVerbosityProfile, } from "../bootstrap/index.js";
|
|
17
17
|
import { deduplicateResults } from "../rag/rag-retriever.js";
|
|
18
18
|
import { createHybridMemoryInjector } from "../rag/hybrid-memory-injector.js";
|
|
@@ -320,7 +320,19 @@ export async function assembleExecutionPrompt(params) {
|
|
|
320
320
|
return { systemPrompt: parentCache.frozenSystemPrompt, systemPromptBlocks: parentCache.frozenSystemPromptBlocks, dynamicPreamble, inlineMemory: undefined };
|
|
321
321
|
}
|
|
322
322
|
// 1. Resolve promptMode
|
|
323
|
-
|
|
323
|
+
// Cron and heartbeat auto-upgrade from "full" -> "operational" to trim
|
|
324
|
+
// interactive-only sections (compaction recovery, silent replies, reactions,
|
|
325
|
+
// media, SEP, sender trust). An explicit `config.bootstrap?.promptMode` wins
|
|
326
|
+
// -- operators can still force "minimal"/"none"/"full" if they have reason.
|
|
327
|
+
const baseMode = config.bootstrap?.promptMode ?? "full";
|
|
328
|
+
const promptMode = (params.operationType === "cron" || params.operationType === "heartbeat") && baseMode === "full"
|
|
329
|
+
? "operational"
|
|
330
|
+
: baseMode;
|
|
331
|
+
// Consolidated lightContext flag: heartbeat implies light-context regardless
|
|
332
|
+
// of the explicit msg.metadata.lightContext flag. Callers that only set the
|
|
333
|
+
// metadata flag OR only set operationType="heartbeat" produce identical
|
|
334
|
+
// prompt output (design-doc §Risks: "Heartbeat lightContext and operationType drift").
|
|
335
|
+
const effectiveLightContext = msg.metadata?.lightContext === true || params.operationType === "heartbeat";
|
|
324
336
|
// 2. Load workspace bootstrap files (skip for "none" mode)
|
|
325
337
|
let bootstrapContextFiles = [];
|
|
326
338
|
if (promptMode !== "none") {
|
|
@@ -337,11 +349,16 @@ export async function assembleExecutionPrompt(params) {
|
|
|
337
349
|
bootstrapFiles = await loadWorkspaceBootstrapFiles(deps.workspaceDir, bootstrapMaxChars);
|
|
338
350
|
sessionBootstrapFileSnapshots.set(bsSnapKey, bootstrapFiles);
|
|
339
351
|
}
|
|
340
|
-
//
|
|
341
|
-
|
|
352
|
+
// Bootstrap filter dispatch:
|
|
353
|
+
// - effectiveLightContext (heartbeat / explicit flag) -> HEARTBEAT.md only
|
|
354
|
+
// - operationType === "cron" -> SOUL.md + ROLE.md only
|
|
355
|
+
// - group chat context -> strip USER.md for privacy
|
|
356
|
+
if (effectiveLightContext) {
|
|
342
357
|
bootstrapFiles = filterBootstrapFilesForLightContext(bootstrapFiles);
|
|
343
358
|
}
|
|
344
|
-
|
|
359
|
+
else if (params.operationType === "cron") {
|
|
360
|
+
bootstrapFiles = filterBootstrapFilesForCron(bootstrapFiles);
|
|
361
|
+
}
|
|
345
362
|
else if (config.bootstrap?.groupChatFiltering !== false &&
|
|
346
363
|
isGroupContext(msg)) {
|
|
347
364
|
bootstrapFiles = filterBootstrapFilesForGroupChat(bootstrapFiles);
|
|
@@ -20,7 +20,7 @@ import { clearSessionTracker } from "./tool-lifecycle.js";
|
|
|
20
20
|
import { clearDiscoveryTracker } from "./discovery-tracker.js";
|
|
21
21
|
import { clearCacheBreakDetectorSession } from "./cache-break-detection.js";
|
|
22
22
|
import { clearSessionLastResponseTs } from "./ttl-guard.js";
|
|
23
|
-
import { clearSessionBetaHeaderLatches } from "./stream-wrappers/request-body-injector.js";
|
|
23
|
+
import { clearSessionBetaHeaderLatches, clearSessionPrefixStability, clearSessionCadenceTracker } from "./stream-wrappers/request-body-injector.js";
|
|
24
24
|
import { clearSessionRenderedToolCache, clearSessionPerToolCache } from "./stream-wrappers/tool-schema-cache.js";
|
|
25
25
|
import { clearSessionBlockStability } from "./block-stability-tracker.js";
|
|
26
26
|
/**
|
|
@@ -49,6 +49,8 @@ export function clearSessionState(formattedKey) {
|
|
|
49
49
|
clearSessionBlockStability(formattedKey);
|
|
50
50
|
clearSessionEvictionCooldown(formattedKey);
|
|
51
51
|
clearSessionCacheSavings(formattedKey);
|
|
52
|
+
clearSessionPrefixStability(formattedKey);
|
|
53
|
+
clearSessionCadenceTracker(formattedKey);
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
56
|
* Subscribe to `session:expired` on the provided event bus and clean up
|
|
@@ -24,3 +24,5 @@ export { createRequestBodyInjector, addCacheControlToLastBlock, CACHEABLE_BLOCK_
|
|
|
24
24
|
export type { RequestBodyInjectorConfig } from "./request-body-injector.js";
|
|
25
25
|
export { sessionRenderedToolCache, getOrCacheRenderedTool, clearSessionRenderedToolCache, clearSessionPerToolCache, } from "./tool-schema-cache.js";
|
|
26
26
|
export type { RenderedToolCacheEntry, PerToolCacheEntry } from "./tool-schema-cache.js";
|
|
27
|
+
export { createStubFilterInjector } from "./stub-filter-injector.js";
|
|
28
|
+
export type { StubFilterInjectorConfig } from "./stub-filter-injector.js";
|
|
@@ -20,3 +20,5 @@ export { createApiPayloadTraceWriter } from "./api-payload-trace-writer.js";
|
|
|
20
20
|
export { createRequestBodyInjector, addCacheControlToLastBlock, CACHEABLE_BLOCK_TYPES, getMinCacheableTokens, resolveCacheRetention, clearSessionBetaHeaderLatches, } from "./request-body-injector.js";
|
|
21
21
|
// Tool schema cache extracted to leaf module
|
|
22
22
|
export { sessionRenderedToolCache, getOrCacheRenderedTool, clearSessionRenderedToolCache, clearSessionPerToolCache, } from "./tool-schema-cache.js";
|
|
23
|
+
// Stub filter injector (auto-discovery stubs)
|
|
24
|
+
export { createStubFilterInjector } from "./stub-filter-injector.js";
|
|
@@ -105,6 +105,14 @@ export interface RequestBodyInjectorConfig {
|
|
|
105
105
|
/** Getter for elapsed ms since last assistant response.
|
|
106
106
|
* Used for time-based microcompact to detect cold-start scenarios. */
|
|
107
107
|
getElapsedSinceLastResponse?: () => number | undefined;
|
|
108
|
+
/** When true, the recent-zone message breakpoint may be promoted from
|
|
109
|
+
* "short" to "long" TTL based on observed inter-turn timing.
|
|
110
|
+
* Requires sessionKey, getElapsedSinceLastResponse, AND getLastResponseTs. */
|
|
111
|
+
promoteRecentZoneOnSlowCadence?: boolean;
|
|
112
|
+
/** Getter for the raw `sessionLastResponseTs.ts` value (ms since epoch)
|
|
113
|
+
* for this session, or undefined on cold-start. Used by the cadence
|
|
114
|
+
* tracker to detect turn boundaries within a single execute(). */
|
|
115
|
+
getLastResponseTs?: () => number | undefined;
|
|
108
116
|
/** Number of recent tool results to preserve during microcompact.
|
|
109
117
|
* Defaults to 25 (matches observation masker keep window). */
|
|
110
118
|
observationKeepWindow?: number;
|
|
@@ -141,6 +149,7 @@ export { clearSessionRenderedToolCache, getOrCacheRenderedTool, clearSessionPerT
|
|
|
141
149
|
export type { RenderedToolCacheEntry } from "./tool-schema-cache.js";
|
|
142
150
|
export declare function clearSessionPrefixStability(sessionKey: string): void;
|
|
143
151
|
export declare function clearSessionBetaHeaderLatches(sessionKey: string): void;
|
|
152
|
+
export declare function clearSessionCadenceTracker(sessionKey: string): void;
|
|
144
153
|
/**
|
|
145
154
|
* Resolve effective cache retention for a model, considering per-model overrides.
|
|
146
155
|
* Uses longest-prefix-first matching: "claude-sonnet-4-6" wins over "claude-sonnet".
|
|
@@ -43,6 +43,13 @@ export function clearSessionPrefixStability(sessionKey) {
|
|
|
43
43
|
export function clearSessionBetaHeaderLatches(sessionKey) {
|
|
44
44
|
sessionBetaHeaderLatches.delete(sessionKey);
|
|
45
45
|
}
|
|
46
|
+
const sessionCadenceTracker = new Map();
|
|
47
|
+
export function clearSessionCadenceTracker(sessionKey) {
|
|
48
|
+
sessionCadenceTracker.delete(sessionKey);
|
|
49
|
+
}
|
|
50
|
+
const SLOW_CADENCE_PROMOTION_THRESHOLD = 3;
|
|
51
|
+
const FAST_CADENCE_DEMOTION_THRESHOLD = 5;
|
|
52
|
+
const SLOW_CADENCE_MS = 5 * 60 * 1000; // 5 minutes
|
|
46
53
|
// ---------------------------------------------------------------------------
|
|
47
54
|
// Per-model cache retention override resolution.
|
|
48
55
|
// Enables selectively disabling or changing prompt caching for specific model
|
|
@@ -402,7 +409,17 @@ function placeCacheBreakpoints(messages, options) {
|
|
|
402
409
|
const startFrom = semiStableIdx >= 0 ? semiStableIdx + 1 : 0;
|
|
403
410
|
const tokensInRange = estimateTokensInRange(startFrom, secondToLastUserIdx);
|
|
404
411
|
if (tokensInRange >= minTokens) {
|
|
405
|
-
|
|
412
|
+
// Promote recent-zone to "long" when cadence indicates user pauses exceed 5m.
|
|
413
|
+
// Monotonicity guard: recent zone can only be promoted when
|
|
414
|
+
// resolvedRetention (tool/system) is already "long".
|
|
415
|
+
let recentRetention = retention;
|
|
416
|
+
if (options.promoteRecentZoneOnSlowCadence && options.sessionKey) {
|
|
417
|
+
const cadence = sessionCadenceTracker.get(options.sessionKey);
|
|
418
|
+
if (cadence?.promoted && resolvedRetention === "long") {
|
|
419
|
+
recentRetention = "long";
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
addCacheControlToLastBlock(messages[secondToLastUserIdx], recentRetention);
|
|
406
423
|
placed++;
|
|
407
424
|
}
|
|
408
425
|
}
|
|
@@ -434,7 +451,7 @@ function placeCacheBreakpoints(messages, options) {
|
|
|
434
451
|
// The Anthropic API uses a 20-block lookback window for cache prefix matching.
|
|
435
452
|
// If any gap exceeds the window and slots remain, place a bridging breakpoint
|
|
436
453
|
// at the midpoint of the gap to prevent silent cache misses.
|
|
437
|
-
if (placed > 0 && placed <
|
|
454
|
+
if (placed > 0 && placed < maxBreakpoints) {
|
|
438
455
|
const breakpointPositions = [];
|
|
439
456
|
for (let i = 0; i < messages.length; i++) {
|
|
440
457
|
const content = messages[i].content;
|
|
@@ -448,7 +465,7 @@ function placeCacheBreakpoints(messages, options) {
|
|
|
448
465
|
}
|
|
449
466
|
}
|
|
450
467
|
// Check gaps between consecutive breakpoints
|
|
451
|
-
for (let g = 1; g < breakpointPositions.length && placed <
|
|
468
|
+
for (let g = 1; g < breakpointPositions.length && placed < maxBreakpoints; g++) {
|
|
452
469
|
const gap = breakpointPositions[g] - breakpointPositions[g - 1];
|
|
453
470
|
if (gap > CACHE_LOOKBACK_WINDOW) {
|
|
454
471
|
// Find a user message near the midpoint of the gap
|
|
@@ -1338,6 +1355,8 @@ export function createRequestBodyInjector(config, logger) {
|
|
|
1338
1355
|
resolvedRetention: inCooldown ? "short" : messageRetention, // Force "short" during cooldown
|
|
1339
1356
|
strategy: resolveBreakpointStrategy(config.cacheBreakpointStrategy, model.provider),
|
|
1340
1357
|
skipCacheWrite: effectiveSkipCacheWrite,
|
|
1358
|
+
promoteRecentZoneOnSlowCadence: config.promoteRecentZoneOnSlowCadence,
|
|
1359
|
+
sessionKey: config.sessionKey,
|
|
1341
1360
|
});
|
|
1342
1361
|
if (placed > 0) {
|
|
1343
1362
|
logger.debug({
|
|
@@ -1478,6 +1497,49 @@ export function createRequestBodyInjector(config, logger) {
|
|
|
1478
1497
|
if (config.onPayloadForCacheDetection) {
|
|
1479
1498
|
config.onPayloadForCacheDetection(result, model, mergedHeaders);
|
|
1480
1499
|
}
|
|
1500
|
+
// Track cadence for recent-zone promotion (symmetric: promote slow, demote on fast).
|
|
1501
|
+
// Runs after onPayloadForCacheDetection so the detection snapshot reflects the
|
|
1502
|
+
// pre-mutation state. Mutation takes effect on the next turn's placeCacheBreakpoints().
|
|
1503
|
+
if (config.promoteRecentZoneOnSlowCadence && config.sessionKey
|
|
1504
|
+
&& config.getElapsedSinceLastResponse && config.getLastResponseTs) {
|
|
1505
|
+
const lastResponseTs = config.getLastResponseTs();
|
|
1506
|
+
if (lastResponseTs !== undefined) {
|
|
1507
|
+
let tracker = sessionCadenceTracker.get(config.sessionKey);
|
|
1508
|
+
if (!tracker) {
|
|
1509
|
+
tracker = {
|
|
1510
|
+
consecutiveSlowTurns: 0,
|
|
1511
|
+
consecutiveFastTurns: 0,
|
|
1512
|
+
promoted: false,
|
|
1513
|
+
lastObservedResponseTs: undefined,
|
|
1514
|
+
};
|
|
1515
|
+
sessionCadenceTracker.set(config.sessionKey, tracker);
|
|
1516
|
+
}
|
|
1517
|
+
// Same-turn guard: successive onPayload calls inside one execute() all
|
|
1518
|
+
// observe the same lastResponseTs. Only count once per turn boundary.
|
|
1519
|
+
if (lastResponseTs !== tracker.lastObservedResponseTs) {
|
|
1520
|
+
tracker.lastObservedResponseTs = lastResponseTs;
|
|
1521
|
+
const elapsed = config.getElapsedSinceLastResponse();
|
|
1522
|
+
if (elapsed !== undefined) {
|
|
1523
|
+
if (elapsed > SLOW_CADENCE_MS) {
|
|
1524
|
+
tracker.consecutiveSlowTurns++;
|
|
1525
|
+
tracker.consecutiveFastTurns = 0;
|
|
1526
|
+
if (!tracker.promoted && tracker.consecutiveSlowTurns >= SLOW_CADENCE_PROMOTION_THRESHOLD) {
|
|
1527
|
+
tracker.promoted = true;
|
|
1528
|
+
logger.info({ sessionKey: config.sessionKey, consecutiveSlowTurns: tracker.consecutiveSlowTurns }, "Recent-zone TTL promoted to long: slow cadence detected");
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
tracker.consecutiveFastTurns++;
|
|
1533
|
+
tracker.consecutiveSlowTurns = 0;
|
|
1534
|
+
if (tracker.promoted && tracker.consecutiveFastTurns >= FAST_CADENCE_DEMOTION_THRESHOLD) {
|
|
1535
|
+
tracker.promoted = false;
|
|
1536
|
+
logger.info({ sessionKey: config.sessionKey, consecutiveFastTurns: tracker.consecutiveFastTurns }, "Recent-zone TTL demoted to short: fast cadence resumed");
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1481
1543
|
// SDK-UPGRADE: Upgrade SDK auto-placed 5m markers to 1h when retention is long.
|
|
1482
1544
|
// The pi-ai SDK places cache_control: { type: "ephemeral" } (5m TTL) on the last
|
|
1483
1545
|
// user message. When the session uses "long" retention, these 5m writes waste money
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stub-filter injector stream wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Removes auto-discovery stub tools (see tool-deferral.ts createAutoDiscoveryStubs)
|
|
5
|
+
* from the API-ready payload. Stubs are present in the SDK's tools array so
|
|
6
|
+
* agent-loop.js tool lookup succeeds, but must not reach the API:
|
|
7
|
+
* - They consume input tokens (~100 tokens per stub schema).
|
|
8
|
+
* - For Anthropic models where supportsToolSearch() is true, unfiltered stub
|
|
9
|
+
* names would match in the DEFER-TOOL block (request-body-injector.ts
|
|
10
|
+
* lines 1599-1643), causing deferCount > 0 which REMOVES client-side
|
|
11
|
+
* discover_tools and APPENDS server-side tool_search_tool_regex —
|
|
12
|
+
* flipping the session to a different control path unintentionally.
|
|
13
|
+
* - For Google AI Studio, unfiltered stubs would be persisted into the
|
|
14
|
+
* Gemini CachedContent entry (gemini-cache-injector.ts line 191-195),
|
|
15
|
+
* bloating the cache for its entire lifetime.
|
|
16
|
+
*
|
|
17
|
+
* Provider-agnostic: filters both top-level `params.tools` (Anthropic,
|
|
18
|
+
* OpenAI, xAI) and nested `params.config.tools` (Google AI Studio).
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import type { ComisLogger } from "@comis/infra";
|
|
23
|
+
import type { StreamFnWrapper } from "./types.js";
|
|
24
|
+
export interface StubFilterInjectorConfig {
|
|
25
|
+
/** Getter for stub tool names. Filtered from the rendered API payload. */
|
|
26
|
+
getStubToolNames: () => ReadonlySet<string>;
|
|
27
|
+
}
|
|
28
|
+
export declare function createStubFilterInjector(config: StubFilterInjectorConfig, logger: ComisLogger): StreamFnWrapper;
|