clementine-agent 1.18.45 → 1.18.47
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/assistant.d.ts +13 -0
- package/dist/agent/assistant.js +17 -0
- package/dist/agent/run-agent-cron.d.ts +79 -0
- package/dist/agent/run-agent-cron.js +335 -0
- package/dist/agent/run-agent-heartbeat.d.ts +24 -0
- package/dist/agent/run-agent-heartbeat.js +84 -0
- package/dist/agent/run-agent-mcp.d.ts +46 -0
- package/dist/agent/run-agent-mcp.js +104 -0
- package/dist/agent/run-agent-team-task.d.ts +27 -0
- package/dist/agent/run-agent-team-task.js +72 -0
- package/dist/gateway/router.js +129 -0
- package/package.json +1 -1
|
@@ -284,6 +284,19 @@ export declare class PersonalAssistant {
|
|
|
284
284
|
* having to recreate the surrounding plumbing.
|
|
285
285
|
*/
|
|
286
286
|
triggerMemoryExtractionPostExchange(userMessage: string, assistantResponse: string, sessionKey?: string, profile?: AgentProfile): Promise<void>;
|
|
287
|
+
/**
|
|
288
|
+
* Public entry point for the post-cron quality reflection. Used by
|
|
289
|
+
* the new runAgentCron path (Phase 4) to keep the existing Haiku
|
|
290
|
+
* verification pass + cron-progress bridge without duplicating it.
|
|
291
|
+
* Always best-effort — failures are swallowed to never block.
|
|
292
|
+
*/
|
|
293
|
+
triggerCronReflection(jobName: string, jobPrompt: string, deliverable: string, successCriteria?: string[]): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Public entry point for procedural-memory skill extraction after a
|
|
296
|
+
* successful execution. Used by the new runAgentCron path (Phase 4)
|
|
297
|
+
* so the new code path keeps growing the skills library.
|
|
298
|
+
*/
|
|
299
|
+
triggerSkillExtractionFromExecution(source: 'unleashed' | 'cron' | 'chat', jobName: string, prompt: string, output: string, durationMs: number, agentSlug?: string): Promise<void>;
|
|
287
300
|
private spawnMemoryExtraction;
|
|
288
301
|
private static readonly MEMORY_TOOL_NAMES;
|
|
289
302
|
private extractMemory;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -4892,6 +4892,23 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4892
4892
|
async triggerMemoryExtractionPostExchange(userMessage, assistantResponse, sessionKey, profile) {
|
|
4893
4893
|
return this.spawnMemoryExtraction(userMessage, assistantResponse, sessionKey, profile);
|
|
4894
4894
|
}
|
|
4895
|
+
/**
|
|
4896
|
+
* Public entry point for the post-cron quality reflection. Used by
|
|
4897
|
+
* the new runAgentCron path (Phase 4) to keep the existing Haiku
|
|
4898
|
+
* verification pass + cron-progress bridge without duplicating it.
|
|
4899
|
+
* Always best-effort — failures are swallowed to never block.
|
|
4900
|
+
*/
|
|
4901
|
+
async triggerCronReflection(jobName, jobPrompt, deliverable, successCriteria) {
|
|
4902
|
+
return this.runCronReflection(jobName, jobPrompt, deliverable, successCriteria);
|
|
4903
|
+
}
|
|
4904
|
+
/**
|
|
4905
|
+
* Public entry point for procedural-memory skill extraction after a
|
|
4906
|
+
* successful execution. Used by the new runAgentCron path (Phase 4)
|
|
4907
|
+
* so the new code path keeps growing the skills library.
|
|
4908
|
+
*/
|
|
4909
|
+
async triggerSkillExtractionFromExecution(source, jobName, prompt, output, durationMs, agentSlug) {
|
|
4910
|
+
return this.extractSkillFromExecution(source, jobName, prompt, output, durationMs, agentSlug);
|
|
4911
|
+
}
|
|
4895
4912
|
async spawnMemoryExtraction(userMessage, assistantResponse, sessionKey, profile) {
|
|
4896
4913
|
// Guard: skip memory extraction if the user message looks like injection
|
|
4897
4914
|
const memScan = scanner.scan(userMessage);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — runAgent cron wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* Cron jobs need more than a bare runAgent() call: they get progress
|
|
8
|
+
* continuity, linked goals, delegated tasks, team context, success
|
|
9
|
+
* criteria, and matched skills injected ahead of the job prompt. This
|
|
10
|
+
* file owns that composition for the new (canonical SDK) path. The
|
|
11
|
+
* legacy assistant.ts:runCronJob keeps its inline equivalents so we
|
|
12
|
+
* can ship Phase 3 without touching legacy code.
|
|
13
|
+
*
|
|
14
|
+
* After Phase 3 verifies and we collapse the legacy path, the
|
|
15
|
+
* duplicated helpers here become the single source of truth.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentProfile } from '../types.js';
|
|
18
|
+
import type { AgentManager } from './agent-manager.js';
|
|
19
|
+
import type { MemoryStore } from '../memory/store.js';
|
|
20
|
+
import { type RunAgentResult } from './run-agent.js';
|
|
21
|
+
/** Minimal interface for the post-task reflection + skill extraction
|
|
22
|
+
* hooks. Lets `runAgentCron` stay decoupled from the full
|
|
23
|
+
* PersonalAssistant import while still benefiting from the existing
|
|
24
|
+
* procedures. */
|
|
25
|
+
export interface CronPostTaskHooks {
|
|
26
|
+
triggerCronReflection: (jobName: string, jobPrompt: string, deliverable: string, successCriteria?: string[]) => Promise<void>;
|
|
27
|
+
triggerSkillExtractionFromExecution: (source: 'unleashed' | 'cron' | 'chat', jobName: string, prompt: string, output: string, durationMs: number, agentSlug?: string) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface RunAgentCronOptions {
|
|
30
|
+
/** Job name from CRON.md. Used for telemetry, progress lookup, skill match. */
|
|
31
|
+
jobName: string;
|
|
32
|
+
/** Job prompt body (the user-defined "do this" text). */
|
|
33
|
+
jobPrompt: string;
|
|
34
|
+
/** Cron tier. Drives effort + budget. */
|
|
35
|
+
tier?: number;
|
|
36
|
+
/** Optional max-turns cap (the SDK runs until done otherwise, bounded by maxBudget). */
|
|
37
|
+
maxTurns?: number;
|
|
38
|
+
/** Profile of the hired agent running this job (Sasha/Ross/Nora/etc). null = Clementine. */
|
|
39
|
+
profile?: AgentProfile | null;
|
|
40
|
+
/** Hired-agent registry — passed through to runAgent so subagent delegation works. */
|
|
41
|
+
agentManager?: AgentManager | null;
|
|
42
|
+
/** Memory store for cost logging + skill use tracking. */
|
|
43
|
+
memoryStore?: MemoryStore | null;
|
|
44
|
+
/** Per-job success criteria from CRON.md frontmatter. */
|
|
45
|
+
successCriteria?: string[];
|
|
46
|
+
/** Optional model override (rare — most jobs let the SDK default decide). */
|
|
47
|
+
model?: string;
|
|
48
|
+
/** Optional max-budget override. Default: tier-1 = $1, tier-2+ = $3. */
|
|
49
|
+
maxBudgetUsd?: number;
|
|
50
|
+
/** Optional working directory override (project-scoped jobs). */
|
|
51
|
+
workDir?: string;
|
|
52
|
+
/** Abort signal for cancellation. */
|
|
53
|
+
abortSignal?: AbortSignal;
|
|
54
|
+
/** Post-task hooks (reflection + skill extraction). Pass the
|
|
55
|
+
* PersonalAssistant — it implements both members. Optional so the
|
|
56
|
+
* helper still works in tests without the full assistant graph. */
|
|
57
|
+
postTaskHooks?: CronPostTaskHooks | null;
|
|
58
|
+
}
|
|
59
|
+
export interface RunAgentCronResult extends RunAgentResult {
|
|
60
|
+
/** The final prompt that was sent to the agent (after context injection).
|
|
61
|
+
* Useful for cron diagnostics + debugging. */
|
|
62
|
+
builtPrompt: string;
|
|
63
|
+
/** Diagnostics: which Composio + external servers were live for this run. */
|
|
64
|
+
composioConnected: string[];
|
|
65
|
+
externalConnected: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Run a cron job via the canonical SDK runAgent path.
|
|
69
|
+
*
|
|
70
|
+
* Composes the same context blocks the legacy runCronJob injects
|
|
71
|
+
* (progress, goals, delegation, team, criteria, skills, fanout
|
|
72
|
+
* directive), wires Composio + external MCP via the dedup-aware
|
|
73
|
+
* helper, then calls runAgent.
|
|
74
|
+
*
|
|
75
|
+
* The SDK handles the loop, compaction, subagent fanout, prompt
|
|
76
|
+
* caching, retries — none of which we wrap manually anymore.
|
|
77
|
+
*/
|
|
78
|
+
export declare function runAgentCron(opts: RunAgentCronOptions): Promise<RunAgentCronResult>;
|
|
79
|
+
//# sourceMappingURL=run-agent-cron.d.ts.map
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — runAgent cron wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* Cron jobs need more than a bare runAgent() call: they get progress
|
|
8
|
+
* continuity, linked goals, delegated tasks, team context, success
|
|
9
|
+
* criteria, and matched skills injected ahead of the job prompt. This
|
|
10
|
+
* file owns that composition for the new (canonical SDK) path. The
|
|
11
|
+
* legacy assistant.ts:runCronJob keeps its inline equivalents so we
|
|
12
|
+
* can ship Phase 3 without touching legacy code.
|
|
13
|
+
*
|
|
14
|
+
* After Phase 3 verifies and we collapse the legacy path, the
|
|
15
|
+
* duplicated helpers here become the single source of truth.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import pino from 'pino';
|
|
20
|
+
import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, } from '../config.js';
|
|
21
|
+
import { runAgent } from './run-agent.js';
|
|
22
|
+
import { buildExtraMcpForRunAgent } from './run-agent-mcp.js';
|
|
23
|
+
import { buildFanoutDirectiveForText, buildAlwaysOnParallelizationHint } from './fanout-policy.js';
|
|
24
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
25
|
+
const CRON_PROGRESS_PENDING_MAX_ITEMS = 20;
|
|
26
|
+
const CRON_PROGRESS_NOTES_MAX_CHARS = 2000;
|
|
27
|
+
const logger = pino({ name: 'clementine.run-agent-cron' });
|
|
28
|
+
const CRON_CONTEXT_ITEM_MAX = 80;
|
|
29
|
+
function capContextItem(s) {
|
|
30
|
+
if (!s)
|
|
31
|
+
return '';
|
|
32
|
+
return s.length <= CRON_CONTEXT_ITEM_MAX ? s : s.slice(0, CRON_CONTEXT_ITEM_MAX - 3) + '...';
|
|
33
|
+
}
|
|
34
|
+
function capContextBlock(s, max) {
|
|
35
|
+
if (!s)
|
|
36
|
+
return '';
|
|
37
|
+
return s.length <= max ? s : s.slice(0, max - 3) + '...';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the previous-progress block from the cron progress JSON file.
|
|
41
|
+
* Lets the agent continue where the prior run left off without re-doing
|
|
42
|
+
* work it already completed.
|
|
43
|
+
*/
|
|
44
|
+
function buildProgressContext(jobName) {
|
|
45
|
+
try {
|
|
46
|
+
const safeJob = jobName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
47
|
+
const progressFile = path.join(CRON_PROGRESS_DIR, `${safeJob}.json`);
|
|
48
|
+
if (!fs.existsSync(progressFile))
|
|
49
|
+
return '';
|
|
50
|
+
const progress = JSON.parse(fs.readFileSync(progressFile, 'utf-8'));
|
|
51
|
+
const parts = [`## Previous Progress (run #${progress.runCount}, ${progress.lastRunAt})`];
|
|
52
|
+
if (progress.completedItems?.length > 0) {
|
|
53
|
+
parts.push(`Completed: ${progress.completedItems.slice(-10).map(capContextItem).join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
if (progress.pendingItems?.length > 0) {
|
|
56
|
+
const pendingItems = progress.pendingItems.slice(0, CRON_PROGRESS_PENDING_MAX_ITEMS).map(capContextItem);
|
|
57
|
+
const suffix = progress.pendingItems.length > CRON_PROGRESS_PENDING_MAX_ITEMS
|
|
58
|
+
? ` (${progress.pendingItems.length - CRON_PROGRESS_PENDING_MAX_ITEMS} more omitted)`
|
|
59
|
+
: '';
|
|
60
|
+
parts.push(`Pending: ${pendingItems.join(', ')}${suffix}`);
|
|
61
|
+
}
|
|
62
|
+
if (progress.notes) {
|
|
63
|
+
parts.push(`Notes: ${capContextBlock(progress.notes, CRON_PROGRESS_NOTES_MAX_CHARS)}`);
|
|
64
|
+
}
|
|
65
|
+
return parts.join('\n') + '\n\n' +
|
|
66
|
+
'Continue from where you left off. Use `cron_progress_write` at the end to save what you completed and what\'s pending.\n\n';
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Build the linked-goals block for jobs that contribute to active goals. */
|
|
73
|
+
function buildGoalContext(jobName) {
|
|
74
|
+
try {
|
|
75
|
+
const linkedGoals = listAllGoals()
|
|
76
|
+
.map(({ goal }) => goal)
|
|
77
|
+
.filter(g => g && g.status === 'active' && g.linkedCronJobs?.includes(jobName));
|
|
78
|
+
if (linkedGoals.length === 0)
|
|
79
|
+
return '';
|
|
80
|
+
const goalLines = linkedGoals.map(g => {
|
|
81
|
+
const goalRecord = g;
|
|
82
|
+
const nextAct = goalRecord.nextActions?.length ? ` Next: ${goalRecord.nextActions[0]}` : '';
|
|
83
|
+
const recentProgress = goalRecord.progressNotes?.length
|
|
84
|
+
? ` Last progress: ${goalRecord.progressNotes[goalRecord.progressNotes.length - 1]}`
|
|
85
|
+
: '';
|
|
86
|
+
return `- **${goalRecord.title}** (${goalRecord.id}): ${goalRecord.description.slice(0, 100)}${nextAct}${recentProgress}`;
|
|
87
|
+
});
|
|
88
|
+
return `## Active Goals Linked to This Job\n${goalLines.join('\n')}\n\n` +
|
|
89
|
+
'After completing your work, update goal progress with `goal_update` if you made meaningful progress.\n\n';
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Build the delegated-tasks block for hired agents that have pending team requests. */
|
|
96
|
+
function buildDelegationContext(agentSlug) {
|
|
97
|
+
if (!agentSlug)
|
|
98
|
+
return '';
|
|
99
|
+
try {
|
|
100
|
+
const tasksDir = path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'tasks');
|
|
101
|
+
if (!fs.existsSync(tasksDir))
|
|
102
|
+
return '';
|
|
103
|
+
const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
|
|
104
|
+
const pendingTasks = taskFiles
|
|
105
|
+
.map(f => {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(fs.readFileSync(path.join(tasksDir, f), 'utf-8'));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.filter((t) => t !== null && t.status === 'pending');
|
|
114
|
+
if (pendingTasks.length === 0)
|
|
115
|
+
return '';
|
|
116
|
+
const taskLines = pendingTasks.map(t => `- [${t.id}] From ${t.fromAgent}: ${t.task.slice(0, 150)} (expected: ${t.expectedOutput.slice(0, 80)})`);
|
|
117
|
+
return `## Delegated Tasks Waiting\n${taskLines.join('\n')}\n\n` +
|
|
118
|
+
'Work on these delegated tasks in addition to your scheduled task. ' +
|
|
119
|
+
'Mark them in_progress/completed by editing the task JSON when done.\n\n';
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Build the team-comms block: pending requests + recent messages for this agent. */
|
|
126
|
+
function buildTeamContext(agentSlug) {
|
|
127
|
+
if (!agentSlug)
|
|
128
|
+
return '';
|
|
129
|
+
try {
|
|
130
|
+
const teamLogPath = path.join(BASE_DIR, 'logs', 'team-comms.jsonl');
|
|
131
|
+
if (!fs.existsSync(teamLogPath))
|
|
132
|
+
return '';
|
|
133
|
+
const teamLines = fs.readFileSync(teamLogPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
134
|
+
const recentForAgent = teamLines
|
|
135
|
+
.slice(-50)
|
|
136
|
+
.map(l => { try {
|
|
137
|
+
return JSON.parse(l);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
} })
|
|
142
|
+
.filter((m) => m !== null && (m.toAgent === agentSlug || m.fromAgent === agentSlug))
|
|
143
|
+
.slice(-5);
|
|
144
|
+
const pendingRequests = recentForAgent.filter(m => m.protocol === 'request' && m.toAgent === agentSlug && !m.response);
|
|
145
|
+
if (pendingRequests.length === 0 && recentForAgent.length === 0)
|
|
146
|
+
return '';
|
|
147
|
+
const parts = ['## Team Context'];
|
|
148
|
+
if (pendingRequests.length > 0) {
|
|
149
|
+
parts.push('### REPLY NEEDED — Pending Requests');
|
|
150
|
+
for (const r of pendingRequests) {
|
|
151
|
+
parts.push(`- From ${r.fromAgent}: ${r.content.slice(0, 200)}`);
|
|
152
|
+
}
|
|
153
|
+
parts.push('Address these requests before your main task.');
|
|
154
|
+
}
|
|
155
|
+
if (recentForAgent.length > 0) {
|
|
156
|
+
parts.push('### Recent Team Messages');
|
|
157
|
+
for (const m of recentForAgent) {
|
|
158
|
+
const dir = m.fromAgent === agentSlug ? 'sent to' : 'from';
|
|
159
|
+
const other = m.fromAgent === agentSlug ? m.toAgent : m.fromAgent;
|
|
160
|
+
parts.push(`- ${dir} ${other}: ${m.content.slice(0, 100)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return parts.join('\n') + '\n\n';
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Build the success-criteria block from job spec. */
|
|
170
|
+
function buildCriteriaContext(successCriteria) {
|
|
171
|
+
if (!successCriteria?.length)
|
|
172
|
+
return '';
|
|
173
|
+
return `## Success Criteria\nYour output will be verified against these criteria:\n` +
|
|
174
|
+
successCriteria.map(c => `- ${c}`).join('\n') + '\n\n';
|
|
175
|
+
}
|
|
176
|
+
/** Build the matched-skills block (procedures learned from prior successful runs). */
|
|
177
|
+
async function buildSkillContext(jobName, jobPrompt, agentSlug, memoryStore) {
|
|
178
|
+
try {
|
|
179
|
+
const { searchSkills, recordSkillUse } = await import('./skill-extractor.js');
|
|
180
|
+
const skillQuery = jobName + ' ' + jobPrompt.slice(0, 200);
|
|
181
|
+
const suppressedNamesRaw = memoryStore
|
|
182
|
+
?.getSkillsToSuppress?.(agentSlug);
|
|
183
|
+
const suppressedNames = Array.isArray(suppressedNamesRaw)
|
|
184
|
+
? new Set(suppressedNamesRaw)
|
|
185
|
+
: (suppressedNamesRaw ?? undefined);
|
|
186
|
+
const matchedSkills = searchSkills(skillQuery, 2, agentSlug, { suppressedNames });
|
|
187
|
+
if (matchedSkills.length === 0)
|
|
188
|
+
return '';
|
|
189
|
+
const skillLines = matchedSkills.map(s => {
|
|
190
|
+
recordSkillUse(s.name);
|
|
191
|
+
memoryStore?.logSkillUse?.({
|
|
192
|
+
skillName: s.name,
|
|
193
|
+
sessionKey: `cron:${agentSlug ?? 'clementine'}:${jobName}`,
|
|
194
|
+
queryText: skillQuery,
|
|
195
|
+
score: s.score,
|
|
196
|
+
agentSlug: agentSlug ?? null,
|
|
197
|
+
});
|
|
198
|
+
let block = `### ${s.title}\n${s.content}`;
|
|
199
|
+
if (s.toolsUsed.length > 0)
|
|
200
|
+
block += `\n**Tools:** ${s.toolsUsed.join(', ')}`;
|
|
201
|
+
if (s.attachments.length > 0) {
|
|
202
|
+
const attDir = path.join(s.skillDir, s.name + '.files');
|
|
203
|
+
for (const attName of s.attachments.slice(0, 3)) {
|
|
204
|
+
const attPath = path.join(attDir, attName);
|
|
205
|
+
if (fs.existsSync(attPath)) {
|
|
206
|
+
try {
|
|
207
|
+
const content = fs.readFileSync(attPath, 'utf-8').slice(0, 2000);
|
|
208
|
+
block += `\n#### ${attName}\n\`\`\`\n${content}\n\`\`\``;
|
|
209
|
+
}
|
|
210
|
+
catch { /* skip */ }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return block;
|
|
215
|
+
});
|
|
216
|
+
return `## Learned Procedures (from past successful executions)\nFollow these proven approaches when applicable:\n\n${skillLines.join('\n\n')}\n\n`;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Run a cron job via the canonical SDK runAgent path.
|
|
224
|
+
*
|
|
225
|
+
* Composes the same context blocks the legacy runCronJob injects
|
|
226
|
+
* (progress, goals, delegation, team, criteria, skills, fanout
|
|
227
|
+
* directive), wires Composio + external MCP via the dedup-aware
|
|
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.
|
|
232
|
+
*/
|
|
233
|
+
export async function runAgentCron(opts) {
|
|
234
|
+
const tier = opts.tier ?? 1;
|
|
235
|
+
const agentSlug = opts.profile?.slug;
|
|
236
|
+
const ownerName = process.env.OWNER_NAME ?? 'the user';
|
|
237
|
+
// ── Compose context blocks (mirrors legacy runCronJob) ─────────────
|
|
238
|
+
const progressContext = buildProgressContext(opts.jobName);
|
|
239
|
+
const goalContext = buildGoalContext(opts.jobName);
|
|
240
|
+
const delegationContext = buildDelegationContext(agentSlug);
|
|
241
|
+
const teamContext = buildTeamContext(agentSlug);
|
|
242
|
+
const criteriaContext = buildCriteriaContext(opts.successCriteria);
|
|
243
|
+
const skillContext = await buildSkillContext(opts.jobName, opts.jobPrompt, agentSlug, opts.memoryStore);
|
|
244
|
+
// Sub-agent fanout directive — when the job description suggests
|
|
245
|
+
// multi-item work, prepend a hard-line fanout mandate.
|
|
246
|
+
const fanoutScope = `${opts.jobName}\n${opts.jobPrompt}\n${opts.profile?.description ?? ''}\n${opts.profile?.systemPromptBody ?? ''}`;
|
|
247
|
+
const { directive: fanoutDirective, report: fanoutReport } = buildFanoutDirectiveForText(fanoutScope);
|
|
248
|
+
if (fanoutReport.needsFanout) {
|
|
249
|
+
logger.info({
|
|
250
|
+
job: opts.jobName,
|
|
251
|
+
signals: fanoutReport.signals.map(s => s.pattern),
|
|
252
|
+
}, 'runAgentCron: fanout directive injected');
|
|
253
|
+
}
|
|
254
|
+
// Final prompt (matches legacy runCronJob's structure).
|
|
255
|
+
const builtPrompt = `[Scheduled task: ${opts.jobName}]\n\n` +
|
|
256
|
+
(fanoutDirective ? fanoutDirective + '\n\n' : '') +
|
|
257
|
+
buildAlwaysOnParallelizationHint() + '\n\n' +
|
|
258
|
+
progressContext +
|
|
259
|
+
goalContext +
|
|
260
|
+
skillContext +
|
|
261
|
+
delegationContext +
|
|
262
|
+
teamContext +
|
|
263
|
+
criteriaContext +
|
|
264
|
+
`${opts.jobPrompt}\n\n` +
|
|
265
|
+
`## How to respond\n` +
|
|
266
|
+
`You're sending this directly to ${ownerName} as a DM. ` +
|
|
267
|
+
`Write like you're texting a friend — casual, warm, concise. ` +
|
|
268
|
+
`Use their name naturally. No headers, bullet lists, or formal structure unless the content genuinely needs it. ` +
|
|
269
|
+
`Skip narrating your process ("I checked X, then Y..."). Just share the interesting stuff.\n\n` +
|
|
270
|
+
`If there's genuinely nothing worth mentioning (no new data, no changes, no alerts), ` +
|
|
271
|
+
`output ONLY: __NOTHING__\n` +
|
|
272
|
+
`But lean toward sharing something — a one-liner is better than silence. ` +
|
|
273
|
+
`"Quiet morning, inbox is clean" beats __NOTHING__ if you did check things.\n\n` +
|
|
274
|
+
`After finishing your work, you MUST write a final text response with your findings — ` +
|
|
275
|
+
`only that final message gets delivered.`;
|
|
276
|
+
// ── Wire Composio + external MCP servers (same dedup as legacy) ───
|
|
277
|
+
const mcp = await buildExtraMcpForRunAgent({
|
|
278
|
+
scopeText: [
|
|
279
|
+
opts.jobName,
|
|
280
|
+
opts.jobPrompt,
|
|
281
|
+
opts.profile?.description,
|
|
282
|
+
opts.profile?.systemPromptBody,
|
|
283
|
+
].filter(Boolean).join('\n\n'),
|
|
284
|
+
profile: opts.profile,
|
|
285
|
+
});
|
|
286
|
+
// ── Run via canonical runAgent ────────────────────────────────────
|
|
287
|
+
const maxBudget = opts.maxBudgetUsd ?? (tier >= 2 ? 3.0 : 1.0);
|
|
288
|
+
const effort = tier >= 2 ? 'high' : 'medium';
|
|
289
|
+
logger.info({
|
|
290
|
+
job: opts.jobName,
|
|
291
|
+
tier,
|
|
292
|
+
profile: agentSlug,
|
|
293
|
+
composioConnected: mcp.composioConnected,
|
|
294
|
+
externalConnected: mcp.externalConnected,
|
|
295
|
+
droppedClaudeAi: mcp.droppedClaudeAi,
|
|
296
|
+
droppedComposio: mcp.droppedComposio,
|
|
297
|
+
promptChars: builtPrompt.length,
|
|
298
|
+
}, 'runAgentCron: dispatching to runAgent');
|
|
299
|
+
const startedAt = Date.now();
|
|
300
|
+
const result = await runAgent(builtPrompt, {
|
|
301
|
+
sessionKey: `cron:${opts.jobName}`,
|
|
302
|
+
source: 'cron',
|
|
303
|
+
profile: opts.profile,
|
|
304
|
+
agentManager: opts.agentManager,
|
|
305
|
+
memoryStore: opts.memoryStore,
|
|
306
|
+
model: opts.model,
|
|
307
|
+
effort,
|
|
308
|
+
maxBudgetUsd: maxBudget,
|
|
309
|
+
maxTurns: opts.maxTurns,
|
|
310
|
+
abortSignal: opts.abortSignal,
|
|
311
|
+
extraMcpServers: mcp.servers,
|
|
312
|
+
});
|
|
313
|
+
// ── Post-task hooks: reflection + skill extraction ────────────────
|
|
314
|
+
// Both fire-and-forget — never block the cron deliverable on these.
|
|
315
|
+
// They are the same passes the legacy runCronJob fires; without them
|
|
316
|
+
// the new path would lose the success-grading + procedural-memory
|
|
317
|
+
// growth that makes Clementine self-improving.
|
|
318
|
+
const deliverable = result.text ?? '';
|
|
319
|
+
if (opts.postTaskHooks && deliverable && deliverable.trim() !== '__NOTHING__') {
|
|
320
|
+
const durationMs = Date.now() - startedAt;
|
|
321
|
+
opts.postTaskHooks
|
|
322
|
+
.triggerCronReflection(opts.jobName, opts.jobPrompt, deliverable, opts.successCriteria)
|
|
323
|
+
.catch(err => logger.debug({ err, job: opts.jobName }, 'runAgentCron: reflection failed (non-fatal)'));
|
|
324
|
+
opts.postTaskHooks
|
|
325
|
+
.triggerSkillExtractionFromExecution('cron', opts.jobName, opts.jobPrompt, deliverable, durationMs, agentSlug)
|
|
326
|
+
.catch(err => logger.debug({ err, job: opts.jobName }, 'runAgentCron: skill extraction failed (non-fatal)'));
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
...result,
|
|
330
|
+
builtPrompt,
|
|
331
|
+
composioConnected: mcp.composioConnected,
|
|
332
|
+
externalConnected: mcp.externalConnected,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=run-agent-cron.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentProfile } from '../types.js';
|
|
2
|
+
import type { MemoryStore } from '../memory/store.js';
|
|
3
|
+
import { type RunAgentResult } from './run-agent.js';
|
|
4
|
+
export interface RunAgentHeartbeatOptions {
|
|
5
|
+
standingInstructions: string;
|
|
6
|
+
changesSummary?: string;
|
|
7
|
+
timeContext?: string;
|
|
8
|
+
dedupContext?: string;
|
|
9
|
+
profile?: AgentProfile | null;
|
|
10
|
+
memoryStore?: MemoryStore | null;
|
|
11
|
+
abortSignal?: AbortSignal;
|
|
12
|
+
/** Optional model override — defaults to Haiku (cheapest, fastest). */
|
|
13
|
+
model?: string;
|
|
14
|
+
/** Optional budget override — defaults to $0.15 (heartbeats are 1 turn). */
|
|
15
|
+
maxBudgetUsd?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run a heartbeat decision via the canonical SDK runAgent path.
|
|
19
|
+
*
|
|
20
|
+
* No tools. No MCP. Single turn. The agent looks at the context
|
|
21
|
+
* blocks, decides, emits text, returns.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runAgentHeartbeat(opts: RunAgentHeartbeatOptions): Promise<RunAgentResult>;
|
|
24
|
+
//# sourceMappingURL=run-agent-heartbeat.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — runAgent heartbeat wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* Heartbeats are tool-free decision-makers. They look at standing
|
|
8
|
+
* instructions, what changed, and the time of day, and decide whether
|
|
9
|
+
* there's anything worth flagging to the owner. Output is plain text;
|
|
10
|
+
* no MCP servers, no Composio toolkits, no subagents.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the legacy assistant.heartbeat() prompt shape exactly so the
|
|
13
|
+
* voice/dedup behavior stays identical, but routes the actual LLM call
|
|
14
|
+
* through the canonical runAgent() instead of buildOptions+query.
|
|
15
|
+
*/
|
|
16
|
+
import pino from 'pino';
|
|
17
|
+
import { OWNER_NAME, MODELS, } from '../config.js';
|
|
18
|
+
const OWNER = OWNER_NAME || 'the user';
|
|
19
|
+
function formatDate(d) {
|
|
20
|
+
return d.toLocaleDateString('en-US', {
|
|
21
|
+
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function formatTime(d) {
|
|
25
|
+
return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
26
|
+
}
|
|
27
|
+
import { runAgent } from './run-agent.js';
|
|
28
|
+
const logger = pino({ name: 'clementine.run-agent-heartbeat' });
|
|
29
|
+
/**
|
|
30
|
+
* Run a heartbeat decision via the canonical SDK runAgent path.
|
|
31
|
+
*
|
|
32
|
+
* No tools. No MCP. Single turn. The agent looks at the context
|
|
33
|
+
* blocks, decides, emits text, returns.
|
|
34
|
+
*/
|
|
35
|
+
export async function runAgentHeartbeat(opts) {
|
|
36
|
+
const now = new Date();
|
|
37
|
+
const localTime = formatTime(now);
|
|
38
|
+
const localDate = formatDate(now);
|
|
39
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
40
|
+
const owner = OWNER;
|
|
41
|
+
const agentName = opts.profile?.name ?? 'personal assistant';
|
|
42
|
+
const promptParts = [
|
|
43
|
+
`[Heartbeat — ${localTime}, ${localDate} (${tz})]`,
|
|
44
|
+
`You're ${agentName}, casually checking in with ${owner}. Talk like a teammate — not a system.`,
|
|
45
|
+
`Do NOT call any tools. Everything you need is in the context below. ` +
|
|
46
|
+
`If you notice something that would need a tool to investigate or act on, just mention it conversationally and ask ${owner} if he wants you to look into it.`,
|
|
47
|
+
];
|
|
48
|
+
if (opts.dedupContext) {
|
|
49
|
+
promptParts.push(`\n${opts.dedupContext}\n\nIf all of the above are unchanged, respond with exactly: __NOTHING__`);
|
|
50
|
+
}
|
|
51
|
+
if (opts.timeContext) {
|
|
52
|
+
promptParts.push(`\nTime of day: ${opts.timeContext}`);
|
|
53
|
+
}
|
|
54
|
+
if (opts.changesSummary) {
|
|
55
|
+
promptParts.push(`\nWhat's new:\n${opts.changesSummary}`);
|
|
56
|
+
}
|
|
57
|
+
promptParts.push(`\nIf nothing changed, respond with exactly: __NOTHING__\n` +
|
|
58
|
+
`Otherwise, keep it casual and brief (1-3 sentences). No bullet lists, no formal reports, no repeating info from previous check-ins. ` +
|
|
59
|
+
`Only mention what's genuinely new or worth flagging. Be a person, not a dashboard. ` +
|
|
60
|
+
`Tag topics with [topic: key] for dedup tracking.\n\n` +
|
|
61
|
+
`Standing instructions:\n${opts.standingInstructions}`);
|
|
62
|
+
const prompt = promptParts.join('\n');
|
|
63
|
+
logger.info({
|
|
64
|
+
agentName,
|
|
65
|
+
profile: opts.profile?.slug,
|
|
66
|
+
promptChars: prompt.length,
|
|
67
|
+
}, 'runAgentHeartbeat: dispatching to runAgent (no tools)');
|
|
68
|
+
return runAgent(prompt, {
|
|
69
|
+
sessionKey: `heartbeat:${opts.profile?.slug ?? 'clementine'}`,
|
|
70
|
+
source: 'heartbeat',
|
|
71
|
+
profile: opts.profile,
|
|
72
|
+
memoryStore: opts.memoryStore,
|
|
73
|
+
model: opts.model ?? MODELS.haiku,
|
|
74
|
+
effort: 'low',
|
|
75
|
+
maxBudgetUsd: opts.maxBudgetUsd ?? 0.15,
|
|
76
|
+
maxTurns: 1,
|
|
77
|
+
// No tools — heartbeats are decision-only. Empty list bypasses the
|
|
78
|
+
// CORE_TOOLS_FOR_AGENT_PARENT default and stops the SDK from
|
|
79
|
+
// exposing any tool schemas, keeping the prompt small.
|
|
80
|
+
allowedTools: [],
|
|
81
|
+
abortSignal: opts.abortSignal,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=run-agent-heartbeat.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — MCP server wiring helper for runAgent.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* runAgent always wires the Clementine MCP server. This helper builds
|
|
8
|
+
* the EXTRA MCP servers callers may need (Composio toolkits, external
|
|
9
|
+
* claude.ai integrations, browser-harness, etc.), applying the same
|
|
10
|
+
* service dedup logic from 1.18.34 so we don't load duplicates of the
|
|
11
|
+
* same service from two providers.
|
|
12
|
+
*
|
|
13
|
+
* The legacy assistant.ts:buildOptions path does roughly the same
|
|
14
|
+
* thing for chat/cron — this is the standalone equivalent.
|
|
15
|
+
*/
|
|
16
|
+
import type { AgentProfile } from '../types.js';
|
|
17
|
+
export interface BuildExtraMcpOptions {
|
|
18
|
+
/** The text we're routing on (job prompt, user message, etc). Used to pick which
|
|
19
|
+
* Composio toolkits + external servers are relevant via the existing bundle router. */
|
|
20
|
+
scopeText?: string;
|
|
21
|
+
/** Active agent profile. When set, profile.allowedMcpServers and
|
|
22
|
+
* profile.allowedComposioToolkits override the bundle router's choices. */
|
|
23
|
+
profile?: AgentProfile | null;
|
|
24
|
+
/** When true, build the FULL surface (no bundle filtering, no dedup).
|
|
25
|
+
* Used by admin/debug callers; not the cron-path default. */
|
|
26
|
+
fullSurface?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface BuildExtraMcpResult {
|
|
29
|
+
/** Map of additional MCP servers to merge into runAgent's mcpServers. */
|
|
30
|
+
servers: Record<string, Record<string, unknown>>;
|
|
31
|
+
/** Diagnostics: which Composio toolkits / external servers ended up live. */
|
|
32
|
+
composioConnected: string[];
|
|
33
|
+
externalConnected: string[];
|
|
34
|
+
/** Diagnostics: which services were de-duped (we kept Composio over claude.ai etc). */
|
|
35
|
+
droppedClaudeAi: string[];
|
|
36
|
+
droppedComposio: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the extra MCP servers (Composio + external) for a runAgent call.
|
|
40
|
+
*
|
|
41
|
+
* Mirrors the legacy assistant.ts:buildOptions chain but as a standalone
|
|
42
|
+
* function so the runAgent path doesn't depend on PersonalAssistant
|
|
43
|
+
* instance state.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildExtraMcpForRunAgent(opts?: BuildExtraMcpOptions): Promise<BuildExtraMcpResult>;
|
|
46
|
+
//# sourceMappingURL=run-agent-mcp.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — MCP server wiring helper for runAgent.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* runAgent always wires the Clementine MCP server. This helper builds
|
|
8
|
+
* the EXTRA MCP servers callers may need (Composio toolkits, external
|
|
9
|
+
* claude.ai integrations, browser-harness, etc.), applying the same
|
|
10
|
+
* service dedup logic from 1.18.34 so we don't load duplicates of the
|
|
11
|
+
* same service from two providers.
|
|
12
|
+
*
|
|
13
|
+
* The legacy assistant.ts:buildOptions path does roughly the same
|
|
14
|
+
* thing for chat/cron — this is the standalone equivalent.
|
|
15
|
+
*/
|
|
16
|
+
import pino from 'pino';
|
|
17
|
+
import { routeToolSurface, applyServiceDedup } from './tool-router.js';
|
|
18
|
+
import { loadClaudeIntegrations, getMcpServersForAgent } from './mcp-bridge.js';
|
|
19
|
+
import { loadToolPreferences, KNOWN_SERVICES } from '../integrations/tool-preferences.js';
|
|
20
|
+
const logger = pino({ name: 'clementine.run-agent-mcp' });
|
|
21
|
+
/**
|
|
22
|
+
* Build the extra MCP servers (Composio + external) for a runAgent call.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors the legacy assistant.ts:buildOptions chain but as a standalone
|
|
25
|
+
* function so the runAgent path doesn't depend on PersonalAssistant
|
|
26
|
+
* instance state.
|
|
27
|
+
*/
|
|
28
|
+
export async function buildExtraMcpForRunAgent(opts = {}) {
|
|
29
|
+
const result = {
|
|
30
|
+
servers: {},
|
|
31
|
+
composioConnected: [],
|
|
32
|
+
externalConnected: [],
|
|
33
|
+
droppedClaudeAi: [],
|
|
34
|
+
droppedComposio: [],
|
|
35
|
+
};
|
|
36
|
+
// 1. Route the tool surface based on scope text.
|
|
37
|
+
let route = opts.fullSurface
|
|
38
|
+
? {
|
|
39
|
+
bundles: [],
|
|
40
|
+
externalMcpServers: undefined,
|
|
41
|
+
composioToolkits: undefined,
|
|
42
|
+
inheritFullClaudeEnv: true,
|
|
43
|
+
fullSurface: true,
|
|
44
|
+
reason: 'full_surface',
|
|
45
|
+
}
|
|
46
|
+
: routeToolSurface(opts.scopeText ?? '');
|
|
47
|
+
// 2. Build Composio MCP servers, honoring profile allowlist when set.
|
|
48
|
+
let composioMcpServers = {};
|
|
49
|
+
try {
|
|
50
|
+
const { buildComposioMcpServers } = await import('../integrations/composio/mcp-bridge.js');
|
|
51
|
+
const profileAllowList = opts.profile?.allowedComposioToolkits;
|
|
52
|
+
const composioAllow = Array.isArray(profileAllowList)
|
|
53
|
+
? profileAllowList
|
|
54
|
+
: route.composioToolkits;
|
|
55
|
+
composioMcpServers = await buildComposioMcpServers(composioAllow);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
logger.debug({ err }, 'Composio MCP servers unavailable (non-fatal)');
|
|
59
|
+
}
|
|
60
|
+
// 3. Build external MCP servers (claude.ai integrations, etc).
|
|
61
|
+
let externalMcpServers = {};
|
|
62
|
+
try {
|
|
63
|
+
const profileAllowList = Array.isArray(opts.profile?.allowedMcpServers)
|
|
64
|
+
? opts.profile?.allowedMcpServers
|
|
65
|
+
: undefined;
|
|
66
|
+
const externalAllow = profileAllowList ?? route.externalMcpServers;
|
|
67
|
+
externalMcpServers = getMcpServersForAgent(externalAllow);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
logger.debug({ err }, 'External MCP servers unavailable (non-fatal)');
|
|
71
|
+
}
|
|
72
|
+
// 4. Apply service dedup so Composio outlook + claude.ai Microsoft 365
|
|
73
|
+
// don't both load (same logic as 1.18.34).
|
|
74
|
+
if (!opts.fullSurface) {
|
|
75
|
+
try {
|
|
76
|
+
const composioConnected = new Set(Object.keys(composioMcpServers));
|
|
77
|
+
const cdIntegrations = loadClaudeIntegrations();
|
|
78
|
+
const claudeDesktopActive = new Set(Object.values(cdIntegrations).filter(i => i.connected).map(i => i.name));
|
|
79
|
+
const prefs = loadToolPreferences();
|
|
80
|
+
const dedup = applyServiceDedup(route, {
|
|
81
|
+
composioConnected,
|
|
82
|
+
claudeDesktopActive,
|
|
83
|
+
preferences: prefs.preferences,
|
|
84
|
+
knownServices: KNOWN_SERVICES,
|
|
85
|
+
});
|
|
86
|
+
route = dedup.route;
|
|
87
|
+
result.droppedClaudeAi = dedup.droppedClaudeAi;
|
|
88
|
+
result.droppedComposio = dedup.droppedComposio;
|
|
89
|
+
for (const name of dedup.droppedClaudeAi)
|
|
90
|
+
delete externalMcpServers[name];
|
|
91
|
+
for (const slug of dedup.droppedComposio)
|
|
92
|
+
delete composioMcpServers[slug];
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
logger.debug({ err }, 'Service dedup failed (non-fatal — using full surface)');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 5. Merge into one map.
|
|
99
|
+
result.servers = { ...externalMcpServers, ...composioMcpServers };
|
|
100
|
+
result.composioConnected = Object.keys(composioMcpServers);
|
|
101
|
+
result.externalConnected = Object.keys(externalMcpServers);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=run-agent-mcp.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AgentProfile } from '../types.js';
|
|
2
|
+
import type { AgentManager } from './agent-manager.js';
|
|
3
|
+
import type { MemoryStore } from '../memory/store.js';
|
|
4
|
+
import { type RunAgentResult } from './run-agent.js';
|
|
5
|
+
export interface RunAgentTeamTaskOptions {
|
|
6
|
+
fromName: string;
|
|
7
|
+
fromSlug: string;
|
|
8
|
+
content: string;
|
|
9
|
+
profile: AgentProfile;
|
|
10
|
+
agentManager?: AgentManager | null;
|
|
11
|
+
memoryStore?: MemoryStore | null;
|
|
12
|
+
abortSignal?: AbortSignal;
|
|
13
|
+
/** Optional model override. Default: SDK default (Sonnet). */
|
|
14
|
+
model?: string;
|
|
15
|
+
/** Optional max-budget override. Default: $1.50 (more than cron because team tasks are
|
|
16
|
+
* often ad-hoc and may need more research/tool calls). */
|
|
17
|
+
maxBudgetUsd?: number;
|
|
18
|
+
/** Optional max-turns cap. Default: undefined (SDK runs until done, bounded by budget). */
|
|
19
|
+
maxTurns?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RunAgentTeamTaskResult extends RunAgentResult {
|
|
22
|
+
builtPrompt: string;
|
|
23
|
+
composioConnected: string[];
|
|
24
|
+
externalConnected: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function runAgentTeamTask(opts: RunAgentTeamTaskOptions): Promise<RunAgentTeamTaskResult>;
|
|
27
|
+
//# sourceMappingURL=run-agent-team-task.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — runAgent team-task wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* A "team task" is one hired agent (or Clementine herself) sending a
|
|
8
|
+
* direct message to another agent. The recipient processes it
|
|
9
|
+
* autonomously — same toolset as cron, plus Composio + external MCP.
|
|
10
|
+
*
|
|
11
|
+
* Legacy `assistant.runTeamTask` ran a 10-phase loop with deadlines,
|
|
12
|
+
* stall guards, manual session resume, and a "recovery" phase. The
|
|
13
|
+
* canonical pattern is one runAgent call with a generous budget — the
|
|
14
|
+
* SDK owns the inner loop, compaction, and retry. Phases were a
|
|
15
|
+
* pre-SDK workaround; we don't need them anymore.
|
|
16
|
+
*/
|
|
17
|
+
import pino from 'pino';
|
|
18
|
+
import { runAgent } from './run-agent.js';
|
|
19
|
+
import { buildExtraMcpForRunAgent } from './run-agent-mcp.js';
|
|
20
|
+
const logger = pino({ name: 'clementine.run-agent-team-task' });
|
|
21
|
+
export async function runAgentTeamTask(opts) {
|
|
22
|
+
const taskName = `team-msg:${opts.fromSlug}-to-${opts.profile.slug}`;
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const timestamp = now.toISOString().slice(0, 16).replace('T', ' ');
|
|
25
|
+
// Match the legacy phase-1 prompt shape so existing agent training
|
|
26
|
+
// (Sasha/Ross/Nora) keeps responding the same way. Phases 2+ are no
|
|
27
|
+
// longer needed — the SDK keeps the conversation in one session.
|
|
28
|
+
const builtPrompt = `[TEAM MESSAGE from ${opts.fromName} (${opts.fromSlug}) — ${timestamp}]\n\n` +
|
|
29
|
+
`You received a direct message from a teammate. Process it fully and autonomously.\n\n` +
|
|
30
|
+
`MESSAGE:\n${opts.content}\n\n` +
|
|
31
|
+
`IMPORTANT:\n` +
|
|
32
|
+
`- Complete the full task described in the message\n` +
|
|
33
|
+
`- Use all tools available to you — Salesforce, DataForSEO, Discord, etc.\n` +
|
|
34
|
+
`- Post results to Discord channels as instructed\n` +
|
|
35
|
+
`- When finished, output "TASK_COMPLETE:" followed by a brief summary of what you did`;
|
|
36
|
+
const mcp = await buildExtraMcpForRunAgent({
|
|
37
|
+
scopeText: [taskName, opts.content, opts.profile.description, opts.profile.systemPromptBody]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join('\n\n'),
|
|
40
|
+
profile: opts.profile,
|
|
41
|
+
});
|
|
42
|
+
logger.info({
|
|
43
|
+
taskName,
|
|
44
|
+
fromSlug: opts.fromSlug,
|
|
45
|
+
toSlug: opts.profile.slug,
|
|
46
|
+
composioConnected: mcp.composioConnected,
|
|
47
|
+
externalConnected: mcp.externalConnected,
|
|
48
|
+
droppedClaudeAi: mcp.droppedClaudeAi,
|
|
49
|
+
droppedComposio: mcp.droppedComposio,
|
|
50
|
+
promptChars: builtPrompt.length,
|
|
51
|
+
}, 'runAgentTeamTask: dispatching to runAgent');
|
|
52
|
+
const result = await runAgent(builtPrompt, {
|
|
53
|
+
sessionKey: `team-task:${opts.fromSlug}->${opts.profile.slug}`,
|
|
54
|
+
source: 'team-task',
|
|
55
|
+
profile: opts.profile,
|
|
56
|
+
agentManager: opts.agentManager,
|
|
57
|
+
memoryStore: opts.memoryStore,
|
|
58
|
+
model: opts.model,
|
|
59
|
+
effort: 'medium',
|
|
60
|
+
maxBudgetUsd: opts.maxBudgetUsd ?? 1.50,
|
|
61
|
+
maxTurns: opts.maxTurns,
|
|
62
|
+
abortSignal: opts.abortSignal,
|
|
63
|
+
extraMcpServers: mcp.servers,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
...result,
|
|
67
|
+
builtPrompt,
|
|
68
|
+
composioConnected: mcp.composioConnected,
|
|
69
|
+
externalConnected: mcp.externalConnected,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=run-agent-team-task.js.map
|
package/dist/gateway/router.js
CHANGED
|
@@ -2578,6 +2578,42 @@ export class Gateway {
|
|
|
2578
2578
|
events.emit('heartbeat:start', { agent, timestamp: Date.now() });
|
|
2579
2579
|
const hbStart = Date.now();
|
|
2580
2580
|
try {
|
|
2581
|
+
// ── Phase 4: opt-in canonical SDK heartbeat path ──────────────
|
|
2582
|
+
// CLEMENTINE_USE_RUNAGENT_HEARTBEAT=1 routes through
|
|
2583
|
+
// runAgentHeartbeat (no tools, Haiku, single turn). Default OFF;
|
|
2584
|
+
// falls back to legacy on error.
|
|
2585
|
+
const useRunAgentHeartbeat = process.env.CLEMENTINE_USE_RUNAGENT_HEARTBEAT === '1';
|
|
2586
|
+
if (useRunAgentHeartbeat) {
|
|
2587
|
+
try {
|
|
2588
|
+
const { runAgentHeartbeat } = await import('../agent/run-agent-heartbeat.js');
|
|
2589
|
+
logger.info({ agent, path: 'runagent_heartbeat' }, 'Phase 4: routing heartbeat through runAgentHeartbeat');
|
|
2590
|
+
const result = await runAgentHeartbeat({
|
|
2591
|
+
standingInstructions,
|
|
2592
|
+
changesSummary,
|
|
2593
|
+
timeContext,
|
|
2594
|
+
dedupContext,
|
|
2595
|
+
profile,
|
|
2596
|
+
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
2597
|
+
});
|
|
2598
|
+
scanner.refreshIntegrity();
|
|
2599
|
+
events.emit('heartbeat:complete', {
|
|
2600
|
+
agent,
|
|
2601
|
+
durationMs: Date.now() - hbStart,
|
|
2602
|
+
responseLength: result.text?.length ?? 0,
|
|
2603
|
+
});
|
|
2604
|
+
logger.info({
|
|
2605
|
+
agent,
|
|
2606
|
+
cost: Number(result.totalCostUsd.toFixed(4)),
|
|
2607
|
+
numTurns: result.numTurns,
|
|
2608
|
+
durationMs: Date.now() - hbStart,
|
|
2609
|
+
}, 'runAgentHeartbeat: heartbeat complete');
|
|
2610
|
+
return result.text;
|
|
2611
|
+
}
|
|
2612
|
+
catch (err) {
|
|
2613
|
+
logger.warn({ err, agent }, 'runAgentHeartbeat path failed — falling back to legacy heartbeat path');
|
|
2614
|
+
// Fall through to legacy.
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2581
2617
|
const response = await this.assistant.heartbeat(standingInstructions, changesSummary, timeContext, dedupContext, profile);
|
|
2582
2618
|
// Re-baseline integrity checksums after heartbeat (may write to vault)
|
|
2583
2619
|
scanner.refreshIntegrity();
|
|
@@ -2602,6 +2638,57 @@ export class Gateway {
|
|
|
2602
2638
|
const cronStart = Date.now();
|
|
2603
2639
|
try {
|
|
2604
2640
|
let response;
|
|
2641
|
+
// ── Phase 3: opt-in canonical SDK cron path ──────────────────
|
|
2642
|
+
// CLEMENTINE_USE_RUNAGENT_CRON=1 routes the job through
|
|
2643
|
+
// runAgentCron() — the canonical SDK pattern. Default OFF.
|
|
2644
|
+
// Falls back to legacy on error so the job always completes.
|
|
2645
|
+
const useRunAgentCron = process.env.CLEMENTINE_USE_RUNAGENT_CRON === '1';
|
|
2646
|
+
if (useRunAgentCron && !opts?.disableAllTools) {
|
|
2647
|
+
try {
|
|
2648
|
+
const { runAgentCron } = await import('../agent/run-agent-cron.js');
|
|
2649
|
+
const profile = agentSlug && agentSlug !== 'clementine'
|
|
2650
|
+
? this.getAgentManager().get(agentSlug) ?? null
|
|
2651
|
+
: null;
|
|
2652
|
+
logger.info({ jobName, agentSlug, tier, path: 'runagent_cron' }, 'Phase 3: routing cron through runAgentCron');
|
|
2653
|
+
const cronResult = await runAgentCron({
|
|
2654
|
+
jobName,
|
|
2655
|
+
jobPrompt,
|
|
2656
|
+
tier,
|
|
2657
|
+
maxTurns,
|
|
2658
|
+
profile,
|
|
2659
|
+
agentManager: this.getAgentManager(),
|
|
2660
|
+
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
2661
|
+
successCriteria,
|
|
2662
|
+
model,
|
|
2663
|
+
workDir,
|
|
2664
|
+
// Phase 4: post-task hooks restore reflection + skill
|
|
2665
|
+
// extraction on the new cron path. The PersonalAssistant
|
|
2666
|
+
// implements both members directly.
|
|
2667
|
+
postTaskHooks: this.assistant,
|
|
2668
|
+
});
|
|
2669
|
+
response = cronResult.text;
|
|
2670
|
+
scanner.refreshIntegrity();
|
|
2671
|
+
events.emit('cron:complete', {
|
|
2672
|
+
jobName,
|
|
2673
|
+
mode: 'runagent',
|
|
2674
|
+
durationMs: Date.now() - cronStart,
|
|
2675
|
+
responseLength: response?.length ?? 0,
|
|
2676
|
+
});
|
|
2677
|
+
logger.info({
|
|
2678
|
+
jobName,
|
|
2679
|
+
cost: Number(cronResult.totalCostUsd.toFixed(4)),
|
|
2680
|
+
numTurns: cronResult.numTurns,
|
|
2681
|
+
composioConnected: cronResult.composioConnected.length,
|
|
2682
|
+
externalConnected: cronResult.externalConnected.length,
|
|
2683
|
+
durationMs: Date.now() - cronStart,
|
|
2684
|
+
}, 'runAgentCron: cron job complete');
|
|
2685
|
+
return response;
|
|
2686
|
+
}
|
|
2687
|
+
catch (err) {
|
|
2688
|
+
logger.warn({ err, jobName }, 'runAgentCron path failed — falling back to legacy cron path');
|
|
2689
|
+
// Fall through to legacy below.
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2605
2692
|
if (mode === 'unleashed') {
|
|
2606
2693
|
response = await this.assistant.runUnleashedTask(jobName, jobPrompt, tier, maxTurns, model, workDir, maxHours, agentSlug);
|
|
2607
2694
|
}
|
|
@@ -2633,6 +2720,48 @@ export class Gateway {
|
|
|
2633
2720
|
const releaseLane = await lanes.acquire('cron');
|
|
2634
2721
|
try {
|
|
2635
2722
|
logger.info({ fromSlug, toSlug: profile.slug }, 'Running team message as autonomous task');
|
|
2723
|
+
// ── Phase 4: opt-in canonical SDK team-task path ───────────────
|
|
2724
|
+
// CLEMENTINE_USE_RUNAGENT_TEAM=1 routes through runAgentTeamTask
|
|
2725
|
+
// (one runAgent call, SDK owns the loop — no phase wrapper).
|
|
2726
|
+
// Default OFF; falls back to legacy on error.
|
|
2727
|
+
const useRunAgentTeam = process.env.CLEMENTINE_USE_RUNAGENT_TEAM === '1';
|
|
2728
|
+
if (useRunAgentTeam) {
|
|
2729
|
+
try {
|
|
2730
|
+
const { runAgentTeamTask } = await import('../agent/run-agent-team-task.js');
|
|
2731
|
+
logger.info({ fromSlug, toSlug: profile.slug, path: 'runagent_team_task' }, 'Phase 4: routing team task through runAgentTeamTask');
|
|
2732
|
+
const result = await runAgentTeamTask({
|
|
2733
|
+
fromName,
|
|
2734
|
+
fromSlug,
|
|
2735
|
+
content,
|
|
2736
|
+
profile,
|
|
2737
|
+
agentManager: this.getAgentManager(),
|
|
2738
|
+
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
2739
|
+
abortSignal: abortController?.signal,
|
|
2740
|
+
});
|
|
2741
|
+
scanner.refreshIntegrity();
|
|
2742
|
+
logger.info({
|
|
2743
|
+
fromSlug,
|
|
2744
|
+
toSlug: profile.slug,
|
|
2745
|
+
cost: Number(result.totalCostUsd.toFixed(4)),
|
|
2746
|
+
numTurns: result.numTurns,
|
|
2747
|
+
composioConnected: result.composioConnected.length,
|
|
2748
|
+
}, 'runAgentTeamTask: team task complete');
|
|
2749
|
+
// Best-effort streaming: if a callback is provided, deliver
|
|
2750
|
+
// the final text in one chunk (the SDK already streamed it
|
|
2751
|
+
// internally to runAgent's onText, but we collected it).
|
|
2752
|
+
if (onText && result.text) {
|
|
2753
|
+
try {
|
|
2754
|
+
onText(result.text);
|
|
2755
|
+
}
|
|
2756
|
+
catch { /* ignore */ }
|
|
2757
|
+
}
|
|
2758
|
+
return result.text;
|
|
2759
|
+
}
|
|
2760
|
+
catch (err) {
|
|
2761
|
+
logger.warn({ err, fromSlug, toSlug: profile.slug }, 'runAgentTeamTask path failed — falling back to legacy team-task path');
|
|
2762
|
+
// Fall through to legacy.
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2636
2765
|
const response = await this.assistant.runTeamTask(fromName, fromSlug, content, profile, onText, abortController);
|
|
2637
2766
|
scanner.refreshIntegrity();
|
|
2638
2767
|
return response;
|