praxis-agent 0.20.21 → 0.21.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/README.md +11 -8
- package/dist/application/background-agent-manager.js +15 -10
- package/dist/application/session-memory.d.ts +96 -0
- package/dist/application/session-memory.js +383 -0
- package/dist/application/session-service.d.ts +18 -0
- package/dist/application/session-service.js +325 -65
- package/dist/application/subagent-service.d.ts +2 -1
- package/dist/application/subagent-service.js +147 -51
- package/dist/cli/interactive.js +17 -5
- package/dist/cli/tui/claude-style.d.ts +4 -0
- package/dist/cli/tui/claude-style.js +11 -0
- package/dist/cli-runtime.js +22 -18
- package/dist/compatibility/claude/schema.d.ts +7 -0
- package/dist/compatibility/claude/schema.js +22 -10
- package/dist/compatibility/claude/sidechain.d.ts +5 -1
- package/dist/compatibility/claude/sidechain.js +15 -2
- package/dist/core/context-budget.d.ts +53 -2
- package/dist/core/context-budget.js +117 -5
- package/dist/tools/claude-capabilities.d.ts +79 -0
- package/dist/tools/claude-capabilities.js +187 -0
- package/package.json +1 -1
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { isClaudeSessionId } from './paths.js';
|
|
3
3
|
const AGENT_ID_PATTERN = /^a[0-9a-f]{16}$/u;
|
|
4
|
-
export function resolveClaudeSidechainPaths(projectRoot, sessionId, agentId) {
|
|
4
|
+
export function resolveClaudeSidechainPaths(projectRoot, sessionId, agentId, options = {}) {
|
|
5
5
|
if (!isClaudeSessionId(sessionId)) {
|
|
6
6
|
throw new Error(`Invalid Claude session ID: ${sessionId}`);
|
|
7
7
|
}
|
|
8
8
|
if (!AGENT_ID_PATTERN.test(agentId)) {
|
|
9
9
|
throw new Error(`Invalid Claude agent ID: ${agentId}`);
|
|
10
10
|
}
|
|
11
|
-
const
|
|
11
|
+
const subdirectory = options.subdirectory;
|
|
12
|
+
if (subdirectory !== undefined &&
|
|
13
|
+
(subdirectory.length === 0 ||
|
|
14
|
+
subdirectory.startsWith('/') ||
|
|
15
|
+
subdirectory.includes('\\') ||
|
|
16
|
+
subdirectory.includes('\0') ||
|
|
17
|
+
subdirectory
|
|
18
|
+
.split('/')
|
|
19
|
+
.some((segment) => segment === '.' || segment === '..'))) {
|
|
20
|
+
throw new Error(`Invalid Claude sidechain subdirectory: ${subdirectory}`);
|
|
21
|
+
}
|
|
22
|
+
const directory = subdirectory === undefined
|
|
23
|
+
? resolve(projectRoot, sessionId, 'subagents')
|
|
24
|
+
: resolve(projectRoot, sessionId, 'subagents', subdirectory);
|
|
12
25
|
return {
|
|
13
26
|
sessionId,
|
|
14
27
|
agentId,
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import type { ModelMessage, ModelToolDefinition } from './runtime.js';
|
|
1
|
+
import type { ModelMessage, ModelToolDefinition, ModelUsage } from './runtime.js';
|
|
2
2
|
export interface ContextBudgetOptions {
|
|
3
3
|
contextWindowTokens: number;
|
|
4
4
|
reserveTokens?: number;
|
|
5
|
+
/** Declares whether the configured window came from a provider capability so
|
|
6
|
+
* reports can distinguish provider-derived decisions from estimates. */
|
|
7
|
+
windowSource?: 'capability' | 'estimate';
|
|
5
8
|
}
|
|
9
|
+
export interface ContextBudgetEvaluateOptions {
|
|
10
|
+
/** Most recent provider usage observation; a positive `contextWindow` is
|
|
11
|
+
* authoritative for the effective window. */
|
|
12
|
+
lastUsage?: ModelUsage;
|
|
13
|
+
/** Provider-reported output tokens to reserve in the overflow accounting. */
|
|
14
|
+
outputTokens?: number;
|
|
15
|
+
/** Force `shouldCompact` when the provider rejected the prompt as too long. */
|
|
16
|
+
promptTooLong?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type ContextBudgetSource = 'provider' | 'capability' | 'estimate';
|
|
6
19
|
export interface ContextBudgetReport {
|
|
7
20
|
estimatedTokens: number;
|
|
8
21
|
contextWindowTokens: number;
|
|
@@ -10,6 +23,7 @@ export interface ContextBudgetReport {
|
|
|
10
23
|
availableTokens: number;
|
|
11
24
|
overflowTokens: number;
|
|
12
25
|
shouldCompact: boolean;
|
|
26
|
+
source: ContextBudgetSource;
|
|
13
27
|
}
|
|
14
28
|
export declare class ContextOverflowError extends Error {
|
|
15
29
|
readonly report: ContextBudgetReport;
|
|
@@ -21,8 +35,45 @@ export declare function estimateModelRequestTokens(messages: readonly ModelMessa
|
|
|
21
35
|
export declare class ContextBudget {
|
|
22
36
|
readonly contextWindowTokens: number;
|
|
23
37
|
readonly reserveTokens: number;
|
|
38
|
+
readonly windowSource: 'capability' | 'estimate';
|
|
39
|
+
private observedUsage;
|
|
24
40
|
constructor(options: ContextBudgetOptions);
|
|
25
|
-
|
|
41
|
+
observeUsage(usage: ModelUsage): void;
|
|
42
|
+
effectiveContextWindow(usage?: ModelUsage): number;
|
|
43
|
+
evaluate(messages: readonly ModelMessage[], tools?: readonly ModelToolDefinition[], options?: ContextBudgetEvaluateOptions): ContextBudgetReport;
|
|
26
44
|
assertFits(report: ContextBudgetReport): void;
|
|
45
|
+
/** Returns the positive provider-reported context window, if any. */
|
|
46
|
+
private providerContextWindow;
|
|
47
|
+
}
|
|
48
|
+
export type ContextRecoveryStage = 'preflight' | 'microcompact' | 'auto-compact' | 'reactive-retry' | 'blocked';
|
|
49
|
+
export interface ContextRecoveryPlannerOptions {
|
|
50
|
+
/** Number of reactive retries allowed before the planner reports `blocked`. */
|
|
51
|
+
maxReactiveRetries?: number;
|
|
52
|
+
/** Consecutive failures that trip the circuit breaker to `blocked`. */
|
|
53
|
+
consecutiveFailureThreshold?: number;
|
|
54
|
+
}
|
|
55
|
+
export declare class ContextRecoveryPlanner {
|
|
56
|
+
private readonly maxReactiveRetries;
|
|
57
|
+
private readonly consecutiveFailureThreshold;
|
|
58
|
+
private currentStage;
|
|
59
|
+
private reactiveRetriesUsed;
|
|
60
|
+
private consecutiveFailures;
|
|
61
|
+
constructor(options?: ContextRecoveryPlannerOptions);
|
|
62
|
+
get stage(): ContextRecoveryStage;
|
|
63
|
+
get reactiveRetriesRemaining(): number;
|
|
64
|
+
/** Advance one escalation stage; bounded so the planner never leaves
|
|
65
|
+
* `blocked`. Side-effect free apart from the internal stage. */
|
|
66
|
+
advance(): ContextRecoveryStage;
|
|
67
|
+
/** Consume one reactive retry decision; reports `blocked` once the maximum
|
|
68
|
+
* reactive retries have been consumed. */
|
|
69
|
+
consumeReactiveRetry(): ContextRecoveryStage;
|
|
70
|
+
/** Record a failed recovery attempt; trips the circuit breaker to `blocked`
|
|
71
|
+
* after `consecutiveFailureThreshold` consecutive failures. */
|
|
72
|
+
recordFailure(): ContextRecoveryStage;
|
|
73
|
+
/** Record a successful recovery/model attempt; resets the planner. */
|
|
74
|
+
recordSuccess(): ContextRecoveryStage;
|
|
27
75
|
}
|
|
76
|
+
/** True when a provider error signals that the request exceeded the model's
|
|
77
|
+
* context window and a reactive compaction retry is the relevant recovery. */
|
|
78
|
+
export declare function isPromptTooLongError(error: unknown): boolean;
|
|
28
79
|
//# sourceMappingURL=context-budget.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ModelProviderError } from './runtime.js';
|
|
1
2
|
export class ContextOverflowError extends Error {
|
|
2
3
|
report;
|
|
3
4
|
name = 'ContextOverflowError';
|
|
@@ -76,6 +77,8 @@ export function estimateModelRequestTokens(messages, tools = []) {
|
|
|
76
77
|
export class ContextBudget {
|
|
77
78
|
contextWindowTokens;
|
|
78
79
|
reserveTokens;
|
|
80
|
+
windowSource;
|
|
81
|
+
observedUsage;
|
|
79
82
|
constructor(options) {
|
|
80
83
|
requirePositiveInteger(options.contextWindowTokens, 'Context window tokens');
|
|
81
84
|
const defaultReserve = Math.min(8192, Math.max(1, Math.floor(options.contextWindowTokens / 10)));
|
|
@@ -86,23 +89,132 @@ export class ContextBudget {
|
|
|
86
89
|
}
|
|
87
90
|
this.contextWindowTokens = options.contextWindowTokens;
|
|
88
91
|
this.reserveTokens = reserveTokens;
|
|
92
|
+
this.windowSource = options.windowSource ?? 'estimate';
|
|
89
93
|
}
|
|
90
|
-
|
|
94
|
+
observeUsage(usage) {
|
|
95
|
+
this.observedUsage = usage;
|
|
96
|
+
}
|
|
97
|
+
effectiveContextWindow(usage) {
|
|
98
|
+
return this.providerContextWindow(usage) ?? this.contextWindowTokens;
|
|
99
|
+
}
|
|
100
|
+
evaluate(messages, tools = [], options = {}) {
|
|
91
101
|
const estimatedTokens = estimateModelRequestTokens(messages, tools);
|
|
92
|
-
const
|
|
93
|
-
const
|
|
102
|
+
const providerWindow = this.providerContextWindow(options.lastUsage);
|
|
103
|
+
const contextWindowTokens = providerWindow ?? this.contextWindowTokens;
|
|
104
|
+
const outputTokens = options.outputTokens !== undefined &&
|
|
105
|
+
Number.isSafeInteger(options.outputTokens) &&
|
|
106
|
+
options.outputTokens >= 0
|
|
107
|
+
? options.outputTokens
|
|
108
|
+
: 0;
|
|
109
|
+
const availableTokens = Math.max(0, contextWindowTokens - this.reserveTokens);
|
|
110
|
+
const overflowTokens = Math.max(0, estimatedTokens + outputTokens - availableTokens);
|
|
111
|
+
const shouldCompact = options.promptTooLong === true || overflowTokens > 0;
|
|
94
112
|
return {
|
|
95
113
|
estimatedTokens,
|
|
96
|
-
contextWindowTokens
|
|
114
|
+
contextWindowTokens,
|
|
97
115
|
reserveTokens: this.reserveTokens,
|
|
98
116
|
availableTokens,
|
|
99
117
|
overflowTokens,
|
|
100
|
-
shouldCompact
|
|
118
|
+
shouldCompact,
|
|
119
|
+
source: providerWindow === undefined ? this.windowSource : 'provider',
|
|
101
120
|
};
|
|
102
121
|
}
|
|
103
122
|
assertFits(report) {
|
|
104
123
|
if (report.shouldCompact)
|
|
105
124
|
throw new ContextOverflowError(report);
|
|
106
125
|
}
|
|
126
|
+
/** Returns the positive provider-reported context window, if any. */
|
|
127
|
+
providerContextWindow(usage) {
|
|
128
|
+
const candidate = usage?.contextWindow ?? this.observedUsage?.contextWindow;
|
|
129
|
+
if (candidate !== undefined &&
|
|
130
|
+
Number.isSafeInteger(candidate) &&
|
|
131
|
+
candidate > 0) {
|
|
132
|
+
return candidate;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const DEFAULT_MAX_REACTIVE_RETRIES = 1;
|
|
138
|
+
const DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD = 3;
|
|
139
|
+
export class ContextRecoveryPlanner {
|
|
140
|
+
maxReactiveRetries;
|
|
141
|
+
consecutiveFailureThreshold;
|
|
142
|
+
currentStage = 'preflight';
|
|
143
|
+
reactiveRetriesUsed = 0;
|
|
144
|
+
consecutiveFailures = 0;
|
|
145
|
+
constructor(options = {}) {
|
|
146
|
+
const maxReactiveRetries = options.maxReactiveRetries ?? DEFAULT_MAX_REACTIVE_RETRIES;
|
|
147
|
+
const consecutiveFailureThreshold = options.consecutiveFailureThreshold ??
|
|
148
|
+
DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD;
|
|
149
|
+
if (!Number.isInteger(maxReactiveRetries) || maxReactiveRetries < 0) {
|
|
150
|
+
throw new Error('maxReactiveRetries must be a nonnegative integer');
|
|
151
|
+
}
|
|
152
|
+
if (!Number.isInteger(consecutiveFailureThreshold) ||
|
|
153
|
+
consecutiveFailureThreshold < 1) {
|
|
154
|
+
throw new Error('consecutiveFailureThreshold must be a positive integer');
|
|
155
|
+
}
|
|
156
|
+
this.maxReactiveRetries = maxReactiveRetries;
|
|
157
|
+
this.consecutiveFailureThreshold = consecutiveFailureThreshold;
|
|
158
|
+
}
|
|
159
|
+
get stage() {
|
|
160
|
+
return this.currentStage;
|
|
161
|
+
}
|
|
162
|
+
get reactiveRetriesRemaining() {
|
|
163
|
+
return Math.max(0, this.maxReactiveRetries - this.reactiveRetriesUsed);
|
|
164
|
+
}
|
|
165
|
+
/** Advance one escalation stage; bounded so the planner never leaves
|
|
166
|
+
* `blocked`. Side-effect free apart from the internal stage. */
|
|
167
|
+
advance() {
|
|
168
|
+
switch (this.currentStage) {
|
|
169
|
+
case 'preflight':
|
|
170
|
+
this.currentStage = 'microcompact';
|
|
171
|
+
break;
|
|
172
|
+
case 'microcompact':
|
|
173
|
+
this.currentStage = 'auto-compact';
|
|
174
|
+
break;
|
|
175
|
+
case 'auto-compact':
|
|
176
|
+
this.currentStage = 'reactive-retry';
|
|
177
|
+
break;
|
|
178
|
+
case 'reactive-retry':
|
|
179
|
+
this.currentStage = 'blocked';
|
|
180
|
+
break;
|
|
181
|
+
case 'blocked':
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
return this.currentStage;
|
|
185
|
+
}
|
|
186
|
+
/** Consume one reactive retry decision; reports `blocked` once the maximum
|
|
187
|
+
* reactive retries have been consumed. */
|
|
188
|
+
consumeReactiveRetry() {
|
|
189
|
+
this.reactiveRetriesUsed += 1;
|
|
190
|
+
this.currentStage =
|
|
191
|
+
this.reactiveRetriesUsed <= this.maxReactiveRetries
|
|
192
|
+
? 'reactive-retry'
|
|
193
|
+
: 'blocked';
|
|
194
|
+
return this.currentStage;
|
|
195
|
+
}
|
|
196
|
+
/** Record a failed recovery attempt; trips the circuit breaker to `blocked`
|
|
197
|
+
* after `consecutiveFailureThreshold` consecutive failures. */
|
|
198
|
+
recordFailure() {
|
|
199
|
+
this.consecutiveFailures += 1;
|
|
200
|
+
if (this.consecutiveFailures >= this.consecutiveFailureThreshold) {
|
|
201
|
+
this.currentStage = 'blocked';
|
|
202
|
+
}
|
|
203
|
+
return this.currentStage;
|
|
204
|
+
}
|
|
205
|
+
/** Record a successful recovery/model attempt; resets the planner. */
|
|
206
|
+
recordSuccess() {
|
|
207
|
+
this.consecutiveFailures = 0;
|
|
208
|
+
this.reactiveRetriesUsed = 0;
|
|
209
|
+
this.currentStage = 'preflight';
|
|
210
|
+
return this.currentStage;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const PROMPT_TOO_LONG_PATTERN = /context(?:[_\s-]?length|[_\s-]?window)|prompt(?:[_\s-]?is)?[_\s-]?too[_\s-]?long|too[_\s-]?many[_\s-]?tokens|maximum[_\s-]?context/iu;
|
|
214
|
+
/** True when a provider error signals that the request exceeded the model's
|
|
215
|
+
* context window and a reactive compaction retry is the relevant recovery. */
|
|
216
|
+
export function isPromptTooLongError(error) {
|
|
217
|
+
return (error instanceof ModelProviderError &&
|
|
218
|
+
PROMPT_TOO_LONG_PATTERN.test(error.message));
|
|
107
219
|
}
|
|
108
220
|
//# sourceMappingURL=context-budget.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ModelToolDefinition } from '../core/runtime.js';
|
|
2
|
+
import type { ModelToolCall, ToolExecutionContext, ToolExecutionResult, ToolRegistry } from '../core/runtime.js';
|
|
3
|
+
/**
|
|
4
|
+
* Capability-driven Claude tool exposure.
|
|
5
|
+
*
|
|
6
|
+
* The advertised/allowed set of capability-gated tools is derived from the
|
|
7
|
+
* runtime role and explicit gates rather than from a Claude version or a fixed
|
|
8
|
+
* global list. Base tools (Bash, Read, Edit, ...) are never part of this
|
|
9
|
+
* resolver; callers keep them and apply the resolved set only to the
|
|
10
|
+
* capability-gated tools they actually expose.
|
|
11
|
+
*/
|
|
12
|
+
export type ClaudeToolRole = 'main' | 'worker' | 'coordinator';
|
|
13
|
+
/**
|
|
14
|
+
* Immutable capability input. `interactive`/`simpleMode` describe the runtime
|
|
15
|
+
* mode; the gate booleans are explicit when present and otherwise fall back to
|
|
16
|
+
* the documented environment overrides, then to the default rules. Explicit
|
|
17
|
+
* booleans always override the environment.
|
|
18
|
+
*/
|
|
19
|
+
export interface ClaudeToolCapabilityInput {
|
|
20
|
+
readonly role: ClaudeToolRole;
|
|
21
|
+
readonly interactive: boolean;
|
|
22
|
+
readonly simpleMode: boolean;
|
|
23
|
+
readonly tasks?: boolean;
|
|
24
|
+
readonly workflowScripts?: boolean;
|
|
25
|
+
readonly agentTriggers?: boolean;
|
|
26
|
+
readonly backgroundAgents?: boolean;
|
|
27
|
+
readonly subagents?: boolean;
|
|
28
|
+
/** Explicit allow-list for capability-gated tools; never enables a gate. */
|
|
29
|
+
readonly tools?: readonly string[];
|
|
30
|
+
/** Always wins over the allow-list and the default rules. */
|
|
31
|
+
readonly disallowedTools?: readonly string[];
|
|
32
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
33
|
+
}
|
|
34
|
+
/** Stable environment names. Explicit input booleans override these. */
|
|
35
|
+
export declare const CLAUDE_CODE_ENABLE_TASKS = "CLAUDE_CODE_ENABLE_TASKS";
|
|
36
|
+
export declare const CLAUDE_CODE_DISABLE_CRON = "CLAUDE_CODE_DISABLE_CRON";
|
|
37
|
+
export declare const PRAXIS_ENABLE_WORKFLOW_SCRIPTS = "PRAXIS_ENABLE_WORKFLOW_SCRIPTS";
|
|
38
|
+
/** task-v2 task-board tools, gated by the `tasks` capability. */
|
|
39
|
+
export declare const CLAUDE_TASK_V2_TOOLS: readonly ["TaskCreate", "TaskGet", "TaskList", "TaskUpdate"];
|
|
40
|
+
/** Workflow tool, gated by the `workflowScripts` capability. */
|
|
41
|
+
export declare const CLAUDE_WORKFLOW_TOOLS: readonly ["Workflow"];
|
|
42
|
+
/** Cron and wakeup tools, gated by the `agentTriggers` capability. */
|
|
43
|
+
export declare const CLAUDE_AGENT_TRIGGER_TOOLS: readonly ["CronCreate", "CronDelete", "CronList", "ScheduleWakeup"];
|
|
44
|
+
/** Agent tool, gated by the `subagents` capability. */
|
|
45
|
+
export declare const CLAUDE_AGENT_TOOLS: readonly ["Agent"];
|
|
46
|
+
/** Background task lifecycle tools, gated by the `backgroundAgents` capability. */
|
|
47
|
+
export declare const CLAUDE_BACKGROUND_TOOLS: readonly ["TaskOutput", "TaskStop"];
|
|
48
|
+
/** Coordination tools advertised by a coordinator only when explicitly enabled. */
|
|
49
|
+
export declare const CLAUDE_COORDINATION_TOOLS: readonly ["AskUserQuestion", "SendMessage", "SendUserMessage", "Monitor", "PushNotification"];
|
|
50
|
+
/** Recursively suppressed for worker agents regardless of any allow-list. */
|
|
51
|
+
export declare const CLAUDE_WORKER_RECURSIVE_TOOLS: readonly ["Agent", "TaskOutput", "TaskStop"];
|
|
52
|
+
export declare const CLAUDE_CAPABILITY_GATED_TOOLS: ReadonlySet<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the deterministic set of capability-gated tool names enabled for the
|
|
55
|
+
* given role, gates, allow-list, and disallow-list. Tools outside this set that
|
|
56
|
+
* are not capability-gated are unaffected by the resolver.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveClaudeToolCapabilities(input: ClaudeToolCapabilityInput): ReadonlySet<string>;
|
|
59
|
+
/** Whether a single tool name is enabled under the given capability input. */
|
|
60
|
+
export declare function isClaudeToolEnabled(input: ClaudeToolCapabilityInput, name: string): boolean;
|
|
61
|
+
/** Whether the tool name participates in capability gating. */
|
|
62
|
+
export declare function isClaudeCapabilityGated(name: string): boolean;
|
|
63
|
+
/** Keep only capability-gated definitions the resolved capabilities include. */
|
|
64
|
+
export declare function filterClaudeToolDefinitions(definitions: readonly ModelToolDefinition[], capabilities: ReadonlySet<string>): readonly ModelToolDefinition[];
|
|
65
|
+
/**
|
|
66
|
+
* Applies the capability set at both advertisement and invocation boundaries.
|
|
67
|
+
* This keeps a caller from executing a capability-gated tool that was omitted
|
|
68
|
+
* from the model-facing tool list.
|
|
69
|
+
*/
|
|
70
|
+
export declare class ClaudeCapabilityToolRegistry implements ToolRegistry {
|
|
71
|
+
private readonly base;
|
|
72
|
+
private readonly capabilities;
|
|
73
|
+
constructor(base: ToolRegistry, capabilities: ReadonlySet<string>);
|
|
74
|
+
definitions(): readonly ModelToolDefinition[];
|
|
75
|
+
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
76
|
+
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
77
|
+
private assertEnabled;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=claude-capabilities.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** Stable environment names. Explicit input booleans override these. */
|
|
2
|
+
export const CLAUDE_CODE_ENABLE_TASKS = 'CLAUDE_CODE_ENABLE_TASKS';
|
|
3
|
+
export const CLAUDE_CODE_DISABLE_CRON = 'CLAUDE_CODE_DISABLE_CRON';
|
|
4
|
+
export const PRAXIS_ENABLE_WORKFLOW_SCRIPTS = 'PRAXIS_ENABLE_WORKFLOW_SCRIPTS';
|
|
5
|
+
/** task-v2 task-board tools, gated by the `tasks` capability. */
|
|
6
|
+
export const CLAUDE_TASK_V2_TOOLS = [
|
|
7
|
+
'TaskCreate',
|
|
8
|
+
'TaskGet',
|
|
9
|
+
'TaskList',
|
|
10
|
+
'TaskUpdate',
|
|
11
|
+
];
|
|
12
|
+
/** Workflow tool, gated by the `workflowScripts` capability. */
|
|
13
|
+
export const CLAUDE_WORKFLOW_TOOLS = ['Workflow'];
|
|
14
|
+
/** Cron and wakeup tools, gated by the `agentTriggers` capability. */
|
|
15
|
+
export const CLAUDE_AGENT_TRIGGER_TOOLS = [
|
|
16
|
+
'CronCreate',
|
|
17
|
+
'CronDelete',
|
|
18
|
+
'CronList',
|
|
19
|
+
'ScheduleWakeup',
|
|
20
|
+
];
|
|
21
|
+
/** Agent tool, gated by the `subagents` capability. */
|
|
22
|
+
export const CLAUDE_AGENT_TOOLS = ['Agent'];
|
|
23
|
+
/** Background task lifecycle tools, gated by the `backgroundAgents` capability. */
|
|
24
|
+
export const CLAUDE_BACKGROUND_TOOLS = ['TaskOutput', 'TaskStop'];
|
|
25
|
+
/** Coordination tools advertised by a coordinator only when explicitly enabled. */
|
|
26
|
+
export const CLAUDE_COORDINATION_TOOLS = [
|
|
27
|
+
'AskUserQuestion',
|
|
28
|
+
'SendMessage',
|
|
29
|
+
'SendUserMessage',
|
|
30
|
+
'Monitor',
|
|
31
|
+
'PushNotification',
|
|
32
|
+
];
|
|
33
|
+
/** Recursively suppressed for worker agents regardless of any allow-list. */
|
|
34
|
+
export const CLAUDE_WORKER_RECURSIVE_TOOLS = [
|
|
35
|
+
'Agent',
|
|
36
|
+
'TaskOutput',
|
|
37
|
+
'TaskStop',
|
|
38
|
+
];
|
|
39
|
+
export const CLAUDE_CAPABILITY_GATED_TOOLS = new Set([
|
|
40
|
+
...CLAUDE_TASK_V2_TOOLS,
|
|
41
|
+
...CLAUDE_WORKFLOW_TOOLS,
|
|
42
|
+
...CLAUDE_AGENT_TRIGGER_TOOLS,
|
|
43
|
+
...CLAUDE_AGENT_TOOLS,
|
|
44
|
+
...CLAUDE_BACKGROUND_TOOLS,
|
|
45
|
+
...CLAUDE_COORDINATION_TOOLS,
|
|
46
|
+
]);
|
|
47
|
+
const EMPTY_ENV = {};
|
|
48
|
+
const ENV_TRUE = /^(?:1|true|yes|on)$/iu;
|
|
49
|
+
const ENV_FALSE = /^(?:0|false|no|off|)$/iu;
|
|
50
|
+
function envBoolean(env, name) {
|
|
51
|
+
const value = env[name];
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (ENV_TRUE.test(value))
|
|
55
|
+
return true;
|
|
56
|
+
if (ENV_FALSE.test(value))
|
|
57
|
+
return false;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function validateNames(names, flag) {
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
if (!CLAUDE_CAPABILITY_GATED_TOOLS.has(name)) {
|
|
63
|
+
throw new Error(`Unknown capability tool in ${flag}: ${name}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the deterministic set of capability-gated tool names enabled for the
|
|
69
|
+
* given role, gates, allow-list, and disallow-list. Tools outside this set that
|
|
70
|
+
* are not capability-gated are unaffected by the resolver.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveClaudeToolCapabilities(input) {
|
|
73
|
+
const env = input.env ?? EMPTY_ENV;
|
|
74
|
+
const tasks = input.tasks ??
|
|
75
|
+
envBoolean(env, CLAUDE_CODE_ENABLE_TASKS) ??
|
|
76
|
+
(input.interactive && !input.simpleMode);
|
|
77
|
+
const workflowScripts = input.workflowScripts ??
|
|
78
|
+
envBoolean(env, PRAXIS_ENABLE_WORKFLOW_SCRIPTS) ??
|
|
79
|
+
false;
|
|
80
|
+
let agentTriggers = input.agentTriggers;
|
|
81
|
+
if (agentTriggers === undefined) {
|
|
82
|
+
const disableCron = envBoolean(env, CLAUDE_CODE_DISABLE_CRON);
|
|
83
|
+
agentTriggers = disableCron === undefined ? false : !disableCron;
|
|
84
|
+
}
|
|
85
|
+
// The local kill switch takes priority over the internal agentTriggers
|
|
86
|
+
// capability input: a truthy CLAUDE_CODE_DISABLE_CRON suppresses the cron
|
|
87
|
+
// tools even when selected scheduled names would otherwise enable them.
|
|
88
|
+
if (envBoolean(env, CLAUDE_CODE_DISABLE_CRON) === true) {
|
|
89
|
+
agentTriggers = false;
|
|
90
|
+
}
|
|
91
|
+
const backgroundAgents = input.backgroundAgents ?? false;
|
|
92
|
+
const subagents = input.subagents ?? false;
|
|
93
|
+
if (input.tools)
|
|
94
|
+
validateNames(input.tools, '--tools');
|
|
95
|
+
if (input.disallowedTools)
|
|
96
|
+
validateNames(input.disallowedTools, '--disallowedTools');
|
|
97
|
+
// Simple mode is an absolute suppressor for every capability-gated tool,
|
|
98
|
+
// regardless of role, explicit gates, environment overrides, or allow-list.
|
|
99
|
+
if (input.simpleMode) {
|
|
100
|
+
return new Set();
|
|
101
|
+
}
|
|
102
|
+
const enabled = new Set(CLAUDE_COORDINATION_TOOLS);
|
|
103
|
+
if (tasks) {
|
|
104
|
+
for (const name of CLAUDE_TASK_V2_TOOLS)
|
|
105
|
+
enabled.add(name);
|
|
106
|
+
}
|
|
107
|
+
if (workflowScripts) {
|
|
108
|
+
for (const name of CLAUDE_WORKFLOW_TOOLS)
|
|
109
|
+
enabled.add(name);
|
|
110
|
+
}
|
|
111
|
+
if (agentTriggers) {
|
|
112
|
+
for (const name of CLAUDE_AGENT_TRIGGER_TOOLS)
|
|
113
|
+
enabled.add(name);
|
|
114
|
+
}
|
|
115
|
+
if (subagents) {
|
|
116
|
+
for (const name of CLAUDE_AGENT_TOOLS)
|
|
117
|
+
enabled.add(name);
|
|
118
|
+
}
|
|
119
|
+
if (backgroundAgents) {
|
|
120
|
+
for (const name of CLAUDE_BACKGROUND_TOOLS)
|
|
121
|
+
enabled.add(name);
|
|
122
|
+
}
|
|
123
|
+
if (input.role === 'worker') {
|
|
124
|
+
for (const name of CLAUDE_WORKER_RECURSIVE_TOOLS)
|
|
125
|
+
enabled.delete(name);
|
|
126
|
+
}
|
|
127
|
+
else if (input.role === 'coordinator') {
|
|
128
|
+
const explicit = new Set(input.tools ?? []);
|
|
129
|
+
for (const name of CLAUDE_COORDINATION_TOOLS) {
|
|
130
|
+
if (!explicit.has(name))
|
|
131
|
+
enabled.delete(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (input.tools) {
|
|
135
|
+
const allowList = new Set(input.tools);
|
|
136
|
+
for (const name of [...enabled]) {
|
|
137
|
+
if (!allowList.has(name))
|
|
138
|
+
enabled.delete(name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const name of input.disallowedTools ?? [])
|
|
142
|
+
enabled.delete(name);
|
|
143
|
+
return enabled;
|
|
144
|
+
}
|
|
145
|
+
/** Whether a single tool name is enabled under the given capability input. */
|
|
146
|
+
export function isClaudeToolEnabled(input, name) {
|
|
147
|
+
return resolveClaudeToolCapabilities(input).has(name);
|
|
148
|
+
}
|
|
149
|
+
/** Whether the tool name participates in capability gating. */
|
|
150
|
+
export function isClaudeCapabilityGated(name) {
|
|
151
|
+
return CLAUDE_CAPABILITY_GATED_TOOLS.has(name);
|
|
152
|
+
}
|
|
153
|
+
/** Keep only capability-gated definitions the resolved capabilities include. */
|
|
154
|
+
export function filterClaudeToolDefinitions(definitions, capabilities) {
|
|
155
|
+
return definitions.filter((definition) => !CLAUDE_CAPABILITY_GATED_TOOLS.has(definition.name) ||
|
|
156
|
+
capabilities.has(definition.name));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Applies the capability set at both advertisement and invocation boundaries.
|
|
160
|
+
* This keeps a caller from executing a capability-gated tool that was omitted
|
|
161
|
+
* from the model-facing tool list.
|
|
162
|
+
*/
|
|
163
|
+
export class ClaudeCapabilityToolRegistry {
|
|
164
|
+
base;
|
|
165
|
+
capabilities;
|
|
166
|
+
constructor(base, capabilities) {
|
|
167
|
+
this.base = base;
|
|
168
|
+
this.capabilities = capabilities;
|
|
169
|
+
}
|
|
170
|
+
definitions() {
|
|
171
|
+
return filterClaudeToolDefinitions(this.base.definitions(), this.capabilities);
|
|
172
|
+
}
|
|
173
|
+
async prepare(call, context) {
|
|
174
|
+
this.assertEnabled(call.name);
|
|
175
|
+
return this.base.prepare(call, context);
|
|
176
|
+
}
|
|
177
|
+
async execute(call, context) {
|
|
178
|
+
this.assertEnabled(call.name);
|
|
179
|
+
return this.base.execute(call, context);
|
|
180
|
+
}
|
|
181
|
+
assertEnabled(name) {
|
|
182
|
+
if (isClaudeCapabilityGated(name) && !this.capabilities.has(name)) {
|
|
183
|
+
throw new Error(`Tool ${name} is unavailable in this runtime`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=claude-capabilities.js.map
|