clementine-agent 1.18.65 → 1.18.66
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/run-agent-cron.d.ts +111 -0
- package/dist/agent/run-agent-cron.js +182 -56
- package/dist/agent/skill-extractor.d.ts +14 -0
- package/dist/agent/skill-extractor.js +50 -0
- package/dist/cli/dashboard.js +1003 -6
- package/dist/dashboard/build-operations.d.ts +10 -0
- package/dist/dashboard/build-operations.js +14 -0
- package/dist/gateway/cron-scheduler.js +55 -3
- package/dist/gateway/router.d.ts +22 -1
- package/dist/gateway/router.js +34 -1
- package/dist/tools/admin-tools.js +252 -2
- package/dist/types.d.ts +32 -0
- package/package.json +1 -1
- package/vault/00-System/CRON.md +66 -0
|
@@ -18,6 +18,53 @@ import type { AgentProfile } from '../types.js';
|
|
|
18
18
|
import type { AgentManager } from './agent-manager.js';
|
|
19
19
|
import type { MemoryStore } from '../memory/store.js';
|
|
20
20
|
import { type RunAgentResult } from './run-agent.js';
|
|
21
|
+
/**
|
|
22
|
+
* Compute the effective tool allowlist for a cron run.
|
|
23
|
+
*
|
|
24
|
+
* Semantics:
|
|
25
|
+
* - Both undefined / empty → return undefined. Caller (`runAgent`)
|
|
26
|
+
* will fall back to `profile.team.allowedTools` then
|
|
27
|
+
* `CORE_TOOLS_FOR_AGENT_PARENT` — preserving today's behavior for
|
|
28
|
+
* legacy CRON.md entries with no `allowed_tools` field.
|
|
29
|
+
* - Job allowlist only → return `['Agent', ...job]` (deduped).
|
|
30
|
+
* Bare tricks (no agentSlug) hit this path too.
|
|
31
|
+
* - Profile allowlist only → return undefined (let runAgent thread it
|
|
32
|
+
* through its own profile path; no need to duplicate work here).
|
|
33
|
+
* - Both → intersection ∪ {Agent}. When intersection is empty the
|
|
34
|
+
* result is just `['Agent']` — degenerate but valid (subagent
|
|
35
|
+
* delegation is the one thing every cron must always be able to do).
|
|
36
|
+
*/
|
|
37
|
+
export declare function computeEffectiveAllowedTools(jobAllow: string[] | undefined, profileAllow: string[] | undefined): string[] | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Compute the effective MCP server map for a cron run by intersecting the
|
|
40
|
+
* trick's `allowedMcpServers` with the already-resolved server map from
|
|
41
|
+
* `buildExtraMcpForRunAgent` (which has already applied the profile
|
|
42
|
+
* allowlist). Returns the unchanged input map when the trick has no
|
|
43
|
+
* MCP allowlist set.
|
|
44
|
+
*/
|
|
45
|
+
export declare function applyMcpAllowlist<T>(servers: Record<string, T>, jobAllowedMcpServers: string[] | undefined): Record<string, T>;
|
|
46
|
+
export interface SkillContextResult {
|
|
47
|
+
/** The rendered "Learned Procedures" block (or empty string when no skills loaded). */
|
|
48
|
+
text: string;
|
|
49
|
+
/** Skills actually injected — the dashboard records this on the run log. */
|
|
50
|
+
applied: Array<{
|
|
51
|
+
name: string;
|
|
52
|
+
source: 'pinned' | 'auto';
|
|
53
|
+
score?: number;
|
|
54
|
+
}>;
|
|
55
|
+
/** Pinned slugs that didn't resolve (deleted/renamed/suppressed). Logged + surfaced. */
|
|
56
|
+
missing: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the matched-skills block (procedures learned from prior successful runs).
|
|
60
|
+
* Pinned skills load first via exact-slug lookup; remaining slots fill from
|
|
61
|
+
* the keyword/semantic auto-match. Total cap = `MAX_INJECTED_SKILLS`. Pins
|
|
62
|
+
* that don't resolve are surfaced via `missing[]` (warned, never fatal) so
|
|
63
|
+
* the dashboard can flag broken references.
|
|
64
|
+
*
|
|
65
|
+
* Exported only for testability — the production caller is `runAgentCron`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildSkillContext(jobName: string, jobPrompt: string, agentSlug: string | undefined, pinnedSkills: string[] | undefined, memoryStore?: MemoryStore | null): Promise<SkillContextResult>;
|
|
21
68
|
/** Minimal interface for the post-task reflection + skill extraction
|
|
22
69
|
* hooks. Lets `runAgentCron` stay decoupled from the full
|
|
23
70
|
* PersonalAssistant import while still benefiting from the existing
|
|
@@ -56,6 +103,18 @@ export interface RunAgentCronOptions {
|
|
|
56
103
|
* PersonalAssistant — it implements both members. Optional so the
|
|
57
104
|
* helper still works in tests without the full assistant graph. */
|
|
58
105
|
postTaskHooks?: CronPostTaskHooks | null;
|
|
106
|
+
/** Pinned skill slugs from the trick definition. Loaded before the
|
|
107
|
+
* auto-match search; total skills injected is capped at
|
|
108
|
+
* `MAX_INJECTED_SKILLS`. */
|
|
109
|
+
pinnedSkills?: string[];
|
|
110
|
+
/** Per-trick tool whitelist. Intersected with `profile.team.allowedTools`
|
|
111
|
+
* when both are present. 'Agent' is always force-included. Undefined
|
|
112
|
+
* preserves today's behavior (falls back to profile or default). */
|
|
113
|
+
allowedTools?: string[];
|
|
114
|
+
/** Per-trick MCP server whitelist (server names from `discoverMcpServers`).
|
|
115
|
+
* Applied after `buildExtraMcpForRunAgent` runs, so the effective set
|
|
116
|
+
* is `profile ∩ trick`. */
|
|
117
|
+
allowedMcpServers?: string[];
|
|
59
118
|
}
|
|
60
119
|
export interface RunAgentCronResult extends RunAgentResult {
|
|
61
120
|
/** The final prompt that was sent to the agent (after context injection).
|
|
@@ -64,7 +123,59 @@ export interface RunAgentCronResult extends RunAgentResult {
|
|
|
64
123
|
/** Diagnostics: which Composio + external servers were live for this run. */
|
|
65
124
|
composioConnected: string[];
|
|
66
125
|
externalConnected: string[];
|
|
126
|
+
/** Skills actually injected (pinned + auto). Surfaced on the run log so the
|
|
127
|
+
* dashboard can render a "ran with: …" line. */
|
|
128
|
+
skillsApplied: Array<{
|
|
129
|
+
name: string;
|
|
130
|
+
source: 'pinned' | 'auto';
|
|
131
|
+
score?: number;
|
|
132
|
+
}>;
|
|
133
|
+
/** Pinned skills that didn't resolve (bad slug / suppressed). Empty array
|
|
134
|
+
* is fine; only populated when the trick had pins that failed to load. */
|
|
135
|
+
skillsMissing: string[];
|
|
136
|
+
/** Effective tool allowlist passed to runAgent (post-intersection). Undefined
|
|
137
|
+
* means the trick didn't override — runAgent fell through to profile/default. */
|
|
138
|
+
allowedToolsApplied?: string[];
|
|
139
|
+
/** MCP servers live for this run after profile + trick intersection. */
|
|
140
|
+
mcpServersApplied: string[];
|
|
67
141
|
}
|
|
142
|
+
/** Plan output from `buildCronExecutionPlan` — everything the runner needs
|
|
143
|
+
* to dispatch, plus the broken-down context blocks so a preview UI can
|
|
144
|
+
* show "what came from where" without re-running the build. */
|
|
145
|
+
export interface CronExecutionPlan {
|
|
146
|
+
builtPrompt: string;
|
|
147
|
+
contextBlocks: {
|
|
148
|
+
memoryContext: string;
|
|
149
|
+
progressContext: string;
|
|
150
|
+
goalContext: string;
|
|
151
|
+
delegationContext: string;
|
|
152
|
+
teamContext: string;
|
|
153
|
+
criteriaContext: string;
|
|
154
|
+
skillContext: string;
|
|
155
|
+
jobPrompt: string;
|
|
156
|
+
howToRespond: string;
|
|
157
|
+
};
|
|
158
|
+
skillsApplied: SkillContextResult['applied'];
|
|
159
|
+
skillsMissing: string[];
|
|
160
|
+
effectiveAllowedTools: string[] | undefined;
|
|
161
|
+
mcpServerMap: Record<string, unknown>;
|
|
162
|
+
mcpServersApplied: string[];
|
|
163
|
+
composioConnected: string[];
|
|
164
|
+
externalConnected: string[];
|
|
165
|
+
tier: number;
|
|
166
|
+
effort: 'low' | 'medium' | 'high';
|
|
167
|
+
maxBudgetUsd: number | undefined;
|
|
168
|
+
agentSlug: string | undefined;
|
|
169
|
+
ownerName: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Plan a cron run — assemble all context, resolve skills, intersect tool/MCP
|
|
173
|
+
* allowlists — without dispatching to the agent. Used by `runAgentCron` for
|
|
174
|
+
* the actual run, and by the dashboard's `GET /api/cron/:name/preview`
|
|
175
|
+
* endpoint so users can see *exactly* what the trick will send to the agent
|
|
176
|
+
* before the next fire.
|
|
177
|
+
*/
|
|
178
|
+
export declare function buildCronExecutionPlan(opts: RunAgentCronOptions): Promise<CronExecutionPlan>;
|
|
68
179
|
/**
|
|
69
180
|
* Run a cron job via the canonical SDK runAgent path.
|
|
70
181
|
*
|
|
@@ -26,6 +26,52 @@ const CRON_PROGRESS_PENDING_MAX_ITEMS = 20;
|
|
|
26
26
|
const CRON_PROGRESS_NOTES_MAX_CHARS = 2000;
|
|
27
27
|
const logger = pino({ name: 'clementine.run-agent-cron' });
|
|
28
28
|
const CRON_CONTEXT_ITEM_MAX = 80;
|
|
29
|
+
/** Total number of skill blocks injected into a cron prompt — pinned + auto. */
|
|
30
|
+
const MAX_INJECTED_SKILLS = 4;
|
|
31
|
+
/**
|
|
32
|
+
* Compute the effective tool allowlist for a cron run.
|
|
33
|
+
*
|
|
34
|
+
* Semantics:
|
|
35
|
+
* - Both undefined / empty → return undefined. Caller (`runAgent`)
|
|
36
|
+
* will fall back to `profile.team.allowedTools` then
|
|
37
|
+
* `CORE_TOOLS_FOR_AGENT_PARENT` — preserving today's behavior for
|
|
38
|
+
* legacy CRON.md entries with no `allowed_tools` field.
|
|
39
|
+
* - Job allowlist only → return `['Agent', ...job]` (deduped).
|
|
40
|
+
* Bare tricks (no agentSlug) hit this path too.
|
|
41
|
+
* - Profile allowlist only → return undefined (let runAgent thread it
|
|
42
|
+
* through its own profile path; no need to duplicate work here).
|
|
43
|
+
* - Both → intersection ∪ {Agent}. When intersection is empty the
|
|
44
|
+
* result is just `['Agent']` — degenerate but valid (subagent
|
|
45
|
+
* delegation is the one thing every cron must always be able to do).
|
|
46
|
+
*/
|
|
47
|
+
export function computeEffectiveAllowedTools(jobAllow, profileAllow) {
|
|
48
|
+
if (!jobAllow?.length)
|
|
49
|
+
return undefined;
|
|
50
|
+
let result;
|
|
51
|
+
if (profileAllow?.length) {
|
|
52
|
+
const jobSet = new Set(jobAllow);
|
|
53
|
+
result = profileAllow.filter(t => jobSet.has(t));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
result = [...jobAllow];
|
|
57
|
+
}
|
|
58
|
+
if (!result.includes('Agent'))
|
|
59
|
+
result.unshift('Agent');
|
|
60
|
+
return Array.from(new Set(result));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Compute the effective MCP server map for a cron run by intersecting the
|
|
64
|
+
* trick's `allowedMcpServers` with the already-resolved server map from
|
|
65
|
+
* `buildExtraMcpForRunAgent` (which has already applied the profile
|
|
66
|
+
* allowlist). Returns the unchanged input map when the trick has no
|
|
67
|
+
* MCP allowlist set.
|
|
68
|
+
*/
|
|
69
|
+
export function applyMcpAllowlist(servers, jobAllowedMcpServers) {
|
|
70
|
+
if (!jobAllowedMcpServers?.length)
|
|
71
|
+
return servers;
|
|
72
|
+
const allow = new Set(jobAllowedMcpServers);
|
|
73
|
+
return Object.fromEntries(Object.entries(servers).filter(([name]) => allow.has(name)));
|
|
74
|
+
}
|
|
29
75
|
function capContextItem(s) {
|
|
30
76
|
if (!s)
|
|
31
77
|
return '';
|
|
@@ -173,29 +219,71 @@ function buildCriteriaContext(successCriteria) {
|
|
|
173
219
|
return `## Success Criteria\nYour output will be verified against these criteria:\n` +
|
|
174
220
|
successCriteria.map(c => `- ${c}`).join('\n') + '\n\n';
|
|
175
221
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Build the matched-skills block (procedures learned from prior successful runs).
|
|
224
|
+
* Pinned skills load first via exact-slug lookup; remaining slots fill from
|
|
225
|
+
* the keyword/semantic auto-match. Total cap = `MAX_INJECTED_SKILLS`. Pins
|
|
226
|
+
* that don't resolve are surfaced via `missing[]` (warned, never fatal) so
|
|
227
|
+
* the dashboard can flag broken references.
|
|
228
|
+
*
|
|
229
|
+
* Exported only for testability — the production caller is `runAgentCron`.
|
|
230
|
+
*/
|
|
231
|
+
export async function buildSkillContext(jobName, jobPrompt, agentSlug, pinnedSkills, memoryStore) {
|
|
232
|
+
const applied = [];
|
|
233
|
+
const missing = [];
|
|
178
234
|
try {
|
|
179
|
-
const { searchSkills, recordSkillUse } = await import('./skill-extractor.js');
|
|
235
|
+
const { searchSkills, recordSkillUse, loadSkillByName } = await import('./skill-extractor.js');
|
|
180
236
|
const skillQuery = jobName + ' ' + jobPrompt.slice(0, 200);
|
|
181
237
|
const suppressedNamesRaw = memoryStore
|
|
182
238
|
?.getSkillsToSuppress?.(agentSlug);
|
|
183
239
|
const suppressedNames = Array.isArray(suppressedNamesRaw)
|
|
184
240
|
? new Set(suppressedNamesRaw)
|
|
185
241
|
: (suppressedNamesRaw ?? undefined);
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
242
|
+
const prepared = [];
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
// 1. Load pinned skills first via exact slug lookup.
|
|
245
|
+
if (pinnedSkills?.length) {
|
|
246
|
+
for (const pinName of pinnedSkills) {
|
|
247
|
+
if (seen.has(pinName))
|
|
248
|
+
continue;
|
|
249
|
+
if (prepared.length >= MAX_INJECTED_SKILLS)
|
|
250
|
+
break;
|
|
251
|
+
const skill = loadSkillByName(pinName, agentSlug, { suppressedNames });
|
|
252
|
+
if (!skill) {
|
|
253
|
+
missing.push(pinName);
|
|
254
|
+
logger.warn({ jobName, pin: pinName, agentSlug }, 'cron: pinned skill not found');
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
prepared.push({ ...skill, source: 'pinned' });
|
|
258
|
+
seen.add(pinName);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// 2. Auto-match fills the remainder, deduped against pins.
|
|
262
|
+
const remaining = MAX_INJECTED_SKILLS - prepared.length;
|
|
263
|
+
if (remaining > 0) {
|
|
264
|
+
const matched = searchSkills(skillQuery, remaining + (pinnedSkills?.length ?? 0), agentSlug, { suppressedNames });
|
|
265
|
+
for (const m of matched) {
|
|
266
|
+
if (prepared.length >= MAX_INJECTED_SKILLS)
|
|
267
|
+
break;
|
|
268
|
+
if (seen.has(m.name))
|
|
269
|
+
continue;
|
|
270
|
+
prepared.push({ ...m, source: 'auto' });
|
|
271
|
+
seen.add(m.name);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (prepared.length === 0)
|
|
275
|
+
return { text: '', applied, missing };
|
|
276
|
+
const skillLines = prepared.map(s => {
|
|
190
277
|
recordSkillUse(s.name);
|
|
191
278
|
memoryStore?.logSkillUse?.({
|
|
192
279
|
skillName: s.name,
|
|
193
280
|
sessionKey: `cron:${agentSlug ?? 'clementine'}:${jobName}`,
|
|
194
281
|
queryText: skillQuery,
|
|
195
|
-
score: s.score,
|
|
282
|
+
score: s.score ?? 0,
|
|
196
283
|
agentSlug: agentSlug ?? null,
|
|
197
284
|
});
|
|
198
|
-
|
|
285
|
+
applied.push({ name: s.name, source: s.source, score: s.score });
|
|
286
|
+
let block = `### ${s.title}${s.source === 'pinned' ? ' _(pinned)_' : ''}\n${s.content}`;
|
|
199
287
|
if (s.toolsUsed.length > 0)
|
|
200
288
|
block += `\n**Tools:** ${s.toolsUsed.join(', ')}`;
|
|
201
289
|
if (s.attachments.length > 0) {
|
|
@@ -213,53 +301,34 @@ async function buildSkillContext(jobName, jobPrompt, agentSlug, memoryStore) {
|
|
|
213
301
|
}
|
|
214
302
|
return block;
|
|
215
303
|
});
|
|
216
|
-
|
|
304
|
+
const text = `## Learned Procedures (from past successful executions)\nFollow these proven approaches when applicable:\n\n${skillLines.join('\n\n')}\n\n`;
|
|
305
|
+
return { text, applied, missing };
|
|
217
306
|
}
|
|
218
|
-
catch {
|
|
219
|
-
|
|
307
|
+
catch (err) {
|
|
308
|
+
logger.debug({ err, jobName }, 'buildSkillContext failed (non-fatal)');
|
|
309
|
+
return { text: '', applied, missing };
|
|
220
310
|
}
|
|
221
311
|
}
|
|
222
312
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* helper, then calls runAgent.
|
|
229
|
-
*
|
|
230
|
-
* The SDK handles the loop, compaction, subagent fanout, prompt
|
|
231
|
-
* caching, retries — none of which we wrap manually anymore.
|
|
313
|
+
* Plan a cron run — assemble all context, resolve skills, intersect tool/MCP
|
|
314
|
+
* allowlists — without dispatching to the agent. Used by `runAgentCron` for
|
|
315
|
+
* the actual run, and by the dashboard's `GET /api/cron/:name/preview`
|
|
316
|
+
* endpoint so users can see *exactly* what the trick will send to the agent
|
|
317
|
+
* before the next fire.
|
|
232
318
|
*/
|
|
233
|
-
export async function
|
|
319
|
+
export async function buildCronExecutionPlan(opts) {
|
|
234
320
|
const tier = opts.tier ?? 1;
|
|
235
321
|
const agentSlug = opts.profile?.slug;
|
|
236
322
|
const ownerName = process.env.OWNER_NAME ?? 'the user';
|
|
237
|
-
// ── Compose context blocks (mirrors legacy runCronJob) ─────────────
|
|
238
|
-
// Memory block goes first so the agent reads its long-term context
|
|
239
|
-
// before the run-specific progress/goals/etc. For a hired agent
|
|
240
|
-
// (Ross/Sasha) this is their own MEMORY.md, not Clementine's global.
|
|
241
323
|
const memoryContext = buildAutonomousMemoryContext(opts.profile);
|
|
242
324
|
const progressContext = buildProgressContext(opts.jobName);
|
|
243
325
|
const goalContext = buildGoalContext(opts.jobName);
|
|
244
326
|
const delegationContext = buildDelegationContext(agentSlug);
|
|
245
327
|
const teamContext = buildTeamContext(agentSlug);
|
|
246
328
|
const criteriaContext = buildCriteriaContext(opts.successCriteria);
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// descriptions auto-match per-item / multi-step work, so the SDK
|
|
251
|
-
// spawns isolated sub-agents without a hand-rolled prompt directive.
|
|
252
|
-
// Final prompt
|
|
253
|
-
const builtPrompt = `[Scheduled task: ${opts.jobName}]\n\n` +
|
|
254
|
-
memoryContext +
|
|
255
|
-
progressContext +
|
|
256
|
-
goalContext +
|
|
257
|
-
skillContext +
|
|
258
|
-
delegationContext +
|
|
259
|
-
teamContext +
|
|
260
|
-
criteriaContext +
|
|
261
|
-
`${opts.jobPrompt}\n\n` +
|
|
262
|
-
`## How to respond\n` +
|
|
329
|
+
const skillResult = await buildSkillContext(opts.jobName, opts.jobPrompt, agentSlug, opts.pinnedSkills, opts.memoryStore);
|
|
330
|
+
const skillContext = skillResult.text;
|
|
331
|
+
const howToRespond = `## How to respond\n` +
|
|
263
332
|
`You're sending this directly to ${ownerName} as a DM. ` +
|
|
264
333
|
`Write like you're texting a friend — casual, warm, concise. ` +
|
|
265
334
|
`Use their name naturally. No headers, bullet lists, or formal structure unless the content genuinely needs it. ` +
|
|
@@ -270,7 +339,16 @@ export async function runAgentCron(opts) {
|
|
|
270
339
|
`"Quiet morning, inbox is clean" beats __NOTHING__ if you did check things.\n\n` +
|
|
271
340
|
`After finishing your work, you MUST write a final text response with your findings — ` +
|
|
272
341
|
`only that final message gets delivered.`;
|
|
273
|
-
|
|
342
|
+
const builtPrompt = `[Scheduled task: ${opts.jobName}]\n\n` +
|
|
343
|
+
memoryContext +
|
|
344
|
+
progressContext +
|
|
345
|
+
goalContext +
|
|
346
|
+
skillContext +
|
|
347
|
+
delegationContext +
|
|
348
|
+
teamContext +
|
|
349
|
+
criteriaContext +
|
|
350
|
+
`${opts.jobPrompt}\n\n` +
|
|
351
|
+
howToRespond;
|
|
274
352
|
const mcp = await buildExtraMcpForRunAgent({
|
|
275
353
|
scopeText: [
|
|
276
354
|
opts.jobName,
|
|
@@ -280,23 +358,66 @@ export async function runAgentCron(opts) {
|
|
|
280
358
|
].filter(Boolean).join('\n\n'),
|
|
281
359
|
profile: opts.profile,
|
|
282
360
|
});
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
361
|
+
// Per-trick MCP allowlist: post-filter on the profile-narrowed map.
|
|
362
|
+
// Effective set = profile ∩ trick.
|
|
363
|
+
const mcpServerMap = applyMcpAllowlist(mcp.servers, opts.allowedMcpServers);
|
|
364
|
+
const allowSet = opts.allowedMcpServers?.length ? new Set(opts.allowedMcpServers) : null;
|
|
365
|
+
const composioConnected = allowSet ? mcp.composioConnected.filter(n => allowSet.has(n)) : mcp.composioConnected;
|
|
366
|
+
const externalConnected = allowSet ? mcp.externalConnected.filter(n => allowSet.has(n)) : mcp.externalConnected;
|
|
367
|
+
const mcpServersApplied = Object.keys(mcpServerMap);
|
|
368
|
+
// Per-trick tool allowlist intersection.
|
|
369
|
+
const effectiveAllowedTools = computeEffectiveAllowedTools(opts.allowedTools, opts.profile?.team?.allowedTools);
|
|
370
|
+
// Per-tier cap from config (BUDGET.cronT1 / BUDGET.cronT2). 0 = uncapped.
|
|
288
371
|
const configuredCap = tier >= 2 ? BUDGET.cronT2 : BUDGET.cronT1;
|
|
289
372
|
const maxBudget = opts.maxBudgetUsd ?? (configuredCap > 0 ? configuredCap : undefined);
|
|
290
373
|
const effort = tier >= 2 ? 'high' : 'medium';
|
|
374
|
+
return {
|
|
375
|
+
builtPrompt,
|
|
376
|
+
contextBlocks: {
|
|
377
|
+
memoryContext, progressContext, goalContext, delegationContext,
|
|
378
|
+
teamContext, criteriaContext, skillContext,
|
|
379
|
+
jobPrompt: opts.jobPrompt, howToRespond,
|
|
380
|
+
},
|
|
381
|
+
skillsApplied: skillResult.applied,
|
|
382
|
+
skillsMissing: skillResult.missing,
|
|
383
|
+
effectiveAllowedTools,
|
|
384
|
+
mcpServerMap,
|
|
385
|
+
mcpServersApplied,
|
|
386
|
+
composioConnected,
|
|
387
|
+
externalConnected,
|
|
388
|
+
tier,
|
|
389
|
+
effort,
|
|
390
|
+
maxBudgetUsd: maxBudget,
|
|
391
|
+
agentSlug,
|
|
392
|
+
ownerName,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Run a cron job via the canonical SDK runAgent path.
|
|
397
|
+
*
|
|
398
|
+
* Composes the same context blocks the legacy runCronJob injects
|
|
399
|
+
* (progress, goals, delegation, team, criteria, skills, fanout
|
|
400
|
+
* directive), wires Composio + external MCP via the dedup-aware
|
|
401
|
+
* helper, then calls runAgent.
|
|
402
|
+
*
|
|
403
|
+
* The SDK handles the loop, compaction, subagent fanout, prompt
|
|
404
|
+
* caching, retries — none of which we wrap manually anymore.
|
|
405
|
+
*/
|
|
406
|
+
export async function runAgentCron(opts) {
|
|
407
|
+
const plan = await buildCronExecutionPlan(opts);
|
|
408
|
+
const { builtPrompt, agentSlug, effort, maxBudgetUsd: maxBudget, effectiveAllowedTools, mcpServerMap, composioConnected, externalConnected, mcpServersApplied, } = plan;
|
|
291
409
|
logger.info({
|
|
292
410
|
job: opts.jobName,
|
|
293
|
-
tier,
|
|
411
|
+
tier: plan.tier,
|
|
294
412
|
profile: agentSlug,
|
|
295
|
-
composioConnected
|
|
296
|
-
externalConnected
|
|
297
|
-
droppedClaudeAi: mcp.droppedClaudeAi,
|
|
298
|
-
droppedComposio: mcp.droppedComposio,
|
|
413
|
+
composioConnected,
|
|
414
|
+
externalConnected,
|
|
299
415
|
promptChars: builtPrompt.length,
|
|
416
|
+
pinnedSkills: opts.pinnedSkills?.length ?? 0,
|
|
417
|
+
skillsApplied: plan.skillsApplied.length,
|
|
418
|
+
skillsMissing: plan.skillsMissing.length,
|
|
419
|
+
trickAllowedTools: effectiveAllowedTools?.length,
|
|
420
|
+
trickAllowedMcp: opts.allowedMcpServers?.length,
|
|
300
421
|
}, 'runAgentCron: dispatching to runAgent');
|
|
301
422
|
const startedAt = Date.now();
|
|
302
423
|
const result = await runAgent(builtPrompt, {
|
|
@@ -310,7 +431,8 @@ export async function runAgentCron(opts) {
|
|
|
310
431
|
...(maxBudget !== undefined ? { maxBudgetUsd: maxBudget } : {}),
|
|
311
432
|
maxTurns: opts.maxTurns,
|
|
312
433
|
abortSignal: opts.abortSignal,
|
|
313
|
-
|
|
434
|
+
...(effectiveAllowedTools ? { allowedTools: effectiveAllowedTools } : {}),
|
|
435
|
+
extraMcpServers: mcpServerMap,
|
|
314
436
|
});
|
|
315
437
|
// Mirror the run into transcripts so future chat recall can see it.
|
|
316
438
|
// Legacy runCronJob did this with role='cron'; canonical needs the
|
|
@@ -346,8 +468,12 @@ export async function runAgentCron(opts) {
|
|
|
346
468
|
return {
|
|
347
469
|
...result,
|
|
348
470
|
builtPrompt,
|
|
349
|
-
composioConnected
|
|
350
|
-
externalConnected
|
|
471
|
+
composioConnected,
|
|
472
|
+
externalConnected,
|
|
473
|
+
skillsApplied: plan.skillsApplied,
|
|
474
|
+
skillsMissing: plan.skillsMissing,
|
|
475
|
+
allowedToolsApplied: effectiveAllowedTools,
|
|
476
|
+
mcpServersApplied,
|
|
351
477
|
};
|
|
352
478
|
}
|
|
353
479
|
//# sourceMappingURL=run-agent-cron.js.map
|
|
@@ -60,6 +60,20 @@ export interface SkillMatch {
|
|
|
60
60
|
export declare function searchSkills(query: string, limit?: number, agentSlug?: string, opts?: {
|
|
61
61
|
suppressedNames?: Set<string>;
|
|
62
62
|
}): SkillMatch[];
|
|
63
|
+
/**
|
|
64
|
+
* Load a single skill by its flattened slug (filename minus `.md`,
|
|
65
|
+
* with directory separators replaced by dashes — e.g.
|
|
66
|
+
* `auto/discord/send.md` → `auto-discord-send`). Walks the same
|
|
67
|
+
* agent-scoped + global directory list as `searchSkills`, with
|
|
68
|
+
* agent-scoped winning on collision. Honors the same suppression set.
|
|
69
|
+
*
|
|
70
|
+
* Returns a `SkillMatch` with `score = 0` so callers can mix pinned
|
|
71
|
+
* skills into the same render path as auto-matched ones, or `null`
|
|
72
|
+
* when the slug doesn't resolve (caller should warn, not fail).
|
|
73
|
+
*/
|
|
74
|
+
export declare function loadSkillByName(name: string, agentSlug?: string, opts?: {
|
|
75
|
+
suppressedNames?: Set<string>;
|
|
76
|
+
}): SkillMatch | null;
|
|
63
77
|
/** Record that a skill was used (bump use count). */
|
|
64
78
|
export declare function recordSkillUse(skillName: string, agentSlug?: string): void;
|
|
65
79
|
/** List all active skills (global + all agent-scoped). */
|
|
@@ -496,6 +496,56 @@ export function searchSkills(query, limit = 3, agentSlug, opts) {
|
|
|
496
496
|
}
|
|
497
497
|
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
498
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Load a single skill by its flattened slug (filename minus `.md`,
|
|
501
|
+
* with directory separators replaced by dashes — e.g.
|
|
502
|
+
* `auto/discord/send.md` → `auto-discord-send`). Walks the same
|
|
503
|
+
* agent-scoped + global directory list as `searchSkills`, with
|
|
504
|
+
* agent-scoped winning on collision. Honors the same suppression set.
|
|
505
|
+
*
|
|
506
|
+
* Returns a `SkillMatch` with `score = 0` so callers can mix pinned
|
|
507
|
+
* skills into the same render path as auto-matched ones, or `null`
|
|
508
|
+
* when the slug doesn't resolve (caller should warn, not fail).
|
|
509
|
+
*/
|
|
510
|
+
export function loadSkillByName(name, agentSlug, opts) {
|
|
511
|
+
const dirs = [];
|
|
512
|
+
if (agentSlug) {
|
|
513
|
+
const agentDir = agentSkillsDir(agentSlug);
|
|
514
|
+
if (existsSync(agentDir))
|
|
515
|
+
dirs.push(agentDir);
|
|
516
|
+
}
|
|
517
|
+
if (existsSync(GLOBAL_SKILLS_DIR))
|
|
518
|
+
dirs.push(GLOBAL_SKILLS_DIR);
|
|
519
|
+
if (dirs.length === 0)
|
|
520
|
+
return null;
|
|
521
|
+
if (opts?.suppressedNames?.has(name))
|
|
522
|
+
return null;
|
|
523
|
+
for (const dir of dirs) {
|
|
524
|
+
const files = walkSkillFiles(dir);
|
|
525
|
+
for (const { filePath, relPath } of files) {
|
|
526
|
+
const slug = relPath.replace(/\.md$/, '').replace(/[\\/]/g, '-');
|
|
527
|
+
if (slug !== name)
|
|
528
|
+
continue;
|
|
529
|
+
try {
|
|
530
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
531
|
+
const parsed = matter(raw);
|
|
532
|
+
return {
|
|
533
|
+
name: slug,
|
|
534
|
+
title: parsed.data.title ?? slug,
|
|
535
|
+
content: parsed.content.slice(0, 1500),
|
|
536
|
+
score: 0,
|
|
537
|
+
toolsUsed: parsed.data.toolsUsed ?? [],
|
|
538
|
+
attachments: parsed.data.attachments ?? [],
|
|
539
|
+
skillDir: dir,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
499
549
|
/** Record that a skill was used (bump use count). */
|
|
500
550
|
export function recordSkillUse(skillName, agentSlug) {
|
|
501
551
|
try {
|