praxis-agent 0.27.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-service.js +8 -1
- package/dist/application/subagent-service.d.ts +1 -0
- package/dist/application/subagent-service.js +23 -1
- 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
|
@@ -16,6 +16,7 @@ import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
|
|
|
16
16
|
import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../compatibility/claude/tool-links.js';
|
|
17
17
|
import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
|
|
18
18
|
import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
|
|
19
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
19
20
|
import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
20
21
|
import { usageCostUsd } from '../core/usage.js';
|
|
21
22
|
import { ContextBudget, ContextRecoveryPlanner, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
@@ -61,6 +62,7 @@ function mainAgentToolNames(tools, agent) {
|
|
|
61
62
|
}
|
|
62
63
|
const emptyToolRegistry = {
|
|
63
64
|
definitions: () => [],
|
|
65
|
+
schedulingPolicy: () => ({ concurrency: 'exclusive' }),
|
|
64
66
|
prepare: async (call) => call,
|
|
65
67
|
execute: async () => ({ content: '', isError: false }),
|
|
66
68
|
};
|
|
@@ -943,6 +945,7 @@ export class ClaudeSessionService {
|
|
|
943
945
|
(rightIndex < 0 ? preferredOrder.length : rightIndex));
|
|
944
946
|
});
|
|
945
947
|
},
|
|
948
|
+
schedulingPolicy: (call) => resolveToolSchedulingPolicy(capabilityRegistry, call),
|
|
946
949
|
prepare: (call, context) => capabilityRegistry.prepare(call, context),
|
|
947
950
|
execute: (call, context) => capabilityRegistry.execute(call, context),
|
|
948
951
|
};
|
|
@@ -2316,6 +2319,10 @@ export class ClaudeSessionService {
|
|
|
2316
2319
|
const fileHistoryTools = fileHistory && interactiveMessageTools
|
|
2317
2320
|
? {
|
|
2318
2321
|
definitions: () => interactiveMessageTools.definitions(),
|
|
2322
|
+
schedulingPolicy: (call) => ({
|
|
2323
|
+
...resolveToolSchedulingPolicy(interactiveMessageTools, call),
|
|
2324
|
+
startAfterAssistant: true,
|
|
2325
|
+
}),
|
|
2319
2326
|
prepare: (call, context) => interactiveMessageTools.prepare(call, context),
|
|
2320
2327
|
execute: async (call, context) => {
|
|
2321
2328
|
const path = call.name === 'Write' || call.name === 'Edit'
|
|
@@ -3083,7 +3090,7 @@ export class ClaudeSessionService {
|
|
|
3083
3090
|
toolResultDirectory,
|
|
3084
3091
|
observer,
|
|
3085
3092
|
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
3086
|
-
...(this.options.maxModelTurns
|
|
3093
|
+
...(this.options.maxModelTurns !== undefined
|
|
3087
3094
|
? { maxModelTurns: this.options.maxModelTurns }
|
|
3088
3095
|
: {}),
|
|
3089
3096
|
...(this.options.betas?.length ? { betas: this.options.betas } : {}),
|
|
@@ -45,6 +45,7 @@ export declare class StructuredOutputRegistry implements ToolRegistry {
|
|
|
45
45
|
value: unknown;
|
|
46
46
|
} | undefined);
|
|
47
47
|
definitions(): readonly ModelToolDefinition[];
|
|
48
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
48
49
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
49
50
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
50
51
|
}
|
|
@@ -11,6 +11,7 @@ import { createClaudeHookAttachmentEntries, translateProviderEvents, } from '../
|
|
|
11
11
|
import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
12
12
|
import { ContextBudget } from '../core/context-budget.js';
|
|
13
13
|
import { AgentRuntime, } from '../core/runtime.js';
|
|
14
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
14
15
|
import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.js';
|
|
15
16
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
16
17
|
import { ClaudeSidechainStore } from '../persistence/claude-sidechain-store.js';
|
|
@@ -26,6 +27,7 @@ const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
|
26
27
|
const SIDECHAIN_DISCOVERY_MAX_DEPTH = 4;
|
|
27
28
|
const structuredOnlyTools = {
|
|
28
29
|
definitions: () => [],
|
|
30
|
+
schedulingPolicy: () => ({ concurrency: 'exclusive' }),
|
|
29
31
|
prepare: async (call) => call,
|
|
30
32
|
execute: async () => ({ content: '', isError: false }),
|
|
31
33
|
};
|
|
@@ -68,6 +70,12 @@ export class StructuredOutputRegistry {
|
|
|
68
70
|
]),
|
|
69
71
|
];
|
|
70
72
|
}
|
|
73
|
+
schedulingPolicy(call) {
|
|
74
|
+
if (call.name === 'StructuredOutput') {
|
|
75
|
+
return { concurrency: 'exclusive' };
|
|
76
|
+
}
|
|
77
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
78
|
+
}
|
|
71
79
|
prepare(call, context) {
|
|
72
80
|
if (call.name !== 'StructuredOutput')
|
|
73
81
|
return this.base.prepare(call, context);
|
|
@@ -106,6 +114,12 @@ class RestrictedToolRegistry {
|
|
|
106
114
|
.definitions()
|
|
107
115
|
.filter((definition) => this.allowed.has(definition.name));
|
|
108
116
|
}
|
|
117
|
+
schedulingPolicy(call) {
|
|
118
|
+
if (!this.allowed.has(call.name)) {
|
|
119
|
+
return { concurrency: 'exclusive' };
|
|
120
|
+
}
|
|
121
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
122
|
+
}
|
|
109
123
|
prepare(call, context) {
|
|
110
124
|
if (!this.allowed.has(call.name))
|
|
111
125
|
throw new Error(`Tool ${call.name} is unavailable to this agent`);
|
|
@@ -1353,7 +1367,9 @@ export class ClaudeSubagentExecutor {
|
|
|
1353
1367
|
const runtime = new AgentRuntime(options.provider, emit, {
|
|
1354
1368
|
tools: runtimeTools,
|
|
1355
1369
|
permissions: runtimePermissions,
|
|
1356
|
-
|
|
1370
|
+
...(customAgent?.maxTurns === undefined
|
|
1371
|
+
? {}
|
|
1372
|
+
: { maxModelTurns: customAgent.maxTurns }),
|
|
1357
1373
|
maxModelOutputBytes: this.options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
|
|
1358
1374
|
maxToolCallsPerTurn: 32,
|
|
1359
1375
|
maxToolInputBytes: 1024 * 1024,
|
|
@@ -1647,6 +1663,12 @@ class ClaudeSubagentToolRegistry {
|
|
|
1647
1663
|
...ordinary.slice(insertionIndex),
|
|
1648
1664
|
];
|
|
1649
1665
|
}
|
|
1666
|
+
schedulingPolicy(call) {
|
|
1667
|
+
if (['Agent', 'SendMessage', 'TaskOutput', 'TaskStop'].includes(call.name)) {
|
|
1668
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
1669
|
+
}
|
|
1670
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
1671
|
+
}
|
|
1650
1672
|
async prepare(call, context) {
|
|
1651
1673
|
if (call.name === 'Agent')
|
|
1652
1674
|
return this.executor.prepare(call, this.depth);
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -322,8 +322,15 @@ export interface ToolExecutionContext {
|
|
|
322
322
|
permissionPhase?: 'request' | 'execute';
|
|
323
323
|
permissionApproved?: boolean;
|
|
324
324
|
}
|
|
325
|
+
export interface ToolSchedulingPolicy {
|
|
326
|
+
concurrency: 'concurrent' | 'exclusive';
|
|
327
|
+
cancelOnInterrupt?: boolean;
|
|
328
|
+
abortGroupOnError?: boolean;
|
|
329
|
+
startAfterAssistant?: boolean;
|
|
330
|
+
}
|
|
325
331
|
export interface ToolRegistry {
|
|
326
332
|
definitions(): readonly ModelToolDefinition[];
|
|
333
|
+
schedulingPolicy?(call: ModelToolCall): ToolSchedulingPolicy;
|
|
327
334
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
328
335
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
329
336
|
}
|
|
@@ -514,6 +521,9 @@ export declare class AgentRuntime {
|
|
|
514
521
|
recoverToolCalls(calls: readonly ModelToolCall[], request: AgentToolRecoveryRequest): Promise<ToolExecutionResult[]>;
|
|
515
522
|
executeDirectToolCall(call: ModelToolCall, request: AgentToolRecoveryRequest): Promise<ToolExecutionResult>;
|
|
516
523
|
private completeToolCall;
|
|
524
|
+
private executeScheduledToolCall;
|
|
525
|
+
private presentToolResult;
|
|
526
|
+
private persistToolCall;
|
|
517
527
|
private executeTool;
|
|
518
528
|
private requireRecoveryApproval;
|
|
519
529
|
private cancel;
|
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);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
3
4
|
const ASK_USER_QUESTION = {
|
|
4
5
|
name: 'AskUserQuestion',
|
|
5
6
|
description: "Use this tool only when you are blocked on a decision that is genuinely the user's to make: one you cannot resolve from the request, the code, or sensible defaults. Users can always provide custom text. Use multiSelect for non-exclusive choices. In plan mode, use this tool to clarify requirements before ExitPlanMode; do not use it to request plan approval.",
|
|
@@ -197,6 +198,12 @@ class ClaudeInteractiveToolRegistry {
|
|
|
197
198
|
...INTERACTIVE_DEFINITIONS.filter(({ name }) => this.manager.enabledNames.has(name)),
|
|
198
199
|
];
|
|
199
200
|
}
|
|
201
|
+
schedulingPolicy(call) {
|
|
202
|
+
if (this.manager.enabledNames.has(call.name)) {
|
|
203
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
204
|
+
}
|
|
205
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
206
|
+
}
|
|
200
207
|
async prepare(call, context) {
|
|
201
208
|
return this.manager.enabledNames.has(call.name)
|
|
202
209
|
? call
|
|
@@ -4,6 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { extname, resolve, sep } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
7
8
|
import { redactSensitiveText, sanitizeChildEnvironment, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
8
9
|
import { formatClaudeLspResult } from './claude-lsp-formatters.js';
|
|
9
10
|
const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
@@ -730,6 +731,12 @@ class ClaudeLspToolRegistry {
|
|
|
730
731
|
definitions() {
|
|
731
732
|
return [...this.base.definitions(), LSP_DEFINITION];
|
|
732
733
|
}
|
|
734
|
+
schedulingPolicy(call) {
|
|
735
|
+
if (call.name === 'LSP') {
|
|
736
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
737
|
+
}
|
|
738
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
739
|
+
}
|
|
733
740
|
prepare(call, context) {
|
|
734
741
|
return call.name === 'LSP'
|
|
735
742
|
? Promise.resolve(call)
|
|
@@ -16,6 +16,7 @@ export declare class ClaudeScheduledToolRegistry implements ToolRegistry {
|
|
|
16
16
|
private readonly now;
|
|
17
17
|
constructor(options: ClaudeScheduledToolRegistryOptions);
|
|
18
18
|
definitions(): readonly ModelToolDefinition[];
|
|
19
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
19
20
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
20
21
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
21
22
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
1
2
|
const SCHEMA = 'https://json-schema.org/draft/2020-12/schema';
|
|
2
3
|
const CRON_CREATE_DESCRIPTION = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.
|
|
3
4
|
|
|
@@ -191,6 +192,12 @@ export class ClaudeScheduledToolRegistry {
|
|
|
191
192
|
...scheduledDefinitions.filter(({ name }) => (this.enabled?.has(name) ?? true) && !existing.has(name)),
|
|
192
193
|
];
|
|
193
194
|
}
|
|
195
|
+
schedulingPolicy(call) {
|
|
196
|
+
if (DEFINITION_NAMES.has(call.name)) {
|
|
197
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
198
|
+
}
|
|
199
|
+
return resolveToolSchedulingPolicy(this.options.base, call);
|
|
200
|
+
}
|
|
194
201
|
async prepare(call, context) {
|
|
195
202
|
if (!DEFINITION_NAMES.has(call.name)) {
|
|
196
203
|
return this.options.base.prepare(call, context);
|
|
@@ -22,6 +22,7 @@ export declare class ClaudeTaskToolRegistry implements ToolRegistry {
|
|
|
22
22
|
stopBackgroundTask(taskId: string): Promise<BackgroundBashToolResult>;
|
|
23
23
|
isEnabled(name: string): boolean;
|
|
24
24
|
definitions(): readonly ModelToolDefinition[];
|
|
25
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
25
26
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
26
27
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
27
28
|
notifications(waitForRunning: boolean): Promise<string[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
2
3
|
import { BackgroundBashManager, } from '../application/background-bash-manager.js';
|
|
3
4
|
import { isBackgroundBashTaskId } from '../application/background-task-id.js';
|
|
4
5
|
import { ClaudeTaskStore, } from '../persistence/claude-task-store.js';
|
|
@@ -260,6 +261,16 @@ export class ClaudeTaskToolRegistry {
|
|
|
260
261
|
...TASK_DEFINITIONS.filter(({ name }) => this.isEnabled(name) && !existing.has(name)),
|
|
261
262
|
];
|
|
262
263
|
}
|
|
264
|
+
schedulingPolicy(call) {
|
|
265
|
+
if (call.name !== 'Bash' &&
|
|
266
|
+
!TASK_DEFINITIONS.some(({ name }) => name === call.name)) {
|
|
267
|
+
return resolveToolSchedulingPolicy(this.options.base, call);
|
|
268
|
+
}
|
|
269
|
+
if (call.name === 'Bash' && call.input.run_in_background !== true) {
|
|
270
|
+
return resolveToolSchedulingPolicy(this.options.base, call);
|
|
271
|
+
}
|
|
272
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
273
|
+
}
|
|
263
274
|
async prepare(call, context) {
|
|
264
275
|
if (call.name === 'Bash') {
|
|
265
276
|
return this.prepareBash(call, context);
|
|
@@ -10,6 +10,7 @@ export declare class ClaudeUserMessageToolRegistry implements ToolRegistry {
|
|
|
10
10
|
private readonly onMessage;
|
|
11
11
|
constructor(base: ToolRegistry, onMessage: (message: UserMessage) => void);
|
|
12
12
|
definitions(): readonly ModelToolDefinition[];
|
|
13
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
13
14
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
14
15
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
15
16
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { extname, resolve } from 'node:path';
|
|
3
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
3
4
|
export const CLAUDE_USER_MESSAGE_PROMPT = 'When brief mode is enabled, SendUserMessage is the primary user-visible reply channel. Use it for the answer, progress checkpoints, and blockers. Set status to normal for a direct reply and proactive for an unsolicited update.';
|
|
4
5
|
const DEFINITION = {
|
|
5
6
|
name: 'SendUserMessage',
|
|
@@ -42,6 +43,12 @@ export class ClaudeUserMessageToolRegistry {
|
|
|
42
43
|
definitions() {
|
|
43
44
|
return [...this.base.definitions(), DEFINITION];
|
|
44
45
|
}
|
|
46
|
+
schedulingPolicy(call) {
|
|
47
|
+
if (call.name === DEFINITION.name) {
|
|
48
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
49
|
+
}
|
|
50
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
51
|
+
}
|
|
45
52
|
prepare(call, context) {
|
|
46
53
|
if (call.name !== DEFINITION.name)
|
|
47
54
|
return this.base.prepare(call, context);
|
|
@@ -21,6 +21,7 @@ export declare class ClaudeWorkflowToolRegistry implements ToolRegistry {
|
|
|
21
21
|
private readonly prepared;
|
|
22
22
|
constructor(options: ClaudeWorkflowToolRegistryOptions);
|
|
23
23
|
definitions(): readonly ModelToolDefinition[];
|
|
24
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
24
25
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
25
26
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
26
27
|
private resolveSource;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
3
4
|
import { parseWorkflowScript, } from '../application/workflow-manager.js';
|
|
4
5
|
const MAX_SCRIPT_BYTES = 524_288;
|
|
5
6
|
function workflowDefinition(dataPlane) {
|
|
@@ -91,6 +92,12 @@ export class ClaudeWorkflowToolRegistry {
|
|
|
91
92
|
}
|
|
92
93
|
return [...base, workflowDefinition(this.options.dataPlane ?? 'claude')];
|
|
93
94
|
}
|
|
95
|
+
schedulingPolicy(call) {
|
|
96
|
+
if (['Workflow', 'TaskOutput', 'TaskStop'].includes(call.name)) {
|
|
97
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
98
|
+
}
|
|
99
|
+
return resolveToolSchedulingPolicy(this.options.base, call);
|
|
100
|
+
}
|
|
94
101
|
async prepare(call, context) {
|
|
95
102
|
if (call.name === 'TaskOutput' || call.name === 'TaskStop') {
|
|
96
103
|
const taskId = call.input.task_id ?? call.input.shell_id;
|
|
@@ -12,6 +12,7 @@ export declare class ClaudeWorktreeToolRegistry implements ToolRegistry {
|
|
|
12
12
|
dataPlane?: DataPlane;
|
|
13
13
|
});
|
|
14
14
|
definitions(): readonly ModelToolDefinition[];
|
|
15
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
15
16
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
16
17
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
17
18
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
1
2
|
const ENTER_DEFINITION = {
|
|
2
3
|
name: 'EnterWorktree',
|
|
3
4
|
description: 'Create or enter an isolated Git worktree and switch the current session into it.',
|
|
@@ -96,6 +97,12 @@ export class ClaudeWorktreeToolRegistry {
|
|
|
96
97
|
...definitions.filter((definition) => this.enabled.has(definition.name) && !existing.has(definition.name)),
|
|
97
98
|
];
|
|
98
99
|
}
|
|
100
|
+
schedulingPolicy(call) {
|
|
101
|
+
if (['EnterWorktree', 'ExitWorktree'].includes(call.name)) {
|
|
102
|
+
return { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
103
|
+
}
|
|
104
|
+
return resolveToolSchedulingPolicy(this.options.base, call);
|
|
105
|
+
}
|
|
99
106
|
async prepare(call, context) {
|
|
100
107
|
if (call.name === 'EnterWorktree' && this.enabled.has(call.name)) {
|
|
101
108
|
const input = objectInput(call);
|
|
@@ -9,6 +9,7 @@ export declare class FilteredToolRegistry implements ToolRegistry {
|
|
|
9
9
|
private readonly enabledNames;
|
|
10
10
|
constructor(base: ToolRegistry, options?: FilteredToolRegistryOptions);
|
|
11
11
|
definitions(): readonly ModelToolDefinition[];
|
|
12
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
12
13
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
13
14
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
14
15
|
private assertEnabled;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
1
2
|
function exactToolNames(rules) {
|
|
2
3
|
return new Set(rules.flatMap((rule) => {
|
|
3
4
|
const match = /^([A-Za-z][\w-]*)$/.exec(rule);
|
|
@@ -34,6 +35,10 @@ export class FilteredToolRegistry {
|
|
|
34
35
|
return definition;
|
|
35
36
|
});
|
|
36
37
|
}
|
|
38
|
+
schedulingPolicy(call) {
|
|
39
|
+
this.assertEnabled(call.name);
|
|
40
|
+
return resolveToolSchedulingPolicy(this.base, call);
|
|
41
|
+
}
|
|
37
42
|
async prepare(call, context) {
|
|
38
43
|
this.assertEnabled(call.name);
|
|
39
44
|
return this.base.prepare(call, context);
|
|
@@ -53,6 +53,23 @@ export declare class LocalToolRegistry implements ToolRegistry {
|
|
|
53
53
|
private assertProtectedBashCommand;
|
|
54
54
|
private currentCwd;
|
|
55
55
|
definitions(): readonly ModelToolDefinition[];
|
|
56
|
+
schedulingPolicy(call: ModelToolCall): {
|
|
57
|
+
concurrency: "concurrent";
|
|
58
|
+
cancelOnInterrupt: boolean;
|
|
59
|
+
abortGroupOnError?: never;
|
|
60
|
+
} | {
|
|
61
|
+
concurrency: "concurrent";
|
|
62
|
+
cancelOnInterrupt: boolean;
|
|
63
|
+
abortGroupOnError: boolean;
|
|
64
|
+
} | {
|
|
65
|
+
abortGroupOnError?: boolean;
|
|
66
|
+
concurrency: "exclusive";
|
|
67
|
+
cancelOnInterrupt: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
concurrency: "exclusive";
|
|
70
|
+
cancelOnInterrupt?: never;
|
|
71
|
+
abortGroupOnError?: never;
|
|
72
|
+
};
|
|
56
73
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
57
74
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
58
75
|
private workspacePath;
|
|
@@ -11,7 +11,7 @@ import { editNotebook, formatNotebookForRead } from './notebook.js';
|
|
|
11
11
|
import { openPdf } from './pdf.js';
|
|
12
12
|
import { validateBashPathSafety } from '../permissions/bash-path-safety.js';
|
|
13
13
|
import { protectedWritePathReason } from '../permissions/bypass-immune-paths.js';
|
|
14
|
-
import { effectiveAdditionalDirectories } from '../permissions/permission-updates.js';
|
|
14
|
+
import { effectiveAdditionalDirectories, shellInputIsReadOnly, } from '../permissions/permission-updates.js';
|
|
15
15
|
const REPORT_FINDINGS_DEFINITION = {
|
|
16
16
|
name: 'ReportFindings',
|
|
17
17
|
description: "Report code-review findings as a typed list so the host UI can render them. Use this only when the active code-review instructions tell you to report findings with this tool; otherwise follow whatever output format those instructions specify. When reporting a review's results, call it once with the verified findings ranked most-severe first (empty array if nothing survived verification) and do not also print the findings as text. When re-reporting after applying fixes (only if the apply instructions ask for it), set outcome on each finding to what actually happened.",
|
|
@@ -277,6 +277,64 @@ function optionalNonNegativeInteger(input, name) {
|
|
|
277
277
|
function isRecord(value) {
|
|
278
278
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
279
279
|
}
|
|
280
|
+
function hasOnlyInputKeys(input, allowed) {
|
|
281
|
+
const names = new Set(allowed);
|
|
282
|
+
return Object.keys(input).every((name) => names.has(name));
|
|
283
|
+
}
|
|
284
|
+
function localSchedulingInputIsValid(call) {
|
|
285
|
+
try {
|
|
286
|
+
if (call.name === 'Read') {
|
|
287
|
+
stringInput(call.input, 'file_path');
|
|
288
|
+
optionalNonNegativeInteger(call.input, 'offset');
|
|
289
|
+
optionalPositiveInteger(call.input, 'limit');
|
|
290
|
+
optionalString(call.input, 'pages');
|
|
291
|
+
return hasOnlyInputKeys(call.input, [
|
|
292
|
+
'file_path',
|
|
293
|
+
'offset',
|
|
294
|
+
'limit',
|
|
295
|
+
'pages',
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
if (call.name === 'Glob') {
|
|
299
|
+
stringInput(call.input, 'pattern', true);
|
|
300
|
+
optionalString(call.input, 'path');
|
|
301
|
+
return hasOnlyInputKeys(call.input, ['pattern', 'path']);
|
|
302
|
+
}
|
|
303
|
+
if (call.name === 'Grep') {
|
|
304
|
+
stringInput(call.input, 'pattern');
|
|
305
|
+
optionalString(call.input, 'path');
|
|
306
|
+
optionalString(call.input, 'glob');
|
|
307
|
+
return hasOnlyInputKeys(call.input, ['pattern', 'path', 'glob']);
|
|
308
|
+
}
|
|
309
|
+
if (call.name === 'Bash') {
|
|
310
|
+
stringInput(call.input, 'command');
|
|
311
|
+
optionalPositiveInteger(call.input, 'timeout');
|
|
312
|
+
const description = call.input.description;
|
|
313
|
+
const background = call.input.run_in_background;
|
|
314
|
+
const disableSandbox = call.input.dangerouslyDisableSandbox;
|
|
315
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (background !== undefined && typeof background !== 'boolean') {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (disableSandbox !== undefined && typeof disableSandbox !== 'boolean') {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return hasOnlyInputKeys(call.input, [
|
|
325
|
+
'command',
|
|
326
|
+
'timeout',
|
|
327
|
+
'description',
|
|
328
|
+
'run_in_background',
|
|
329
|
+
'dangerouslyDisableSandbox',
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
280
338
|
const REPORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
281
339
|
const REPORT_VERDICTS = ['CONFIRMED', 'PLAUSIBLE'];
|
|
282
340
|
const REPORT_OUTCOMES = ['fixed', 'skipped', 'no_change_needed'];
|
|
@@ -535,6 +593,33 @@ export class LocalToolRegistry {
|
|
|
535
593
|
}
|
|
536
594
|
: definition);
|
|
537
595
|
}
|
|
596
|
+
schedulingPolicy(call) {
|
|
597
|
+
if (['Read', 'Glob', 'Grep'].includes(call.name) &&
|
|
598
|
+
localSchedulingInputIsValid(call)) {
|
|
599
|
+
return { concurrency: 'concurrent', cancelOnInterrupt: true };
|
|
600
|
+
}
|
|
601
|
+
if (call.name === 'Bash' &&
|
|
602
|
+
localSchedulingInputIsValid(call) &&
|
|
603
|
+
call.input.run_in_background !== true &&
|
|
604
|
+
typeof call.input.command === 'string' &&
|
|
605
|
+
shellInputIsReadOnly(call.input.command)) {
|
|
606
|
+
return {
|
|
607
|
+
concurrency: 'concurrent',
|
|
608
|
+
cancelOnInterrupt: true,
|
|
609
|
+
abortGroupOnError: true,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
if (TOOL_DEFINITIONS.some(({ name }) => name === call.name)) {
|
|
613
|
+
return {
|
|
614
|
+
concurrency: 'exclusive',
|
|
615
|
+
cancelOnInterrupt: true,
|
|
616
|
+
...(call.name === 'Bash' ? { abortGroupOnError: true } : {}),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
concurrency: 'exclusive',
|
|
621
|
+
};
|
|
622
|
+
}
|
|
538
623
|
async prepare(call, context) {
|
|
539
624
|
if (context.signal?.aborted)
|
|
540
625
|
throw abortError();
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export declare class WebToolRegistry implements ToolRegistry {
|
|
|
36
36
|
private cacheBytes;
|
|
37
37
|
constructor(options: WebToolRegistryOptions);
|
|
38
38
|
definitions(): readonly ModelToolDefinition[];
|
|
39
|
+
schedulingPolicy(call: ModelToolCall): import("../core/runtime.js").ToolSchedulingPolicy;
|
|
39
40
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
40
41
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
41
42
|
private webFetch;
|
package/dist/tools/web.js
CHANGED
|
@@ -6,6 +6,7 @@ import { isIP } from 'node:net';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import ipaddr from 'ipaddr.js';
|
|
8
8
|
import TurndownService from 'turndown';
|
|
9
|
+
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
9
10
|
const MAX_URL_LENGTH = 2_000;
|
|
10
11
|
const MAX_MARKDOWN_LENGTH = 100_000;
|
|
11
12
|
const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
@@ -273,6 +274,24 @@ function parseWebSearchInput(input) {
|
|
|
273
274
|
...(blockedDomains ? { blockedDomains } : {}),
|
|
274
275
|
};
|
|
275
276
|
}
|
|
277
|
+
function webSchedulingInputIsValid(call) {
|
|
278
|
+
try {
|
|
279
|
+
if (call.name === 'WebFetch') {
|
|
280
|
+
parseWebFetchInput(call.input);
|
|
281
|
+
return Object.keys(call.input).every((name) => name === 'url' || name === 'prompt');
|
|
282
|
+
}
|
|
283
|
+
if (call.name === 'WebSearch') {
|
|
284
|
+
parseWebSearchInput(call.input);
|
|
285
|
+
return Object.keys(call.input).every((name) => name === 'query' ||
|
|
286
|
+
name === 'allowed_domains' ||
|
|
287
|
+
name === 'blocked_domains');
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
276
295
|
function normalizedAddress(value) {
|
|
277
296
|
const parsed = ipaddr.parse(value);
|
|
278
297
|
return parsed instanceof ipaddr.IPv6 && parsed.isIPv4MappedAddress()
|
|
@@ -475,6 +494,14 @@ export class WebToolRegistry {
|
|
|
475
494
|
: []),
|
|
476
495
|
];
|
|
477
496
|
}
|
|
497
|
+
schedulingPolicy(call) {
|
|
498
|
+
if (call.name === 'WebFetch' || call.name === 'WebSearch') {
|
|
499
|
+
return webSchedulingInputIsValid(call)
|
|
500
|
+
? { concurrency: 'concurrent', cancelOnInterrupt: true }
|
|
501
|
+
: { concurrency: 'exclusive', cancelOnInterrupt: true };
|
|
502
|
+
}
|
|
503
|
+
return resolveToolSchedulingPolicy(this.options.base, call);
|
|
504
|
+
}
|
|
478
505
|
async prepare(call, context) {
|
|
479
506
|
if (context.signal?.aborted)
|
|
480
507
|
throw abortError();
|