praxis-agent 0.26.0 → 0.28.0
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/application/session-memory.d.ts +43 -10
- package/dist/application/session-memory.js +263 -62
- package/dist/application/session-service.d.ts +13 -4
- package/dist/application/session-service.js +139 -36
- package/dist/application/subagent-service.d.ts +1 -0
- package/dist/application/subagent-service.js +23 -1
- package/dist/cli-runtime.d.ts +4 -0
- package/dist/cli-runtime.js +10 -0
- package/dist/core/runtime.d.ts +10 -0
- package/dist/core/runtime.js +111 -28
- package/dist/core/tool-execution-scheduler.d.ts +38 -0
- package/dist/core/tool-execution-scheduler.js +95 -0
- package/dist/core/tool-scheduling-policy.d.ts +4 -0
- package/dist/core/tool-scheduling-policy.js +24 -0
- package/dist/extensions/claude-extension-tools.d.ts +1 -0
- package/dist/extensions/claude-extension-tools.js +6 -0
- package/dist/hooks/claude-hook-tools.d.ts +4 -0
- package/dist/hooks/claude-hook-tools.js +6 -0
- package/dist/mcp/claude-mcp-tools.d.ts +1 -0
- package/dist/mcp/claude-mcp-tools.js +42 -0
- package/dist/permissions/claude-permission-resolver.js +3 -3
- package/dist/permissions/permission-updates.d.ts +1 -0
- package/dist/permissions/permission-updates.js +13 -1
- package/dist/tools/claude-capabilities.d.ts +1 -0
- package/dist/tools/claude-capabilities.js +5 -0
- package/dist/tools/claude-interactive-tools.js +7 -0
- package/dist/tools/claude-lsp-tool.js +7 -0
- package/dist/tools/claude-scheduled-tools.d.ts +1 -0
- package/dist/tools/claude-scheduled-tools.js +7 -0
- package/dist/tools/claude-task-tools.d.ts +1 -0
- package/dist/tools/claude-task-tools.js +11 -0
- package/dist/tools/claude-user-message.d.ts +1 -0
- package/dist/tools/claude-user-message.js +7 -0
- package/dist/tools/claude-workflow-tools.d.ts +1 -0
- package/dist/tools/claude-workflow-tools.js +7 -0
- package/dist/tools/claude-worktree-tools.d.ts +1 -0
- package/dist/tools/claude-worktree-tools.js +7 -0
- package/dist/tools/filtered-tool-registry.d.ts +1 -0
- package/dist/tools/filtered-tool-registry.js +5 -0
- package/dist/tools/local-tools.d.ts +17 -0
- package/dist/tools/local-tools.js +86 -1
- package/dist/tools/web.d.ts +1 -0
- package/dist/tools/web.js +27 -0
- package/package.json +1 -1
package/dist/core/runtime.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ToolExecutionScheduler } from './tool-execution-scheduler.js';
|
|
2
|
+
import { resolveToolSchedulingPolicy } from './tool-scheduling-policy.js';
|
|
1
3
|
const permissionDecisionSources = new WeakMap();
|
|
2
4
|
const autoModePermissionOutcomes = new WeakMap();
|
|
3
5
|
export function annotatePermissionDecision(decision, source) {
|
|
@@ -274,13 +276,23 @@ export class AgentRuntime {
|
|
|
274
276
|
const definitions = this.provider.capabilities.tools
|
|
275
277
|
? (this.options.tools?.definitions() ?? [])
|
|
276
278
|
: [];
|
|
277
|
-
const maxModelTurns = request.maxModelTurns ?? this.options.maxModelTurns
|
|
279
|
+
const maxModelTurns = request.maxModelTurns ?? this.options.maxModelTurns;
|
|
280
|
+
if (maxModelTurns !== undefined &&
|
|
281
|
+
(!Number.isSafeInteger(maxModelTurns) || maxModelTurns <= 0)) {
|
|
282
|
+
throw new TypeError('maxModelTurns must be a positive integer');
|
|
283
|
+
}
|
|
278
284
|
const maxModelOutputBytes = this.options.maxModelOutputBytes ?? 1024 * 1024;
|
|
279
285
|
const maxToolCallsPerTurn = this.options.maxToolCallsPerTurn ?? 32;
|
|
280
286
|
const maxToolInputBytes = this.options.maxToolInputBytes ?? 1024 * 1024;
|
|
281
287
|
let pendingToolUseSummary;
|
|
282
288
|
try {
|
|
283
|
-
|
|
289
|
+
let modelTurns = 0;
|
|
290
|
+
while (true) {
|
|
291
|
+
if (request.signal?.aborted)
|
|
292
|
+
return this.cancel();
|
|
293
|
+
if (maxModelTurns !== undefined && modelTurns >= maxModelTurns) {
|
|
294
|
+
throw new Error(`Maximum model turns of ${maxModelTurns} exceeded`);
|
|
295
|
+
}
|
|
284
296
|
if (pendingToolUseSummary) {
|
|
285
297
|
const summaryRequest = pendingToolUseSummary;
|
|
286
298
|
pendingToolUseSummary = undefined;
|
|
@@ -326,6 +338,7 @@ export class AgentRuntime {
|
|
|
326
338
|
spent >= this.options.maxBudgetUsd) {
|
|
327
339
|
throw new Error(`Maximum budget of $${this.options.maxBudgetUsd.toFixed(6)} exceeded`);
|
|
328
340
|
}
|
|
341
|
+
modelTurns += 1;
|
|
329
342
|
this.emit({ type: 'state', state: 'awaiting-model' });
|
|
330
343
|
const providerRequest = {
|
|
331
344
|
messages: prepareProviderMessages(messages, this.provider.capabilities.images === true, this.provider.capabilities.documents === true),
|
|
@@ -347,6 +360,25 @@ export class AgentRuntime {
|
|
|
347
360
|
let streaming = false;
|
|
348
361
|
const toolCalls = [];
|
|
349
362
|
let terminalReason;
|
|
363
|
+
const toolScheduler = new ToolExecutionScheduler({
|
|
364
|
+
...(request.signal ? { parentSignal: request.signal } : {}),
|
|
365
|
+
execute: (call, signal) => this.executeScheduledToolCall(call, request, messages, signal),
|
|
366
|
+
isError: (result) => result.isError,
|
|
367
|
+
cancelledResult: (call, reason) => ({
|
|
368
|
+
content: `${call.name} cancelled: ${reason instanceof Error ? reason.message : String(reason)}`,
|
|
369
|
+
isError: true,
|
|
370
|
+
}),
|
|
371
|
+
failedResult: (call, reason) => ({
|
|
372
|
+
content: `${call.name} failed: ${reason instanceof Error ? reason.message : String(reason)}`,
|
|
373
|
+
isError: true,
|
|
374
|
+
}),
|
|
375
|
+
resultCompleted: (call, result) => this.presentToolResult(call, result),
|
|
376
|
+
});
|
|
377
|
+
const failScheduledTurn = async (error) => {
|
|
378
|
+
toolScheduler.abort(error);
|
|
379
|
+
await toolScheduler.settle().catch(() => undefined);
|
|
380
|
+
throw error;
|
|
381
|
+
};
|
|
350
382
|
const apiStartedAt = request.collectMetrics ? performance.now() : 0;
|
|
351
383
|
let turnApiDurationMs = 0;
|
|
352
384
|
let turnApiDurationWithoutRetriesMs;
|
|
@@ -422,6 +454,7 @@ export class AgentRuntime {
|
|
|
422
454
|
}
|
|
423
455
|
toolCalls.push(event.call);
|
|
424
456
|
this.emit(event);
|
|
457
|
+
toolScheduler.schedule(event.call, resolveToolSchedulingPolicy(this.options.tools, event.call));
|
|
425
458
|
}
|
|
426
459
|
else if (event.type === 'terminal') {
|
|
427
460
|
terminalReason = event.reason;
|
|
@@ -433,6 +466,19 @@ export class AgentRuntime {
|
|
|
433
466
|
}
|
|
434
467
|
}
|
|
435
468
|
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (!request.signal?.aborted &&
|
|
471
|
+
!(error instanceof AgentRunCancelledError)) {
|
|
472
|
+
toolScheduler.abort(error);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
toolScheduler.releaseExclusiveTools();
|
|
476
|
+
}
|
|
477
|
+
await toolScheduler.settle().catch(() => undefined);
|
|
478
|
+
if (request.signal?.aborted)
|
|
479
|
+
return this.cancel();
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
436
482
|
finally {
|
|
437
483
|
if (request.collectMetrics) {
|
|
438
484
|
turnApiDurationMs = Math.max(0, performance.now() - apiStartedAt);
|
|
@@ -442,15 +488,15 @@ export class AgentRuntime {
|
|
|
442
488
|
}
|
|
443
489
|
if (this.provider.capabilities.terminalReasons === true &&
|
|
444
490
|
terminalReason === undefined) {
|
|
445
|
-
|
|
491
|
+
return failScheduledTurn(new ModelProviderError('Provider stream ended without a terminal reason', { retryable: true }));
|
|
446
492
|
}
|
|
447
493
|
if (terminalReason === 'tool_use' && toolCalls.length === 0) {
|
|
448
|
-
|
|
494
|
+
return failScheduledTurn(new ModelProviderError('Provider reported tool_use without a completed tool call', { retryable: false }));
|
|
449
495
|
}
|
|
450
496
|
if (terminalReason !== undefined &&
|
|
451
497
|
terminalReason !== 'tool_use' &&
|
|
452
498
|
toolCalls.length > 0) {
|
|
453
|
-
|
|
499
|
+
return failScheduledTurn(new ModelProviderError(`Provider reported ${terminalReason} with completed tool calls`, { retryable: false }));
|
|
454
500
|
}
|
|
455
501
|
if (request.collectMetrics) {
|
|
456
502
|
const turnApiDurationWithoutRetriesMsResolved = turnApiDurationWithoutRetriesMs ?? turnApiDurationMs;
|
|
@@ -480,8 +526,14 @@ export class AgentRuntime {
|
|
|
480
526
|
...(thinkingBlocks.length > 0 ? { thinkingBlocks } : {}),
|
|
481
527
|
toolCalls,
|
|
482
528
|
};
|
|
483
|
-
|
|
529
|
+
try {
|
|
530
|
+
await request.observer?.assistantCompleted(assistantMessage);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
return failScheduledTurn(error);
|
|
534
|
+
}
|
|
484
535
|
messages.push(assistantMessage);
|
|
536
|
+
toolScheduler.releaseExclusiveTools();
|
|
485
537
|
if (toolCalls.length === 0) {
|
|
486
538
|
const stopResult = (await request.onStop?.(text)) ?? [];
|
|
487
539
|
const stopBatch = Array.isArray(stopResult)
|
|
@@ -547,10 +599,11 @@ export class AgentRuntime {
|
|
|
547
599
|
...(linesRemoved === 0 ? {} : { linesRemoved }),
|
|
548
600
|
};
|
|
549
601
|
}
|
|
602
|
+
const scheduledToolResults = await toolScheduler.settle();
|
|
550
603
|
const followUpUserMessages = [];
|
|
551
604
|
const completedTools = [];
|
|
552
|
-
for (const call of
|
|
553
|
-
|
|
605
|
+
for (const { call, result } of scheduledToolResults) {
|
|
606
|
+
await this.persistToolCall(call, result, request);
|
|
554
607
|
completedTools.push({
|
|
555
608
|
name: call.name,
|
|
556
609
|
input: call.input,
|
|
@@ -584,6 +637,10 @@ export class AgentRuntime {
|
|
|
584
637
|
});
|
|
585
638
|
followUpUserMessages.push(...(result.followUpUserMessages ?? []));
|
|
586
639
|
}
|
|
640
|
+
const toolFailure = toolScheduler.failure;
|
|
641
|
+
if (toolFailure) {
|
|
642
|
+
throw toolFailure.error;
|
|
643
|
+
}
|
|
587
644
|
if (this.options.generateToolUseSummary) {
|
|
588
645
|
const summarySignal = request.signal ?? new AbortController().signal;
|
|
589
646
|
pendingToolUseSummary = {
|
|
@@ -609,7 +666,6 @@ export class AgentRuntime {
|
|
|
609
666
|
messages.splice(0, messages.length, ...reloadedMessages);
|
|
610
667
|
}
|
|
611
668
|
}
|
|
612
|
-
throw new Error(`Agent exceeded ${maxModelTurns} model turns`);
|
|
613
669
|
}
|
|
614
670
|
catch (error) {
|
|
615
671
|
if (request.signal?.aborted)
|
|
@@ -659,19 +715,38 @@ export class AgentRuntime {
|
|
|
659
715
|
return result;
|
|
660
716
|
}
|
|
661
717
|
async completeToolCall(call, request, messages = request.messages ?? [], emitPresentation = true) {
|
|
718
|
+
const signal = request.signal ?? new AbortController().signal;
|
|
719
|
+
let result;
|
|
720
|
+
try {
|
|
721
|
+
result = await this.executeScheduledToolCall(call, request, messages, signal, emitPresentation);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (request.signal?.aborted)
|
|
725
|
+
return this.cancel();
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
if (emitPresentation)
|
|
729
|
+
this.presentToolResult(call, result);
|
|
730
|
+
await this.persistToolCall(call, result, request);
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
async executeScheduledToolCall(call, request, messages, signal, emitPresentation = true) {
|
|
662
734
|
const startedAt = performance.now();
|
|
663
|
-
const emitProgress = () => this.emit({
|
|
735
|
+
const emitProgress = (elapsedTimeSeconds) => this.emit({
|
|
664
736
|
type: 'tool-progress',
|
|
665
737
|
toolUseId: call.id,
|
|
666
738
|
toolName: call.name,
|
|
667
|
-
elapsedTimeSeconds:
|
|
739
|
+
elapsedTimeSeconds: elapsedTimeSeconds ??
|
|
740
|
+
Math.max(0, Math.round(((performance.now() - startedAt) / 1000) * 1000) / 1000),
|
|
668
741
|
});
|
|
669
742
|
const progressTimer = emitPresentation
|
|
670
743
|
? setInterval(emitProgress, 1000)
|
|
671
744
|
: undefined;
|
|
672
745
|
progressTimer?.unref();
|
|
673
746
|
try {
|
|
674
|
-
|
|
747
|
+
if (emitPresentation)
|
|
748
|
+
emitProgress(0);
|
|
749
|
+
const executed = await this.executeTool(call, { ...request, signal }, messages);
|
|
675
750
|
const unsupportedImages = executed.images?.length && this.provider.capabilities.images !== true;
|
|
676
751
|
const unsupportedDocuments = executed.documents?.length &&
|
|
677
752
|
this.provider.capabilities.documents !== true;
|
|
@@ -690,16 +765,8 @@ export class AgentRuntime {
|
|
|
690
765
|
: {}),
|
|
691
766
|
}
|
|
692
767
|
: executed;
|
|
693
|
-
this.emit({ type: 'state', state: 'persisting-results' });
|
|
694
|
-
await request.observer?.toolCompleted(call, result);
|
|
695
768
|
if (emitPresentation) {
|
|
696
769
|
emitProgress();
|
|
697
|
-
this.emit({
|
|
698
|
-
type: 'tool-result',
|
|
699
|
-
callId: call.id,
|
|
700
|
-
content: result.content,
|
|
701
|
-
isError: result.isError,
|
|
702
|
-
});
|
|
703
770
|
}
|
|
704
771
|
return result;
|
|
705
772
|
}
|
|
@@ -708,6 +775,18 @@ export class AgentRuntime {
|
|
|
708
775
|
clearInterval(progressTimer);
|
|
709
776
|
}
|
|
710
777
|
}
|
|
778
|
+
presentToolResult(call, result) {
|
|
779
|
+
this.emit({
|
|
780
|
+
type: 'tool-result',
|
|
781
|
+
callId: call.id,
|
|
782
|
+
content: result.content,
|
|
783
|
+
isError: result.isError,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
async persistToolCall(call, result, request) {
|
|
787
|
+
this.emit({ type: 'state', state: 'persisting-results' });
|
|
788
|
+
await request.observer?.toolCompleted(call, result);
|
|
789
|
+
}
|
|
711
790
|
async executeTool(call, request, messages) {
|
|
712
791
|
const tools = this.options.tools;
|
|
713
792
|
const permissions = this.options.permissions;
|
|
@@ -808,8 +887,9 @@ export class AgentRuntime {
|
|
|
808
887
|
: 'Permission approval was not provided');
|
|
809
888
|
return { content: reason, isError: true };
|
|
810
889
|
}
|
|
811
|
-
if (request.signal?.aborted)
|
|
812
|
-
|
|
890
|
+
if (request.signal?.aborted) {
|
|
891
|
+
throw request.signal.reason ?? new AgentRunCancelledError();
|
|
892
|
+
}
|
|
813
893
|
this.emit({ type: 'state', state: 'executing-tools' });
|
|
814
894
|
context.permissionPhase = 'execute';
|
|
815
895
|
context.permissionApproved = true;
|
|
@@ -829,8 +909,9 @@ export class AgentRuntime {
|
|
|
829
909
|
};
|
|
830
910
|
}
|
|
831
911
|
catch (error) {
|
|
832
|
-
if (request.signal?.aborted)
|
|
833
|
-
|
|
912
|
+
if (request.signal?.aborted) {
|
|
913
|
+
throw request.signal.reason ?? new AgentRunCancelledError();
|
|
914
|
+
}
|
|
834
915
|
const durationToolMs = Math.max(0, performance.now() - toolStartedAt);
|
|
835
916
|
return {
|
|
836
917
|
content: error instanceof Error ? error.message : String(error),
|
|
@@ -840,13 +921,15 @@ export class AgentRuntime {
|
|
|
840
921
|
}
|
|
841
922
|
}
|
|
842
923
|
async requireRecoveryApproval(call, request) {
|
|
843
|
-
if (request.signal?.aborted)
|
|
844
|
-
|
|
924
|
+
if (request.signal?.aborted) {
|
|
925
|
+
throw request.signal.reason ?? new AgentRunCancelledError();
|
|
926
|
+
}
|
|
845
927
|
if (request.approveRecovery && !(await request.approveRecovery(call))) {
|
|
846
928
|
throw new Error(`Tool call ${call.id} recovery was declined`);
|
|
847
929
|
}
|
|
848
|
-
if (request.signal?.aborted)
|
|
849
|
-
|
|
930
|
+
if (request.signal?.aborted) {
|
|
931
|
+
throw request.signal.reason ?? new AgentRunCancelledError();
|
|
932
|
+
}
|
|
850
933
|
}
|
|
851
934
|
cancel() {
|
|
852
935
|
this.emit({ type: 'state', state: 'cancelled' });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ModelToolCall, ToolSchedulingPolicy } from './runtime.js';
|
|
2
|
+
interface ScheduledToolResult<TResult> {
|
|
3
|
+
call: ModelToolCall;
|
|
4
|
+
result: TResult;
|
|
5
|
+
}
|
|
6
|
+
interface ToolExecutionSchedulerOptions<TResult> {
|
|
7
|
+
parentSignal?: AbortSignal;
|
|
8
|
+
execute(call: ModelToolCall, signal: AbortSignal): Promise<TResult>;
|
|
9
|
+
isError(result: TResult): boolean;
|
|
10
|
+
cancelledResult(call: ModelToolCall, reason: unknown): TResult;
|
|
11
|
+
failedResult(call: ModelToolCall, reason: unknown): TResult;
|
|
12
|
+
resultCompleted(call: ModelToolCall, result: TResult): void;
|
|
13
|
+
}
|
|
14
|
+
export declare class ToolExecutionScheduler<TResult> {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly groupController;
|
|
17
|
+
private readonly exclusiveStart;
|
|
18
|
+
private releaseExclusiveStart;
|
|
19
|
+
private exclusiveStarted;
|
|
20
|
+
private hasFailure;
|
|
21
|
+
private firstFailure;
|
|
22
|
+
private readonly scheduled;
|
|
23
|
+
private readonly completionOrder;
|
|
24
|
+
private precedingBarrier;
|
|
25
|
+
private concurrentGroup;
|
|
26
|
+
constructor(options: ToolExecutionSchedulerOptions<TResult>);
|
|
27
|
+
schedule(call: ModelToolCall, policy: ToolSchedulingPolicy): void;
|
|
28
|
+
abort(reason?: unknown): void;
|
|
29
|
+
releaseExclusiveTools(): void;
|
|
30
|
+
settle(): Promise<readonly ScheduledToolResult<TResult>[]>;
|
|
31
|
+
get failure(): {
|
|
32
|
+
error: unknown;
|
|
33
|
+
} | undefined;
|
|
34
|
+
private record;
|
|
35
|
+
private executionSignal;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=tool-execution-scheduler.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const settled = Promise.resolve();
|
|
2
|
+
export class ToolExecutionScheduler {
|
|
3
|
+
options;
|
|
4
|
+
groupController = new AbortController();
|
|
5
|
+
exclusiveStart;
|
|
6
|
+
releaseExclusiveStart;
|
|
7
|
+
exclusiveStarted = false;
|
|
8
|
+
hasFailure = false;
|
|
9
|
+
firstFailure;
|
|
10
|
+
scheduled = [];
|
|
11
|
+
completionOrder = [];
|
|
12
|
+
precedingBarrier = settled;
|
|
13
|
+
concurrentGroup = [];
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
this.exclusiveStart = new Promise((resolve) => {
|
|
17
|
+
this.releaseExclusiveStart = resolve;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
schedule(call, policy) {
|
|
21
|
+
const predecessors = policy.concurrency === 'exclusive'
|
|
22
|
+
? Promise.allSettled([
|
|
23
|
+
...(policy.startAfterAssistant ? [this.exclusiveStart] : []),
|
|
24
|
+
this.precedingBarrier,
|
|
25
|
+
...this.concurrentGroup,
|
|
26
|
+
]).then(() => undefined)
|
|
27
|
+
: this.precedingBarrier;
|
|
28
|
+
const signal = this.executionSignal(policy);
|
|
29
|
+
const execution = predecessors.then(async () => {
|
|
30
|
+
let result;
|
|
31
|
+
try {
|
|
32
|
+
if (signal.aborted)
|
|
33
|
+
throw signal.reason;
|
|
34
|
+
result = await this.options.execute(call, signal);
|
|
35
|
+
if (signal.aborted)
|
|
36
|
+
throw signal.reason;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (signal.aborted) {
|
|
40
|
+
result = this.options.cancelledResult(call, error);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (!this.hasFailure) {
|
|
44
|
+
this.hasFailure = true;
|
|
45
|
+
this.firstFailure = error;
|
|
46
|
+
}
|
|
47
|
+
result = this.options.failedResult(call, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.record(call, result);
|
|
51
|
+
if (policy.abortGroupOnError && this.options.isError(result)) {
|
|
52
|
+
this.groupController.abort(new Error(`Tool ${call.name} failed; cancelling streamed siblings`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this.scheduled.push(execution);
|
|
56
|
+
if (policy.concurrency === 'exclusive') {
|
|
57
|
+
this.precedingBarrier = execution.catch(() => undefined);
|
|
58
|
+
this.concurrentGroup = [];
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.concurrentGroup.push(execution.catch(() => undefined));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
abort(reason) {
|
|
65
|
+
this.releaseExclusiveTools();
|
|
66
|
+
this.groupController.abort(reason);
|
|
67
|
+
}
|
|
68
|
+
releaseExclusiveTools() {
|
|
69
|
+
if (this.exclusiveStarted)
|
|
70
|
+
return;
|
|
71
|
+
this.exclusiveStarted = true;
|
|
72
|
+
this.releaseExclusiveStart();
|
|
73
|
+
}
|
|
74
|
+
async settle() {
|
|
75
|
+
await Promise.all(this.scheduled);
|
|
76
|
+
return this.completionOrder;
|
|
77
|
+
}
|
|
78
|
+
get failure() {
|
|
79
|
+
return this.hasFailure ? { error: this.firstFailure } : undefined;
|
|
80
|
+
}
|
|
81
|
+
record(call, result) {
|
|
82
|
+
this.completionOrder.push({ call, result });
|
|
83
|
+
this.options.resultCompleted(call, result);
|
|
84
|
+
}
|
|
85
|
+
executionSignal(policy) {
|
|
86
|
+
const signals = [this.groupController.signal];
|
|
87
|
+
if (policy.cancelOnInterrupt && this.options.parentSignal) {
|
|
88
|
+
signals.push(this.options.parentSignal);
|
|
89
|
+
}
|
|
90
|
+
return signals.length === 1
|
|
91
|
+
? this.groupController.signal
|
|
92
|
+
: AbortSignal.any(signals);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=tool-execution-scheduler.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ModelToolCall, ToolRegistry, ToolSchedulingPolicy } from './runtime.js';
|
|
2
|
+
export declare const exclusiveToolSchedulingPolicy: ToolSchedulingPolicy;
|
|
3
|
+
export declare function resolveToolSchedulingPolicy(tools: ToolRegistry | undefined, call: ModelToolCall): ToolSchedulingPolicy;
|
|
4
|
+
//# sourceMappingURL=tool-scheduling-policy.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const exclusiveToolSchedulingPolicy = {
|
|
2
|
+
concurrency: 'exclusive',
|
|
3
|
+
};
|
|
4
|
+
export function resolveToolSchedulingPolicy(tools, call) {
|
|
5
|
+
try {
|
|
6
|
+
const policy = tools?.schedulingPolicy?.(structuredClone(call));
|
|
7
|
+
if (!policy ||
|
|
8
|
+
(policy.concurrency !== 'concurrent' &&
|
|
9
|
+
policy.concurrency !== 'exclusive') ||
|
|
10
|
+
(policy.cancelOnInterrupt !== undefined &&
|
|
11
|
+
typeof policy.cancelOnInterrupt !== 'boolean') ||
|
|
12
|
+
(policy.abortGroupOnError !== undefined &&
|
|
13
|
+
typeof policy.abortGroupOnError !== 'boolean') ||
|
|
14
|
+
(policy.startAfterAssistant !== undefined &&
|
|
15
|
+
typeof policy.startAfterAssistant !== 'boolean')) {
|
|
16
|
+
return exclusiveToolSchedulingPolicy;
|
|
17
|
+
}
|
|
18
|
+
return policy;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return exclusiveToolSchedulingPolicy;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=tool-scheduling-policy.js.map
|
|
@@ -5,6 +5,7 @@ export declare class ClaudeExtensionToolRegistry implements ToolRegistry {
|
|
|
5
5
|
private readonly catalog;
|
|
6
6
|
constructor(base: ToolRegistry, catalog: ClaudeExtensionCatalog);
|
|
7
7
|
definitions(): readonly ModelToolDefinition[];
|
|
8
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
8
9
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
9
10
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
10
11
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
1
2
|
function skillInput(call) {
|
|
2
3
|
const skill = call.input.skill;
|
|
3
4
|
const args = call.input.args ?? '';
|
|
@@ -39,6 +40,11 @@ export class ClaudeExtensionToolRegistry {
|
|
|
39
40
|
},
|
|
40
41
|
];
|
|
41
42
|
}
|
|
43
|
+
schedulingPolicy(call) {
|
|
44
|
+
if (call.name === 'Skill')
|
|
45
|
+
return { concurrency: 'exclusive' };
|
|
46
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
47
|
+
}
|
|
42
48
|
async prepare(call, context) {
|
|
43
49
|
if (call.name !== 'Skill')
|
|
44
50
|
return this.base.prepare(call, context);
|
|
@@ -19,6 +19,10 @@ export declare class ClaudeHookToolCoordinator implements ToolRegistry, Permissi
|
|
|
19
19
|
private readonly prepared;
|
|
20
20
|
constructor(options: ClaudeHookToolCoordinatorOptions);
|
|
21
21
|
definitions(): readonly import("../core/runtime.js").ModelToolDefinition[];
|
|
22
|
+
schedulingPolicy(): {
|
|
23
|
+
concurrency: "exclusive";
|
|
24
|
+
startAfterAssistant: boolean;
|
|
25
|
+
};
|
|
22
26
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
23
27
|
resolve(call: ModelToolCall, context?: PermissionResolutionContext): Promise<PermissionDecision>;
|
|
24
28
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
@@ -7,6 +7,12 @@ export class ClaudeHookToolCoordinator {
|
|
|
7
7
|
definitions() {
|
|
8
8
|
return this.options.tools.definitions();
|
|
9
9
|
}
|
|
10
|
+
schedulingPolicy() {
|
|
11
|
+
return {
|
|
12
|
+
concurrency: 'exclusive',
|
|
13
|
+
startAfterAssistant: true,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
10
16
|
async prepare(call, context) {
|
|
11
17
|
const outcome = await this.options.hooks.run({
|
|
12
18
|
...this.options.session,
|
|
@@ -102,6 +102,7 @@ export declare class ClaudeMcpToolRegistry implements ToolRegistry, ClaudeMcpRun
|
|
|
102
102
|
private constructor();
|
|
103
103
|
static connect(options: ClaudeMcpToolRegistryOptions): Promise<ClaudeMcpToolRegistry>;
|
|
104
104
|
definitions(): readonly ModelToolDefinition[];
|
|
105
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
105
106
|
serverStatuses(): readonly ClaudeMcpServerStatus[];
|
|
106
107
|
inspect(): Promise<readonly ClaudeMcpServerStatus[]>;
|
|
107
108
|
reconnect(name: string): Promise<void>;
|
|
@@ -7,6 +7,8 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
|
7
7
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
8
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
9
9
|
import { ElicitRequestSchema, ElicitationCompleteNotificationSchema, PromptListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
11
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
10
12
|
import { redactSensitiveError, redactSensitiveText, redactSensitiveValue, sanitizeChildEnvironment, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
11
13
|
import { parsePermissionUpdates } from '../permissions/permission-updates.js';
|
|
12
14
|
import { loadMcpOAuthProvider, mcpOAuthServerIdentity, } from './claude-mcp-oauth.js';
|
|
@@ -77,6 +79,28 @@ const MCP_RESOURCE_TOOL_DEFINITIONS = [
|
|
|
77
79
|
},
|
|
78
80
|
},
|
|
79
81
|
];
|
|
82
|
+
const schedulingAjv = new Ajv2020({ strict: false, validateFormats: false });
|
|
83
|
+
function schedulingInputValidator(schema) {
|
|
84
|
+
try {
|
|
85
|
+
const validate = schedulingAjv.compile(schema);
|
|
86
|
+
return (input) => validate(input) === true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function mcpResourceSchedulingInputIsValid(call) {
|
|
93
|
+
const keys = Object.keys(call.input);
|
|
94
|
+
if (call.name === 'ListMcpResourcesTool') {
|
|
95
|
+
return (keys.every((name) => name === 'server') &&
|
|
96
|
+
(call.input.server === undefined || typeof call.input.server === 'string'));
|
|
97
|
+
}
|
|
98
|
+
return ((call.name === 'ReadMcpResourceDirTool' ||
|
|
99
|
+
call.name === 'ReadMcpResourceTool') &&
|
|
100
|
+
keys.every((name) => name === 'server' || name === 'uri') &&
|
|
101
|
+
typeof call.input.server === 'string' &&
|
|
102
|
+
typeof call.input.uri === 'string');
|
|
103
|
+
}
|
|
80
104
|
function isRecord(value) {
|
|
81
105
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
82
106
|
}
|
|
@@ -687,6 +711,21 @@ export class ClaudeMcpToolRegistry {
|
|
|
687
711
|
...(this.resourceServers.size > 0 ? MCP_RESOURCE_TOOL_DEFINITIONS : []),
|
|
688
712
|
];
|
|
689
713
|
}
|
|
714
|
+
schedulingPolicy(call) {
|
|
715
|
+
if (MCP_RESOURCE_TOOL_DEFINITIONS.some((definition) => definition.name === call.name)) {
|
|
716
|
+
return mcpResourceSchedulingInputIsValid(call)
|
|
717
|
+
? { concurrency: 'concurrent', cancelOnInterrupt: true }
|
|
718
|
+
: { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
719
|
+
}
|
|
720
|
+
const connected = this.connectedTools.get(call.name);
|
|
721
|
+
if (connected) {
|
|
722
|
+
return connected.readOnly &&
|
|
723
|
+
connected.schedulingInputIsValid?.(call.input)
|
|
724
|
+
? { concurrency: 'concurrent', cancelOnInterrupt: true }
|
|
725
|
+
: { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
726
|
+
}
|
|
727
|
+
return resolveToolSchedulingPolicy(this.options.base, call);
|
|
728
|
+
}
|
|
690
729
|
serverStatuses() {
|
|
691
730
|
return [...this.statuses.values()].map(({ name, status }) => ({
|
|
692
731
|
name,
|
|
@@ -1070,10 +1109,13 @@ export class ClaudeMcpToolRegistry {
|
|
|
1070
1109
|
if (this.connectedTools.has(name) || connectedTools.has(name)) {
|
|
1071
1110
|
throw new Error(`Duplicate MCP tool ${name}`);
|
|
1072
1111
|
}
|
|
1112
|
+
const inputIsValid = schedulingInputValidator(tool.inputSchema);
|
|
1073
1113
|
connectedTools.set(name, {
|
|
1074
1114
|
client,
|
|
1075
1115
|
serverName,
|
|
1076
1116
|
toolName: tool.name,
|
|
1117
|
+
readOnly: tool.annotations?.readOnlyHint === true,
|
|
1118
|
+
...(inputIsValid ? { schedulingInputIsValid: inputIsValid } : {}),
|
|
1077
1119
|
sensitiveValues,
|
|
1078
1120
|
definition: {
|
|
1079
1121
|
name,
|
|
@@ -9,7 +9,7 @@ import { shellPermissionMatchCandidates } from './bash-normalization.js';
|
|
|
9
9
|
import { pathIsInsideRoots, validateBashPathSafety, } from './bash-path-safety.js';
|
|
10
10
|
import { validateSedSafety } from './sed-safety.js';
|
|
11
11
|
import { parseShellRule, shellRuleMatches } from './shell-rule-matching.js';
|
|
12
|
-
import { effectiveAdditionalDirectories, effectivePermissionMode, filePermissionSuggestions, permissionRuleValueFromString, permissionRuleValueToString,
|
|
12
|
+
import { effectiveAdditionalDirectories, effectivePermissionMode, filePermissionSuggestions, permissionRuleValueFromString, permissionRuleValueToString, shellInputIsReadOnly, shellPermissionSuggestions, shellSubcommands, skillPermissionSuggestions, } from './permission-updates.js';
|
|
13
13
|
const DEFAULT_BEHAVIOR = {
|
|
14
14
|
Agent: 'allow',
|
|
15
15
|
SendMessage: 'allow',
|
|
@@ -536,8 +536,8 @@ export class ClaudePermissionResolver {
|
|
|
536
536
|
if (command &&
|
|
537
537
|
(permissionMode !== 'auto' || !this.shouldClassify(call)) &&
|
|
538
538
|
subcommands.length > 0 &&
|
|
539
|
-
|
|
540
|
-
matchingRule('allow', subcommandCall(subcommand)) !== undefined)) {
|
|
539
|
+
(shellInputIsReadOnly(command) ||
|
|
540
|
+
subcommands.every((subcommand) => matchingRule('allow', subcommandCall(subcommand)) !== undefined))) {
|
|
541
541
|
return annotatePermissionDecision({ behavior: 'allow' }, 'rule');
|
|
542
542
|
}
|
|
543
543
|
const filePath = FILE_TOOLS.has(call.name) ? permissionTarget(call) : null;
|
|
@@ -11,6 +11,7 @@ export declare function readDirectoryPermissionUpdate(path: string, cwd: string,
|
|
|
11
11
|
export declare function filePermissionSuggestions(path: string, cwd: string, operation: 'read' | 'write', mode: PermissionMode, outsideWorkingDirectory: boolean, pathsToCheck?: readonly string[], targetIsDirectory?: boolean): readonly PermissionUpdate[];
|
|
12
12
|
export declare function shellSubcommands(command: string, shell?: 'bash' | 'powershell'): readonly string[];
|
|
13
13
|
export declare function shellCommandIsReadOnly(command: string): boolean;
|
|
14
|
+
export declare function shellInputIsReadOnly(command: string): boolean;
|
|
14
15
|
export declare function shellPermissionSuggestions(toolName: 'Bash' | 'PowerShell', command: string, include?: (subcommand: string) => boolean): readonly PermissionUpdate[];
|
|
15
16
|
export declare function skillPermissionSuggestions(skill: string): readonly PermissionUpdate[];
|
|
16
17
|
//# sourceMappingURL=permission-updates.d.ts.map
|
|
@@ -281,11 +281,23 @@ const READ_ONLY_SHELL_COMMANDS = new Set([
|
|
|
281
281
|
function commandName(command) {
|
|
282
282
|
return command.trim().split(/\s+/u)[0] ?? '';
|
|
283
283
|
}
|
|
284
|
+
const AMBIGUOUS_SHELL_SYNTAX = /[<>`$*?[\]{}~]/u;
|
|
284
285
|
export function shellCommandIsReadOnly(command) {
|
|
285
|
-
if (/[
|
|
286
|
+
if (AMBIGUOUS_SHELL_SYNTAX.test(command) || /[&;|\n\r]/u.test(command)) {
|
|
286
287
|
return false;
|
|
288
|
+
}
|
|
287
289
|
return READ_ONLY_SHELL_COMMANDS.has(commandName(command));
|
|
288
290
|
}
|
|
291
|
+
export function shellInputIsReadOnly(command) {
|
|
292
|
+
if (AMBIGUOUS_SHELL_SYNTAX.test(command) ||
|
|
293
|
+
/(^|[^&])&($|[^&])/u.test(command)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const analysis = analyzeBashCommands(command);
|
|
297
|
+
return (analysis.parsed &&
|
|
298
|
+
analysis.commands.length > 0 &&
|
|
299
|
+
analysis.commands.every(shellCommandIsReadOnly));
|
|
300
|
+
}
|
|
289
301
|
export function shellPermissionSuggestions(toolName, command, include = () => true) {
|
|
290
302
|
const parts = shellSubcommands(command, toolName === 'Bash' ? 'bash' : 'powershell');
|
|
291
303
|
const candidates = (parts.length > 0 ? parts : [command]).filter((part) => !shellCommandIsReadOnly(part) && include(part));
|
|
@@ -72,6 +72,7 @@ export declare class ClaudeCapabilityToolRegistry implements ToolRegistry {
|
|
|
72
72
|
private readonly capabilities;
|
|
73
73
|
constructor(base: ToolRegistry, capabilities: ReadonlySet<string>);
|
|
74
74
|
definitions(): readonly ModelToolDefinition[];
|
|
75
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
75
76
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
76
77
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
77
78
|
private assertEnabled;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
1
2
|
/** Stable environment names. Explicit input booleans override these. */
|
|
2
3
|
export const CLAUDE_CODE_ENABLE_TASKS = 'CLAUDE_CODE_ENABLE_TASKS';
|
|
3
4
|
export const CLAUDE_CODE_DISABLE_CRON = 'CLAUDE_CODE_DISABLE_CRON';
|
|
@@ -170,6 +171,10 @@ export class ClaudeCapabilityToolRegistry {
|
|
|
170
171
|
definitions() {
|
|
171
172
|
return filterClaudeToolDefinitions(this.base.definitions(), this.capabilities);
|
|
172
173
|
}
|
|
174
|
+
schedulingPolicy(call) {
|
|
175
|
+
this.assertEnabled(call.name);
|
|
176
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
177
|
+
}
|
|
173
178
|
async prepare(call, context) {
|
|
174
179
|
this.assertEnabled(call.name);
|
|
175
180
|
return this.base.prepare(call, context);
|