clementine-agent 1.18.0 → 1.18.1
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;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -4069,10 +4069,19 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4069
4069
|
}
|
|
4070
4070
|
// ── Plan Step Execution ───────────────────────────────────────────
|
|
4071
4071
|
async runPlanStep(stepId, prompt, opts = {}) {
|
|
4072
|
-
const { tier = 2, maxTurns = 15, model, disableTools = false, outputFormat, delegateProfile } = opts;
|
|
4072
|
+
const { tier = 2, maxTurns = 15, model, disableTools = false, outputFormat, delegateProfile, abortSignal } = opts;
|
|
4073
4073
|
// Don't mutate the global — pass source through the closure instead
|
|
4074
4074
|
// Per-step stall guard so concurrent steps don't cross-contaminate
|
|
4075
4075
|
const stepGuard = new StallGuard();
|
|
4076
|
+
// Per-step AbortController, mirroring the parent signal so the orchestrator
|
|
4077
|
+
// (or gateway, via the session AC) can stop in-flight SDK streams.
|
|
4078
|
+
const stepAc = new AbortController();
|
|
4079
|
+
if (abortSignal) {
|
|
4080
|
+
if (abortSignal.aborted)
|
|
4081
|
+
stepAc.abort(abortSignal.reason);
|
|
4082
|
+
else
|
|
4083
|
+
abortSignal.addEventListener('abort', () => stepAc.abort(abortSignal.reason), { once: true });
|
|
4084
|
+
}
|
|
4076
4085
|
const sdkOptions = await this.buildOptions({
|
|
4077
4086
|
isHeartbeat: false,
|
|
4078
4087
|
cronTier: tier,
|
|
@@ -4085,6 +4094,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4085
4094
|
outputFormat,
|
|
4086
4095
|
stallGuard: stepGuard,
|
|
4087
4096
|
profile: delegateProfile ?? null,
|
|
4097
|
+
abortController: stepAc,
|
|
4088
4098
|
});
|
|
4089
4099
|
const trace = [];
|
|
4090
4100
|
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;
|