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.
- package/dist/agent/assistant.d.ts +1 -0
- package/dist/agent/assistant.js +18 -2
- package/dist/agent/orchestrator.d.ts +6 -1
- package/dist/agent/orchestrator.js +32 -2
- package/dist/gateway/router.js +7 -1
- package/dist/integrations/tool-preferences.d.ts +10 -0
- package/dist/integrations/tool-preferences.js +16 -0
- package/package.json +1 -1
|
@@ -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;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -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() {
|
package/dist/gateway/router.js
CHANGED
|
@@ -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
|