clementine-agent 1.18.46 → 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.
@@ -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;
@@ -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);
@@ -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
@@ -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();
@@ -2625,6 +2661,10 @@ export class Gateway {
2625
2661
  successCriteria,
2626
2662
  model,
2627
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,
2628
2668
  });
2629
2669
  response = cronResult.text;
2630
2670
  scanner.refreshIntegrity();
@@ -2680,6 +2720,48 @@ export class Gateway {
2680
2720
  const releaseLane = await lanes.acquire('cron');
2681
2721
  try {
2682
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
+ }
2683
2765
  const response = await this.assistant.runTeamTask(fromName, fromSlug, content, profile, onText, abortController);
2684
2766
  scanner.refreshIntegrity();
2685
2767
  return response;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.46",
3
+ "version": "1.18.47",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",