praxis-agent 0.29.0 → 0.30.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.
@@ -30,6 +30,14 @@ const WORKER_RUNTIME_ENVIRONMENT = [
30
30
  'PRAXIS_MAX_OUTPUT_TOKENS',
31
31
  'PRAXIS_ANTHROPIC_VERSION',
32
32
  'PRAXIS_ANTHROPIC_WEB_SEARCH',
33
+ 'PRAXIS_ANTHROPIC_PROMPT_CACHING',
34
+ 'PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL',
35
+ 'DISABLE_PROMPT_CACHING',
36
+ 'DISABLE_PROMPT_CACHING_HAIKU',
37
+ 'DISABLE_PROMPT_CACHING_SONNET',
38
+ 'DISABLE_PROMPT_CACHING_OPUS',
39
+ 'ENABLE_PROMPT_CACHING_1H',
40
+ 'FORCE_PROMPT_CACHING_5M',
33
41
  'PRAXIS_CONTEXT_WINDOW_TOKENS',
34
42
  'PRAXIS_CONTEXT_RESERVE_TOKENS',
35
43
  'PRAXIS_PRICING_JSON',
@@ -39,6 +39,7 @@ import { writeFileAtomically } from './platform/atomic-write.js';
39
39
  import { detectInstalledClaudeVersion } from './platform/claude-version.js';
40
40
  import { redactSensitiveText, sensitiveEnvironmentValues, } from './platform/sensitive-data.js';
41
41
  import { AnthropicCompatibleProvider } from './providers/anthropic-compatible.js';
42
+ import { createAnthropicPromptCachePolicyResolver } from './providers/anthropic-prompt-cache.js';
42
43
  import { FallbackModelProvider } from './providers/fallback-provider.js';
43
44
  import { OpenAICompatibleProvider } from './providers/openai-compatible.js';
44
45
  import { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
@@ -89,34 +90,37 @@ function fileResourceHeaders(environment, providerEnvironment, credential) {
89
90
  : {}),
90
91
  };
91
92
  }
