clementine-agent 1.18.46 → 1.18.48
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 -2
- package/dist/agent/assistant.js +21 -31
- package/dist/agent/run-agent-cron.d.ts +12 -0
- package/dist/agent/run-agent-cron.js +16 -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-team-task.d.ts +27 -0
- package/dist/agent/run-agent-team-task.js +72 -0
- package/dist/gateway/router.d.ts +0 -1
- package/dist/gateway/router.js +94 -75
- package/package.json +1 -1
|
@@ -29,8 +29,6 @@ import { type ToolsetName } from './toolsets.js';
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function estimateTokens(text: string): number;
|
|
31
31
|
export declare function looksLikeContextThrashText(value: unknown): boolean;
|
|
32
|
-
export declare function contextThrashRecoveryNotice(): string;
|
|
33
|
-
export declare function buildContextThrashRecoveryPrompt(userRequest: string, priorFailureText?: string): string;
|
|
34
32
|
/** Format a millisecond duration as a human-friendly "X ago" string. */
|
|
35
33
|
export declare function formatTimeAgo(ms: number): string;
|
|
36
34
|
export declare function scrubInternalContextBlocks(text: string): string;
|
|
@@ -284,6 +282,19 @@ export declare class PersonalAssistant {
|
|
|
284
282
|
* having to recreate the surrounding plumbing.
|
|
285
283
|
*/
|
|
286
284
|
triggerMemoryExtractionPostExchange(userMessage: string, assistantResponse: string, sessionKey?: string, profile?: AgentProfile): Promise<void>;
|
|
285
|
+
/**
|
|
286
|
+
* Public entry point for the post-cron quality reflection. Used by
|
|
287
|
+
* the new runAgentCron path (Phase 4) to keep the existing Haiku
|
|
288
|
+
* verification pass + cron-progress bridge without duplicating it.
|
|
289
|
+
* Always best-effort — failures are swallowed to never block.
|
|
290
|
+
*/
|
|
291
|
+
triggerCronReflection(jobName: string, jobPrompt: string, deliverable: string, successCriteria?: string[]): Promise<void>;
|
|
292
|
+
/**
|
|
293
|
+
* Public entry point for procedural-memory skill extraction after a
|
|
294
|
+
* successful execution. Used by the new runAgentCron path (Phase 4)
|
|
295
|
+
* so the new code path keeps growing the skills library.
|
|
296
|
+
*/
|
|
297
|
+
triggerSkillExtractionFromExecution(source: 'unleashed' | 'cron' | 'chat', jobName: string, prompt: string, output: string, durationMs: number, agentSlug?: string): Promise<void>;
|
|
287
298
|
private spawnMemoryExtraction;
|
|
288
299
|
private static readonly MEMORY_TOOL_NAMES;
|
|
289
300
|
private extractMemory;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -200,33 +200,6 @@ class UnleashedTaskFailedError extends Error {
|
|
|
200
200
|
this.name = 'UnleashedTaskFailedError';
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
export function contextThrashRecoveryNotice() {
|
|
204
|
-
return [
|
|
205
|
-
'I hit a context-size recovery issue while working on that.',
|
|
206
|
-
'I saved the request and reset the session so I can continue with smaller reads instead of repeating the same large-output path.',
|
|
207
|
-
].join(' ');
|
|
208
|
-
}
|
|
209
|
-
export function buildContextThrashRecoveryPrompt(userRequest, priorFailureText = '') {
|
|
210
|
-
const parts = [
|
|
211
|
-
'[CONTEXT-THRASH RECOVERY]',
|
|
212
|
-
'',
|
|
213
|
-
'The previous interactive attempt failed because tool output filled the context window and SDK autocompact thrashed. Continue the user request, but use a small diagnostic pass.',
|
|
214
|
-
'',
|
|
215
|
-
'User request:',
|
|
216
|
-
userRequest,
|
|
217
|
-
'',
|
|
218
|
-
'Recovery rules:',
|
|
219
|
-
'- Do not repeat broad reads, full log dumps, full JSON dumps, or unbounded API/list commands.',
|
|
220
|
-
'- Prefer status files, summaries, indexes, `rg`, `tail -80`, `head -80`, and `sed -n` slices.',
|
|
221
|
-
'- For cron or unleashed jobs, inspect only `status.json`, the tail of `progress.jsonl`, and the latest run preview first. Do not read full run logs unless a short slice identifies the exact file and range.',
|
|
222
|
-
'- Preserve the user intent. Identify what failed, what you changed or verified, and the next action.',
|
|
223
|
-
'- Finish with `TASK_COMPLETE:` followed by a concise user-facing summary.',
|
|
224
|
-
];
|
|
225
|
-
if (priorFailureText.trim()) {
|
|
226
|
-
parts.push('', 'Prior failure excerpt:', priorFailureText.trim().slice(0, 1200));
|
|
227
|
-
}
|
|
228
|
-
return parts.join('\n');
|
|
229
|
-
}
|
|
230
203
|
/**
|
|
231
204
|
* Strip lone Unicode surrogates (U+D800–U+DFFF) from a string so it can be
|
|
232
205
|
* safely serialized to JSON. Lone surrogates are valid in JS strings but
|
|
@@ -3893,7 +3866,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3893
3866
|
responseText = '';
|
|
3894
3867
|
continue;
|
|
3895
3868
|
}
|
|
3896
|
-
responseText = responseText ||
|
|
3869
|
+
responseText = responseText || 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
|
|
3897
3870
|
}
|
|
3898
3871
|
else if (errStr.includes('prompt is too long') || errStr.includes('prompt too long') || errStr.includes('context_length')) {
|
|
3899
3872
|
responseText = responseText || ('The conversation got too large to process (tool responses filled the context window). ' +
|
|
@@ -3941,7 +3914,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3941
3914
|
responseText = '';
|
|
3942
3915
|
if (contextRecovery) {
|
|
3943
3916
|
if (contextRecoveryRetries >= 1) {
|
|
3944
|
-
responseText =
|
|
3917
|
+
responseText = 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
|
|
3945
3918
|
staleSession = false;
|
|
3946
3919
|
contextRecovery = false;
|
|
3947
3920
|
}
|
|
@@ -3958,7 +3931,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3958
3931
|
}
|
|
3959
3932
|
}
|
|
3960
3933
|
if (staleSession && contextRecovery && !responseText.trim()) {
|
|
3961
|
-
responseText =
|
|
3934
|
+
responseText = 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
|
|
3962
3935
|
}
|
|
3963
3936
|
if (hitRateLimit && attempt < PersonalAssistant.RATE_LIMIT_MAX_RETRIES) {
|
|
3964
3937
|
const base = rateLimitRetryAfterMs
|
|
@@ -3992,7 +3965,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3992
3965
|
responseText = '';
|
|
3993
3966
|
continue;
|
|
3994
3967
|
}
|
|
3995
|
-
responseText =
|
|
3968
|
+
responseText = 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
|
|
3996
3969
|
}
|
|
3997
3970
|
if (looksLikeNoResponseRequested(responseText)) {
|
|
3998
3971
|
logger.warn({ sessionKey, attempt }, 'SDK/model returned no-response sentinel during interactive chat');
|
|
@@ -4892,6 +4865,23 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4892
4865
|
async triggerMemoryExtractionPostExchange(userMessage, assistantResponse, sessionKey, profile) {
|
|
4893
4866
|
return this.spawnMemoryExtraction(userMessage, assistantResponse, sessionKey, profile);
|
|
4894
4867
|
}
|
|
4868
|
+
/**
|
|
4869
|
+
* Public entry point for the post-cron quality reflection. Used by
|
|
4870
|
+
* the new runAgentCron path (Phase 4) to keep the existing Haiku
|
|
4871
|
+
* verification pass + cron-progress bridge without duplicating it.
|
|
4872
|
+
* Always best-effort — failures are swallowed to never block.
|
|
4873
|
+
*/
|
|
4874
|
+
async triggerCronReflection(jobName, jobPrompt, deliverable, successCriteria) {
|
|
4875
|
+
return this.runCronReflection(jobName, jobPrompt, deliverable, successCriteria);
|
|
4876
|
+
}
|
|
4877
|
+
/**
|
|
4878
|
+
* Public entry point for procedural-memory skill extraction after a
|
|
4879
|
+
* successful execution. Used by the new runAgentCron path (Phase 4)
|
|
4880
|
+
* so the new code path keeps growing the skills library.
|
|
4881
|
+
*/
|
|
4882
|
+
async triggerSkillExtractionFromExecution(source, jobName, prompt, output, durationMs, agentSlug) {
|
|
4883
|
+
return this.extractSkillFromExecution(source, jobName, prompt, output, durationMs, agentSlug);
|
|
4884
|
+
}
|
|
4895
4885
|
async spawnMemoryExtraction(userMessage, assistantResponse, sessionKey, profile) {
|
|
4896
4886
|
// Guard: skip memory extraction if the user message looks like injection
|
|
4897
4887
|
const memScan = scanner.scan(userMessage);
|
|
@@ -18,6 +18,14 @@ 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
|
+
/** 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
|
+
}
|
|
21
29
|
export interface RunAgentCronOptions {
|
|
22
30
|
/** Job name from CRON.md. Used for telemetry, progress lookup, skill match. */
|
|
23
31
|
jobName: string;
|
|
@@ -43,6 +51,10 @@ export interface RunAgentCronOptions {
|
|
|
43
51
|
workDir?: string;
|
|
44
52
|
/** Abort signal for cancellation. */
|
|
45
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;
|
|
46
58
|
}
|
|
47
59
|
export interface RunAgentCronResult extends RunAgentResult {
|
|
48
60
|
/** The final prompt that was sent to the agent (after context injection).
|
|
@@ -296,6 +296,7 @@ export async function runAgentCron(opts) {
|
|
|
296
296
|
droppedComposio: mcp.droppedComposio,
|
|
297
297
|
promptChars: builtPrompt.length,
|
|
298
298
|
}, 'runAgentCron: dispatching to runAgent');
|
|
299
|
+
const startedAt = Date.now();
|
|
299
300
|
const result = await runAgent(builtPrompt, {
|
|
300
301
|
sessionKey: `cron:${opts.jobName}`,
|
|
301
302
|
source: 'cron',
|
|
@@ -309,6 +310,21 @@ export async function runAgentCron(opts) {
|
|
|
309
310
|
abortSignal: opts.abortSignal,
|
|
310
311
|
extraMcpServers: mcp.servers,
|
|
311
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
|
+
}
|
|
312
328
|
return {
|
|
313
329
|
...result,
|
|
314
330
|
builtPrompt,
|
|
@@ -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,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.d.ts
CHANGED
|
@@ -84,7 +84,6 @@ export declare class Gateway {
|
|
|
84
84
|
*/
|
|
85
85
|
private _deliverDeepResult;
|
|
86
86
|
private startInteractiveBackgroundTask;
|
|
87
|
-
private startContextThrashRecovery;
|
|
88
87
|
/**
|
|
89
88
|
* For Clementine-owned sessions, classify whether the message should be
|
|
90
89
|
* delegated to a specialist agent. Returns null when routing isn't
|
package/dist/gateway/router.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import pino from 'pino';
|
|
10
|
-
import {
|
|
10
|
+
import { isAutonomousNothingOutput, looksLikeProviderApiErrorResponse, oneMillionContextRecoveryMessage, PersonalAssistant, } from '../agent/assistant.js';
|
|
11
11
|
import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
|
|
12
12
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
13
13
|
import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
|
|
@@ -59,7 +59,7 @@ export function classifyChatError(err) {
|
|
|
59
59
|
return 'rate_limit';
|
|
60
60
|
if (looksLikeClaudeOneMillionContextError(msg))
|
|
61
61
|
return 'one_million_context';
|
|
62
|
-
if (
|
|
62
|
+
if (/context.?length|token.?limit|maximum.?context|prompt.?too.?long|rapid_refill_breaker|autocompact|context.?refilled/i.test(msg))
|
|
63
63
|
return 'context_overflow';
|
|
64
64
|
if (/\b401\b|\b403\b|auth|forbidden|invalid.?api.?key|permission|does not have access|please run \/login/i.test(msg))
|
|
65
65
|
return 'auth';
|
|
@@ -839,46 +839,6 @@ export class Gateway {
|
|
|
839
839
|
return opts.ack
|
|
840
840
|
?? `On it — running this in the background. I'll follow up when it's done. Task ${task.id}. Reply "status" to check in or "cancel" to stop.`;
|
|
841
841
|
}
|
|
842
|
-
startContextThrashRecovery(sessionKey, text, priorFailureText, details = {}) {
|
|
843
|
-
const currentSess = this.getSession(sessionKey);
|
|
844
|
-
const jobName = `recovery-${Date.now()}`;
|
|
845
|
-
currentSess.deepTask = {
|
|
846
|
-
jobName,
|
|
847
|
-
taskDesc: `Recover after context overflow: ${text.slice(0, 160)}`,
|
|
848
|
-
startedAt: new Date().toISOString(),
|
|
849
|
-
};
|
|
850
|
-
const agentSlug = this._agentSlugFromSessionKey(sessionKey);
|
|
851
|
-
this.recordInteractiveFailure(sessionKey, text, priorFailureText, 'context_thrash', {
|
|
852
|
-
jobName,
|
|
853
|
-
...details,
|
|
854
|
-
});
|
|
855
|
-
this.assistant.runUnleashedTask(jobName, buildContextThrashRecoveryPrompt(text, priorFailureText), 2, undefined, undefined, undefined, 1, agentSlug).then(async (result) => {
|
|
856
|
-
if (this.sessions.get(sessionKey)?.deepTask?.jobName !== jobName) {
|
|
857
|
-
logger.info({ sessionKey, jobName }, 'Context-thrash recovery resolved after cancellation/replacement; suppressing follow-up');
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
logger.info({ sessionKey, jobName, resultLen: result?.length ?? 0 }, 'Context-thrash recovery completed');
|
|
861
|
-
if (result && !isAutonomousNothingOutput(result)) {
|
|
862
|
-
this.assistant.injectPendingContext(sessionKey, text, result);
|
|
863
|
-
await this._deliverDeepResult(sessionKey, `[CONTEXT_THRASH_RECOVERY_RESULT] You just completed the smaller recovery pass. Summarize the result conversationally and briefly. Lead with whether the original request is fixed, still blocked, or needs approval.\n\nOriginal request: ${text.slice(0, 500)}\n\nResult:\n${result.slice(0, 3000)}`, result);
|
|
864
|
-
}
|
|
865
|
-
}).catch(async (err) => {
|
|
866
|
-
if (this.sessions.get(sessionKey)?.deepTask?.jobName !== jobName) {
|
|
867
|
-
logger.info({ sessionKey, jobName }, 'Context-thrash recovery failed after cancellation/replacement; suppressing failure follow-up');
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
logger.error({ err, sessionKey, jobName }, 'Context-thrash recovery failed');
|
|
871
|
-
this.recordInteractiveFailure(sessionKey, text, err, 'context_thrash_recovery_failed', { jobName });
|
|
872
|
-
const failMsg = `Recovery pass failed: ${String(err).slice(0, 200)}`;
|
|
873
|
-
this.assistant.injectPendingContext(sessionKey, text, failMsg);
|
|
874
|
-
await this._deliverDeepResult(sessionKey, `[CONTEXT_THRASH_RECOVERY_RESULT] The smaller recovery pass failed: ${failMsg}. Tell the user briefly and suggest checking status/log slices, not full logs.`, failMsg);
|
|
875
|
-
}).finally(() => {
|
|
876
|
-
const s = this.sessions.get(sessionKey);
|
|
877
|
-
if (s?.deepTask?.jobName === jobName)
|
|
878
|
-
delete s.deepTask;
|
|
879
|
-
});
|
|
880
|
-
return `${contextThrashRecoveryNotice()} I restarted it as a smaller background recovery pass and will follow up here.`;
|
|
881
|
-
}
|
|
882
842
|
/**
|
|
883
843
|
* For Clementine-owned sessions, classify whether the message should be
|
|
884
844
|
* delegated to a specialist agent. Returns null when routing isn't
|
|
@@ -2088,20 +2048,12 @@ export class Gateway {
|
|
|
2088
2048
|
delete sessState.pendingInterrupt;
|
|
2089
2049
|
}
|
|
2090
2050
|
try {
|
|
2091
|
-
// ── Phase
|
|
2092
|
-
//
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
//
|
|
2096
|
-
|
|
2097
|
-
// Clementine MCP server — Composio/external integrations
|
|
2098
|
-
// come in Phase 3. Useful for testing the new path on
|
|
2099
|
-
// tool-light sessions like cron-fix or memory queries.
|
|
2100
|
-
//
|
|
2101
|
-
// The legacy path (default) keeps full Composio/external
|
|
2102
|
-
// routing + all post-response handlers, so this flag is
|
|
2103
|
-
// safe to leave off until we're ready.
|
|
2104
|
-
if (process.env.CLEMENTINE_USE_RUNAGENT_CHAT === '1'
|
|
2051
|
+
// ── Phase 5: canonical SDK chat path is now DEFAULT ──────────
|
|
2052
|
+
// The new runAgent() wrapper is the canonical path. Set
|
|
2053
|
+
// CLEMENTINE_USE_RUNAGENT_CHAT=0 to fall back to legacy.
|
|
2054
|
+
// The legacy path remains as the in-process error fallback
|
|
2055
|
+
// when runAgent throws.
|
|
2056
|
+
if (process.env.CLEMENTINE_USE_RUNAGENT_CHAT !== '0'
|
|
2105
2057
|
&& this.isTrustedPersonalSession(sessionKey)
|
|
2106
2058
|
&& !sessState.pendingInterrupt) {
|
|
2107
2059
|
const { runAgent } = await import('../agent/run-agent.js');
|
|
@@ -2351,13 +2303,6 @@ export class Gateway {
|
|
|
2351
2303
|
this.clearSession(effectiveSessionKey);
|
|
2352
2304
|
return "Claude returned a provider API error instead of a normal answer. I've reset this session so the error does not get replayed into future context. Please try that question again.";
|
|
2353
2305
|
}
|
|
2354
|
-
if (response && looksLikeContextThrashText(response)) {
|
|
2355
|
-
logger.warn({ sessionKey, responsePreview: response.slice(0, 200) }, 'Context-thrash text returned from assistant — starting recovery pass');
|
|
2356
|
-
return this.startContextThrashRecovery(sessionKey, text, response, {
|
|
2357
|
-
toolActivityCount,
|
|
2358
|
-
source: 'assistant_response',
|
|
2359
|
-
});
|
|
2360
|
-
}
|
|
2361
2306
|
// ── Auto-plan detection ──────────────────────────────────────
|
|
2362
2307
|
// If the agent signals a complex task, auto-route to the orchestrator
|
|
2363
2308
|
const planMatch = response?.match(/^\[PLAN_NEEDED:\s*(.+?)\]\s*/);
|
|
@@ -2480,13 +2425,6 @@ export class Gateway {
|
|
|
2480
2425
|
if (chatAc.signal.aborted) {
|
|
2481
2426
|
return "Stopped. What would you like to do instead?";
|
|
2482
2427
|
}
|
|
2483
|
-
if (looksLikeContextThrashText(err)) {
|
|
2484
|
-
logger.warn({ sessionKey, err: String(err).slice(0, 300) }, 'Context-thrash exception — starting recovery pass');
|
|
2485
|
-
return this.startContextThrashRecovery(sessionKey, text, String(err), {
|
|
2486
|
-
toolActivityCount,
|
|
2487
|
-
source: 'exception',
|
|
2488
|
-
});
|
|
2489
|
-
}
|
|
2490
2428
|
// ── Max turns hit — auto-escalate to deep mode instead of failing silently ──
|
|
2491
2429
|
// This is the #1 cause of "agent stops responding": it ran out of turns
|
|
2492
2430
|
// exploring files, the SDK throws, and the user gets nothing.
|
|
@@ -2578,6 +2516,42 @@ export class Gateway {
|
|
|
2578
2516
|
events.emit('heartbeat:start', { agent, timestamp: Date.now() });
|
|
2579
2517
|
const hbStart = Date.now();
|
|
2580
2518
|
try {
|
|
2519
|
+
// ── Phase 5: canonical SDK heartbeat path is now DEFAULT ──────
|
|
2520
|
+
// runAgentHeartbeat is the canonical path (no tools, Haiku,
|
|
2521
|
+
// single turn). Set CLEMENTINE_USE_RUNAGENT_HEARTBEAT=0 to
|
|
2522
|
+
// fall back to legacy.
|
|
2523
|
+
const useRunAgentHeartbeat = process.env.CLEMENTINE_USE_RUNAGENT_HEARTBEAT !== '0';
|
|
2524
|
+
if (useRunAgentHeartbeat) {
|
|
2525
|
+
try {
|
|
2526
|
+
const { runAgentHeartbeat } = await import('../agent/run-agent-heartbeat.js');
|
|
2527
|
+
logger.info({ agent, path: 'runagent_heartbeat' }, 'Phase 4: routing heartbeat through runAgentHeartbeat');
|
|
2528
|
+
const result = await runAgentHeartbeat({
|
|
2529
|
+
standingInstructions,
|
|
2530
|
+
changesSummary,
|
|
2531
|
+
timeContext,
|
|
2532
|
+
dedupContext,
|
|
2533
|
+
profile,
|
|
2534
|
+
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
2535
|
+
});
|
|
2536
|
+
scanner.refreshIntegrity();
|
|
2537
|
+
events.emit('heartbeat:complete', {
|
|
2538
|
+
agent,
|
|
2539
|
+
durationMs: Date.now() - hbStart,
|
|
2540
|
+
responseLength: result.text?.length ?? 0,
|
|
2541
|
+
});
|
|
2542
|
+
logger.info({
|
|
2543
|
+
agent,
|
|
2544
|
+
cost: Number(result.totalCostUsd.toFixed(4)),
|
|
2545
|
+
numTurns: result.numTurns,
|
|
2546
|
+
durationMs: Date.now() - hbStart,
|
|
2547
|
+
}, 'runAgentHeartbeat: heartbeat complete');
|
|
2548
|
+
return result.text;
|
|
2549
|
+
}
|
|
2550
|
+
catch (err) {
|
|
2551
|
+
logger.warn({ err, agent }, 'runAgentHeartbeat path failed — falling back to legacy heartbeat path');
|
|
2552
|
+
// Fall through to legacy.
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2581
2555
|
const response = await this.assistant.heartbeat(standingInstructions, changesSummary, timeContext, dedupContext, profile);
|
|
2582
2556
|
// Re-baseline integrity checksums after heartbeat (may write to vault)
|
|
2583
2557
|
scanner.refreshIntegrity();
|
|
@@ -2602,11 +2576,10 @@ export class Gateway {
|
|
|
2602
2576
|
const cronStart = Date.now();
|
|
2603
2577
|
try {
|
|
2604
2578
|
let response;
|
|
2605
|
-
// ── Phase
|
|
2606
|
-
//
|
|
2607
|
-
//
|
|
2608
|
-
|
|
2609
|
-
const useRunAgentCron = process.env.CLEMENTINE_USE_RUNAGENT_CRON === '1';
|
|
2579
|
+
// ── Phase 5: canonical SDK cron path is now DEFAULT ──────────
|
|
2580
|
+
// runAgentCron() is the canonical path. Set
|
|
2581
|
+
// CLEMENTINE_USE_RUNAGENT_CRON=0 to fall back to legacy.
|
|
2582
|
+
const useRunAgentCron = process.env.CLEMENTINE_USE_RUNAGENT_CRON !== '0';
|
|
2610
2583
|
if (useRunAgentCron && !opts?.disableAllTools) {
|
|
2611
2584
|
try {
|
|
2612
2585
|
const { runAgentCron } = await import('../agent/run-agent-cron.js');
|
|
@@ -2625,6 +2598,10 @@ export class Gateway {
|
|
|
2625
2598
|
successCriteria,
|
|
2626
2599
|
model,
|
|
2627
2600
|
workDir,
|
|
2601
|
+
// Phase 4: post-task hooks restore reflection + skill
|
|
2602
|
+
// extraction on the new cron path. The PersonalAssistant
|
|
2603
|
+
// implements both members directly.
|
|
2604
|
+
postTaskHooks: this.assistant,
|
|
2628
2605
|
});
|
|
2629
2606
|
response = cronResult.text;
|
|
2630
2607
|
scanner.refreshIntegrity();
|
|
@@ -2680,6 +2657,48 @@ export class Gateway {
|
|
|
2680
2657
|
const releaseLane = await lanes.acquire('cron');
|
|
2681
2658
|
try {
|
|
2682
2659
|
logger.info({ fromSlug, toSlug: profile.slug }, 'Running team message as autonomous task');
|
|
2660
|
+
// ── Phase 5: canonical SDK team-task path is now DEFAULT ───────
|
|
2661
|
+
// runAgentTeamTask is the canonical path (one runAgent call —
|
|
2662
|
+
// SDK owns the inner loop). Set CLEMENTINE_USE_RUNAGENT_TEAM=0
|
|
2663
|
+
// to fall back to legacy.
|
|
2664
|
+
const useRunAgentTeam = process.env.CLEMENTINE_USE_RUNAGENT_TEAM !== '0';
|
|
2665
|
+
if (useRunAgentTeam) {
|
|
2666
|
+
try {
|
|
2667
|
+
const { runAgentTeamTask } = await import('../agent/run-agent-team-task.js');
|
|
2668
|
+
logger.info({ fromSlug, toSlug: profile.slug, path: 'runagent_team_task' }, 'Phase 4: routing team task through runAgentTeamTask');
|
|
2669
|
+
const result = await runAgentTeamTask({
|
|
2670
|
+
fromName,
|
|
2671
|
+
fromSlug,
|
|
2672
|
+
content,
|
|
2673
|
+
profile,
|
|
2674
|
+
agentManager: this.getAgentManager(),
|
|
2675
|
+
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
2676
|
+
abortSignal: abortController?.signal,
|
|
2677
|
+
});
|
|
2678
|
+
scanner.refreshIntegrity();
|
|
2679
|
+
logger.info({
|
|
2680
|
+
fromSlug,
|
|
2681
|
+
toSlug: profile.slug,
|
|
2682
|
+
cost: Number(result.totalCostUsd.toFixed(4)),
|
|
2683
|
+
numTurns: result.numTurns,
|
|
2684
|
+
composioConnected: result.composioConnected.length,
|
|
2685
|
+
}, 'runAgentTeamTask: team task complete');
|
|
2686
|
+
// Best-effort streaming: if a callback is provided, deliver
|
|
2687
|
+
// the final text in one chunk (the SDK already streamed it
|
|
2688
|
+
// internally to runAgent's onText, but we collected it).
|
|
2689
|
+
if (onText && result.text) {
|
|
2690
|
+
try {
|
|
2691
|
+
onText(result.text);
|
|
2692
|
+
}
|
|
2693
|
+
catch { /* ignore */ }
|
|
2694
|
+
}
|
|
2695
|
+
return result.text;
|
|
2696
|
+
}
|
|
2697
|
+
catch (err) {
|
|
2698
|
+
logger.warn({ err, fromSlug, toSlug: profile.slug }, 'runAgentTeamTask path failed — falling back to legacy team-task path');
|
|
2699
|
+
// Fall through to legacy.
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2683
2702
|
const response = await this.assistant.runTeamTask(fromName, fromSlug, content, profile, onText, abortController);
|
|
2684
2703
|
scanner.refreshIntegrity();
|
|
2685
2704
|
return response;
|