clementine-agent 1.18.0 → 1.18.2

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.
@@ -267,6 +267,7 @@ export declare class PersonalAssistant {
267
267
  schema: Record<string, unknown>;
268
268
  };
269
269
  delegateProfile?: AgentProfile;
270
+ abortSignal?: AbortSignal;
270
271
  }): Promise<string>;
271
272
  runCronJob(jobName: string, jobPrompt: string, tier?: number, maxTurns?: number, model?: string, workDir?: string, timeoutMs?: number, successCriteria?: string[], agentSlug?: string, opts?: {
272
273
  disableAllTools?: boolean;
@@ -1578,11 +1578,17 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1578
1578
  // every turn regardless.
1579
1579
  if (!isAutonomous) {
1580
1580
  try {
1581
- const { loadToolPreferences, computeAvailability, buildPromptInstruction } = require('../integrations/tool-preferences.js');
1581
+ const { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock } = require('../integrations/tool-preferences.js');
1582
1582
  const { loadClaudeIntegrations } = require('./mcp-bridge.js');
1583
1583
  const composioSet = new Set(composioConnectedSlugs);
1584
1584
  const cdIntegrations = loadClaudeIntegrations();
1585
1585
  const cdActive = new Set(Object.values(cdIntegrations).filter(i => i.connected).map(i => i.name));
1586
+ // Status block first — gives the model ground truth that Composio
1587
+ // is configured and which toolkits are live, so it stops guessing
1588
+ // whether `mcp__<slug>__*` tools are Composio or something else.
1589
+ const statusBlock = buildComposioStatusBlock(composioConnectedSlugs);
1590
+ if (statusBlock)
1591
+ volatileParts.push(statusBlock);
1586
1592
  const prefs = loadToolPreferences();
1587
1593
  const availability = computeAvailability(composioSet, cdActive, prefs.preferences);
1588
1594
  const instruction = buildPromptInstruction(availability, prefs.preferences);
@@ -4069,10 +4075,19 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
4069
4075
  }
4070
4076
  // ── Plan Step Execution ───────────────────────────────────────────
4071
4077
  async runPlanStep(stepId, prompt, opts = {}) {
4072
- const { tier = 2, maxTurns = 15, model, disableTools = false, outputFormat, delegateProfile } = opts;
4078
+ const { tier = 2, maxTurns = 15, model, disableTools = false, outputFormat, delegateProfile, abortSignal } = opts;
4073
4079
  // Don't mutate the global — pass source through the closure instead
4074
4080
  // Per-step stall guard so concurrent steps don't cross-contaminate
4075
4081
  const stepGuard = new StallGuard();
4082
+ // Per-step AbortController, mirroring the parent signal so the orchestrator
4083
+ // (or gateway, via the session AC) can stop in-flight SDK streams.
4084
+ const stepAc = new AbortController();
4085
+ if (abortSignal) {
4086
+ if (abortSignal.aborted)
4087
+ stepAc.abort(abortSignal.reason);
4088
+ else
4089
+ abortSignal.addEventListener('abort', () => stepAc.abort(abortSignal.reason), { once: true });
4090
+ }
4076
4091
  const sdkOptions = await this.buildOptions({
4077
4092
  isHeartbeat: false,
4078
4093
  cronTier: tier,
@@ -4085,6 +4100,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
4085
4100
  outputFormat,
4086
4101
  stallGuard: stepGuard,
4087
4102
  profile: delegateProfile ?? null,
4103
+ abortController: stepAc,
4088
4104
  });
4089
4105
  const trace = [];
4090
4106
  const stream = query({ prompt, options: sdkOptions });
@@ -39,6 +39,7 @@ export declare class PlanOrchestrator {
39
39
  private startTime;
40
40
  private stateId;
41
41
  private agentProfiles;
42
+ private abortSignal?;
42
43
  constructor(assistant: PersonalAssistant);
43
44
  /** Fire-and-forget autonomy telemetry — must never throw. */
44
45
  private logAutonomy;
@@ -48,8 +49,12 @@ export declare class PlanOrchestrator {
48
49
  static loadState(stateId: string): PlanState | null;
49
50
  /**
50
51
  * Main entry: plan → approve → execute → synthesize → return final response.
52
+ *
53
+ * `abortSignal` (typically the gateway session controller) lets the user stop
54
+ * a running plan: the orchestrator checks it between waves and forwards it to
55
+ * every `runPlanStep` call so in-flight SDK streams abort immediately.
51
56
  */
52
- run(taskDescription: string, onProgress?: (updates: PlanProgressUpdate[]) => Promise<void>, onApproval?: (planSummary: string, steps: PlanStep[]) => Promise<boolean | string>, availableAgents?: AgentProfile[]): Promise<string>;
57
+ run(taskDescription: string, onProgress?: (updates: PlanProgressUpdate[]) => Promise<void>, onApproval?: (planSummary: string, steps: PlanStep[]) => Promise<boolean | string>, availableAgents?: AgentProfile[], abortSignal?: AbortSignal): Promise<string>;
53
58
  /**
54
59
  * Goal-backward verification pass using Haiku after plan synthesis.
55
60
  * Verifies outcomes rather than just rating quality:
@@ -123,6 +123,7 @@ export class PlanOrchestrator {
123
123
  startTime = 0;
124
124
  stateId;
125
125
  agentProfiles = new Map();
126
+ abortSignal;
126
127
  constructor(assistant) {
127
128
  this.assistant = assistant;
128
129
  this.stateId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
@@ -172,13 +173,18 @@ export class PlanOrchestrator {
172
173
  }
173
174
  /**
174
175
  * Main entry: plan → approve → execute → synthesize → return final response.
176
+ *
177
+ * `abortSignal` (typically the gateway session controller) lets the user stop
178
+ * a running plan: the orchestrator checks it between waves and forwards it to
179
+ * every `runPlanStep` call so in-flight SDK streams abort immediately.
175
180
  */
176
- async run(taskDescription, onProgress, onApproval, availableAgents) {
181
+ async run(taskDescription, onProgress, onApproval, availableAgents, abortSignal) {
177
182
  // Reset instance state for reuse safety
178
183
  this.stepStatuses.clear();
179
184
  this.stepStartTimes.clear();
180
185
  this.agentProfiles.clear();
181
186
  this.startTime = Date.now();
187
+ this.abortSignal = abortSignal;
182
188
  // Index available agents for delegation lookups
183
189
  if (availableAgents) {
184
190
  for (const agent of availableAgents) {
@@ -281,6 +287,15 @@ export class PlanOrchestrator {
281
287
  };
282
288
  this.saveState(state);
283
289
  for (const wave of waves) {
290
+ // User-initiated cancellation: bail before starting the next wave so we
291
+ // don't kick off SDK streams the user already asked to stop.
292
+ if (this.abortSignal?.aborted) {
293
+ logger.info({ goal: taskDescription, wavesCompleted: state.wavesCompleted }, 'Plan cancelled by user');
294
+ state.status = 'failed';
295
+ this.saveState(state);
296
+ this.logAutonomy('plan_aborted', { reason: 'user_cancelled', wavesCompleted: state.wavesCompleted });
297
+ return 'Plan cancelled.';
298
+ }
284
299
  // Mark running
285
300
  for (const step of wave) {
286
301
  this.stepStatuses.set(step.id, {
@@ -303,6 +318,7 @@ export class PlanOrchestrator {
303
318
  maxTurns: step.maxTurns ?? 15,
304
319
  model: step.model,
305
320
  delegateProfile,
321
+ abortSignal: this.abortSignal,
306
322
  });
307
323
  return { stepId: step.id, result };
308
324
  }), MAX_CONCURRENT_STEPS);
@@ -341,6 +357,16 @@ export class PlanOrchestrator {
341
357
  }
342
358
  }
343
359
  await safeProgress(this.getAllUpdates());
360
+ // If the user aborted mid-wave, the SDK calls above already threw and
361
+ // `outcome.reason` will reflect the abort. Surface that as a clean
362
+ // cancellation instead of falling through to spot-check / repair / synthesis.
363
+ if (this.abortSignal?.aborted) {
364
+ logger.info({ goal: taskDescription, wavesCompleted: state.wavesCompleted }, 'Plan cancelled by user mid-wave');
365
+ state.status = 'failed';
366
+ this.saveState(state);
367
+ this.logAutonomy('plan_aborted', { reason: 'user_cancelled', wavesCompleted: state.wavesCompleted });
368
+ return 'Plan cancelled.';
369
+ }
344
370
  // Inter-wave spot-check with severity levels: critical issues trigger repair
345
371
  const spotCheckIssues = this.spotCheckWaveResults(wave, results);
346
372
  if (spotCheckIssues.length > 0) {
@@ -362,6 +388,7 @@ export class PlanOrchestrator {
362
388
  tier: step.tier ?? 2,
363
389
  maxTurns: step.maxTurns ?? 15,
364
390
  model: step.model,
391
+ abortSignal: this.abortSignal,
365
392
  });
366
393
  results.set(step.id, retryResult || '[No output on retry]');
367
394
  this.stepStatuses.set(step.id, {
@@ -449,6 +476,7 @@ export class PlanOrchestrator {
449
476
  tier: 2,
450
477
  maxTurns: 5,
451
478
  disableTools: true,
479
+ abortSignal: this.abortSignal,
452
480
  });
453
481
  }
454
482
  catch (err) {
@@ -619,6 +647,7 @@ export class PlanOrchestrator {
619
647
  maxTurns: 1,
620
648
  model: 'haiku',
621
649
  disableTools: true,
650
+ abortSignal: this.abortSignal,
622
651
  });
623
652
  const cleaned = decision.trim().toLowerCase();
624
653
  if (cleaned.includes('retry'))
@@ -645,7 +674,7 @@ export class PlanOrchestrator {
645
674
  `If a step matches an agent's specialty, add "delegateTo": "agent-slug" to that step. ` +
646
675
  `The delegated agent will run the step with their own personality, tools, and expertise.\n`;
647
676
  }
648
- const plannerResult = await this.assistant.runPlanStep('planner', PLANNER_PROMPT + agentContext + task + PLANNER_PROMPT_SUFFIX, { tier: 2, maxTurns: 1, model: 'sonnet', disableTools: true });
677
+ const plannerResult = await this.assistant.runPlanStep('planner', PLANNER_PROMPT + agentContext + task + PLANNER_PROMPT_SUFFIX, { tier: 2, maxTurns: 1, model: 'sonnet', disableTools: true, abortSignal: this.abortSignal });
649
678
  // Parse JSON from the planner response
650
679
  const parsed = this.parseJsonFromResponse(plannerResult);
651
680
  if (!parsed?.steps || !Array.isArray(parsed.steps) || parsed.steps.length === 0) {
@@ -798,6 +827,7 @@ Work through this task narrating your reasoning:
798
827
  return this.assistant.runPlanStep('fallback', task, {
799
828
  tier: 2,
800
829
  maxTurns: 25,
830
+ abortSignal: this.abortSignal,
801
831
  });
802
832
  }
803
833
  getAllUpdates() {
@@ -1424,9 +1424,15 @@ export class Gateway {
1424
1424
  }
1425
1425
  // Register provenance for the orchestrator session
1426
1426
  this.ensureProvenance(sessionKey);
1427
+ // Register a session AbortController so a follow-up message ("stop", or
1428
+ // any new prompt) can interrupt the plan via acquireSessionLock — which
1429
+ // calls abortController.abort('interrupted-by-new-message') when it
1430
+ // sees an in-flight query on this session.
1431
+ const planAc = new AbortController();
1432
+ this.getSession(sessionKey).abortController = planAc;
1427
1433
  const { PlanOrchestrator } = await import('../agent/orchestrator.js');
1428
1434
  const orchestrator = new PlanOrchestrator(this.assistant);
1429
- const result = await orchestrator.run(taskDescription, onProgress, onApproval);
1435
+ const result = await orchestrator.run(taskDescription, onProgress, onApproval, undefined, planAc.signal);
1430
1436
  scanner.refreshIntegrity();
1431
1437
  this.assistant.injectContext(sessionKey, `[Plan: ${taskDescription}]`, result);
1432
1438
  return result;
@@ -68,4 +68,14 @@ export declare function computeAvailability(composioConnectedSlugs: Set<string>,
68
68
  * Compare to the previous always-on hardcoded block which was ~700 chars.
69
69
  */
70
70
  export declare function buildPromptInstruction(availability: ServiceAvailability[], preferences: Record<string, ToolSource>): string;
71
+ /**
72
+ * Ground-truth header naming the Composio toolkits currently connected.
73
+ * Without this, the model has to guess what `mcp__<slug>__*` tools are
74
+ * (often misattributing them to claude.ai integrations or "extensions"),
75
+ * because Composio's slug-prefixed naming is ambiguous in isolation.
76
+ *
77
+ * Emits an empty string when no toolkits are connected — zero overhead
78
+ * for users who haven't set up Composio.
79
+ */
80
+ export declare function buildComposioStatusBlock(connectedSlugs: string[]): string;
71
81
  //# sourceMappingURL=tool-preferences.d.ts.map
@@ -116,4 +116,20 @@ export function buildPromptInstruction(availability, preferences) {
116
116
  return '';
117
117
  return `## Tool Source Preferences\n\nThese rules OVERRIDE any tool-source preference you may recall from memory. Do not consult memory for this — the canonical source is the dashboard's Tool Source Preferences (Settings → Integrations).\n\n${lines.join('\n')}`;
118
118
  }
119
+ /**
120
+ * Ground-truth header naming the Composio toolkits currently connected.
121
+ * Without this, the model has to guess what `mcp__<slug>__*` tools are
122
+ * (often misattributing them to claude.ai integrations or "extensions"),
123
+ * because Composio's slug-prefixed naming is ambiguous in isolation.
124
+ *
125
+ * Emits an empty string when no toolkits are connected — zero overhead
126
+ * for users who haven't set up Composio.
127
+ */
128
+ export function buildComposioStatusBlock(connectedSlugs) {
129
+ if (!connectedSlugs.length)
130
+ return '';
131
+ const sorted = [...connectedSlugs].sort();
132
+ const example = sorted[0];
133
+ return `## Composio Integration\n\nComposio is configured. Connected toolkits: ${sorted.join(', ')}.\n\nTools from these toolkits are namespaced as \`mcp__<slug>__<TOOL_NAME>\` (e.g., \`mcp__${example}__*\`). These are local Composio MCP servers, NOT claude.ai integrations (which use \`mcp__claude_ai_*\`).`;
134
+ }
119
135
  //# sourceMappingURL=tool-preferences.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.0",
3
+ "version": "1.18.2",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",