92
- function createProviderForModel(apiKey, providerEnvironment, context, controls, explicitThinkingControls) {
93
+ function createProviderForModel({ apiKey, environment, dataPlane, provider, context, controls, explicitThinkingControls, }) {
94
+ const resolvePromptCachePolicy = createAnthropicPromptCachePolicyResolver(environment, dataPlane);
93
95
  return (selectedModel) => {
94
96
  const providerOptions = {
95
97
  apiKey,
96
98
  model: selectedModel,
97
- baseUrl: providerEnvironment.baseUrl,
99
+ baseUrl: provider.baseUrl,
98
100
  ...('contextWindowTokens' in context
99
101
  ? { contextWindowTokens: context.contextWindowTokens }
100
102
  : {}),
101
103
  };
102
- return providerEnvironment.provider === 'anthropic'
104
+ return provider.provider === 'anthropic'
103
105
  ? new AnthropicCompatibleProvider({
104
106
  ...providerOptions,
107
+ promptCaching: resolvePromptCachePolicy({
108
+ baseUrl: provider.baseUrl,
109
+ model: selectedModel,
110
+ }),
105
111
  thinking: {
106
112
  mode: controls.thinking ?? 'enabled',
107
113
  ...(controls.maxThinkingTokens === undefined
108
114
  ? {}
109
115
  : { maxTokens: controls.maxThinkingTokens }),
110
116
  },
111
- ...('maxOutputTokens' in providerEnvironment
112
- ? { maxOutputTokens: providerEnvironment.maxOutputTokens }
113
- : {}),
114
- ...('anthropicVersion' in providerEnvironment
115
- ? { anthropicVersion: providerEnvironment.anthropicVersion }
117
+ ...('maxOutputTokens' in provider
118
+ ? { maxOutputTokens: provider.maxOutputTokens }
116
119
  : {}),
117
- ...('webSearch' in providerEnvironment
118
- ? { webSearch: providerEnvironment.webSearch }
120
+ ...('anthropicVersion' in provider
121
+ ? { anthropicVersion: provider.anthropicVersion }
119
122
  : {}),
123
+ ...('webSearch' in provider ? { webSearch: provider.webSearch } : {}),
120
124
  })
121
125
  : new OpenAICompatibleProvider({
122
126
  ...providerOptions,
@@ -245,6 +249,8 @@ Provider environment:
245
249
  PRAXIS_PROVIDER=openai|anthropic, PRAXIS_API_KEY, PRAXIS_MODEL
246
250
  PRAXIS_BASE_URL, PRAXIS_MAX_OUTPUT_TOKENS, PRAXIS_ANTHROPIC_VERSION
247
251
  PRAXIS_ANTHROPIC_WEB_SEARCH=true|false
252
+ PRAXIS_ANTHROPIC_PROMPT_CACHING=true|false
253
+ PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL=5m|1h
248
254
  PRAXIS_CONTEXT_WINDOW_TOKENS, PRAXIS_CONTEXT_RESERVE_TOKENS
249
255
  `;
250
256
  const AGENTS_HELP = `Usage: praxis agents [options]
@@ -837,7 +843,15 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
837
843
  if (!providerEnvironment) {
838
844
  throw new Error('Provider environment is unavailable');
839
845
  }
840
- providerForModel = createProviderForModel(apiKey, providerEnvironment, context, cli, controls);
846
+ providerForModel = createProviderForModel({
847
+ apiKey,
848
+ environment: runtimeEnvironment,
849
+ dataPlane,
850
+ provider: providerEnvironment,
851
+ context,
852
+ controls: cli,
853
+ explicitThinkingControls: controls,
854
+ });
841
855
  const createProvider = providerForModel;
842
856
  providerForMainModel = (primaryModel) => {
843
857
  const models = [primaryModel, ...(cli.fallbackModels ?? [])].filter((candidate, index, all) => all.indexOf(candidate) === index);
@@ -1705,7 +1719,16 @@ const createDefaultAutoModeCritic = async ({ model, dataPlane, configRoot, state
1705
1719
  if (!apiKey || !selectedModel) {
1706
1720
  throw new Error('PRAXIS_API_KEY and a model (--model or PRAXIS_MODEL) are required');
1707
1721
  }
1708
- return createProviderForModel(apiKey, parseProviderEnvironment(process.env), parseContextEnvironment(process.env), {}, {})(selectedModel);
1722
+ const providerEnvironment = parseProviderEnvironment(process.env);
1723
+ return createProviderForModel({
1724
+ apiKey,
1725
+ environment: process.env,
1726
+ dataPlane: resolvedDataPlane,
1727
+ provider: providerEnvironment,
1728
+ context: parseContextEnvironment(process.env),
1729
+ controls: {},
1730
+ explicitThinkingControls: {},
1731
+ })(selectedModel);
1709
1732
  };
1710
1733
  const defaultPluginEvalRuntimeFactory = {
1711
1734
  create: async (options) => {
@@ -1805,7 +1828,16 @@ const defaultPluginEvalJudge = {
1805
1828
  const apiKey = environment.PRAXIS_API_KEY;
1806
1829
  if (!apiKey)
1807
1830
  throw new Error('PRAXIS_API_KEY is required for paid eval graders');
1808
- const provider = createProviderForModel(apiKey, parseProviderEnvironment(environment), parseContextEnvironment(environment), {}, {})(model);
1831
+ const providerEnvironment = parseProviderEnvironment(environment);
1832
+ const provider = createProviderForModel({
1833
+ apiKey,
1834
+ environment,
1835
+ dataPlane: resolveDataPlane(environment),
1836
+ provider: providerEnvironment,
1837
+ context: parseContextEnvironment(environment),
1838
+ controls: {},
1839
+ explicitThinkingControls: {},
1840
+ })(model);
1809
1841
  const prompt = `You are an eval judge. Return only JSON matching {"passed":boolean,"explanation":string}.
1810
1842
 
1811
1843
  Criteria:
@@ -706,10 +706,11 @@ export class ClaudeMcpToolRegistry {
706
706
  definitions() {
707
707
  return [
708
708
  ...this.options.base.definitions(),
709
+ ...(this.resourceServers.size > 0 ? MCP_RESOURCE_TOOL_DEFINITIONS : []),
709
710
  ...[...this.connectedTools.entries()]
710
711
  .filter(([name]) => !this.reservedTools.has(name))
712
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
711
713
  .map(([, tool]) => tool.definition),
712
- ...(this.resourceServers.size > 0 ? MCP_RESOURCE_TOOL_DEFINITIONS : []),
713
714
  ];
714
715
  }
715
716
  schedulingPolicy(call) {
@@ -1,4 +1,5 @@
1
1
  import { type ModelProvider, type ModelRequest, type ModelStreamEvent, type ModelThinkingConfig } from '../core/runtime.js';
2
+ import { type AnthropicPromptCachePolicy } from './anthropic-prompt-cache.js';
2
3
  export interface AnthropicCompatibleProviderOptions {
3
4
  baseUrl: string;
4
5
  apiKey: string;
@@ -6,6 +7,7 @@ export interface AnthropicCompatibleProviderOptions {
6
7
  maxOutputTokens?: number;
7
8
  anthropicVersion?: string;
8
9
  webSearch?: boolean;
10
+ promptCaching?: AnthropicPromptCachePolicy;
9
11
  contextWindowTokens?: number;
10
12
  thinking?: ModelThinkingConfig;
11
13
  maxStreamBufferBytes?: number;
@@ -29,6 +31,7 @@ export declare class AnthropicCompatibleProvider implements ModelProvider {
29
31
  private readonly maxToolMetadataBytes;
30
32
  private readonly maxErrorBodyBytes;
31
33
  private readonly thinking;
34
+ private readonly promptCaching;
32
35
  constructor(options: AnthropicCompatibleProviderOptions);
33
36
  complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
34
37
  }
@@ -1,5 +1,6 @@
1
1
  import { ModelProviderError, } from '../core/runtime.js';
2
2
  import { transportFailureKind } from './provider-errors.js';
3
+ import { createAnthropicPromptCachePolicyResolver, } from './anthropic-prompt-cache.js';
3
4
  function isRecord(value) {
4
5
  return typeof value === 'object' && value !== null && !Array.isArray(value);
5
6
  }
@@ -451,6 +452,34 @@ function parseSseEvent(data, state, maxToolArgumentsBytes, maxToolCallsPerRespon
451
452
  }
452
453
  return [];
453
454
  }
455
+ const ANTHROPIC_CACHE_LOOKBACK_BLOCKS = 20;
456
+ function cacheControl(ttl) {
457
+ return {
458
+ type: 'ephemeral',
459
+ ...(ttl === '1h' ? { ttl } : {}),
460
+ };
461
+ }
462
+ function markLatestCacheableMessageBlock(messages, control) {
463
+ let inspectedBlocks = 0;
464
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex--) {
465
+ const content = messages[messageIndex]?.content;
466
+ if (!content)
467
+ continue;
468
+ for (let blockIndex = content.length - 1; blockIndex >= 0; blockIndex--) {
469
+ if (inspectedBlocks >= ANTHROPIC_CACHE_LOOKBACK_BLOCKS)
470
+ return;
471
+ inspectedBlocks += 1;
472
+ const block = content[blockIndex];
473
+ if (!block ||
474
+ block.type === 'thinking' ||
475
+ block.type === 'redacted_thinking') {
476
+ continue;
477
+ }
478
+ content[blockIndex] = { ...block, cache_control: control };
479
+ return;
480
+ }
481
+ }
482
+ }
454
483
  function serializeMediaContent(message) {
455
484
  const blocks = message.contentBlocks
456
485
  ? [...message.contentBlocks]
@@ -481,9 +510,10 @@ function serializeMediaContent(message) {
481
510
  },
482
511
  });
483
512
  }
484
- function serializeMessages(messages) {
513
+ function serializeMessages(messages, stableSystemMessageCount, promptCaching) {
485
514
  const system = [];
486
515
  const serialized = [];
516
+ let systemIndex = 0;
487
517
  const append = (role, content) => {
488
518
  if (content.length === 0)
489
519
  return;
@@ -495,7 +525,15 @@ function serializeMessages(messages) {
495
525
  };
496
526
  for (const message of messages) {
497
527
  if (message.role === 'system') {
498
- system.push(message.content);
528
+ const block = {
529
+ type: 'text',
530
+ text: `${systemIndex === 0 ? '' : '\n\n'}${message.content}`,
531
+ ...(promptCaching && systemIndex + 1 === stableSystemMessageCount
532
+ ? { cache_control: promptCaching }
533
+ : {}),
534
+ };
535
+ system.push(block);
536
+ systemIndex += 1;
499
537
  continue;
500
538
  }
501
539
  if (message.role === 'user') {
@@ -553,7 +591,10 @@ function serializeMessages(messages) {
553
591
  }
554
592
  append('assistant', content);
555
593
  }
556
- return { system: system.join('\n\n'), messages: serialized };
594
+ if (promptCaching) {
595
+ markLatestCacheableMessageBlock(serialized, promptCaching);
596
+ }
597
+ return { system, messages: serialized };
557
598
  }
558
599
  function validateStableSystemPrefix(request) {
559
600
  const count = request.stableSystemMessageCount;
@@ -583,6 +624,7 @@ export class AnthropicCompatibleProvider {
583
624
  maxToolMetadataBytes;
584
625
  maxErrorBodyBytes;
585
626
  thinking;
627
+ promptCaching;
586
628
  constructor(options) {
587
629
  this.options = options;
588
630
  if (options.contextWindowTokens !== undefined) {
@@ -615,6 +657,16 @@ export class AnthropicCompatibleProvider {
615
657
  terminalReasons: true,
616
658
  };
617
659
  this.thinking = validateThinking(options.thinking);
660
+ const promptCaching = options.promptCaching === false
661
+ ? undefined
662
+ : (options.promptCaching ??
663
+ createAnthropicPromptCachePolicyResolver({}, 'native')({
664
+ baseUrl: options.baseUrl,
665
+ model: options.model,
666
+ }));
667
+ this.promptCaching = promptCaching
668
+ ? cacheControl(promptCaching.ttl)
669
+ : undefined;
618
670
  this.anthropicVersion = options.anthropicVersion ?? '2023-06-01';
619
671
  this.maxStreamBufferBytes = options.maxStreamBufferBytes ?? 1024 * 1024;
620
672
  this.maxToolArgumentsBytes = options.maxToolArgumentsBytes ?? 1024 * 1024;
@@ -646,7 +698,8 @@ export class AnthropicCompatibleProvider {
646
698
  ? ['interleaved-thinking-2025-05-14']
647
699
  : []),
648
700
  ].filter((beta, index, all) => all.indexOf(beta) === index);
649
- const serialized = serializeMessages(request.messages);
701
+ const serialized = serializeMessages(request.messages, request.stableSystemMessageCount, this.promptCaching);
702
+ const requestTools = request.tools;
650
703
  const requestInit = {
651
704
  method: 'POST',
652
705
  headers: {
@@ -664,7 +717,7 @@ export class AnthropicCompatibleProvider {
664
717
  ...(request.effort
665
718
  ? { output_config: { effort: request.effort } }
666
719
  : {}),
667
- ...(serialized.system ? { system: serialized.system } : {}),
720
+ ...(serialized.system.length ? { system: serialized.system } : {}),
668
721
  ...(request.webSearch
669
722
  ? {
670
723
  tools: [
@@ -682,12 +735,15 @@ export class AnthropicCompatibleProvider {
682
735
  ],
683
736
  tool_choice: { type: 'tool', name: 'web_search' },
684
737
  }
685
- : request.tools?.length
738
+ : requestTools?.length
686
739
  ? {
687
- tools: request.tools.map((tool) => ({
740
+ tools: requestTools.map((tool, index) => ({
688
741
  name: tool.name,
689
742
  description: tool.description,
690
743
  input_schema: tool.inputSchema,
744
+ ...(this.promptCaching && index === requestTools.length - 1
745
+ ? { cache_control: this.promptCaching }
746
+ : {}),
691
747
  })),
692
748
  }
693
749
  : {}),
@@ -0,0 +1,11 @@
1
+ export type AnthropicPromptCachePolicy = false | {
2
+ ttl: '5m' | '1h';
3
+ };
4
+ export interface AnthropicPromptCacheTarget {
5
+ baseUrl: string;
6
+ model: string;
7
+ }
8
+ /** Captures prompt-cache configuration once, then resolves a policy for each
9
+ * Anthropic model adapter created during the same session. */
10
+ export declare function createAnthropicPromptCachePolicyResolver(environment: NodeJS.ProcessEnv, dataPlane: 'native' | 'claude'): (target: AnthropicPromptCacheTarget) => AnthropicPromptCachePolicy;
11
+ //# sourceMappingURL=anthropic-prompt-cache.d.ts.map
@@ -0,0 +1,58 @@
1
+ function officialAnthropicEndpoint(baseUrl) {
2
+ try {
3
+ return new URL(baseUrl).hostname === 'api.anthropic.com';
4
+ }
5
+ catch {
6
+ return false;
7
+ }
8
+ }
9
+ function modelFamily(model) {
10
+ const normalized = model.toLowerCase();
11
+ if (normalized.includes('haiku'))
12
+ return 'haiku';
13
+ if (normalized.includes('sonnet'))
14
+ return 'sonnet';
15
+ if (normalized.includes('opus'))
16
+ return 'opus';
17
+ return undefined;
18
+ }
19
+ /** Captures prompt-cache configuration once, then resolves a policy for each
20
+ * Anthropic model adapter created during the same session. */
21
+ export function createAnthropicPromptCachePolicyResolver(environment, dataPlane) {
22
+ const praxisEnabled = environment.PRAXIS_ANTHROPIC_PROMPT_CACHING;
23
+ if (praxisEnabled !== undefined &&
24
+ praxisEnabled !== 'true' &&
25
+ praxisEnabled !== 'false') {
26
+ throw new Error('PRAXIS_ANTHROPIC_PROMPT_CACHING must be true or false');
27
+ }
28
+ const praxisTtl = environment.PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL;
29
+ if (praxisTtl !== undefined && praxisTtl !== '5m' && praxisTtl !== '1h') {
30
+ throw new Error('PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL must be 5m or 1h');
31
+ }
32
+ if (praxisEnabled === 'false' && praxisTtl !== undefined) {
33
+ throw new Error('PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL cannot be set when prompt caching is false');
34
+ }
35
+ const claudeCompatibility = dataPlane === 'claude';
36
+ const disableAll = environment.DISABLE_PROMPT_CACHING === '1';
37
+ const disabledFamilies = new Set(['haiku', 'sonnet', 'opus'].filter((family) => environment[`DISABLE_PROMPT_CACHING_${family.toUpperCase()}`] === '1'));
38
+ const forceFiveMinutes = environment.FORCE_PROMPT_CACHING_5M === '1';
39
+ const enableOneHour = environment.ENABLE_PROMPT_CACHING_1H === '1';
40
+ return ({ baseUrl, model }) => {
41
+ if (claudeCompatibility) {
42
+ const family = modelFamily(model);
43
+ if (disableAll || (family && disabledFamilies.has(family)))
44
+ return false;
45
+ }
46
+ if (praxisEnabled === 'false')
47
+ return false;
48
+ if (claudeCompatibility && forceFiveMinutes)
49
+ return { ttl: '5m' };
50
+ if (claudeCompatibility && enableOneHour)
51
+ return { ttl: '1h' };
52
+ if (praxisEnabled === 'true' || praxisTtl !== undefined) {
53
+ return { ttl: praxisTtl ?? '5m' };
54
+ }
55
+ return officialAnthropicEndpoint(baseUrl) ? { ttl: '5m' } : false;
56
+ };
57
+ }
58
+ //# sourceMappingURL=anthropic-prompt-cache.js.map
@@ -1,3 +1,4 @@
1
+ import { createAnthropicPromptCachePolicyResolver } from './anthropic-prompt-cache.js';
1
2
  export function parseProviderEnvironment(environment) {
2
3
  const provider = environment.PRAXIS_PROVIDER ?? 'openai';
3
4
  if (provider !== 'openai' && provider !== 'anthropic') {
@@ -29,6 +30,17 @@ export function parseProviderEnvironment(environment) {
29
30
  if (provider === 'openai' && webSearch !== undefined) {
30
31
  throw new Error('PRAXIS_ANTHROPIC_WEB_SEARCH requires PRAXIS_PROVIDER=anthropic');
31
32
  }
33
+ for (const name of [
34
+ 'PRAXIS_ANTHROPIC_PROMPT_CACHING',
35
+ 'PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL',
36
+ ]) {
37
+ if (provider === 'openai' && environment[name] !== undefined) {
38
+ throw new Error(`${name} requires PRAXIS_PROVIDER=anthropic`);
39
+ }
40
+ }
41
+ if (provider === 'anthropic') {
42
+ createAnthropicPromptCachePolicyResolver(environment, 'native');
43
+ }
32
44
  return {
33
45
  provider,
34
46
  baseUrl: environment.PRAXIS_BASE_URL ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",