@cjhyy/code-shell-core 0.9.2 → 0.9.4

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.
Files changed (38) hide show
  1. package/dist/cli/agent-server-stdio.js +26 -1
  2. package/dist/cli/agent-server-tcp.js +58 -7
  3. package/dist/engine/engine.d.ts +8 -0
  4. package/dist/engine/engine.js +16 -12
  5. package/dist/engine/model-facade.d.ts +3 -0
  6. package/dist/engine/model-facade.js +2 -0
  7. package/dist/engine/run-tooling.js +8 -8
  8. package/dist/engine/streaming-tool-queue.d.ts +4 -1
  9. package/dist/engine/streaming-tool-queue.js +17 -2
  10. package/dist/engine/turn-loop.d.ts +14 -5
  11. package/dist/engine/turn-loop.js +92 -37
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/llm/prompt-cache.d.ts +48 -0
  15. package/dist/llm/prompt-cache.js +100 -0
  16. package/dist/llm/providers/anthropic.d.ts +3 -0
  17. package/dist/llm/providers/anthropic.js +77 -53
  18. package/dist/llm/providers/openai.d.ts +7 -27
  19. package/dist/llm/providers/openai.js +120 -68
  20. package/dist/llm/types.d.ts +3 -0
  21. package/dist/onboarding.js +72 -49
  22. package/dist/panel-apps/manifest.d.ts +12 -12
  23. package/dist/profile/types.d.ts +26 -26
  24. package/dist/protocol/background-result-wakeup.d.ts +3 -1
  25. package/dist/protocol/background-result-wakeup.js +5 -5
  26. package/dist/protocol/server.d.ts +13 -0
  27. package/dist/protocol/server.js +22 -3
  28. package/dist/services/index.d.ts +0 -1
  29. package/dist/services/index.js +0 -1
  30. package/dist/session/memory.js +2 -2
  31. package/dist/session/session-manager.js +30 -1
  32. package/dist/tool-system/builtin/agent-notifications.d.ts +25 -1
  33. package/dist/tool-system/builtin/agent-notifications.js +334 -2
  34. package/dist/tool-system/context.d.ts +9 -4
  35. package/dist/tool-system/external-tool-exposure.js +11 -10
  36. package/package.json +2 -1
  37. package/dist/services/notifier.d.ts +0 -33
  38. package/dist/services/notifier.js +0 -83
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Provider-neutral prompt-cache planning.
3
+ *
4
+ * This module owns the semantic cache policy (session affinity and reusable
5
+ * prefix boundaries). Provider clients only translate the plan to their wire
6
+ * format: OpenAI `prompt_cache_*` fields or Anthropic `cache_control` blocks.
7
+ */
8
+ export type PromptCacheStrategy = "openai-explicit" | "openai-implicit" | "anthropic-explicit" | "provider-managed";
9
+ export type PromptCacheBreakpoint = "system" | "tools" | "stable-history" | "rolling-history";
10
+ export interface PromptCacheRequestContext {
11
+ /** Stable run/session namespace used for provider cache affinity. */
12
+ scopeId?: string;
13
+ /**
14
+ * Number of source messages before the first volatile context message.
15
+ * Providers use this to retain a reusable durable-history breakpoint while
16
+ * also advancing a rolling breakpoint over the append-only in-run tail.
17
+ */
18
+ stablePrefixMessageCount?: number;
19
+ }
20
+ export interface PromptCachePolicy {
21
+ strategy: PromptCacheStrategy;
22
+ layoutVersion: string;
23
+ breakpoints: readonly PromptCacheBreakpoint[];
24
+ /** Opaque and <=64 chars, as required by OpenAI's prompt_cache_key. */
25
+ cacheKey?: string;
26
+ /** GPT-5.6+ explicit-cache request mode. */
27
+ promptCacheOptions?: {
28
+ mode: "explicit";
29
+ ttl: "30m";
30
+ };
31
+ }
32
+ export interface ResolvePromptCachePolicyInput {
33
+ provider: string;
34
+ providerKind?: string;
35
+ model: string;
36
+ request?: PromptCacheRequestContext;
37
+ /** Sticky compatibility fallback after an endpoint rejects explicit fields. */
38
+ explicitDisabled?: boolean;
39
+ }
40
+ /**
41
+ * Produce a privacy-preserving stable affinity key without leaking a raw
42
+ * session id to the provider. The prefix plus 48 hex chars is 51 characters.
43
+ */
44
+ export declare function createPromptCacheKey(scopeId: string, namespace: string): string;
45
+ /** Resolve one cache policy from the actual provider route and model family. */
46
+ export declare function resolvePromptCachePolicy(input: ResolvePromptCachePolicyInput): PromptCachePolicy;
47
+ /** Deduplicate semantic boundaries while preserving their left-to-right order. */
48
+ export declare function uniquePromptCacheBreakpointIndexes(indexes: readonly (number | undefined)[]): number[];
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Provider-neutral prompt-cache planning.
3
+ *
4
+ * This module owns the semantic cache policy (session affinity and reusable
5
+ * prefix boundaries). Provider clients only translate the plan to their wire
6
+ * format: OpenAI `prompt_cache_*` fields or Anthropic `cache_control` blocks.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ const OPENAI_EXPLICIT_BREAKPOINTS = ["system", "stable-history", "rolling-history"];
10
+ const ANTHROPIC_BREAKPOINTS = ["system", "tools", "stable-history", "rolling-history"];
11
+ const OPENROUTER_ANTHROPIC_BREAKPOINTS = ["system", "stable-history", "rolling-history"];
12
+ function normalizedModel(model) {
13
+ return model.replace(/^~/, "");
14
+ }
15
+ function isAnthropicModel(model) {
16
+ return /^anthropic\/claude-/i.test(normalizedModel(model));
17
+ }
18
+ function isOpenAIModel(model) {
19
+ const normalized = normalizedModel(model);
20
+ return /^openai\//i.test(normalized) || /^(?:gpt-|o\d)/i.test(normalized);
21
+ }
22
+ /** GPT-5.6 and later 5.x releases support explicit prompt-cache breakpoints. */
23
+ function supportsOpenAIExplicitCaching(model) {
24
+ const normalized = normalizedModel(model).replace(/^openai\//i, "");
25
+ const match = /^gpt-5\.(\d+)(?:[-.]|$)/i.exec(normalized);
26
+ return match !== null && Number(match[1]) >= 6;
27
+ }
28
+ /**
29
+ * Produce a privacy-preserving stable affinity key without leaking a raw
30
+ * session id to the provider. The prefix plus 48 hex chars is 51 characters.
31
+ */
32
+ export function createPromptCacheKey(scopeId, namespace) {
33
+ const digest = createHash("sha256")
34
+ .update("codeshell-prompt-cache-v1\0")
35
+ .update(namespace)
36
+ .update("\0")
37
+ .update(scopeId)
38
+ .digest("hex")
39
+ .slice(0, 48);
40
+ return `cs:${digest}`;
41
+ }
42
+ /** Resolve one cache policy from the actual provider route and model family. */
43
+ export function resolvePromptCachePolicy(input) {
44
+ const kind = (input.providerKind ?? input.provider).toLowerCase();
45
+ const model = normalizedModel(input.model);
46
+ const key = input.request?.scopeId
47
+ ? createPromptCacheKey(input.request.scopeId, `${kind}:${model}`)
48
+ : undefined;
49
+ if (input.provider === "anthropic" || kind === "anthropic") {
50
+ return {
51
+ strategy: "anthropic-explicit",
52
+ layoutVersion: "system-tools-stable-rolling-v2",
53
+ breakpoints: ANTHROPIC_BREAKPOINTS,
54
+ };
55
+ }
56
+ if (kind === "openrouter" && isAnthropicModel(model)) {
57
+ return {
58
+ strategy: "anthropic-explicit",
59
+ // OpenRouter/Anthropic includes tools in the system-prefix cache entry,
60
+ // so a separate tool marker is unnecessary and preserves one slot.
61
+ layoutVersion: "system-stable-rolling-v2",
62
+ breakpoints: OPENROUTER_ANTHROPIC_BREAKPOINTS,
63
+ };
64
+ }
65
+ const openAIRoute = kind === "openai" || (kind === "openrouter" && isOpenAIModel(model));
66
+ if (openAIRoute && supportsOpenAIExplicitCaching(model) && input.explicitDisabled !== true) {
67
+ return {
68
+ strategy: "openai-explicit",
69
+ layoutVersion: "system-stable-rolling-v1",
70
+ breakpoints: OPENAI_EXPLICIT_BREAKPOINTS,
71
+ ...(key ? { cacheKey: key } : {}),
72
+ promptCacheOptions: { mode: "explicit", ttl: "30m" },
73
+ };
74
+ }
75
+ if (openAIRoute) {
76
+ return {
77
+ strategy: "openai-implicit",
78
+ layoutVersion: "implicit-affinity-v1",
79
+ breakpoints: [],
80
+ ...(key ? { cacheKey: key } : {}),
81
+ };
82
+ }
83
+ return {
84
+ strategy: "provider-managed",
85
+ layoutVersion: "append-only-v1",
86
+ breakpoints: [],
87
+ };
88
+ }
89
+ /** Deduplicate semantic boundaries while preserving their left-to-right order. */
90
+ export function uniquePromptCacheBreakpointIndexes(indexes) {
91
+ const seen = new Set();
92
+ const result = [];
93
+ for (const index of indexes) {
94
+ if (index === undefined || index < 0 || seen.has(index))
95
+ continue;
96
+ seen.add(index);
97
+ result.push(index);
98
+ }
99
+ return result;
100
+ }
@@ -16,6 +16,7 @@ export declare class AnthropicClient extends LLMClientBase {
16
16
  */
17
17
  private _capability;
18
18
  private get capability();
19
+ private promptCachePolicy;
19
20
  getPromptCacheConfigIdentity(): Readonly<Record<string, unknown>>;
20
21
  /**
21
22
  * Translate the resolved ReasoningSetting into Anthropic's `thinking` field,
@@ -44,7 +45,9 @@ export declare class AnthropicClient extends LLMClientBase {
44
45
  private nonStreamMessage;
45
46
  private streamMessage;
46
47
  private processResponse;
48
+ private buildSystem;
47
49
  private buildMessages;
50
+ private markAnthropicCacheBreakpoint;
48
51
  private convertTools;
49
52
  private handleApiError;
50
53
  }
@@ -9,6 +9,7 @@ import { countTokens } from "../token-counter.js";
9
9
  import { capabilitiesFor } from "../capabilities/index.js";
10
10
  import { resolveApiKey, resolveHeaders } from "../provider-auth.js";
11
11
  import { stripVisionFromHistory } from "../strip-vision.js";
12
+ import { resolvePromptCachePolicy, uniquePromptCacheBreakpointIndexes, } from "../prompt-cache.js";
12
13
  /**
13
14
  * Anthropic's `max_tokens` is required, so unlike OpenAI we can't omit it when
14
15
  * the model's ceiling is unknown. Use a conservative floor in that rare case
@@ -75,12 +76,22 @@ export class AnthropicClient extends LLMClientBase {
75
76
  }
76
77
  return this._capability;
77
78
  }
79
+ promptCachePolicy(request) {
80
+ return resolvePromptCachePolicy({
81
+ provider: this.provider,
82
+ providerKind: this.config.providerKind,
83
+ model: this.model,
84
+ request,
85
+ });
86
+ }
78
87
  getPromptCacheConfigIdentity() {
88
+ const cachePolicy = this.promptCachePolicy();
79
89
  return {
80
90
  ...super.getPromptCacheConfigIdentity(),
81
- cacheStrategy: "anthropic-explicit",
82
- cacheLayoutVersion: "system-tools-history-v1",
83
- breakpointCount: 3,
91
+ cacheStrategy: cachePolicy.strategy,
92
+ cacheLayoutVersion: cachePolicy.layoutVersion,
93
+ cacheBreakpoints: cachePolicy.breakpoints,
94
+ breakpointCount: cachePolicy.breakpoints.length,
84
95
  reasoningShape: this.capability.reasoning,
85
96
  };
86
97
  }
@@ -133,8 +144,9 @@ export class AnthropicClient extends LLMClientBase {
133
144
  }
134
145
  async createMessage(options) {
135
146
  return this.withRetry(async (requestSignal) => {
136
- const messages = this.buildMessages(options.messages);
137
- const tools = options.tools ? this.convertTools(options.tools) : undefined;
147
+ const cachePolicy = this.promptCachePolicy(options.promptCache);
148
+ const messages = this.buildMessages(options.messages, options.promptCache, cachePolicy);
149
+ const tools = options.tools ? this.convertTools(options.tools, cachePolicy) : undefined;
138
150
  // One span per outbound LLM request. Begin emits debug-level so the
139
151
  // info log isn't spammed during normal operation; end emits info with
140
152
  // duration_ms + usage so `--debug=llm` already gives a quick latency
@@ -146,6 +158,7 @@ export class AnthropicClient extends LLMClientBase {
146
158
  stream: !!(options.stream && options.onChunk),
147
159
  messageCount: messages.length,
148
160
  toolCount: tools?.length ?? 0,
161
+ cacheStrategy: cachePolicy.strategy,
149
162
  });
150
163
  try {
151
164
  const response = options.stream && options.onChunk
@@ -176,13 +189,7 @@ export class AnthropicClient extends LLMClientBase {
176
189
  const response = await this.client.messages.create({
177
190
  model: this.model,
178
191
  max_tokens: maxTokens,
179
- system: [
180
- {
181
- type: "text",
182
- text: options.systemPrompt,
183
- cache_control: { type: "ephemeral" },
184
- },
185
- ],
192
+ system: this.buildSystem(options.systemPrompt, this.promptCachePolicy(options.promptCache)),
186
193
  messages,
187
194
  ...(tools?.length ? { tools } : {}),
188
195
  ...(thinking ? { thinking } : {}),
@@ -207,13 +214,7 @@ export class AnthropicClient extends LLMClientBase {
207
214
  const stream = this.client.messages.stream({
208
215
  model: this.model,
209
216
  max_tokens: maxTokens,
210
- system: [
211
- {
212
- type: "text",
213
- text: options.systemPrompt,
214
- cache_control: { type: "ephemeral" },
215
- },
216
- ],
217
+ system: this.buildSystem(options.systemPrompt, this.promptCachePolicy(options.promptCache)),
217
218
  messages,
218
219
  ...(tools?.length ? { tools } : {}),
219
220
  ...(thinking ? { thinking } : {}),
@@ -316,10 +317,27 @@ export class AnthropicClient extends LLMClientBase {
316
317
  stopReason: response.stop_reason ?? undefined,
317
318
  };
318
319
  }
319
- buildMessages(messages) {
320
+ buildSystem(systemPrompt, policy) {
321
+ return [
322
+ {
323
+ type: "text",
324
+ text: systemPrompt,
325
+ ...(policy.breakpoints.includes("system")
326
+ ? { cache_control: { type: "ephemeral" } }
327
+ : {}),
328
+ },
329
+ ];
330
+ }
331
+ buildMessages(messages, promptCache, policy) {
320
332
  messages = stripVisionFromHistory(messages, this.capability.supportsVision);
321
333
  const result = [];
322
- for (const msg of messages) {
334
+ const stablePrefixMessageCount = Math.max(0, Math.min(messages.length, promptCache?.stablePrefixMessageCount ?? messages.length));
335
+ let stablePrefixEndMessage;
336
+ for (let sourceIndex = 0; sourceIndex < messages.length; sourceIndex++) {
337
+ if (sourceIndex === stablePrefixMessageCount) {
338
+ stablePrefixEndMessage = result[result.length - 1];
339
+ }
340
+ const msg = messages[sourceIndex];
323
341
  if (msg.role === "system")
324
342
  continue;
325
343
  const role = msg.role === "tool" ? "user" : msg.role;
@@ -390,40 +408,45 @@ export class AnthropicClient extends LLMClientBase {
390
408
  }
391
409
  }
392
410
  }
393
- // Prompt-cache breakpoint on the history: mark the LAST content block of
394
- // the LAST message. The API caches everything up to the marker, so the
395
- // stable prefix (all prior turns) is reused; only the growing tail is
396
- // re-billed. CC does exactly this (one marker at messages.length - 1) and
397
- // warns a second history marker causes KV page eviction. We only ANNOTATE
398
- // the tail block — never reorder — so the tool_use/tool_result adjacency
399
- // invariant is untouched. A string-content message is lifted to a single
400
- // text block so it can carry cache_control. See
401
- // docs/todo/prompt-cache-optimization.md.
402
- const lastMsg = result[result.length - 1];
403
- if (lastMsg) {
404
- if (typeof lastMsg.content === "string") {
405
- lastMsg.content = [
406
- {
407
- type: "text",
408
- text: lastMsg.content,
409
- cache_control: { type: "ephemeral" },
410
- },
411
- ];
412
- }
413
- else {
414
- const lastBlock = lastMsg.content[lastMsg.content.length - 1];
415
- // Skip thinking/redacted_thinking blocks: they reject cache_control and
416
- // marking them would 400. buildMessages never emits them today (thinking
417
- // is a top-level request field, not history content), but guard anyway
418
- // so a future block type can't silently break the request.
419
- if (lastBlock && lastBlock.type !== "thinking" && lastBlock.type !== "redacted_thinking") {
420
- lastBlock.cache_control = { type: "ephemeral" };
421
- }
422
- }
411
+ stablePrefixEndMessage ??= result[result.length - 1];
412
+ const stablePrefixEndIndex = stablePrefixEndMessage
413
+ ? result.indexOf(stablePrefixEndMessage)
414
+ : undefined;
415
+ const breakpointIndexes = uniquePromptCacheBreakpointIndexes([
416
+ policy.breakpoints.includes("stable-history") && stablePrefixEndIndex !== undefined
417
+ ? stablePrefixEndIndex
418
+ : undefined,
419
+ policy.breakpoints.includes("rolling-history") ? result.length - 1 : undefined,
420
+ ]);
421
+ for (const index of breakpointIndexes) {
422
+ this.markAnthropicCacheBreakpoint(result[index]);
423
423
  }
424
424
  return result;
425
425
  }
426
- convertTools(tools) {
426
+ markAnthropicCacheBreakpoint(message) {
427
+ if (!message)
428
+ return;
429
+ if (typeof message.content === "string") {
430
+ message.content = [
431
+ {
432
+ type: "text",
433
+ text: message.content,
434
+ cache_control: { type: "ephemeral" },
435
+ },
436
+ ];
437
+ return;
438
+ }
439
+ for (let index = message.content.length - 1; index >= 0; index--) {
440
+ const block = message.content[index];
441
+ // Thinking blocks reject cache_control. Walk backward so a preceding
442
+ // cacheable text/tool block can still anchor the prefix.
443
+ if (block.type === "thinking" || block.type === "redacted_thinking")
444
+ continue;
445
+ block.cache_control = { type: "ephemeral" };
446
+ return;
447
+ }
448
+ }
449
+ convertTools(tools, policy) {
427
450
  const converted = tools.map((t) => ({
428
451
  name: t.name,
429
452
  description: t.description,
@@ -436,8 +459,9 @@ export class AnthropicClient extends LLMClientBase {
436
459
  // not per-tool); a second marker here would waste a scarce cache_control
437
460
  // slot (max 4) and risk KV eviction. See docs/todo/prompt-cache-optimization.md.
438
461
  const last = converted[converted.length - 1];
439
- if (last)
462
+ if (last && policy.breakpoints.includes("tools")) {
440
463
  last.cache_control = { type: "ephemeral" };
464
+ }
441
465
  return converted;
442
466
  }
443
467
  handleApiError(err) {
@@ -52,6 +52,9 @@ export declare class OpenAIClient extends LLMClientBase {
52
52
  private readonly dangerouslyAllowBrowser;
53
53
  private _forceMaxCompletionTokens;
54
54
  private _dropReasoningEffort;
55
+ /** Compatibility fallbacks for OpenAI-compatible gateways that lag the API. */
56
+ private _disableExplicitPromptCache;
57
+ private _disablePromptCacheKey;
55
58
  constructor(config: LLMConfig, defaults?: ClientDefaults, runtimeOptions?: {
56
59
  dangerouslyAllowBrowser?: boolean;
57
60
  });
@@ -66,20 +69,7 @@ export declare class OpenAIClient extends LLMClientBase {
66
69
  */
67
70
  private _capability;
68
71
  private get capability();
69
- /**
70
- * True when this client routes an Anthropic-family model through OpenRouter's
71
- * OpenAI-compatible endpoint. Anthropic caching is EXPLICIT — nothing is
72
- * cached unless the request carries `cache_control` breakpoints (verified live
73
- * 2026-07-02: plain requests to anthropic/claude-opus-4.7-fast via OpenRouter
74
- * report cached_tokens 0 on every repeat; a single system-block breakpoint
75
- * turns the whole stable prefix — tools + system — into a cache hit, ~89%
76
- * cheaper on the follow-up). OpenAI and other OpenRouter models cache
77
- * automatically, so they must NOT get breakpoints. The slug arrives resolved
78
- * (e.g. "anthropic/claude-opus-4.7-fast") or as the router alias
79
- * ("~anthropic/claude-opus-latest") — both start with an optional "~" then
80
- * "anthropic/".
81
- */
82
- private get isOpenRouterAnthropic();
72
+ private promptCachePolicy;
83
73
  getPromptCacheConfigIdentity(): Readonly<Record<string, unknown>>;
84
74
  createMessage(options: CreateMessageOptions): Promise<LLMResponse>;
85
75
  /**
@@ -93,20 +83,10 @@ export declare class OpenAIClient extends LLMClientBase {
93
83
  private processChoice;
94
84
  private buildMessages;
95
85
  /**
96
- * In-place: add prompt-cache breakpoints for Anthropic-over-OpenRouter.
97
- * Mirrors the native anthropic provider (≤4 breakpoints):
98
- * 1. System block — the stable prefix. Anthropic sees tools BEFORE the
99
- * system prompt, so one marker on the system block caches tools too
100
- * (verified live: system-only marker cached 3511/3952 prompt tokens
101
- * including tool defs).
102
- * 2. Last message — one rolling breakpoint so the growing conversation
103
- * history becomes a cached prefix. Not scrolled: as history grows the
104
- * "last message" naturally advances and its tail is the next write.
105
- * A string `content` is lifted to a single-element `[{type:"text",...}]`
106
- * array so it can carry `cache_control`; OpenRouter accepts this OpenAI
107
- * multimodal wire form for text.
86
+ * Translate semantic prefix boundaries to the active wire format. Both
87
+ * formats annotate content blocks without reordering messages.
108
88
  */
109
- private applyAnthropicCacheBreakpoints;
89
+ private applyPromptCacheBreakpoints;
110
90
  private convertTools;
111
91
  private handleApiError;
112
92
  }