clementine-agent 1.18.169 → 1.18.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/background-tasks.js +13 -0
- package/dist/agent/chat-skill-resolver.d.ts +117 -0
- package/dist/agent/chat-skill-resolver.js +282 -0
- package/dist/agent/complex-task-detector.d.ts +9 -0
- package/dist/agent/complex-task-detector.js +118 -0
- package/dist/agent/run-agent-mcp.d.ts +13 -0
- package/dist/agent/run-agent-mcp.js +30 -0
- package/dist/cli/dashboard.js +199 -23
- package/dist/gateway/router.d.ts +21 -1
- package/dist/gateway/router.js +264 -11
- package/dist/gateway/turn-ledger.d.ts +7 -0
- package/dist/gateway/turn-ledger.js +7 -1
- package/dist/tools/background-task-tools.js +4 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -113,6 +113,14 @@ export function markRunning(id, opts) {
|
|
|
113
113
|
safeWrite(pathFor(id, opts), task);
|
|
114
114
|
return task;
|
|
115
115
|
}
|
|
116
|
+
function writeFullResultFile(id, result, opts) {
|
|
117
|
+
if (result.length <= RESULT_TRUNCATE_BYTES)
|
|
118
|
+
return undefined;
|
|
119
|
+
const file = path.join(dirFor(opts), `${id}.result.md`);
|
|
120
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
+
writeFileSync(file, result);
|
|
122
|
+
return file;
|
|
123
|
+
}
|
|
116
124
|
/** Transition to 'done' with final result. */
|
|
117
125
|
export function markDone(id, result, deliverableNote, opts) {
|
|
118
126
|
const task = loadBackgroundTask(id, opts);
|
|
@@ -122,9 +130,14 @@ export function markDone(id, result, deliverableNote, opts) {
|
|
|
122
130
|
return task;
|
|
123
131
|
task.status = 'done';
|
|
124
132
|
task.completedAt = new Date().toISOString();
|
|
133
|
+
const resultPath = writeFullResultFile(id, result, opts);
|
|
134
|
+
if (resultPath)
|
|
135
|
+
task.resultPath = resultPath;
|
|
125
136
|
task.result = result;
|
|
126
137
|
if (deliverableNote)
|
|
127
138
|
task.deliverableNote = deliverableNote;
|
|
139
|
+
else if (resultPath)
|
|
140
|
+
task.deliverableNote = resultPath;
|
|
128
141
|
safeWrite(pathFor(id, opts), task);
|
|
129
142
|
return task;
|
|
130
143
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-skill-resolver — match a chat message against the skill catalog,
|
|
3
|
+
* extract the MCP servers / toolkits the matched skills imply, and
|
|
4
|
+
* produce a system-prompt block listing them as "Relevant Skills."
|
|
5
|
+
*
|
|
6
|
+
* Why this exists (1.18.170)
|
|
7
|
+
* ──────────────────────────
|
|
8
|
+
* Before this module, the modern chat path (`router.ts → runAgent`) did
|
|
9
|
+
* three things in order:
|
|
10
|
+
* 1. Build extra MCP servers from `routeToolSurface(userText)` — a
|
|
11
|
+
* regex-bundle matcher over 19 fixed bundles in `tool-router.ts`.
|
|
12
|
+
* 2. Build vault context (SOUL.md / MEMORY.md / AGENTS.md).
|
|
13
|
+
* 3. Call runAgent with the assembled pieces.
|
|
14
|
+
*
|
|
15
|
+
* Step 1 has a fundamental gap: anything outside the 19 hardcoded
|
|
16
|
+
* bundles (Salesforce, HubSpot, Asana, ClickUp, Airtable, an installed
|
|
17
|
+
* CLI like `sf`) silently fails to route. The model gets no Salesforce
|
|
18
|
+
* tools loaded even when the user just connected the Salesforce MCP.
|
|
19
|
+
*
|
|
20
|
+
* Clementine already auto-generates one skill per MCP tool whenever a
|
|
21
|
+
* server's schema is fetched (`auto-skills.ts` → `~/.clementine/vault/
|
|
22
|
+
* 00-System/skills/auto/<server>/<tool>.md`). Those auto-skills include
|
|
23
|
+
* server-aware triggers ("salesforce", "my salesforce", "query records
|
|
24
|
+
* salesforce", …) and a `mcp__<server>__<tool>` reference in their body.
|
|
25
|
+
*
|
|
26
|
+
* The legacy chat path (`assistant.ts:1487-1538`) already searched the
|
|
27
|
+
* skill catalog and injected a "## Relevant Skill" block — but that
|
|
28
|
+
* code lives in the deprecated PersonalAssistant.query() path. The
|
|
29
|
+
* modern runAgent path skipped it entirely.
|
|
30
|
+
*
|
|
31
|
+
* This module ports + extends the legacy behavior:
|
|
32
|
+
* • Top-3 match aggregation (not just top-1) — handles category queries
|
|
33
|
+
* like "salesforce" where many tool-specific auto-skills match
|
|
34
|
+
* similarly. The parent sees all 3 bodies and picks the right tool.
|
|
35
|
+
* • Extracts `mcp__<server>__<tool>` references from every matched
|
|
36
|
+
* skill's body. Used to widen `buildExtraMcpForRunAgent` so the
|
|
37
|
+
* right Composio toolkits / claude.ai integrations load even though
|
|
38
|
+
* "Salesforce" isn't in the regex-bundle list.
|
|
39
|
+
* • Extracts `clementine.tools.allow` from user-authored skills (the
|
|
40
|
+
* auto-skills don't set this field but human-authored ones often do).
|
|
41
|
+
* • Auto-skills get a small penalty in `searchSkills` already (-0.5)
|
|
42
|
+
* so user-authored skills outrank them at parity — preserved here.
|
|
43
|
+
*
|
|
44
|
+
* Not a sandbox: we never *restrict* tools based on matched skills (the
|
|
45
|
+
* SDK still receives the parent's full allowedTools). We only widen the
|
|
46
|
+
* MCP server selection. Hard tool-scope enforcement happens via the
|
|
47
|
+
* `runSkill` primitive (`run-skill.ts:300`), not chat.
|
|
48
|
+
*
|
|
49
|
+
* Failure mode: this module never throws. A match search that errors
|
|
50
|
+
* (corrupt skill file, missing dir, etc.) returns empty results and the
|
|
51
|
+
* caller proceeds with `routeToolSurface` alone — i.e. today's behavior.
|
|
52
|
+
*/
|
|
53
|
+
import { type SkillMatch } from './skill-extractor.js';
|
|
54
|
+
import type { AgentProfile } from '../types.js';
|
|
55
|
+
import type { MemoryStore } from '../memory/store.js';
|
|
56
|
+
export interface ResolveSkillsOptions {
|
|
57
|
+
/** Active hired-agent profile, when set. Used for agent-scoped skill
|
|
58
|
+
* priority (matches the legacy boost). */
|
|
59
|
+
profile?: AgentProfile | null;
|
|
60
|
+
/** Optional memory store — used to read the user's suppression list
|
|
61
|
+
* ("never auto-match this skill again"). */
|
|
62
|
+
memoryStore?: MemoryStore | null;
|
|
63
|
+
/** Override the top-K aggregation cap. Defaults to 3. */
|
|
64
|
+
limit?: number;
|
|
65
|
+
/** Override the minimum match score. Defaults to 4. */
|
|
66
|
+
minScore?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface ResolvedSkillContext {
|
|
69
|
+
/** Match records from searchSkills, filtered to those above minScore
|
|
70
|
+
* and capped at `limit`. Length 0 means no skill matched — the
|
|
71
|
+
* caller should fall back to today's behavior (regex bundles only). */
|
|
72
|
+
matches: SkillMatch[];
|
|
73
|
+
/** MCP server slugs referenced by any matched skill's body. Caller
|
|
74
|
+
* unions this with `route.externalMcpServers` and `route.composioToolkits`. */
|
|
75
|
+
hintedMcpServers: string[];
|
|
76
|
+
/** Tools declared under `clementine.tools.allow` on matched skills.
|
|
77
|
+
* Caller can use these to widen the SDK's `allowedTools` if the
|
|
78
|
+
* matched skill expects access beyond the chat default. */
|
|
79
|
+
hintedTools: string[];
|
|
80
|
+
/** Pre-rendered "## Relevant Skills" markdown block, ready to append
|
|
81
|
+
* to the system prompt. Empty when `matches.length === 0`. */
|
|
82
|
+
promptBlock: string;
|
|
83
|
+
/** Diagnostics for log/telemetry. */
|
|
84
|
+
diagnostics: {
|
|
85
|
+
queryChars: number;
|
|
86
|
+
candidatesConsidered: number;
|
|
87
|
+
matchesAboveThreshold: number;
|
|
88
|
+
topScore: number;
|
|
89
|
+
mcpRefsExtracted: number;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Extract the `mcp__<server>__<tool>` server-name component from a
|
|
93
|
+
* match's body content + frontmatter. Returns the set of server slugs. */
|
|
94
|
+
declare function extractMcpServersFromMatch(match: SkillMatch): string[];
|
|
95
|
+
/** Extract `clementine.tools.allow` from a match's frontmatter.
|
|
96
|
+
* Returns the list of declared tool names (or [] if none). */
|
|
97
|
+
declare function extractAllowedToolsFromMatch(match: SkillMatch): string[];
|
|
98
|
+
/**
|
|
99
|
+
* Render the system-prompt block injected for the matched skills.
|
|
100
|
+
* Single-match keeps the legacy `## Relevant Skill: <title>` shape;
|
|
101
|
+
* multi-match nests them under `## Relevant Skills` with per-skill
|
|
102
|
+
* subheadings. Tools-required warnings (legacy `assistant.ts:1504-1513`)
|
|
103
|
+
* are NOT included here — they belong to the run-time tool policy
|
|
104
|
+
* check, not the prompt context.
|
|
105
|
+
*/
|
|
106
|
+
declare function renderPromptBlock(matches: SkillMatch[]): string;
|
|
107
|
+
/**
|
|
108
|
+
* Match a user message against the skill catalog and return the routing
|
|
109
|
+
* hints + prompt block the chat path should layer on top of the static
|
|
110
|
+
* `routeToolSurface` decision.
|
|
111
|
+
*
|
|
112
|
+
* Never throws — telemetry / matching errors degrade gracefully to
|
|
113
|
+
* empty hints, and the caller proceeds with today's behavior.
|
|
114
|
+
*/
|
|
115
|
+
export declare function resolveSkillsForChat(userMessage: string, opts?: ResolveSkillsOptions): ResolvedSkillContext;
|
|
116
|
+
export { extractMcpServersFromMatch, extractAllowedToolsFromMatch, renderPromptBlock };
|
|
117
|
+
//# sourceMappingURL=chat-skill-resolver.d.ts.map
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-skill-resolver — match a chat message against the skill catalog,
|
|
3
|
+
* extract the MCP servers / toolkits the matched skills imply, and
|
|
4
|
+
* produce a system-prompt block listing them as "Relevant Skills."
|
|
5
|
+
*
|
|
6
|
+
* Why this exists (1.18.170)
|
|
7
|
+
* ──────────────────────────
|
|
8
|
+
* Before this module, the modern chat path (`router.ts → runAgent`) did
|
|
9
|
+
* three things in order:
|
|
10
|
+
* 1. Build extra MCP servers from `routeToolSurface(userText)` — a
|
|
11
|
+
* regex-bundle matcher over 19 fixed bundles in `tool-router.ts`.
|
|
12
|
+
* 2. Build vault context (SOUL.md / MEMORY.md / AGENTS.md).
|
|
13
|
+
* 3. Call runAgent with the assembled pieces.
|
|
14
|
+
*
|
|
15
|
+
* Step 1 has a fundamental gap: anything outside the 19 hardcoded
|
|
16
|
+
* bundles (Salesforce, HubSpot, Asana, ClickUp, Airtable, an installed
|
|
17
|
+
* CLI like `sf`) silently fails to route. The model gets no Salesforce
|
|
18
|
+
* tools loaded even when the user just connected the Salesforce MCP.
|
|
19
|
+
*
|
|
20
|
+
* Clementine already auto-generates one skill per MCP tool whenever a
|
|
21
|
+
* server's schema is fetched (`auto-skills.ts` → `~/.clementine/vault/
|
|
22
|
+
* 00-System/skills/auto/<server>/<tool>.md`). Those auto-skills include
|
|
23
|
+
* server-aware triggers ("salesforce", "my salesforce", "query records
|
|
24
|
+
* salesforce", …) and a `mcp__<server>__<tool>` reference in their body.
|
|
25
|
+
*
|
|
26
|
+
* The legacy chat path (`assistant.ts:1487-1538`) already searched the
|
|
27
|
+
* skill catalog and injected a "## Relevant Skill" block — but that
|
|
28
|
+
* code lives in the deprecated PersonalAssistant.query() path. The
|
|
29
|
+
* modern runAgent path skipped it entirely.
|
|
30
|
+
*
|
|
31
|
+
* This module ports + extends the legacy behavior:
|
|
32
|
+
* • Top-3 match aggregation (not just top-1) — handles category queries
|
|
33
|
+
* like "salesforce" where many tool-specific auto-skills match
|
|
34
|
+
* similarly. The parent sees all 3 bodies and picks the right tool.
|
|
35
|
+
* • Extracts `mcp__<server>__<tool>` references from every matched
|
|
36
|
+
* skill's body. Used to widen `buildExtraMcpForRunAgent` so the
|
|
37
|
+
* right Composio toolkits / claude.ai integrations load even though
|
|
38
|
+
* "Salesforce" isn't in the regex-bundle list.
|
|
39
|
+
* • Extracts `clementine.tools.allow` from user-authored skills (the
|
|
40
|
+
* auto-skills don't set this field but human-authored ones often do).
|
|
41
|
+
* • Auto-skills get a small penalty in `searchSkills` already (-0.5)
|
|
42
|
+
* so user-authored skills outrank them at parity — preserved here.
|
|
43
|
+
*
|
|
44
|
+
* Not a sandbox: we never *restrict* tools based on matched skills (the
|
|
45
|
+
* SDK still receives the parent's full allowedTools). We only widen the
|
|
46
|
+
* MCP server selection. Hard tool-scope enforcement happens via the
|
|
47
|
+
* `runSkill` primitive (`run-skill.ts:300`), not chat.
|
|
48
|
+
*
|
|
49
|
+
* Failure mode: this module never throws. A match search that errors
|
|
50
|
+
* (corrupt skill file, missing dir, etc.) returns empty results and the
|
|
51
|
+
* caller proceeds with `routeToolSurface` alone — i.e. today's behavior.
|
|
52
|
+
*/
|
|
53
|
+
import { readFileSync } from 'node:fs';
|
|
54
|
+
import { join, dirname } from 'node:path';
|
|
55
|
+
import { existsSync } from 'node:fs';
|
|
56
|
+
import matter from 'gray-matter';
|
|
57
|
+
import pino from 'pino';
|
|
58
|
+
import { searchSkills } from './skill-extractor.js';
|
|
59
|
+
const logger = pino({ name: 'clementine.chat-skill-resolver' });
|
|
60
|
+
// ── Tunables ──────────────────────────────────────────────────────────
|
|
61
|
+
/** Default minimum score to consider a skill match real. Mirrors the
|
|
62
|
+
* legacy `assistant.ts:1492` threshold. Skill auto-match is heuristic;
|
|
63
|
+
* this filter keeps weak matches from injecting unrelated tooling. */
|
|
64
|
+
const DEFAULT_MIN_SCORE = 4;
|
|
65
|
+
/** Default top-K matches to aggregate. Single-tool requests usually
|
|
66
|
+
* return one strong match; category requests ("salesforce") return
|
|
67
|
+
* several similarly-scored auto-skills. Top-3 covers both. Raising
|
|
68
|
+
* this bloats the system prompt without much routing benefit. */
|
|
69
|
+
const DEFAULT_TOP_K = 3;
|
|
70
|
+
/** Cap per-skill body excerpt in the injected prompt. The full body is
|
|
71
|
+
* often a multi-KB args table for an MCP tool — fine if the user
|
|
72
|
+
* needs to look at it, wasteful for routing context. Mirrors
|
|
73
|
+
* `assistant.ts:1501` which sliced at 800 chars. */
|
|
74
|
+
const PER_SKILL_BODY_CHARS = 1000;
|
|
75
|
+
/** Same MCP_TOOL_REF pattern used in `run-skill.ts:153` / `tool-router.ts:172`.
|
|
76
|
+
* Matches `mcp__<server>__<tool>` references inside skill body text. */
|
|
77
|
+
const MCP_TOOL_REF = /mcp__([A-Za-z0-9-]+(?:_[A-Za-z0-9-]+)*)__[A-Za-z0-9_-]+/g;
|
|
78
|
+
// ── Skill discovery support ──────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Find a matched skill's frontmatter on disk. `SkillMatch` carries
|
|
81
|
+
* `name` + `skillDir` but not the full frontmatter — and we need
|
|
82
|
+
* `clementine.tools.allow` + the explicit `tool:` field from auto-skills.
|
|
83
|
+
*
|
|
84
|
+
* Walks `<skillDir>` looking for any file whose basename or folder
|
|
85
|
+
* matches `name`. Returns null on any failure; the caller already has
|
|
86
|
+
* the matched body content from searchSkills and that's enough for
|
|
87
|
+
* MCP-ref extraction. The frontmatter read here is opportunistic.
|
|
88
|
+
*/
|
|
89
|
+
function readMatchFrontmatter(match) {
|
|
90
|
+
// The match's `skillDir` is the SEARCH root. Auto-skills live nested
|
|
91
|
+
// under `<skillDir>/auto/<server>/<tool>.md`; user skills can be flat
|
|
92
|
+
// (`<skillDir>/<slug>.md`) or folder-form (`<skillDir>/<slug>/SKILL.md`).
|
|
93
|
+
const slug = match.name;
|
|
94
|
+
const candidates = [
|
|
95
|
+
join(match.skillDir, `${slug}.md`),
|
|
96
|
+
join(match.skillDir, slug, 'SKILL.md'),
|
|
97
|
+
];
|
|
98
|
+
// For auto-skill slugs like `auto-dataforseo-…`, also try the nested
|
|
99
|
+
// path that the slug encodes. This is a best-effort lookup; on miss
|
|
100
|
+
// we just skip frontmatter and use the body alone.
|
|
101
|
+
if (slug.startsWith('auto-')) {
|
|
102
|
+
const parts = slug.slice(5).split('-');
|
|
103
|
+
if (parts.length >= 2) {
|
|
104
|
+
const server = parts[0];
|
|
105
|
+
const rest = parts.slice(1).join('-');
|
|
106
|
+
candidates.push(join(match.skillDir, 'auto', server, `${rest}.md`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const p of candidates) {
|
|
110
|
+
try {
|
|
111
|
+
if (!existsSync(p))
|
|
112
|
+
continue;
|
|
113
|
+
const raw = readFileSync(p, 'utf-8');
|
|
114
|
+
const parsed = matter(raw);
|
|
115
|
+
return parsed.data;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// try next candidate
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Extract the `mcp__<server>__<tool>` server-name component from a
|
|
124
|
+
* match's body content + frontmatter. Returns the set of server slugs. */
|
|
125
|
+
function extractMcpServersFromMatch(match) {
|
|
126
|
+
const servers = new Set();
|
|
127
|
+
// From body text — works for auto-skills (which list the tool name
|
|
128
|
+
// in their "## Tool call" section) and user skills that paste tool
|
|
129
|
+
// refs into their instructions.
|
|
130
|
+
MCP_TOOL_REF.lastIndex = 0;
|
|
131
|
+
let m;
|
|
132
|
+
while ((m = MCP_TOOL_REF.exec(match.content)) !== null) {
|
|
133
|
+
servers.add(m[1]);
|
|
134
|
+
}
|
|
135
|
+
// From frontmatter — auto-skills have `tool: mcp__<server>__<name>` AND
|
|
136
|
+
// `server: <name>` set explicitly. Either is authoritative.
|
|
137
|
+
const fm = readMatchFrontmatter(match);
|
|
138
|
+
if (fm) {
|
|
139
|
+
if (typeof fm.server === 'string' && fm.server.trim()) {
|
|
140
|
+
servers.add(fm.server.trim());
|
|
141
|
+
}
|
|
142
|
+
if (typeof fm.tool === 'string') {
|
|
143
|
+
const mm = MCP_TOOL_REF.exec(fm.tool);
|
|
144
|
+
if (mm)
|
|
145
|
+
servers.add(mm[1]);
|
|
146
|
+
MCP_TOOL_REF.lastIndex = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return [...servers];
|
|
150
|
+
}
|
|
151
|
+
/** Extract `clementine.tools.allow` from a match's frontmatter.
|
|
152
|
+
* Returns the list of declared tool names (or [] if none). */
|
|
153
|
+
function extractAllowedToolsFromMatch(match) {
|
|
154
|
+
const fm = readMatchFrontmatter(match);
|
|
155
|
+
if (!fm)
|
|
156
|
+
return [];
|
|
157
|
+
const clementine = fm.clementine;
|
|
158
|
+
const allow = clementine?.tools?.allow;
|
|
159
|
+
if (!Array.isArray(allow))
|
|
160
|
+
return [];
|
|
161
|
+
return allow.filter((t) => typeof t === 'string' && t.length > 0);
|
|
162
|
+
}
|
|
163
|
+
// ── Prompt rendering ──────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Render the system-prompt block injected for the matched skills.
|
|
166
|
+
* Single-match keeps the legacy `## Relevant Skill: <title>` shape;
|
|
167
|
+
* multi-match nests them under `## Relevant Skills` with per-skill
|
|
168
|
+
* subheadings. Tools-required warnings (legacy `assistant.ts:1504-1513`)
|
|
169
|
+
* are NOT included here — they belong to the run-time tool policy
|
|
170
|
+
* check, not the prompt context.
|
|
171
|
+
*/
|
|
172
|
+
function renderPromptBlock(matches) {
|
|
173
|
+
if (matches.length === 0)
|
|
174
|
+
return '';
|
|
175
|
+
if (matches.length === 1) {
|
|
176
|
+
const s = matches[0];
|
|
177
|
+
return `## Relevant Skill: ${s.title}\n\n${s.content.slice(0, PER_SKILL_BODY_CHARS)}`;
|
|
178
|
+
}
|
|
179
|
+
const parts = ['## Relevant Skills', ''];
|
|
180
|
+
parts.push(`Top ${matches.length} skill matches for this request, ordered by relevance. ` +
|
|
181
|
+
`Use these as a guide for which tools to call. If multiple skills suggest the same MCP server, ` +
|
|
182
|
+
`prefer the highest-scored one's procedure.\n`);
|
|
183
|
+
for (let i = 0; i < matches.length; i++) {
|
|
184
|
+
const s = matches[i];
|
|
185
|
+
parts.push(`### ${i + 1}. ${s.title}`, '');
|
|
186
|
+
parts.push(s.content.slice(0, PER_SKILL_BODY_CHARS));
|
|
187
|
+
parts.push('');
|
|
188
|
+
}
|
|
189
|
+
return parts.join('\n');
|
|
190
|
+
}
|
|
191
|
+
// ── Public entry point ────────────────────────────────────────────────
|
|
192
|
+
/**
|
|
193
|
+
* Match a user message against the skill catalog and return the routing
|
|
194
|
+
* hints + prompt block the chat path should layer on top of the static
|
|
195
|
+
* `routeToolSurface` decision.
|
|
196
|
+
*
|
|
197
|
+
* Never throws — telemetry / matching errors degrade gracefully to
|
|
198
|
+
* empty hints, and the caller proceeds with today's behavior.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveSkillsForChat(userMessage, opts = {}) {
|
|
201
|
+
const queryChars = userMessage.length;
|
|
202
|
+
const empty = {
|
|
203
|
+
matches: [],
|
|
204
|
+
hintedMcpServers: [],
|
|
205
|
+
hintedTools: [],
|
|
206
|
+
promptBlock: '',
|
|
207
|
+
diagnostics: {
|
|
208
|
+
queryChars,
|
|
209
|
+
candidatesConsidered: 0,
|
|
210
|
+
matchesAboveThreshold: 0,
|
|
211
|
+
topScore: 0,
|
|
212
|
+
mcpRefsExtracted: 0,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
if (!userMessage || !userMessage.trim())
|
|
216
|
+
return empty;
|
|
217
|
+
const limit = Math.max(1, opts.limit ?? DEFAULT_TOP_K);
|
|
218
|
+
const minScore = opts.minScore ?? DEFAULT_MIN_SCORE;
|
|
219
|
+
const suppressedNames = opts.memoryStore?.getSkillsToSuppress?.(opts.profile?.slug);
|
|
220
|
+
let candidates = [];
|
|
221
|
+
try {
|
|
222
|
+
candidates = searchSkills(userMessage,
|
|
223
|
+
// Ask for a bit more than `limit` so we can filter by minScore and
|
|
224
|
+
// still hit the cap when the distribution has a tail of weak hits.
|
|
225
|
+
Math.max(limit + 2, 5), opts.profile?.slug, { ...(suppressedNames ? { suppressedNames } : {}) });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
logger.debug({ err }, 'chat-skill-resolver: searchSkills failed (non-fatal)');
|
|
229
|
+
return empty;
|
|
230
|
+
}
|
|
231
|
+
const matches = candidates
|
|
232
|
+
.filter((m) => m.score >= minScore)
|
|
233
|
+
.slice(0, limit);
|
|
234
|
+
if (matches.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
...empty,
|
|
237
|
+
diagnostics: {
|
|
238
|
+
queryChars,
|
|
239
|
+
candidatesConsidered: candidates.length,
|
|
240
|
+
matchesAboveThreshold: 0,
|
|
241
|
+
topScore: candidates[0]?.score ?? 0,
|
|
242
|
+
mcpRefsExtracted: 0,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const mcpServerSet = new Set();
|
|
247
|
+
const toolSet = new Set();
|
|
248
|
+
for (const m of matches) {
|
|
249
|
+
for (const s of extractMcpServersFromMatch(m))
|
|
250
|
+
mcpServerSet.add(s);
|
|
251
|
+
for (const t of extractAllowedToolsFromMatch(m))
|
|
252
|
+
toolSet.add(t);
|
|
253
|
+
}
|
|
254
|
+
const result = {
|
|
255
|
+
matches,
|
|
256
|
+
hintedMcpServers: [...mcpServerSet],
|
|
257
|
+
hintedTools: [...toolSet],
|
|
258
|
+
promptBlock: renderPromptBlock(matches),
|
|
259
|
+
diagnostics: {
|
|
260
|
+
queryChars,
|
|
261
|
+
candidatesConsidered: candidates.length,
|
|
262
|
+
matchesAboveThreshold: matches.length,
|
|
263
|
+
topScore: matches[0].score,
|
|
264
|
+
mcpRefsExtracted: mcpServerSet.size,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
logger.info({
|
|
268
|
+
matches: matches.map(m => ({ name: m.name, score: Number(m.score.toFixed(2)) })),
|
|
269
|
+
hintedMcpServers: result.hintedMcpServers,
|
|
270
|
+
hintedToolCount: result.hintedTools.length,
|
|
271
|
+
queryChars,
|
|
272
|
+
}, 'chat-skill-resolver: skills matched');
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
// Re-export the helpers so tests can target them directly.
|
|
276
|
+
export { extractMcpServersFromMatch, extractAllowedToolsFromMatch, renderPromptBlock };
|
|
277
|
+
// Silence lint for the dirname import — kept for future use when the
|
|
278
|
+
// matcher needs the parent folder of a matched skill (e.g. to surface
|
|
279
|
+
// bundled attachments in the prompt). Removing the import would be a
|
|
280
|
+
// pre-emptive cleanup but the file is already tagged for follow-up work.
|
|
281
|
+
void dirname;
|
|
282
|
+
//# sourceMappingURL=chat-skill-resolver.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ComplexTaskRecommendation {
|
|
2
|
+
score: number;
|
|
3
|
+
reasons: string[];
|
|
4
|
+
suggestedMaxMinutes: number;
|
|
5
|
+
plan: string[];
|
|
6
|
+
queueImmediately: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function detectComplexTaskForBackground(text: string): ComplexTaskRecommendation | null;
|
|
9
|
+
//# sourceMappingURL=complex-task-detector.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const SKILL_AUTHORING_RE = /\b(create|make|build|draft|write|teach|save|update)\b.{0,40}\b(skill|SKILL\.md)\b|\bskill[- ]creator\b/i;
|
|
2
|
+
const EXPLICIT_BACKGROUND_RE = /\b(background|deep mode|keep working|don't stop|dont stop|autonomous|long[- ]running|run overnight|take your time)\b/i;
|
|
3
|
+
const COMPLEX_WORK_RE = /\b(audit|research|analy[sz]e|review|scrape|crawl|extract|enrich|compile|compare|verify|cross[- ]check|triage|reconcile|draft|generate|update|sync|report back|write back)\b/i;
|
|
4
|
+
const BATCH_RE = /\b(all|every|each|bulk|batch|list of|contacts?|leads?|accounts?|tasks?|tickets?|records?|rows?|pages?|repos?|projects?)\b/i;
|
|
5
|
+
const SIDE_EFFECT_RE = /\b(update|write|create|draft|send|post|comment|reply|upload|append|sync|mark|close|move)\b/i;
|
|
6
|
+
const MULTI_STEP_RE = /\b(and then|then|after that|finally|from .* to |against .* and |across|compile .* into|check .* then)\b/i;
|
|
7
|
+
const SYSTEM_KEYWORDS = [
|
|
8
|
+
'asana',
|
|
9
|
+
'salesforce',
|
|
10
|
+
'google sheet',
|
|
11
|
+
'google sheets',
|
|
12
|
+
'sheet',
|
|
13
|
+
'sheets',
|
|
14
|
+
'dataforseo',
|
|
15
|
+
'hubspot',
|
|
16
|
+
'notion',
|
|
17
|
+
'github',
|
|
18
|
+
'gmail',
|
|
19
|
+
'outlook',
|
|
20
|
+
'slack',
|
|
21
|
+
'discord',
|
|
22
|
+
'website',
|
|
23
|
+
'websites',
|
|
24
|
+
'crm',
|
|
25
|
+
'spreadsheet',
|
|
26
|
+
'csv',
|
|
27
|
+
'airtable',
|
|
28
|
+
'linear',
|
|
29
|
+
'jira',
|
|
30
|
+
];
|
|
31
|
+
function countSystemMentions(text) {
|
|
32
|
+
const lower = text.toLowerCase();
|
|
33
|
+
let count = 0;
|
|
34
|
+
for (const keyword of SYSTEM_KEYWORDS) {
|
|
35
|
+
if (lower.includes(keyword))
|
|
36
|
+
count++;
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
function estimatedMinutes(score, systemCount) {
|
|
41
|
+
if (score >= 8 || systemCount >= 4)
|
|
42
|
+
return 90;
|
|
43
|
+
if (score >= 6 || systemCount >= 3)
|
|
44
|
+
return 60;
|
|
45
|
+
return 30;
|
|
46
|
+
}
|
|
47
|
+
function buildPlan(text, systemCount) {
|
|
48
|
+
const lower = text.toLowerCase();
|
|
49
|
+
const plan = [];
|
|
50
|
+
plan.push('Confirm the exact scope, filters, and write/send permissions before making side-effecting changes.');
|
|
51
|
+
if (systemCount > 0) {
|
|
52
|
+
plan.push('Connect to the named systems with official MCP/API/CLI tools and use the narrowest reliable query.');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
plan.push('Gather the source material with the available project, file, web, memory, or CLI tools.');
|
|
56
|
+
}
|
|
57
|
+
if (BATCH_RE.test(text)) {
|
|
58
|
+
plan.push('Process records in batches, track counts, and keep skipped/error records separate.');
|
|
59
|
+
}
|
|
60
|
+
if (lower.includes('enrich') || lower.includes('dataforseo')) {
|
|
61
|
+
plan.push('Enrich only qualified records and keep the signal used for each output row or draft.');
|
|
62
|
+
}
|
|
63
|
+
if (SIDE_EFFECT_RE.test(text)) {
|
|
64
|
+
plan.push('Create drafts or updates first; only send or commit irreversible changes after explicit approval.');
|
|
65
|
+
}
|
|
66
|
+
plan.push('Return a concise final report with counts, changed locations, failures, and recommended next action.');
|
|
67
|
+
return plan.slice(0, 6);
|
|
68
|
+
}
|
|
69
|
+
export function detectComplexTaskForBackground(text) {
|
|
70
|
+
const trimmed = text.trim();
|
|
71
|
+
if (!trimmed)
|
|
72
|
+
return null;
|
|
73
|
+
if (SKILL_AUTHORING_RE.test(trimmed))
|
|
74
|
+
return null;
|
|
75
|
+
const systemCount = countSystemMentions(trimmed);
|
|
76
|
+
const reasons = [];
|
|
77
|
+
let score = 0;
|
|
78
|
+
if (EXPLICIT_BACKGROUND_RE.test(trimmed)) {
|
|
79
|
+
score += 4;
|
|
80
|
+
reasons.push('explicit background/deep-work wording');
|
|
81
|
+
}
|
|
82
|
+
if (COMPLEX_WORK_RE.test(trimmed)) {
|
|
83
|
+
score += 2;
|
|
84
|
+
reasons.push('multi-step work verb');
|
|
85
|
+
}
|
|
86
|
+
if (BATCH_RE.test(trimmed)) {
|
|
87
|
+
score += 2;
|
|
88
|
+
reasons.push('batch or many-record scope');
|
|
89
|
+
}
|
|
90
|
+
if (SIDE_EFFECT_RE.test(trimmed)) {
|
|
91
|
+
score += 1;
|
|
92
|
+
reasons.push('write/draft/update side effects');
|
|
93
|
+
}
|
|
94
|
+
if (MULTI_STEP_RE.test(trimmed)) {
|
|
95
|
+
score += 1;
|
|
96
|
+
reasons.push('multi-step sequencing');
|
|
97
|
+
}
|
|
98
|
+
if (systemCount >= 2) {
|
|
99
|
+
score += Math.min(4, systemCount);
|
|
100
|
+
reasons.push(`${systemCount} named systems or data surfaces`);
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.length > 450) {
|
|
103
|
+
score += 1;
|
|
104
|
+
reasons.push('long detailed request');
|
|
105
|
+
}
|
|
106
|
+
const queueImmediately = EXPLICIT_BACKGROUND_RE.test(trimmed) && score >= 5;
|
|
107
|
+
const shouldOffer = queueImmediately || score >= 5 || (systemCount >= 2 && (BATCH_RE.test(trimmed) || SIDE_EFFECT_RE.test(trimmed)));
|
|
108
|
+
if (!shouldOffer)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
score,
|
|
112
|
+
reasons,
|
|
113
|
+
suggestedMaxMinutes: estimatedMinutes(score, systemCount),
|
|
114
|
+
plan: buildPlan(trimmed, systemCount),
|
|
115
|
+
queueImmediately,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=complex-task-detector.js.map
|
|
@@ -24,6 +24,19 @@ export interface BuildExtraMcpOptions {
|
|
|
24
24
|
/** When true, build the FULL surface (no bundle filtering, no dedup).
|
|
25
25
|
* Used by admin/debug callers; not the cron-path default. */
|
|
26
26
|
fullSurface?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Additional MCP server slugs that should be considered "in scope" beyond
|
|
29
|
+
* what `routeToolSurface(scopeText)` produced. The chat path (1.18.170)
|
|
30
|
+
* derives these from `mcp__<server>__<tool>` references on auto-matched
|
|
31
|
+
* skills — see `chat-skill-resolver.ts`. The same slug is tried as both a
|
|
32
|
+
* Composio toolkit and an external MCP server name, mirroring the
|
|
33
|
+
* `explicit_mcp` handling in `tool-router.ts:223-234` so whichever source
|
|
34
|
+
* is actually connected mounts and the other no-ops.
|
|
35
|
+
*
|
|
36
|
+
* Profile-level allowlists still WIN over these hints — a hint cannot
|
|
37
|
+
* loosen a profile-restricted agent's tool surface.
|
|
38
|
+
*/
|
|
39
|
+
skillHintedMcpServers?: string[];
|
|
27
40
|
}
|
|
28
41
|
export interface BuildExtraMcpResult {
|
|
29
42
|
/** Map of additional MCP servers to merge into runAgent's mcpServers. */
|
|
@@ -44,6 +44,36 @@ export async function buildExtraMcpForRunAgent(opts = {}) {
|
|
|
44
44
|
reason: 'full_surface',
|
|
45
45
|
}
|
|
46
46
|
: routeToolSurface(opts.scopeText ?? '');
|
|
47
|
+
// 1b. Widen with skill-hinted MCP servers (1.18.170). Each hint slug is
|
|
48
|
+
// tried as both an external MCP server name and a Composio toolkit;
|
|
49
|
+
// unmatched/disconnected ones no-op below at server-construction time.
|
|
50
|
+
// We don't touch `route` when `fullSurface` is set — nothing to widen.
|
|
51
|
+
if (!opts.fullSurface && opts.skillHintedMcpServers && opts.skillHintedMcpServers.length > 0) {
|
|
52
|
+
const ext = new Set(Array.isArray(route.externalMcpServers) ? route.externalMcpServers : []);
|
|
53
|
+
const com = new Set(Array.isArray(route.composioToolkits) ? route.composioToolkits : []);
|
|
54
|
+
for (const slug of opts.skillHintedMcpServers) {
|
|
55
|
+
const trimmed = slug.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
continue;
|
|
58
|
+
if (trimmed.startsWith('claude_ai_')) {
|
|
59
|
+
ext.add(trimmed.slice('claude_ai_'.length));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
ext.add(trimmed);
|
|
63
|
+
com.add(trimmed);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Keep the existing reason union ('empty' | 'matched' | 'full_surface').
|
|
67
|
+
// The widening just lifts an 'empty' route into 'matched' once we've
|
|
68
|
+
// added any skill-derived server. Diagnostics about skill_hint
|
|
69
|
+
// contribution flow via the caller's log line.
|
|
70
|
+
route = {
|
|
71
|
+
...route,
|
|
72
|
+
externalMcpServers: [...ext],
|
|
73
|
+
composioToolkits: [...com],
|
|
74
|
+
reason: ext.size > 0 || com.size > 0 ? 'matched' : route.reason,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
47
77
|
// 2. Build Composio MCP servers, honoring profile allowlist when set.
|
|
48
78
|
let composioMcpServers = {};
|
|
49
79
|
try {
|