praxis-agent 0.48.1 → 0.50.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 +10 -4
- package/dist/application/session-service.d.ts +1 -0
- package/dist/application/session-service.js +28 -8
- package/dist/application/subagent-service.d.ts +1 -0
- package/dist/application/subagent-service.js +11 -6
- package/dist/application/top-level-agent-manager.js +3 -0
- package/dist/cli-runtime.d.ts +5 -0
- package/dist/cli-runtime.js +16 -0
- package/dist/core/runtime.d.ts +3 -0
- package/dist/core/runtime.js +6 -3
- package/dist/hooks/claude-hook-tools.d.ts +3 -1
- package/dist/hooks/claude-hook-tools.js +8 -1
- package/dist/permissions/claude-permission-resolver.js +1 -0
- package/dist/providers/anthropic-compatible.d.ts +2 -0
- package/dist/providers/anthropic-compatible.js +161 -6
- package/dist/providers/codex-subscription.js +11 -3
- package/dist/providers/deadline-provider.d.ts +8 -0
- package/dist/providers/deadline-provider.js +102 -10
- package/dist/providers/environment.d.ts +3 -0
- package/dist/providers/environment.js +21 -1
- package/dist/providers/non-streaming-fallback-provider.d.ts +17 -0
- package/dist/providers/non-streaming-fallback-provider.js +107 -0
- package/dist/providers/openai-compatible.js +7 -0
- package/dist/providers/provider-registry.js +24 -4
- package/dist/providers/provider-transport-activity.d.ts +7 -0
- package/dist/providers/provider-transport-activity.js +11 -0
- package/dist/tools/deferred-tool-catalog.d.ts +11 -0
- package/dist/tools/deferred-tool-catalog.js +145 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -212,11 +212,17 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
212
212
|
imports, memory, skills, commands, agents, hooks, settings, MCP servers,
|
|
213
213
|
plugins, and append-only `praxis.transcript` JSONL sessions under `~/.praxis`,
|
|
214
214
|
with bounded MCP connection, discovery, and tool operations plus safe
|
|
215
|
-
disconnect recovery that never replays an already-dispatched call.
|
|
215
|
+
disconnect recovery that never replays an already-dispatched call. Default
|
|
216
|
+
tool selection defers `mcp__*` schemas behind a turn-scoped `ToolSearch`;
|
|
217
|
+
each query activates at most eight deterministic matches for the next model
|
|
218
|
+
request. Explicit concrete `--tools` selections load selected tools directly, while
|
|
219
|
+
`--disallowedTools ToolSearch` restores the complete tool list.
|
|
216
220
|
- **Provider-neutral models** — native Provider Registry/Vault routing, API
|
|
217
221
|
adapters, an experimental Codex OAuth adapter, explicit capability checks,
|
|
218
|
-
per-attempt
|
|
219
|
-
arguments without tool execution or
|
|
222
|
+
separate per-attempt connect, byte-idle, and absolute-total timeouts, typed
|
|
223
|
+
recovery for malformed streamed tool arguments without tool execution or
|
|
224
|
+
lost resumability, one default-on bounded Anthropic non-streaming replay for
|
|
225
|
+
eligible stream/idle failures without exposing failed-attempt output, and
|
|
220
226
|
token-only/no-API-dollar accounting for subscription runs.
|
|
221
227
|
- **Transactional self-update** — `praxis update` verifies the package before
|
|
222
228
|
installing it, rejects concurrent updates, and can roll back after an
|
|
@@ -302,7 +308,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
|
|
|
302
308
|
`npm run test:coverage` measures all production code under `src/**` with V8 and
|
|
303
309
|
enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
|
|
304
310
|
and rejects any production runtime module with zero covered statements (while allowing
|
|
305
|
-
type-only modules). `npm run test:fixtures` executes the
|
|
311
|
+
type-only modules). `npm run test:fixtures` executes the 70-behavior native contract; 62 behaviors
|
|
306
312
|
are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
|
|
307
313
|
performs the structural check and is part of `npm run check`.
|
|
308
314
|
`npm run test:core-completion` is retained as a compatibility alias for
|
|
@@ -39,6 +39,7 @@ export interface ClaudeSessionServiceOptions {
|
|
|
39
39
|
claudeVersion: string;
|
|
40
40
|
provider?: ModelProvider;
|
|
41
41
|
tools?: ToolRegistry;
|
|
42
|
+
deferMcpTools?: boolean;
|
|
42
43
|
permissions?: PermissionResolver;
|
|
43
44
|
permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
|
|
44
45
|
permissionMode?: ClaudePermissionMode;
|
|
@@ -49,6 +49,7 @@ import { ClaudeWorktreeToolRegistry } from '../tools/claude-worktree-tools.js';
|
|
|
49
49
|
import { completeMeteredModelRequest } from './metered-model-completion.js';
|
|
50
50
|
import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, } from './session-memory.js';
|
|
51
51
|
import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
|
|
52
|
+
import { DeferredToolCatalog } from '../tools/deferred-tool-catalog.js';
|
|
52
53
|
import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
|
|
53
54
|
import { generateToolUseSummary } from './tool-use-summary.js';
|
|
54
55
|
import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
|
|
@@ -1108,6 +1109,9 @@ export class ClaudeSessionService {
|
|
|
1108
1109
|
? { providerForModel: this.options.providerForModel }
|
|
1109
1110
|
: {}),
|
|
1110
1111
|
baseTools: wrappedBase,
|
|
1112
|
+
...(this.options.deferMcpTools === undefined
|
|
1113
|
+
? {}
|
|
1114
|
+
: { deferMcpTools: this.options.deferMcpTools }),
|
|
1111
1115
|
permissions: this.options.permissions,
|
|
1112
1116
|
...(this.options.permissionResolverForMode
|
|
1113
1117
|
? {
|
|
@@ -2846,6 +2850,9 @@ export class ClaudeSessionService {
|
|
|
2846
2850
|
? { providerForModel: this.options.providerForModel }
|
|
2847
2851
|
: {}),
|
|
2848
2852
|
baseTools,
|
|
2853
|
+
...(this.options.deferMcpTools === undefined
|
|
2854
|
+
? {}
|
|
2855
|
+
: { deferMcpTools: this.options.deferMcpTools }),
|
|
2849
2856
|
permissions: turnPermissions,
|
|
2850
2857
|
...(this.options.permissionResolverForMode
|
|
2851
2858
|
? {
|
|
@@ -3024,9 +3031,16 @@ export class ClaudeSessionService {
|
|
|
3024
3031
|
const structuredTools = this.options.structuredOutputSchema && structuredCapture
|
|
3025
3032
|
? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
|
|
3026
3033
|
: agentScopedTools;
|
|
3027
|
-
const
|
|
3034
|
+
const activeTurnTools = structuredTools
|
|
3035
|
+
? new DeferredToolCatalog(structuredTools).startTurn({
|
|
3036
|
+
enabled: this.options.deferMcpTools !== false &&
|
|
3037
|
+
!(agent && agent.tools !== undefined),
|
|
3038
|
+
restoredToolNames: unresolvedToolCalls.map((call) => call.name),
|
|
3039
|
+
})
|
|
3040
|
+
: undefined;
|
|
3041
|
+
const hookTools = this.options.hooks && activeTurnTools && turnPermissions
|
|
3028
3042
|
? new ClaudeHookToolCoordinator({
|
|
3029
|
-
tools:
|
|
3043
|
+
tools: activeTurnTools,
|
|
3030
3044
|
permissions: turnPermissions,
|
|
3031
3045
|
hooks: this.options.hooks,
|
|
3032
3046
|
session: hookSession,
|
|
@@ -3063,7 +3077,7 @@ export class ClaudeSessionService {
|
|
|
3063
3077
|
...(hookTools
|
|
3064
3078
|
? { tools: hookTools, permissions: hookTools }
|
|
3065
3079
|
: {
|
|
3066
|
-
...(
|
|
3080
|
+
...(activeTurnTools ? { tools: activeTurnTools } : {}),
|
|
3067
3081
|
...(turnPermissions ? { permissions: turnPermissions } : {}),
|
|
3068
3082
|
}),
|
|
3069
3083
|
});
|
|
@@ -3446,8 +3460,8 @@ export class ClaudeSessionService {
|
|
|
3446
3460
|
let compactionDurationMs;
|
|
3447
3461
|
let compactionDurationWithoutRetriesMs;
|
|
3448
3462
|
let compactionModelUsage;
|
|
3449
|
-
const
|
|
3450
|
-
? (
|
|
3463
|
+
const currentDefinitions = () => provider.capabilities.tools
|
|
3464
|
+
? (activeTurnTools?.definitions() ?? [])
|
|
3451
3465
|
: [];
|
|
3452
3466
|
const budget = this.contextBudget(provider);
|
|
3453
3467
|
const contextEngine = new ContextEngine({
|
|
@@ -3526,7 +3540,7 @@ export class ClaudeSessionService {
|
|
|
3526
3540
|
...pendingMessages,
|
|
3527
3541
|
]),
|
|
3528
3542
|
],
|
|
3529
|
-
tools:
|
|
3543
|
+
tools: currentDefinitions(),
|
|
3530
3544
|
};
|
|
3531
3545
|
},
|
|
3532
3546
|
irreducible: () => ({
|
|
@@ -3540,12 +3554,13 @@ export class ClaudeSessionService {
|
|
|
3540
3554
|
})),
|
|
3541
3555
|
]),
|
|
3542
3556
|
],
|
|
3543
|
-
tools:
|
|
3557
|
+
tools: currentDefinitions(),
|
|
3544
3558
|
}),
|
|
3545
3559
|
propose: async () => {
|
|
3546
3560
|
const activeNativeLease = nativeLease;
|
|
3547
3561
|
if (!budget)
|
|
3548
3562
|
throw new Error('Context budget is unavailable');
|
|
3563
|
+
const definitions = currentDefinitions();
|
|
3549
3564
|
const historyMessages = activeTurnMessages();
|
|
3550
3565
|
if (historyMessages.length === 0)
|
|
3551
3566
|
throw new Error('Cannot compact an empty native transcript');
|
|
@@ -3754,7 +3769,10 @@ export class ClaudeSessionService {
|
|
|
3754
3769
|
]),
|
|
3755
3770
|
];
|
|
3756
3771
|
return {
|
|
3757
|
-
envelope: {
|
|
3772
|
+
envelope: {
|
|
3773
|
+
messages: proposedMessages,
|
|
3774
|
+
tools: definitions,
|
|
3775
|
+
},
|
|
3758
3776
|
commit: async () => {
|
|
3759
3777
|
if (signal?.aborted)
|
|
3760
3778
|
throw new AgentRunCancelledError();
|
|
@@ -4062,6 +4080,7 @@ export class ClaudeSessionService {
|
|
|
4062
4080
|
};
|
|
4063
4081
|
}
|
|
4064
4082
|
if (shellCommand === undefined && budget) {
|
|
4083
|
+
const definitions = currentDefinitions();
|
|
4065
4084
|
await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
|
|
4066
4085
|
budget.assertFits(budget.evaluate([
|
|
4067
4086
|
...contextMessages,
|
|
@@ -4387,6 +4406,7 @@ export class ClaudeSessionService {
|
|
|
4387
4406
|
...contextMessages,
|
|
4388
4407
|
...injectTurnContext(memorySnapshot),
|
|
4389
4408
|
];
|
|
4409
|
+
const definitions = currentDefinitions();
|
|
4390
4410
|
const currentContextTokens = contextEngine.report({
|
|
4391
4411
|
messages: providerVisibleMessages,
|
|
4392
4412
|
tools: definitions,
|
|
@@ -59,6 +59,7 @@ export interface ClaudeSubagentExecutorOptions {
|
|
|
59
59
|
claudeVersion: string;
|
|
60
60
|
provider: ModelProvider;
|
|
61
61
|
baseTools: ToolRegistry;
|
|
62
|
+
deferMcpTools?: boolean;
|
|
62
63
|
permissions: PermissionResolver;
|
|
63
64
|
permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
|
|
64
65
|
parentPermissionMode?: () => AgentPermissionMode;
|
|
@@ -10,6 +10,7 @@ import { ContextBudget } from '../core/context-budget.js';
|
|
|
10
10
|
import { AgentRuntime, } from '../core/runtime.js';
|
|
11
11
|
import { composePermissionResolvers } from '../core/permission-policy.js';
|
|
12
12
|
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
13
|
+
import { DeferredToolCatalog } from '../tools/deferred-tool-catalog.js';
|
|
13
14
|
import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.js';
|
|
14
15
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
15
16
|
import { InMemorySidechainStore } from '../persistence/in-memory-sidechain-store.js';
|
|
@@ -2059,9 +2060,14 @@ export class ClaudeSubagentExecutor {
|
|
|
2059
2060
|
? new RestrictedToolRegistry(structuredOnlyTools, ['Read', 'Edit'])
|
|
2060
2061
|
: structuredOnlyTools, options.outputSchema, options.structuredOutput)
|
|
2061
2062
|
: scopedTools;
|
|
2063
|
+
const activeAgentTools = new DeferredToolCatalog(agentTools).startTurn({
|
|
2064
|
+
enabled: this.options.deferMcpTools !== false &&
|
|
2065
|
+
customAgent?.tools === undefined,
|
|
2066
|
+
restoredToolNames: nativeRecoveryCalls.map((call) => call.name),
|
|
2067
|
+
});
|
|
2062
2068
|
const runtimeTools = scopedHooks
|
|
2063
2069
|
? new ClaudeHookToolCoordinator({
|
|
2064
|
-
tools:
|
|
2070
|
+
tools: activeAgentTools,
|
|
2065
2071
|
permissions,
|
|
2066
2072
|
hooks: scopedHooks,
|
|
2067
2073
|
session: hookSession,
|
|
@@ -2072,7 +2078,7 @@ export class ClaudeSubagentExecutor {
|
|
|
2072
2078
|
}
|
|
2073
2079
|
: {}),
|
|
2074
2080
|
})
|
|
2075
|
-
:
|
|
2081
|
+
: activeAgentTools;
|
|
2076
2082
|
const runtimePermissions = scopedHooks
|
|
2077
2083
|
? runtimeTools
|
|
2078
2084
|
: permissions;
|
|
@@ -2241,9 +2247,7 @@ export class ClaudeSubagentExecutor {
|
|
|
2241
2247
|
: { reserveTokens: this.options.contextReserveTokens }),
|
|
2242
2248
|
})
|
|
2243
2249
|
: null;
|
|
2244
|
-
const
|
|
2245
|
-
? runtimeTools.definitions()
|
|
2246
|
-
: [];
|
|
2250
|
+
const currentDefinitions = () => options.provider.capabilities.tools ? runtimeTools.definitions() : [];
|
|
2247
2251
|
let observedMessages;
|
|
2248
2252
|
let stableSystemMessageCount = 0;
|
|
2249
2253
|
const assembleMessages = async () => {
|
|
@@ -2272,6 +2276,7 @@ export class ClaudeSubagentExecutor {
|
|
|
2272
2276
|
];
|
|
2273
2277
|
observedMessages = messages;
|
|
2274
2278
|
if (contextBudget) {
|
|
2279
|
+
const definitions = currentDefinitions();
|
|
2275
2280
|
contextBudget.assertFits(contextBudget.evaluate(messages, definitions));
|
|
2276
2281
|
}
|
|
2277
2282
|
return messages;
|
|
@@ -2393,7 +2398,7 @@ export class ClaudeSubagentExecutor {
|
|
|
2393
2398
|
...(options.signal ? { signal: options.signal } : {}),
|
|
2394
2399
|
};
|
|
2395
2400
|
const result = await runtime.run(runtimeRequest);
|
|
2396
|
-
contextBudget?.observeUsage(result.usage, observedMessages ?? [],
|
|
2401
|
+
contextBudget?.observeUsage(result.usage, observedMessages ?? [], currentDefinitions());
|
|
2397
2402
|
this.options.eventSink?.({
|
|
2398
2403
|
type: 'task-progress',
|
|
2399
2404
|
taskId: options.agentId,
|
|
@@ -29,10 +29,13 @@ const WORKER_RUNTIME_ENVIRONMENT = [
|
|
|
29
29
|
'PRAXIS_PROVIDER',
|
|
30
30
|
'PRAXIS_PROVIDER_PROFILE',
|
|
31
31
|
'PRAXIS_PROVIDER_DEADLINE_MS',
|
|
32
|
+
'PRAXIS_PROVIDER_CONNECT_TIMEOUT_MS',
|
|
33
|
+
'PRAXIS_PROVIDER_IDLE_TIMEOUT_MS',
|
|
32
34
|
'PRAXIS_BASE_URL',
|
|
33
35
|
'PRAXIS_MAX_OUTPUT_TOKENS',
|
|
34
36
|
'PRAXIS_ANTHROPIC_VERSION',
|
|
35
37
|
'PRAXIS_ANTHROPIC_WEB_SEARCH',
|
|
38
|
+
'PRAXIS_DISABLE_NONSTREAMING_FALLBACK',
|
|
36
39
|
'PRAXIS_ANTHROPIC_PROMPT_CACHING',
|
|
37
40
|
'PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL',
|
|
38
41
|
'PRAXIS_CONTEXT_WINDOW_TOKENS',
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ import { executeProviderAuthCommand } from './cli/provider-auth-command.js';
|
|
|
24
24
|
import { type PluginEvalDependencies } from './plugins/claude-plugin-eval.js';
|
|
25
25
|
import { type ProjectEvalDependencies } from './evals/project-eval.js';
|
|
26
26
|
import { type SelfUpdateResult } from './maintenance/self-update.js';
|
|
27
|
+
export declare function shouldDeferMcpTools(input: {
|
|
28
|
+
simpleMode: boolean;
|
|
29
|
+
tools?: readonly string[] | undefined;
|
|
30
|
+
disallowedTools: readonly string[];
|
|
31
|
+
}): boolean;
|
|
27
32
|
export { parseContextEnvironment, parseProviderEnvironment };
|
|
28
33
|
export interface CliIO {
|
|
29
34
|
stdout(message: string | Uint8Array): void;
|
package/dist/cli-runtime.js
CHANGED
|
@@ -77,6 +77,16 @@ import { formatDoctorReport, runDoctor } from './maintenance/doctor.js';
|
|
|
77
77
|
import { runSelfUpdate, } from './maintenance/self-update.js';
|
|
78
78
|
import { executeClaudeProjectPurge, planClaudeProjectPurge, } from './application/claude-project-purge.js';
|
|
79
79
|
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
80
|
+
export function shouldDeferMcpTools(input) {
|
|
81
|
+
if (input.tools?.includes('ToolSearch')) {
|
|
82
|
+
throw new Error('Unknown tool in --tools: ToolSearch');
|
|
83
|
+
}
|
|
84
|
+
return (!input.simpleMode &&
|
|
85
|
+
!input.disallowedTools.includes('ToolSearch') &&
|
|
86
|
+
(input.tools === undefined ||
|
|
87
|
+
(input.tools.length > 0 &&
|
|
88
|
+
input.tools.every((tool) => tool === 'default'))));
|
|
89
|
+
}
|
|
80
90
|
function fileResourceBaseUrl(environment, providerEnvironment) {
|
|
81
91
|
return (environment.PRAXIS_FILES_BASE_URL ??
|
|
82
92
|
providerEnvironment?.baseUrl ??
|
|
@@ -981,6 +991,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
981
991
|
? {}
|
|
982
992
|
: { permissionMode: interactivePermissionMode }),
|
|
983
993
|
}, cwd);
|
|
994
|
+
const deferMcpTools = shouldDeferMcpTools({
|
|
995
|
+
simpleMode,
|
|
996
|
+
tools: cli.tools,
|
|
997
|
+
disallowedTools: cli.disallowedTools,
|
|
998
|
+
});
|
|
984
999
|
if (experimentalNativeTranscriptWrites) {
|
|
985
1000
|
const incompatible = [
|
|
986
1001
|
['sessionPersistence', cli.sessionPersistence === false],
|
|
@@ -1208,6 +1223,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1208
1223
|
pricing: ModelPricingRegistry.fromEnvironment(runtimeEnvironment.PRAXIS_PRICING_JSON),
|
|
1209
1224
|
}),
|
|
1210
1225
|
collectMetrics: true,
|
|
1226
|
+
deferMcpTools,
|
|
1211
1227
|
...(!experimentalNativeTranscriptWrites && sessionMemoryProviderFactory
|
|
1212
1228
|
? { sessionMemoryProviderFactory }
|
|
1213
1229
|
: {}),
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -537,14 +537,17 @@ export declare class ModelProviderError extends Error {
|
|
|
537
537
|
readonly status?: number;
|
|
538
538
|
readonly retryDelayMs?: number;
|
|
539
539
|
readonly kind?: ProviderErrorKind;
|
|
540
|
+
readonly timeoutPhase?: ProviderTimeoutPhase;
|
|
540
541
|
constructor(message: string, options: {
|
|
541
542
|
retryable: boolean;
|
|
542
543
|
kind?: ProviderErrorKind;
|
|
543
544
|
status?: number;
|
|
544
545
|
retryDelayMs?: number;
|
|
546
|
+
timeoutPhase?: ProviderTimeoutPhase;
|
|
545
547
|
cause?: unknown;
|
|
546
548
|
});
|
|
547
549
|
}
|
|
550
|
+
export type ProviderTimeoutPhase = 'connect' | 'idle' | 'total';
|
|
548
551
|
export declare class AgentRunCancelledError extends Error {
|
|
549
552
|
readonly name = "AgentRunCancelledError";
|
|
550
553
|
constructor();
|
package/dist/core/runtime.js
CHANGED
|
@@ -35,6 +35,7 @@ export class ModelProviderError extends Error {
|
|
|
35
35
|
status;
|
|
36
36
|
retryDelayMs;
|
|
37
37
|
kind;
|
|
38
|
+
timeoutPhase;
|
|
38
39
|
constructor(message, options) {
|
|
39
40
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
40
41
|
this.retryable = options.retryable;
|
|
@@ -44,6 +45,8 @@ export class ModelProviderError extends Error {
|
|
|
44
45
|
this.status = options.status;
|
|
45
46
|
if (options.retryDelayMs !== undefined)
|
|
46
47
|
this.retryDelayMs = options.retryDelayMs;
|
|
48
|
+
if (options.timeoutPhase !== undefined)
|
|
49
|
+
this.timeoutPhase = options.timeoutPhase;
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
export class AgentRunCancelledError extends Error {
|
|
@@ -334,9 +337,6 @@ export class AgentRuntime {
|
|
|
334
337
|
let linesRemoved = 0;
|
|
335
338
|
let activeAttemptHasPresentation = false;
|
|
336
339
|
let activeAttemptDiscarded = false;
|
|
337
|
-
const definitions = this.provider.capabilities.tools
|
|
338
|
-
? (this.options.tools?.definitions() ?? [])
|
|
339
|
-
: [];
|
|
340
340
|
const maxModelTurns = request.maxModelTurns ?? this.options.maxModelTurns;
|
|
341
341
|
if (maxModelTurns !== undefined &&
|
|
342
342
|
(!Number.isSafeInteger(maxModelTurns) || maxModelTurns <= 0)) {
|
|
@@ -411,6 +411,9 @@ export class AgentRuntime {
|
|
|
411
411
|
stableSystemMessageCount: request.stableSystemMessageCount,
|
|
412
412
|
}),
|
|
413
413
|
};
|
|
414
|
+
const definitions = this.provider.capabilities.tools
|
|
415
|
+
? (this.options.tools?.definitions() ?? [])
|
|
416
|
+
: [];
|
|
414
417
|
if (definitions.length > 0)
|
|
415
418
|
providerRequest.tools = definitions;
|
|
416
419
|
if (request.signal)
|
|
@@ -20,9 +20,11 @@ export declare class ClaudeHookToolCoordinator implements ToolRegistry, Permissi
|
|
|
20
20
|
private readonly prepared;
|
|
21
21
|
constructor(options: ClaudeHookToolCoordinatorOptions);
|
|
22
22
|
definitions(): readonly import("../core/runtime.js").ModelToolDefinition[];
|
|
23
|
-
schedulingPolicy(): {
|
|
23
|
+
schedulingPolicy(call: ModelToolCall): {
|
|
24
24
|
concurrency: "exclusive";
|
|
25
25
|
startAfterAssistant: boolean;
|
|
26
|
+
abortGroupOnError?: boolean;
|
|
27
|
+
cancelOnInterrupt?: boolean;
|
|
26
28
|
};
|
|
27
29
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
28
30
|
resolve(call: ModelToolCall, context?: PermissionResolutionContext): Promise<PermissionDecision>;
|
|
@@ -8,8 +8,15 @@ export class ClaudeHookToolCoordinator {
|
|
|
8
8
|
definitions() {
|
|
9
9
|
return this.options.tools.definitions();
|
|
10
10
|
}
|
|
11
|
-
schedulingPolicy() {
|
|
11
|
+
schedulingPolicy(call) {
|
|
12
|
+
const wrapped = this.options.tools.schedulingPolicy?.(call);
|
|
12
13
|
return {
|
|
14
|
+
...(wrapped?.cancelOnInterrupt === undefined
|
|
15
|
+
? {}
|
|
16
|
+
: { cancelOnInterrupt: wrapped.cancelOnInterrupt }),
|
|
17
|
+
...(wrapped?.abortGroupOnError === undefined
|
|
18
|
+
? {}
|
|
19
|
+
: { abortGroupOnError: wrapped.abortGroupOnError }),
|
|
13
20
|
concurrency: 'exclusive',
|
|
14
21
|
startAfterAssistant: true,
|
|
15
22
|
};
|
|
@@ -12,6 +12,7 @@ import { validateSedSafety } from './sed-safety.js';
|
|
|
12
12
|
import { parseShellRule, shellRuleMatches } from './shell-rule-matching.js';
|
|
13
13
|
import { effectiveAdditionalDirectories, effectivePermissionMode, filePermissionSuggestions, permissionRuleValueFromString, permissionRuleStringIsValid, permissionRuleValueToString, shellInputIsReadOnly, shellPermissionSuggestions, shellSubcommands, skillPermissionSuggestions, } from './permission-updates.js';
|
|
14
14
|
const DEFAULT_BEHAVIOR = {
|
|
15
|
+
ToolSearch: 'allow',
|
|
15
16
|
Agent: 'allow',
|
|
16
17
|
SendMessage: 'allow',
|
|
17
18
|
SendUserMessage: 'allow',
|
|
@@ -16,6 +16,7 @@ export interface AnthropicCompatibleProviderOptions {
|
|
|
16
16
|
maxToolMetadataBytes?: number;
|
|
17
17
|
maxErrorBodyBytes?: number;
|
|
18
18
|
fetchImplementation?: typeof fetch;
|
|
19
|
+
streaming?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export declare class AnthropicCompatibleProvider implements ModelProvider {
|
|
21
22
|
private readonly options;
|
|
@@ -32,6 +33,7 @@ export declare class AnthropicCompatibleProvider implements ModelProvider {
|
|
|
32
33
|
private readonly maxErrorBodyBytes;
|
|
33
34
|
private readonly thinking;
|
|
34
35
|
private readonly promptCaching;
|
|
36
|
+
private readonly streaming;
|
|
35
37
|
constructor(options: AnthropicCompatibleProviderOptions);
|
|
36
38
|
complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
|
|
37
39
|
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { ModelProviderError, malformedModelToolCall, } from '../core/runtime.js';
|
|
2
2
|
import { transportFailureKind } from './provider-errors.js';
|
|
3
|
+
import { reportProviderTransportActivity } from './provider-transport-activity.js';
|
|
4
|
+
import { markNonStreamingFallbackEligible } from './non-streaming-fallback-provider.js';
|
|
3
5
|
import { createAnthropicPromptCachePolicyResolver, } from './anthropic-prompt-cache.js';
|
|
4
6
|
function isRecord(value) {
|
|
5
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
8
|
}
|
|
9
|
+
function readNonNegativeTokenCount(usage, field, required) {
|
|
10
|
+
const value = usage[field];
|
|
11
|
+
if (value === undefined && !required)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
14
|
+
throw new ModelProviderError(`Provider returned an invalid ${field} counter`, { retryable: false });
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
7
18
|
function webSearchLinks(value) {
|
|
8
19
|
if (!Array.isArray(value))
|
|
9
20
|
return [];
|
|
@@ -633,6 +644,7 @@ export class AnthropicCompatibleProvider {
|
|
|
633
644
|
maxErrorBodyBytes;
|
|
634
645
|
thinking;
|
|
635
646
|
promptCaching;
|
|
647
|
+
streaming;
|
|
636
648
|
constructor(options) {
|
|
637
649
|
this.options = options;
|
|
638
650
|
if (options.contextWindowTokens !== undefined) {
|
|
@@ -640,6 +652,7 @@ export class AnthropicCompatibleProvider {
|
|
|
640
652
|
}
|
|
641
653
|
this.endpoint = `${options.baseUrl.replace(/\/+$/, '')}/messages`;
|
|
642
654
|
this.model = options.model;
|
|
655
|
+
this.streaming = options.streaming ?? true;
|
|
643
656
|
this.fetchImplementation = options.fetchImplementation ?? fetch;
|
|
644
657
|
this.maxOutputTokens = positiveInteger(options.maxOutputTokens ??
|
|
645
658
|
(options.model.includes('claude-opus-4-6')
|
|
@@ -648,7 +661,7 @@ export class AnthropicCompatibleProvider {
|
|
|
648
661
|
? 32_000
|
|
649
662
|
: 8192), 'Max output tokens');
|
|
650
663
|
this.capabilities = {
|
|
651
|
-
streaming:
|
|
664
|
+
streaming: this.streaming,
|
|
652
665
|
usage: true,
|
|
653
666
|
tools: true,
|
|
654
667
|
images: true,
|
|
@@ -720,7 +733,7 @@ export class AnthropicCompatibleProvider {
|
|
|
720
733
|
model: this.options.model,
|
|
721
734
|
max_tokens: maxTokens,
|
|
722
735
|
messages: serialized.messages,
|
|
723
|
-
stream:
|
|
736
|
+
stream: this.streaming,
|
|
724
737
|
...(thinkingPayload ? { thinking: thinkingPayload } : {}),
|
|
725
738
|
...(request.effort
|
|
726
739
|
? { output_config: { effort: request.effort } }
|
|
@@ -761,7 +774,9 @@ export class AnthropicCompatibleProvider {
|
|
|
761
774
|
requestInit.signal = request.signal;
|
|
762
775
|
let response;
|
|
763
776
|
try {
|
|
777
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
764
778
|
response = await this.fetchImplementation(this.endpoint, requestInit);
|
|
779
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
765
780
|
}
|
|
766
781
|
catch (error) {
|
|
767
782
|
const kind = transportFailureKind(error, request.signal);
|
|
@@ -792,6 +807,8 @@ export class AnthropicCompatibleProvider {
|
|
|
792
807
|
ended = true;
|
|
793
808
|
break;
|
|
794
809
|
}
|
|
810
|
+
if (value.byteLength > 0)
|
|
811
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
795
812
|
size += value.byteLength;
|
|
796
813
|
if (size > this.maxErrorBodyBytes) {
|
|
797
814
|
throw new ModelProviderError(`Provider error response exceeded ${this.maxErrorBodyBytes} bytes`, { retryable: false, status: response.status });
|
|
@@ -824,11 +841,144 @@ export class AnthropicCompatibleProvider {
|
|
|
824
841
|
status: response.status,
|
|
825
842
|
});
|
|
826
843
|
}
|
|
844
|
+
if (!this.streaming) {
|
|
845
|
+
if (!response.body) {
|
|
846
|
+
throw new ModelProviderError('Provider response has no body', {
|
|
847
|
+
kind: 'transport_error',
|
|
848
|
+
retryable: true,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
const reader = response.body.getReader();
|
|
852
|
+
const chunks = [];
|
|
853
|
+
let size = 0;
|
|
854
|
+
let ended = false;
|
|
855
|
+
try {
|
|
856
|
+
while (true) {
|
|
857
|
+
const { done, value } = await reader.read();
|
|
858
|
+
if (done) {
|
|
859
|
+
ended = true;
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
if (value.byteLength > 0)
|
|
863
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
864
|
+
size += value.byteLength;
|
|
865
|
+
if (size > this.maxStreamBufferBytes) {
|
|
866
|
+
throw new ModelProviderError(`Provider stream buffer exceeded ${this.maxStreamBufferBytes} bytes`, { retryable: false });
|
|
867
|
+
}
|
|
868
|
+
if (value.byteLength > 0)
|
|
869
|
+
chunks.push(value);
|
|
870
|
+
}
|
|
871
|
+
let payload;
|
|
872
|
+
try {
|
|
873
|
+
payload = JSON.parse(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8'));
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
throw new ModelProviderError('Provider returned malformed JSON', {
|
|
877
|
+
retryable: false,
|
|
878
|
+
cause: error,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (!isRecord(payload) ||
|
|
882
|
+
payload.type !== 'message' ||
|
|
883
|
+
payload.role !== 'assistant' ||
|
|
884
|
+
!Array.isArray(payload.content) ||
|
|
885
|
+
!isRecord(payload.usage)) {
|
|
886
|
+
throw new ModelProviderError('Provider returned an invalid message', {
|
|
887
|
+
retryable: false,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
const state = {
|
|
891
|
+
blocks: new Map(),
|
|
892
|
+
thinking: new Map(),
|
|
893
|
+
tools: new Map(),
|
|
894
|
+
toolCallsSeen: 0,
|
|
895
|
+
metadataBytes: 0,
|
|
896
|
+
inputTokens: 0,
|
|
897
|
+
cacheReadInputTokens: 0,
|
|
898
|
+
cacheCreationInputTokens: 0,
|
|
899
|
+
outputTokens: 0,
|
|
900
|
+
webSearchRequests: 0,
|
|
901
|
+
usageSeen: false,
|
|
902
|
+
messageStarted: false,
|
|
903
|
+
messageDeltaSeen: false,
|
|
904
|
+
terminal: false,
|
|
905
|
+
};
|
|
906
|
+
const inputTokens = readNonNegativeTokenCount(payload.usage, 'input_tokens', true);
|
|
907
|
+
const outputTokens = readNonNegativeTokenCount(payload.usage, 'output_tokens', true);
|
|
908
|
+
const cacheReadInputTokens = readNonNegativeTokenCount(payload.usage, 'cache_read_input_tokens', false);
|
|
909
|
+
const cacheCreationInputTokens = readNonNegativeTokenCount(payload.usage, 'cache_creation_input_tokens', false);
|
|
910
|
+
const usage = {
|
|
911
|
+
input_tokens: inputTokens,
|
|
912
|
+
output_tokens: outputTokens,
|
|
913
|
+
...(cacheReadInputTokens === undefined
|
|
914
|
+
? {}
|
|
915
|
+
: { cache_read_input_tokens: cacheReadInputTokens }),
|
|
916
|
+
...(cacheCreationInputTokens === undefined
|
|
917
|
+
? {}
|
|
918
|
+
: { cache_creation_input_tokens: cacheCreationInputTokens }),
|
|
919
|
+
...(payload.usage.server_tool_use === undefined
|
|
920
|
+
? {}
|
|
921
|
+
: { server_tool_use: payload.usage.server_tool_use }),
|
|
922
|
+
};
|
|
923
|
+
const synthetic = [
|
|
924
|
+
{
|
|
925
|
+
type: 'message_start',
|
|
926
|
+
message: { usage },
|
|
927
|
+
},
|
|
928
|
+
...payload.content.flatMap((block, index) => {
|
|
929
|
+
if (!isRecord(block) || typeof block.type !== 'string') {
|
|
930
|
+
throw new ModelProviderError('Provider returned an invalid content block', { retryable: false });
|
|
931
|
+
}
|
|
932
|
+
return [
|
|
933
|
+
{ type: 'content_block_start', index, content_block: block },
|
|
934
|
+
{ type: 'content_block_stop', index },
|
|
935
|
+
];
|
|
936
|
+
}),
|
|
937
|
+
{
|
|
938
|
+
type: 'message_delta',
|
|
939
|
+
delta: { stop_reason: payload.stop_reason },
|
|
940
|
+
usage: {
|
|
941
|
+
output_tokens: outputTokens,
|
|
942
|
+
...(usage.server_tool_use === undefined
|
|
943
|
+
? {}
|
|
944
|
+
: { server_tool_use: usage.server_tool_use }),
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
{ type: 'message_stop' },
|
|
948
|
+
];
|
|
949
|
+
for (const event of synthetic) {
|
|
950
|
+
for (const normalized of parseSseEvent(JSON.stringify(event), state, this.maxToolArgumentsBytes, this.maxToolCallsPerResponse, this.maxToolMetadataBytes))
|
|
951
|
+
yield normalized;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
if (error instanceof ModelProviderError)
|
|
956
|
+
throw error;
|
|
957
|
+
const kind = transportFailureKind(error, request.signal);
|
|
958
|
+
throw new ModelProviderError('Provider response failed', {
|
|
959
|
+
kind,
|
|
960
|
+
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
961
|
+
cause: error,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
finally {
|
|
965
|
+
if (!ended) {
|
|
966
|
+
try {
|
|
967
|
+
await reader.cancel();
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
// Preserve the primary provider error.
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
reader.releaseLock();
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
827
977
|
if (!response.body) {
|
|
828
|
-
throw new ModelProviderError('Provider response has no body', {
|
|
978
|
+
throw markNonStreamingFallbackEligible(new ModelProviderError('Provider response has no body', {
|
|
829
979
|
kind: 'transport_error',
|
|
830
980
|
retryable: true,
|
|
831
|
-
});
|
|
981
|
+
}));
|
|
832
982
|
}
|
|
833
983
|
const reader = response.body.getReader();
|
|
834
984
|
const decoder = new TextDecoder();
|
|
@@ -853,6 +1003,8 @@ export class AnthropicCompatibleProvider {
|
|
|
853
1003
|
try {
|
|
854
1004
|
stream: while (true) {
|
|
855
1005
|
const { done, value } = await reader.read();
|
|
1006
|
+
if (!done && value.byteLength > 0)
|
|
1007
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
856
1008
|
buffer += decoder.decode(value, { stream: !done });
|
|
857
1009
|
buffer = buffer.replaceAll('\r\n', '\n');
|
|
858
1010
|
if (Buffer.byteLength(buffer) > this.maxStreamBufferBytes) {
|
|
@@ -882,18 +1034,21 @@ export class AnthropicCompatibleProvider {
|
|
|
882
1034
|
}
|
|
883
1035
|
}
|
|
884
1036
|
if (!state.terminal) {
|
|
885
|
-
throw new ModelProviderError('Provider stream ended before a terminal event', { retryable: true });
|
|
1037
|
+
throw markNonStreamingFallbackEligible(new ModelProviderError('Provider stream ended before a terminal event', { retryable: true }));
|
|
886
1038
|
}
|
|
887
1039
|
}
|
|
888
1040
|
catch (error) {
|
|
889
1041
|
if (error instanceof ModelProviderError)
|
|
890
1042
|
throw error;
|
|
891
1043
|
const kind = transportFailureKind(error, request.signal);
|
|
892
|
-
|
|
1044
|
+
const streamError = new ModelProviderError('Provider stream failed', {
|
|
893
1045
|
kind,
|
|
894
1046
|
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
895
1047
|
cause: error,
|
|
896
1048
|
});
|
|
1049
|
+
throw kind === 'timeout' || kind === 'transport_error'
|
|
1050
|
+
? markNonStreamingFallbackEligible(streamError)
|
|
1051
|
+
: streamError;
|
|
897
1052
|
}
|
|
898
1053
|
finally {
|
|
899
1054
|
if (!streamEnded) {
|