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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Stub-filter injector stream wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Removes auto-discovery stub tools (see tool-deferral.ts createAutoDiscoveryStubs)
|
|
6
|
+
* from the API-ready payload. Stubs are present in the SDK's tools array so
|
|
7
|
+
* agent-loop.js tool lookup succeeds, but must not reach the API:
|
|
8
|
+
* - They consume input tokens (~100 tokens per stub schema).
|
|
9
|
+
* - For Anthropic models where supportsToolSearch() is true, unfiltered stub
|
|
10
|
+
* names would match in the DEFER-TOOL block (request-body-injector.ts
|
|
11
|
+
* lines 1599-1643), causing deferCount > 0 which REMOVES client-side
|
|
12
|
+
* discover_tools and APPENDS server-side tool_search_tool_regex —
|
|
13
|
+
* flipping the session to a different control path unintentionally.
|
|
14
|
+
* - For Google AI Studio, unfiltered stubs would be persisted into the
|
|
15
|
+
* Gemini CachedContent entry (gemini-cache-injector.ts line 191-195),
|
|
16
|
+
* bloating the cache for its entire lifetime.
|
|
17
|
+
*
|
|
18
|
+
* Provider-agnostic: filters both top-level `params.tools` (Anthropic,
|
|
19
|
+
* OpenAI, xAI) and nested `params.config.tools` (Google AI Studio).
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
export function createStubFilterInjector(config, logger) {
|
|
24
|
+
return function stubFilterInjector(next) {
|
|
25
|
+
return (model, context, options) => {
|
|
26
|
+
const existingOnPayload = options?.onPayload;
|
|
27
|
+
const enhancedOptions = {
|
|
28
|
+
...options,
|
|
29
|
+
onPayload: async (payload, payloadModel) => {
|
|
30
|
+
// Let any upstream onPayload run first (we are the innermost wrapper,
|
|
31
|
+
// so upstream here is the SDK's own or a user-provided one).
|
|
32
|
+
const resolvedParams = (existingOnPayload
|
|
33
|
+
? await Promise.resolve(existingOnPayload(payload, payloadModel))
|
|
34
|
+
: payload);
|
|
35
|
+
const stubNames = config.getStubToolNames();
|
|
36
|
+
if (stubNames.size === 0)
|
|
37
|
+
return resolvedParams;
|
|
38
|
+
let removed = 0;
|
|
39
|
+
// Top-level tools (Anthropic, OpenAI, xAI)
|
|
40
|
+
if (Array.isArray(resolvedParams.tools)) {
|
|
41
|
+
const before = resolvedParams.tools.length;
|
|
42
|
+
resolvedParams.tools = resolvedParams.tools
|
|
43
|
+
.filter(t => !stubNames.has(t.name));
|
|
44
|
+
removed += before - resolvedParams.tools.length;
|
|
45
|
+
}
|
|
46
|
+
// Nested config.tools (Google AI Studio / Gemini)
|
|
47
|
+
const cfg = resolvedParams.config;
|
|
48
|
+
if (cfg && Array.isArray(cfg.tools)) {
|
|
49
|
+
const before = cfg.tools.length;
|
|
50
|
+
cfg.tools = cfg.tools
|
|
51
|
+
.filter(t => !stubNames.has(t.name));
|
|
52
|
+
removed += before - cfg.tools.length;
|
|
53
|
+
}
|
|
54
|
+
if (removed > 0) {
|
|
55
|
+
logger.debug({ removed, provider: payloadModel?.provider }, "Stub filter removed auto-discovery stubs from payload");
|
|
56
|
+
}
|
|
57
|
+
return resolvedParams;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return next(model, context, enhancedOptions);
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -50,6 +50,11 @@ export interface DeferralContext {
|
|
|
50
50
|
* "default") do not inject mid-turn, so MCP tools must be active from the
|
|
51
51
|
* start. When undefined, defaults to deferring (backward compat). */
|
|
52
52
|
providerFamily?: string;
|
|
53
|
+
/** Names of tools currently ACTIVE in this session (post-deferral).
|
|
54
|
+
* Consumed by discover_tools to return "already active" guidance when
|
|
55
|
+
* queries re-ask for loaded MCPs. Must NOT include names that were
|
|
56
|
+
* deferred -- pass the post-deferral set, not mergedCustomTools. */
|
|
57
|
+
activeToolNames?: ReadonlySet<string>;
|
|
53
58
|
}
|
|
54
59
|
/** Entry describing a deferred tool with its display description and original definition. */
|
|
55
60
|
export interface DeferredToolEntry {
|
|
@@ -153,7 +158,19 @@ export interface ToolDiscoveryScoreConfig {
|
|
|
153
158
|
* Receives DeferredToolEntry[] (with original schemas and display descriptions)
|
|
154
159
|
* so it can serve full schemas and lean descriptions when queried.
|
|
155
160
|
*
|
|
161
|
+
* BM25 scores are normalized to [0, 1] (fraction of top match) BEFORE the
|
|
162
|
+
* score-floor filter applies, matching the semantics of hybrid-mode scoring.
|
|
163
|
+
* This ensures the top match always clears any floor <= 1.0 whenever any
|
|
164
|
+
* positive signal exists (fixes the srv1593437 08:06:39Z "install MCP"
|
|
165
|
+
* regression where raw BM25 ~0.74 was dropped by the 0.8 raw-score floor).
|
|
166
|
+
*
|
|
156
167
|
* @param scoreConfig Optional score-floor override (defaults to 0.8 BM25 /
|
|
157
168
|
* 0.35 hybrid). Zero or negative floors disable the filter.
|
|
169
|
+
* @param activeToolNames Names of tools currently ACTIVE in this session
|
|
170
|
+
* (post-deferral: active + discovered). Used to return "already active"
|
|
171
|
+
* guidance when queries re-ask for loaded MCPs. Must NOT include names
|
|
172
|
+
* that were deferred. Default empty set keeps callers backward-compatible.
|
|
158
173
|
*/
|
|
159
|
-
export declare function createDiscoverTool(deferredEntries: DeferredToolEntry[], logger: ComisLogger, embeddingPort?: EmbeddingPort, scoreConfig?: ToolDiscoveryScoreConfig): ToolDefinition;
|
|
174
|
+
export declare function createDiscoverTool(deferredEntries: DeferredToolEntry[], logger: ComisLogger, embeddingPort?: EmbeddingPort, scoreConfig?: ToolDiscoveryScoreConfig, activeToolNames?: ReadonlySet<string>): ToolDefinition;
|
|
175
|
+
export declare const DEFERRAL_STUB_MARKER: "__comis_deferral_stub__";
|
|
176
|
+
export declare function createAutoDiscoveryStubs(deferredEntries: DeferredToolEntry[], discoveryTracker: DiscoveryTracker, logger: ComisLogger): ToolDefinition[];
|
|
@@ -302,9 +302,14 @@ export function applyToolDeferral(tools, _contextWindow, deferralContext, logger
|
|
|
302
302
|
remainingDeferred.push(entry);
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
// Create discover_tools only when remaining deferred entries exist
|
|
305
|
+
// Create discover_tools only when remaining deferred entries exist.
|
|
306
|
+
// Thread active-tool names through so "already active" guidance works.
|
|
307
|
+
// NOTE: executor-tool-assembly.ts rebuilds this tool a second time with the
|
|
308
|
+
// post-deferral active set (active + discovered) since the final active set
|
|
309
|
+
// isn't known until after this function returns.
|
|
310
|
+
const activeNamesForDiscover = deferralContext.activeToolNames ?? new Set();
|
|
306
311
|
const discoverTool = remainingDeferred.length > 0
|
|
307
|
-
? createDiscoverTool(remainingDeferred, logger, embeddingPort, scoreConfig)
|
|
312
|
+
? createDiscoverTool(remainingDeferred, logger, embeddingPort, scoreConfig, activeNamesForDiscover)
|
|
308
313
|
: null;
|
|
309
314
|
const deferredNames = [...deferredSet];
|
|
310
315
|
// Log deferral
|
|
@@ -400,10 +405,20 @@ const DEFAULT_TOOL_DISCOVERY_SCORES = {
|
|
|
400
405
|
* Receives DeferredToolEntry[] (with original schemas and display descriptions)
|
|
401
406
|
* so it can serve full schemas and lean descriptions when queried.
|
|
402
407
|
*
|
|
408
|
+
* BM25 scores are normalized to [0, 1] (fraction of top match) BEFORE the
|
|
409
|
+
* score-floor filter applies, matching the semantics of hybrid-mode scoring.
|
|
410
|
+
* This ensures the top match always clears any floor <= 1.0 whenever any
|
|
411
|
+
* positive signal exists (fixes the srv1593437 08:06:39Z "install MCP"
|
|
412
|
+
* regression where raw BM25 ~0.74 was dropped by the 0.8 raw-score floor).
|
|
413
|
+
*
|
|
403
414
|
* @param scoreConfig Optional score-floor override (defaults to 0.8 BM25 /
|
|
404
415
|
* 0.35 hybrid). Zero or negative floors disable the filter.
|
|
416
|
+
* @param activeToolNames Names of tools currently ACTIVE in this session
|
|
417
|
+
* (post-deferral: active + discovered). Used to return "already active"
|
|
418
|
+
* guidance when queries re-ask for loaded MCPs. Must NOT include names
|
|
419
|
+
* that were deferred. Default empty set keeps callers backward-compatible.
|
|
405
420
|
*/
|
|
406
|
-
export function createDiscoverTool(deferredEntries, logger, embeddingPort, scoreConfig = DEFAULT_TOOL_DISCOVERY_SCORES) {
|
|
421
|
+
export function createDiscoverTool(deferredEntries, logger, embeddingPort, scoreConfig = DEFAULT_TOOL_DISCOVERY_SCORES, activeToolNames = new Set()) {
|
|
407
422
|
// Build ToolDefinition[] view for structuredSearch compatibility
|
|
408
423
|
const deferredTools = deferredEntries.map(e => e.original);
|
|
409
424
|
/**
|
|
@@ -436,14 +451,10 @@ export function createDiscoverTool(deferredEntries, logger, embeddingPort, score
|
|
|
436
451
|
async execute(_toolCallId, params) {
|
|
437
452
|
const p = params;
|
|
438
453
|
const query = String(p.query ?? "");
|
|
439
|
-
//
|
|
454
|
+
// ---------- Path 1: structured deterministic modes ----------
|
|
440
455
|
const structuredResults = structuredSearch(deferredTools, query, 10);
|
|
441
|
-
let matches;
|
|
442
|
-
let searchMode;
|
|
443
456
|
if (structuredResults.length > 0) {
|
|
444
|
-
|
|
445
|
-
matches = structuredResults;
|
|
446
|
-
searchMode = query.toLowerCase().trim().startsWith("select:")
|
|
457
|
+
const searchMode = query.toLowerCase().trim().startsWith("select:")
|
|
447
458
|
? "select"
|
|
448
459
|
: query.toLowerCase().trim().startsWith("mcp__") || query.toLowerCase().trim().startsWith("mcp:")
|
|
449
460
|
? "prefix"
|
|
@@ -453,151 +464,248 @@ export function createDiscoverTool(deferredEntries, logger, embeddingPort, score
|
|
|
453
464
|
query,
|
|
454
465
|
searchMode,
|
|
455
466
|
candidateCount: deferredEntries.length,
|
|
456
|
-
structuredMatchCount:
|
|
457
|
-
topMatch:
|
|
467
|
+
structuredMatchCount: structuredResults.length,
|
|
468
|
+
topMatch: structuredResults[0]?.name ?? "none",
|
|
458
469
|
}, "discover_tools search completed");
|
|
470
|
+
return formatDiscoveryResponse(structuredResults, deferredEntries);
|
|
459
471
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
472
|
+
// ---------- Path 2: BM25 (+ optional hybrid) fallback ----------
|
|
473
|
+
const documents = deferredTools.map(t => ({
|
|
474
|
+
name: t.name,
|
|
475
|
+
text: resolveBM25Text(t),
|
|
476
|
+
}));
|
|
477
|
+
const rankedRaw = bm25Score(query, documents);
|
|
478
|
+
const rawTopScore = rankedRaw[0]?.score ?? 0;
|
|
479
|
+
// NORMALIZE UP FRONT -- both modes now operate in [0, 1] space.
|
|
480
|
+
// This makes `minBm25Score` semantically equivalent to `minHybridScore`:
|
|
481
|
+
// a fraction of the top match. After this step, ranked[0].score === 1.0
|
|
482
|
+
// whenever rawTopScore > 0, so the top match always clears any floor <= 1.0.
|
|
483
|
+
let ranked = rawTopScore > 0
|
|
484
|
+
? rankedRaw.map(r => ({ name: r.name, score: r.score / rawTopScore }))
|
|
485
|
+
: rankedRaw.map(r => ({ name: r.name, score: 0 }));
|
|
486
|
+
// Optional: semantic re-ranking via EmbeddingPort
|
|
487
|
+
let embeddingUsed = false;
|
|
488
|
+
if (embeddingPort && ranked.length > 0) {
|
|
489
|
+
try {
|
|
490
|
+
const queryResult = await embeddingPort.embed(query);
|
|
491
|
+
if (queryResult.ok) {
|
|
492
|
+
const queryVec = queryResult.value;
|
|
493
|
+
const textsToEmbed = ranked.map(r => {
|
|
494
|
+
const doc = documents.find(d => d.name === r.name);
|
|
495
|
+
return doc?.text ?? r.name;
|
|
496
|
+
});
|
|
497
|
+
const batchResult = await embeddingPort.embedBatch(textsToEmbed);
|
|
498
|
+
if (batchResult.ok) {
|
|
499
|
+
const docVecs = batchResult.value;
|
|
500
|
+
// `ranked` is already BM25-normalized; combine with cosine.
|
|
501
|
+
// NO second normalization -- that would double-normalize and
|
|
502
|
+
// change the scoring contract.
|
|
503
|
+
const combined = ranked.map((r, i) => ({
|
|
504
|
+
name: r.name,
|
|
505
|
+
score: 0.5 * r.score + 0.5 * cosine(queryVec, docVecs[i]),
|
|
506
|
+
}));
|
|
507
|
+
combined.sort((a, b) => b.score - a.score);
|
|
508
|
+
ranked = combined;
|
|
509
|
+
embeddingUsed = true;
|
|
491
510
|
}
|
|
492
511
|
}
|
|
493
|
-
catch (embeddingErr) {
|
|
494
|
-
logger.warn({
|
|
495
|
-
err: embeddingErr,
|
|
496
|
-
hint: "discover_tools falling back to BM25-only search; check embedding provider",
|
|
497
|
-
errorKind: "dependency",
|
|
498
|
-
}, "discover_tools embedding re-ranking failed");
|
|
499
|
-
}
|
|
500
512
|
}
|
|
501
|
-
|
|
502
|
-
// matches (token overlap on common words) and cosine-noise in hybrid
|
|
503
|
-
// mode. Filter below the floor before slicing so "no matches" is
|
|
504
|
-
// reported honestly instead of surfacing misleading top-10 noise.
|
|
505
|
-
const floor = embeddingUsed ? scoreConfig.minHybridScore : scoreConfig.minBm25Score;
|
|
506
|
-
const rawTop = ranked[0];
|
|
507
|
-
const filtered = ranked.filter(r => r.score >= floor);
|
|
508
|
-
const topResults = filtered.slice(0, 10);
|
|
509
|
-
searchMode = embeddingUsed ? "hybrid" : "bm25";
|
|
510
|
-
logger.debug({
|
|
511
|
-
toolName: "discover_tools",
|
|
512
|
-
query,
|
|
513
|
-
searchMode,
|
|
514
|
-
candidateCount: deferredEntries.length,
|
|
515
|
-
resultCount: topResults.length,
|
|
516
|
-
topScore: topResults[0]?.score ?? 0,
|
|
517
|
-
topMatch: topResults[0]?.name ?? "none",
|
|
518
|
-
floor,
|
|
519
|
-
filteredOut: ranked.length - filtered.length,
|
|
520
|
-
}, "discover_tools search completed");
|
|
521
|
-
if (topResults.length === 0) {
|
|
513
|
+
catch (embeddingErr) {
|
|
522
514
|
logger.warn({
|
|
515
|
+
err: embeddingErr,
|
|
516
|
+
hint: "discover_tools falling back to BM25-only search; check embedding provider health",
|
|
517
|
+
errorKind: "dependency",
|
|
518
|
+
}, "discover_tools embedding re-ranking failed");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// ---------- Floor check ----------
|
|
522
|
+
const floor = embeddingUsed ? scoreConfig.minHybridScore : scoreConfig.minBm25Score;
|
|
523
|
+
const normalizedTopScore = ranked[0]?.score ?? 0;
|
|
524
|
+
const filtered = ranked.filter(r => r.score >= floor);
|
|
525
|
+
const topResults = filtered.slice(0, 10);
|
|
526
|
+
const searchMode = embeddingUsed ? "hybrid" : "bm25";
|
|
527
|
+
logger.debug({
|
|
528
|
+
toolName: "discover_tools",
|
|
529
|
+
query,
|
|
530
|
+
searchMode,
|
|
531
|
+
candidateCount: deferredEntries.length,
|
|
532
|
+
resultCount: topResults.length,
|
|
533
|
+
normalizedTopScore,
|
|
534
|
+
rawTopScore,
|
|
535
|
+
topMatch: topResults[0]?.name ?? "none",
|
|
536
|
+
floor,
|
|
537
|
+
filteredOut: ranked.length - filtered.length,
|
|
538
|
+
}, "discover_tools search completed");
|
|
539
|
+
// ---------- No BM25 match -- check active tools before giving up ----------
|
|
540
|
+
if (topResults.length === 0) {
|
|
541
|
+
const activeMatches = findActiveToolMatches(query, activeToolNames);
|
|
542
|
+
if (activeMatches.length > 0) {
|
|
543
|
+
logger.info({
|
|
544
|
+
toolName: "discover_tools",
|
|
523
545
|
query,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
topCandidate: rawTop?.name ?? "none",
|
|
528
|
-
filteredOut: ranked.length - filtered.length,
|
|
529
|
-
hint: "No tool scored above the discover_tools floor; lower skills.toolDiscovery.minBm25Score / minHybridScore if this query should have matched, or retry with an exact tool name or 'select:...' syntax",
|
|
530
|
-
errorKind: "validation",
|
|
531
|
-
}, "discover_tools: no good matches found");
|
|
546
|
+
activeMatchCount: activeMatches.length,
|
|
547
|
+
topActiveMatch: activeMatches[0],
|
|
548
|
+
}, "discover_tools: query matches already-active tools");
|
|
532
549
|
return {
|
|
533
550
|
content: [{
|
|
534
551
|
type: "text",
|
|
535
|
-
text:
|
|
552
|
+
text: `Tool(s) already active -- call directly, no discovery needed:\n${activeMatches.slice(0, 20).map(n => ` - ${n}`).join("\n")}${activeMatches.length > 20 ? `\n ... (${activeMatches.length} total)` : ""}`,
|
|
536
553
|
}],
|
|
537
554
|
isError: false,
|
|
538
555
|
details: undefined,
|
|
539
556
|
sideEffects: { discoveredTools: [] },
|
|
540
557
|
};
|
|
541
558
|
}
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
559
|
+
// Distinguish "corpus has signal but filtered" vs "query terms absent from corpus".
|
|
560
|
+
// After normalization, the former is only reachable in hybrid mode with adversarial
|
|
561
|
+
// cosine (combined < floor). In BM25-only mode it's unreachable because the top
|
|
562
|
+
// match always normalizes to 1.0 >= any floor <= 1.0.
|
|
563
|
+
const warnMsg = rawTopScore > 0
|
|
564
|
+
? "discover_tools: no matches above floor"
|
|
565
|
+
: "discover_tools: query tokens absent from deferred corpus";
|
|
566
|
+
logger.warn({
|
|
567
|
+
query,
|
|
568
|
+
searchMode,
|
|
569
|
+
floor,
|
|
570
|
+
rawTopScore,
|
|
571
|
+
normalizedTopScore,
|
|
572
|
+
topCandidate: ranked[0]?.name ?? "none",
|
|
573
|
+
filteredOut: ranked.length - filtered.length,
|
|
574
|
+
activeCorpusSize: activeToolNames.size,
|
|
575
|
+
hint: rawTopScore > 0
|
|
576
|
+
? "No tool scored above the discover_tools floor. Lower skills.toolDiscovery.minHybridScore, retry with an exact tool name, or use 'select:<name>' syntax."
|
|
577
|
+
: "Query tokens do not appear in any deferred tool description. Use 'select:<name>' for exact match, or reconsider whether the tool you want is already active.",
|
|
578
|
+
errorKind: "validation",
|
|
579
|
+
}, warnMsg);
|
|
580
|
+
return {
|
|
581
|
+
content: [{
|
|
582
|
+
type: "text",
|
|
583
|
+
text: "No matching tools found. Try an exact tool name, MCP server name (e.g. 'yfinance'), or select:tool1,tool2 syntax.",
|
|
584
|
+
}],
|
|
585
|
+
isError: false,
|
|
586
|
+
details: undefined,
|
|
587
|
+
sideEffects: { discoveredTools: [] },
|
|
588
|
+
};
|
|
554
589
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
590
|
+
// ---------- Resolve matches, expand, format ----------
|
|
591
|
+
const matches = topResults
|
|
592
|
+
.map(r => deferredTools.find(t => t.name === r.name))
|
|
593
|
+
.filter((t) => t !== undefined);
|
|
594
|
+
return formatDiscoveryResponse(matches, deferredEntries);
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// Helpers: active-tool match + output formatting
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
/**
|
|
602
|
+
* Check whether `query` refers to any already-active tool.
|
|
603
|
+
* Used to return "already active" guidance instead of "no matches" when
|
|
604
|
+
* the agent re-discovers a previously-installed MCP or active builtin.
|
|
605
|
+
*
|
|
606
|
+
* Match modes (checked in order, first non-empty wins):
|
|
607
|
+
* 1. Exact name match (case-insensitive) against the full query.
|
|
608
|
+
* 2. `mcp__` / `mcp:` prefix match against the full query.
|
|
609
|
+
* 3. Bare server-name match on full query (`"yfinance"` -> all `mcp__yfinance--*`).
|
|
610
|
+
* 4. Per-token server-name fallback: for multi-word queries like
|
|
611
|
+
* `"yfinance get_stock"`, check each whitespace-separated token as a
|
|
612
|
+
* potential MCP server name. Catches the srv1593437 08:06:39Z scenario
|
|
613
|
+
* where the agent emits `{query: "yfinance get_stock"}` rather than just
|
|
614
|
+
* `{query: "yfinance"}`.
|
|
615
|
+
*/
|
|
616
|
+
function findActiveToolMatches(query, activeToolNames) {
|
|
617
|
+
const q = query.toLowerCase().trim();
|
|
618
|
+
if (!q || activeToolNames.size === 0)
|
|
619
|
+
return [];
|
|
620
|
+
const names = [...activeToolNames];
|
|
621
|
+
const lowerMap = new Map(names.map(n => [n.toLowerCase(), n]));
|
|
622
|
+
// Mode 1: exact match
|
|
623
|
+
const exact = lowerMap.get(q);
|
|
624
|
+
if (exact)
|
|
625
|
+
return [exact];
|
|
626
|
+
// Mode 2: prefix match (mcp__ or mcp:)
|
|
627
|
+
if ((q.startsWith("mcp__") || q.startsWith("mcp:")) && q.length > 5) {
|
|
628
|
+
const prefix = names.filter(n => n.toLowerCase().startsWith(q));
|
|
629
|
+
if (prefix.length > 0)
|
|
630
|
+
return prefix;
|
|
631
|
+
}
|
|
632
|
+
// Mode 3: bare server name -> mcp__<server>--*
|
|
633
|
+
const serverPrefix = `mcp__${q}--`;
|
|
634
|
+
const server = names.filter(n => n.toLowerCase().startsWith(serverPrefix));
|
|
635
|
+
if (server.length > 0)
|
|
636
|
+
return server;
|
|
637
|
+
// Mode 4: per-token server fallback for multi-word queries.
|
|
638
|
+
// Each whitespace-separated token is probed as a server name. The first
|
|
639
|
+
// token that resolves to >= 1 active tool wins. This handles the common
|
|
640
|
+
// "{server} {verb}" pattern like "yfinance get_stock".
|
|
641
|
+
const tokens = q.split(/\s+/).filter(t => /^[a-z0-9_-]+$/.test(t));
|
|
642
|
+
for (const token of tokens) {
|
|
643
|
+
const tokenServerPrefix = `mcp__${token}--`;
|
|
644
|
+
const tokenMatches = names.filter(n => n.toLowerCase().startsWith(tokenServerPrefix));
|
|
645
|
+
if (tokenMatches.length > 0)
|
|
646
|
+
return tokenMatches;
|
|
647
|
+
}
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Format matched tool definitions as a `<functions>` block with full JSON schemas,
|
|
652
|
+
* applying server-expansion and co-discovery.
|
|
653
|
+
*
|
|
654
|
+
* Extracted from the inline block in `createDiscoverTool.execute()` so the two
|
|
655
|
+
* return paths (structured + BM25) share one formatter.
|
|
656
|
+
*/
|
|
657
|
+
function formatDiscoveryResponse(matches, deferredEntries) {
|
|
658
|
+
const discoveredNames = matches.map(m => m.name);
|
|
659
|
+
// Server-level activation: expand to all tools from same MCP server(s)
|
|
660
|
+
const serverNames = new Set();
|
|
661
|
+
for (const name of discoveredNames) {
|
|
662
|
+
const server = extractMcpServerName(name);
|
|
663
|
+
if (server)
|
|
664
|
+
serverNames.add(server);
|
|
665
|
+
}
|
|
666
|
+
if (serverNames.size > 0) {
|
|
667
|
+
for (const entry of deferredEntries) {
|
|
668
|
+
const server = extractMcpServerName(entry.name);
|
|
669
|
+
if (server && serverNames.has(server) && !discoveredNames.includes(entry.name)) {
|
|
670
|
+
discoveredNames.push(entry.name);
|
|
562
671
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Co-discovery: expand to related tools via ComisToolMetadata.coDiscoverWith
|
|
675
|
+
const coDiscoveryNames = [];
|
|
676
|
+
for (const name of discoveredNames) {
|
|
677
|
+
const meta = getToolMetadata(name);
|
|
678
|
+
if (meta?.coDiscoverWith) {
|
|
679
|
+
for (const coName of meta.coDiscoverWith) {
|
|
680
|
+
if (!discoveredNames.includes(coName) && !coDiscoveryNames.includes(coName)) {
|
|
681
|
+
// Only add if the tool exists in the deferred set
|
|
682
|
+
if (deferredEntries.some(e => e.name === coName)) {
|
|
683
|
+
coDiscoveryNames.push(coName);
|
|
575
684
|
}
|
|
576
685
|
}
|
|
577
686
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
},
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
discoveredNames.push(...coDiscoveryNames);
|
|
690
|
+
// Add co-discovered tool schemas to the display output
|
|
691
|
+
const expandedMatches = [...matches];
|
|
692
|
+
for (const coName of coDiscoveryNames) {
|
|
693
|
+
const coEntry = deferredEntries.find(e => e.name === coName);
|
|
694
|
+
if (coEntry && !expandedMatches.some(m => m.name === coName)) {
|
|
695
|
+
expandedMatches.push(coEntry.original);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Format output as <functions> block with full JSON schemas (after all expansions)
|
|
699
|
+
const functionsBlock = expandedMatches.map(m => `<function>${JSON.stringify({
|
|
700
|
+
name: m.name,
|
|
701
|
+
description: resolveToolDescription(m),
|
|
702
|
+
parameters: m.parameters,
|
|
703
|
+
})}</function>`).join("\n");
|
|
704
|
+
return {
|
|
705
|
+
content: [{ type: "text", text: `<functions>\n${functionsBlock}\n</functions>` }],
|
|
706
|
+
isError: false,
|
|
707
|
+
details: undefined,
|
|
708
|
+
sideEffects: { discoveredTools: discoveredNames },
|
|
601
709
|
};
|
|
602
710
|
}
|
|
603
711
|
// ---------------------------------------------------------------------------
|
|
@@ -615,3 +723,37 @@ function cosine(a, b) {
|
|
|
615
723
|
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
616
724
|
return denom === 0 ? 0 : dot / denom;
|
|
617
725
|
}
|
|
726
|
+
// ---------------------------------------------------------------------------
|
|
727
|
+
// Auto-discovery stubs
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
export const DEFERRAL_STUB_MARKER = "__comis_deferral_stub__";
|
|
730
|
+
export function createAutoDiscoveryStubs(deferredEntries, discoveryTracker, logger) {
|
|
731
|
+
return deferredEntries.map(entry => {
|
|
732
|
+
// `label` is a required field on ToolDefinition
|
|
733
|
+
// (pi-coding-agent/core/extensions/types.d.ts). Some existing code paths
|
|
734
|
+
// dereference it (pi-executor.ts mid-turn injection at line ~826), so
|
|
735
|
+
// copy from the original rather than leave undefined.
|
|
736
|
+
const originalLabel = entry.original.label;
|
|
737
|
+
const stub = {
|
|
738
|
+
name: entry.name,
|
|
739
|
+
label: originalLabel ?? entry.name,
|
|
740
|
+
description: entry.description,
|
|
741
|
+
parameters: entry.original.parameters,
|
|
742
|
+
[DEFERRAL_STUB_MARKER]: true,
|
|
743
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
744
|
+
const result = await entry.original.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
745
|
+
// Mark discovered only after a SUCCESSFUL execution. An MCP tool can
|
|
746
|
+
// return `{ isError: true, content: [...] }` without throwing -- those
|
|
747
|
+
// results must not promote the tool to the active set, or a broken tool
|
|
748
|
+
// would persist across turns and keep wasting discovery budget.
|
|
749
|
+
const isError = result?.isError === true;
|
|
750
|
+
if (!isError) {
|
|
751
|
+
discoveryTracker.markDiscovered([entry.name]);
|
|
752
|
+
}
|
|
753
|
+
logger.info({ toolName: entry.name, toolCallId, isError }, "Auto-discovery stub triggered — forwarding to real tool");
|
|
754
|
+
return result;
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
return stub;
|
|
758
|
+
});
|
|
759
|
+
}
|
|
@@ -48,6 +48,12 @@ export declare function clearSessionLastResponseTs(sessionKey: string): void;
|
|
|
48
48
|
* Used by the idle-based thinking clear to determine if cache is cold.
|
|
49
49
|
*/
|
|
50
50
|
export declare function getElapsedSinceLastResponse(sessionKey: string): number | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Get the raw timestamp of the last recorded response for a session.
|
|
53
|
+
* Returns undefined if no response has been recorded (cold-start).
|
|
54
|
+
* Used by the cadence tracker to detect turn boundaries.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getLastResponseTs(sessionKey: string): number | undefined;
|
|
51
57
|
/** Expose the internal Map for test assertions. Underscore prefix per project convention. */
|
|
52
58
|
export declare function _getSessionLastResponseTsForTest(): Map<string, {
|
|
53
59
|
ts: number;
|
|
@@ -94,6 +94,14 @@ export function getElapsedSinceLastResponse(sessionKey) {
|
|
|
94
94
|
return undefined;
|
|
95
95
|
return Date.now() - entry.ts;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Get the raw timestamp of the last recorded response for a session.
|
|
99
|
+
* Returns undefined if no response has been recorded (cold-start).
|
|
100
|
+
* Used by the cadence tracker to detect turn boundaries.
|
|
101
|
+
*/
|
|
102
|
+
export function getLastResponseTs(sessionKey) {
|
|
103
|
+
return sessionLastResponseTs.get(sessionKey)?.ts;
|
|
104
|
+
}
|
|
97
105
|
// ---------------------------------------------------------------------------
|
|
98
106
|
// Test-only export
|
|
99
107
|
// ---------------------------------------------------------------------------
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { SessionManager as SdkSessionManager } from "@mariozechner/pi-coding-agent";
|
|
19
19
|
import { formatSessionKey, safePath } from "@comis/core";
|
|
20
20
|
import { suppressError } from "@comis/shared";
|
|
21
|
-
import { mkdir, unlink, rm } from "node:fs/promises";
|
|
21
|
+
import { mkdir, unlink, rm, rmdir } from "node:fs/promises";
|
|
22
22
|
import { existsSync, writeFileSync, readFileSync } from "node:fs";
|
|
23
23
|
import { dirname } from "node:path";
|
|
24
24
|
import { sessionKeyToPath } from "./session-key-mapper.js";
|
|
@@ -61,8 +61,13 @@ export function createComisSessionManager(deps) {
|
|
|
61
61
|
await unlink(sessionPath);
|
|
62
62
|
}
|
|
63
63
|
catch { /* ENOENT ok */ }
|
|
64
|
-
const
|
|
64
|
+
const sessionDir = dirname(sessionPath);
|
|
65
|
+
const toolResultsDir = safePath(sessionDir, "tool-results");
|
|
65
66
|
await suppressError(rm(toolResultsDir, { recursive: true, force: true }), "tool-results dir may not exist");
|
|
67
|
+
try {
|
|
68
|
+
await rmdir(sessionDir);
|
|
69
|
+
}
|
|
70
|
+
catch { /* non-empty or already gone */ }
|
|
66
71
|
}, { retries: 10, retryMinTimeout: 500 });
|
|
67
72
|
},
|
|
68
73
|
writeSessionMetadata(sessionKey, metadata) {
|