praxis-agent 0.28.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.
@@ -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,24 @@ 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 };
598
+ }
599
+ function validateStableSystemPrefix(request) {
600
+ const count = request.stableSystemMessageCount;
601
+ if (count === undefined)
602
+ return;
603
+ const systemMessageCount = request.messages.filter((message) => message.role === 'system').length;
604
+ if (!Number.isSafeInteger(count) || count < 0 || count > systemMessageCount) {
605
+ throw new Error('Stable system message count must identify a valid system-message prefix');
606
+ }
607
+ if (request.messages
608
+ .slice(0, count)
609
+ .some((message) => message.role !== 'system')) {
610
+ throw new Error('Stable system message count must identify a valid system-message prefix');
611
+ }
557
612
  }
558
613
  export class AnthropicCompatibleProvider {
559
614
  options;
@@ -569,6 +624,7 @@ export class AnthropicCompatibleProvider {
569
624
  maxToolMetadataBytes;
570
625
  maxErrorBodyBytes;
571
626
  thinking;
627
+ promptCaching;
572
628
  constructor(options) {
573
629
  this.options = options;
574
630
  if (options.contextWindowTokens !== undefined) {
@@ -601,6 +657,16 @@ export class AnthropicCompatibleProvider {
601
657
  terminalReasons: true,
602
658
  };
603
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;
604
670
  this.anthropicVersion = options.anthropicVersion ?? '2023-06-01';
605
671
  this.maxStreamBufferBytes = options.maxStreamBufferBytes ?? 1024 * 1024;
606
672
  this.maxToolArgumentsBytes = options.maxToolArgumentsBytes ?? 1024 * 1024;
@@ -609,6 +675,7 @@ export class AnthropicCompatibleProvider {
609
675
  this.maxErrorBodyBytes = options.maxErrorBodyBytes ?? 64 * 1024;
610
676
  }
611
677
  async *complete(request) {
678
+ validateStableSystemPrefix(request);
612
679
  if (request.webSearch && !this.capabilities.webSearch) {
613
680
  throw new Error('Provider does not support web search');
614
681
  }
@@ -631,7 +698,8 @@ export class AnthropicCompatibleProvider {
631
698
  ? ['interleaved-thinking-2025-05-14']
632
699
  : []),
633
700
  ].filter((beta, index, all) => all.indexOf(beta) === index);
634
- const serialized = serializeMessages(request.messages);
701
+ const serialized = serializeMessages(request.messages, request.stableSystemMessageCount, this.promptCaching);
702
+ const requestTools = request.tools;
635
703
  const requestInit = {
636
704
  method: 'POST',
637
705
  headers: {
@@ -649,7 +717,7 @@ export class AnthropicCompatibleProvider {
649
717
  ...(request.effort
650
718
  ? { output_config: { effort: request.effort } }
651
719
  : {}),
652
- ...(serialized.system ? { system: serialized.system } : {}),
720
+ ...(serialized.system.length ? { system: serialized.system } : {}),
653
721
  ...(request.webSearch
654
722
  ? {
655
723
  tools: [
@@ -667,12 +735,15 @@ export class AnthropicCompatibleProvider {
667
735
  ],
668
736
  tool_choice: { type: 'tool', name: 'web_search' },
669
737
  }
670
- : request.tools?.length
738
+ : requestTools?.length
671
739
  ? {
672
- tools: request.tools.map((tool) => ({
740
+ tools: requestTools.map((tool, index) => ({
673
741
  name: tool.name,
674
742
  description: tool.description,
675
743
  input_schema: tool.inputSchema,
744
+ ...(this.promptCaching && index === requestTools.length - 1
745
+ ? { cache_control: this.promptCaching }
746
+ : {}),
676
747
  })),
677
748
  }
678
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.28.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",