@xopcai/xopc 0.0.202 → 0.0.204

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 (179) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{activity-store-DOhnAQOQ.js → activity-store-DJBadfSX.js} +1 -1
  4. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-CAjfa48Q.js → agent-avatar-dicebear-BaH7Poqk.js} +1 -1
  5. package/dist/gateway/static/root/assets/agents-HctDDhtH.js +68 -0
  6. package/dist/gateway/static/root/assets/{api-E9Bk-Idp.js → api-B5O7fKXa.js} +1 -1
  7. package/dist/gateway/static/root/assets/{app-management-settings-panel-GNT3iv0V.js → app-management-settings-panel-CCLK9wdi.js} +1 -1
  8. package/dist/gateway/static/root/assets/{appearance-settings-BFond2ll.js → appearance-settings-DYSHvGJP.js} +1 -1
  9. package/dist/gateway/static/root/assets/apps-page-kCUkEIoM.js +1 -0
  10. package/dist/gateway/static/root/assets/{archive-plugin-C6qpchzp.js → archive-plugin-CZX6pfRb.js} +1 -1
  11. package/dist/gateway/static/root/assets/{attachment-load-B4CcSDdV.js → attachment-load-CGYpcBXN.js} +1 -1
  12. package/dist/gateway/static/root/assets/automations-page-D2Ao4Dwi.js +6 -0
  13. package/dist/gateway/static/root/assets/{binary-plugins-xYL_41XP.js → binary-plugins-BJ1AQfTa.js} +1 -1
  14. package/dist/gateway/static/root/assets/{block-editor-nYiJQItv.js → block-editor-DR6LCljI.js} +46 -46
  15. package/dist/gateway/static/root/assets/browser-settings-page-B_IjOjtT.js +8 -0
  16. package/dist/gateway/static/root/assets/{browser-workflow-inputs-D_jnndyz.js → browser-workflow-inputs-Bd68yluo.js} +1 -1
  17. package/dist/gateway/static/root/assets/{browser-workflows-page-pT7Hvwwh.js → browser-workflows-page-BG-hEoGn.js} +2 -2
  18. package/dist/gateway/static/root/assets/capabilities-settings-panel-DDC5-wqk.js +7 -0
  19. package/dist/gateway/static/root/assets/{capability-presets-api--tz0mGLY.js → capability-presets-api-BerPMp6f.js} +1 -1
  20. package/dist/gateway/static/root/assets/capability-presets-settings-panel-CxpKaTzc.js +3 -0
  21. package/dist/gateway/static/root/assets/{channel-recipient-api-CkOKF9nc.js → channel-recipient-api-D2d21_XA.js} +1 -1
  22. package/dist/gateway/static/root/assets/channels-settings-D7pmMIbZ.js +1 -0
  23. package/dist/gateway/static/root/assets/channels-status-swr-CZ5P9jbB.js +1 -0
  24. package/dist/gateway/static/root/assets/{connectors-api-X5ukNasZ.js → connectors-api-Sw0BrUmJ.js} +1 -1
  25. package/dist/gateway/static/root/assets/connectors-page-DTyM5bMS.js +2 -0
  26. package/dist/gateway/static/root/assets/cron-expression-DfZsNMAq.js +2 -0
  27. package/dist/gateway/static/root/assets/{date-picker-CGm8EH8G.js → date-picker-DjHSmgq9.js} +1 -1
  28. package/dist/gateway/static/root/assets/dependency-picker-CRajhh0O.js +1 -0
  29. package/dist/gateway/static/root/assets/desktop-pet-a3DXD4IR.js +1 -0
  30. package/dist/gateway/static/root/assets/{desktop-pet-settings-s4kmnf-W.js → desktop-pet-settings-BWx0Duz-.js} +1 -1
  31. package/dist/gateway/static/root/assets/{directory-picker-path-field-DmfFg4Gq.js → directory-picker-path-field-CEZrPGZx.js} +1 -1
  32. package/dist/gateway/static/root/assets/download-Dk-ZjtO6.js +1 -0
  33. package/dist/gateway/static/root/assets/{extension-debug-page-DrLEK8GU.js → extension-debug-page-CwJ-R0DC.js} +1 -1
  34. package/dist/gateway/static/root/assets/{extension-page-DtyFPtMR.js → extension-page-BpXWrZFh.js} +1 -1
  35. package/dist/gateway/static/root/assets/{extension-settings-page-Xok6T2KD.js → extension-settings-page-D0c4RgxT.js} +1 -1
  36. package/dist/gateway/static/root/assets/{gateway-config-api-DwSpEYiw.js → gateway-config-api-Dn2FSNBa.js} +1 -1
  37. package/dist/gateway/static/root/assets/{gateway-config-swr-Cv2ywpdb.js → gateway-config-swr-VVcfd3sG.js} +1 -1
  38. package/dist/gateway/static/root/assets/{gateway-settings-B5Zh2rx9.js → gateway-settings-BNamAC3x.js} +1 -1
  39. package/dist/gateway/static/root/assets/{gateway-startup-retry-undf-k5g.js → gateway-startup-retry-qAX_T2t2.js} +1 -1
  40. package/dist/gateway/static/root/assets/{heartbeat-settings-C2RxqeK0.js → heartbeat-settings-BagVvGe6.js} +1 -1
  41. package/dist/gateway/static/root/assets/home-page-Ci0h-uKG.js +3 -0
  42. package/dist/gateway/static/root/assets/index-DHGnTdgl.js +85 -0
  43. package/dist/gateway/static/root/assets/index-Dm-cvIpl.css +2 -0
  44. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BN3p-1wI.js → keyboard-shortcuts-settings-BOoz5DNd.js} +1 -1
  45. package/dist/gateway/static/root/assets/local-app-workbench-page-B1mLlQT6.js +2 -0
  46. package/dist/gateway/static/root/assets/local-apps-page-B2mV4Olf.js +1 -0
  47. package/dist/gateway/static/root/assets/{locale-store-CnamlR07.js → locale-store-CbPVxonP.js} +1 -1
  48. package/dist/gateway/static/root/assets/logs-page-DLFwiLa6.js +2 -0
  49. package/dist/gateway/static/root/assets/{management-settings-CTIaHfgU.js → management-settings-BEduNvMc.js} +1 -1
  50. package/dist/gateway/static/root/assets/{markdown-split-C0iKadAm.js → markdown-split-BgE9kKG6.js} +1 -1
  51. package/dist/gateway/static/root/assets/markdown-view-B12xeflb.css +1 -0
  52. package/dist/gateway/static/root/assets/markdown-view-Dl-0_9JR.js +69 -0
  53. package/dist/gateway/static/root/assets/{media-plugins-DcM72GSJ.js → media-plugins-DNESLGDB.js} +1 -1
  54. package/dist/gateway/static/root/assets/messages-9zCa8KkW.js +3 -0
  55. package/dist/gateway/static/root/assets/{notes-page-By_EzRhP.js → notes-page-COgmlorx.js} +1 -1
  56. package/dist/gateway/static/root/assets/notes-workbench-Ce5ORQus.js +4 -0
  57. package/dist/gateway/static/root/assets/plus-Q2yW6OE7.js +1 -0
  58. package/dist/gateway/static/root/assets/preview-open-alternatives-BV_58lf7.js +1 -0
  59. package/dist/gateway/static/root/assets/{product-open-page-dQ2_5PLu.js → product-open-page-Bh8WERvg.js} +1 -1
  60. package/dist/gateway/static/root/assets/project-detail-page-C0vjVpKC.js +1 -0
  61. package/dist/gateway/static/root/assets/projects-page-RGaztvxc.js +1 -0
  62. package/dist/gateway/static/root/assets/remote-access-hub-C7EfVrqr.js +1 -0
  63. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DiPj9FLN.js → runtime-tools-settings-panel-HZYyqA5T.js} +2 -2
  64. package/dist/gateway/static/root/assets/{schema-form-BDLYItBP.js → schema-form-C6EnKgW2.js} +1 -1
  65. package/dist/gateway/static/root/assets/sessions-page-BTpLFg2e.js +1 -0
  66. package/dist/gateway/static/root/assets/settings-advanced-gate-RV6nphHJ.js +1 -0
  67. package/dist/gateway/static/root/assets/{settings-form-section-Dr4Uf_CY.js → settings-form-section-sAjYWtCZ.js} +1 -1
  68. package/dist/gateway/static/root/assets/{settings-loading-skeleton-Bqi7tsXj.js → settings-loading-skeleton-Bw56H64J.js} +1 -1
  69. package/dist/gateway/static/root/assets/{settings-page-BbookgxJ.js → settings-page-aduxT-Nu.js} +1 -1
  70. package/dist/gateway/static/root/assets/setup-status-panel-CVg8oOTw.js +1 -0
  71. package/dist/gateway/static/root/assets/share-preview-page-SmtgZyVf.js +2 -0
  72. package/dist/gateway/static/root/assets/shares-settings-DNXQwu2U.js +1 -0
  73. package/dist/gateway/static/root/assets/skill-reload-api-DhgLGYw8.js +1 -0
  74. package/dist/gateway/static/root/assets/skills-page-BHBDl1pN.js +2 -0
  75. package/dist/gateway/static/root/assets/{skills-page.utils-b9byH-KL.js → skills-page.utils-D5SnlPVg.js} +1 -1
  76. package/dist/gateway/static/root/assets/{system-settings-panel-CL_AXjw1.js → system-settings-panel-wiXKgbG9.js} +1 -1
  77. package/dist/gateway/static/root/assets/task-detail-page-DBu-p3Iv.js +1 -0
  78. package/dist/gateway/static/root/assets/{theme-store-BDyUTObE.js → theme-store-Bc_eRUev.js} +1 -1
  79. package/dist/gateway/static/root/assets/{use-autosave-CUI3q_I-.js → use-autosave-CiuRY85U.js} +1 -1
  80. package/dist/gateway/static/root/assets/user-context-page-B-uZKYIg.js +1 -0
  81. package/dist/gateway/static/root/assets/{voice-api-key-field-DLXOB1jw.js → voice-api-key-field-CX9xuc7h.js} +1 -1
  82. package/dist/gateway/static/root/assets/work-discovery-overlay-DrTD6gtS.js +1 -0
  83. package/dist/gateway/static/root/assets/work-discovery-page-C4Ivt1_C.js +1 -0
  84. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-DKpUxB-i.js → workflow-run-setup-panel-CG0fSFFv.js} +2 -2
  85. package/dist/gateway/static/root/assets/workflows-page-CT5g5who.js +5 -0
  86. package/dist/gateway/static/root/assets/{working-directory-picker-modal-BcyiDXU3.js → working-directory-picker-modal-CYWdZUJ5.js} +1 -1
  87. package/dist/gateway/static/root/assets/x-B_N4OPjy.js +1 -0
  88. package/dist/gateway/static/root/index.html +19 -14
  89. package/dist/package.js +1 -1
  90. package/dist/packages/realtime-client/src/index.js +11 -7
  91. package/dist/src/agent/agent-manager.js +3 -2
  92. package/dist/src/agent/embedded/run-for-session.js +3 -2
  93. package/dist/src/agent/embedded/run-turn.js +29 -16
  94. package/dist/src/agent/embedded/types.d.ts +2 -2
  95. package/dist/src/agent/embedded/xopc-stream-bridge.d.ts +2 -1
  96. package/dist/src/agent/embedded/xopc-stream-bridge.js +2 -2
  97. package/dist/src/agent/memory/compaction-planner.js +20 -4
  98. package/dist/src/agent/memory/compaction.js +22 -4
  99. package/dist/src/agent/memory/context-budget.d.ts +6 -1
  100. package/dist/src/agent/memory/context-budget.js +94 -2
  101. package/dist/src/agent/memory/create-memory-manager.js +1 -1
  102. package/dist/src/agent/prompt/cache-boundary.d.ts +2 -0
  103. package/dist/src/agent/prompt/cache-boundary.js +10 -1
  104. package/dist/src/agent/prompt/sections/project-context.d.ts +1 -4
  105. package/dist/src/agent/prompt/sections/project-context.js +6 -13
  106. package/dist/src/agent/prompt/service-prompt-builder.js +2 -1
  107. package/dist/src/agent/prompt/system-prompt.d.ts +1 -3
  108. package/dist/src/agent/prompt/system-prompt.js +6 -15
  109. package/dist/src/agent/tools/cache-stability.d.ts +3 -0
  110. package/dist/src/agent/tools/cache-stability.js +19 -0
  111. package/dist/src/agent/tools/factory.js +2 -1
  112. package/dist/src/agent-manifest/schema.d.ts +40 -20
  113. package/dist/src/agent-manifest/schema.js +4 -5
  114. package/dist/src/agent-runtime/index.js +1 -1
  115. package/dist/src/agent-runtime/runtime-profile.js +1 -1
  116. package/dist/src/cli/commands/gateway/lifecycle.js +1 -1
  117. package/dist/src/config/schema.d.ts +40 -20
  118. package/dist/src/extensions/types/providers.d.ts +0 -2
  119. package/dist/src/gateway/chat-stream/mapper.js +44 -4
  120. package/dist/src/gateway/heartbeat/service.js +1 -1
  121. package/dist/src/gateway/hono/lib/config-payload.d.ts +4 -1
  122. package/dist/src/heartbeat/index.js +1 -1
  123. package/dist/src/infra/update-restart.js +1 -1
  124. package/dist/src/providers/extension-stream-bridge.js +0 -2
  125. package/dist/src/providers/google-managed-prompt-cache.d.ts +14 -0
  126. package/dist/src/providers/google-managed-prompt-cache.js +125 -0
  127. package/dist/src/providers/index.js +5 -4
  128. package/dist/src/providers/model-call.js +2 -2
  129. package/dist/src/providers/model-supplements.d.ts +2 -0
  130. package/dist/src/providers/model-supplements.js +23 -2
  131. package/dist/src/providers/prompt-cache-fingerprint.d.ts +4 -0
  132. package/dist/src/providers/prompt-cache-fingerprint.js +44 -0
  133. package/dist/src/providers/prompt-cache-lifecycle.d.ts +9 -0
  134. package/dist/src/providers/prompt-cache-lifecycle.js +31 -0
  135. package/dist/src/providers/prompt-cache-observability.d.ts +20 -0
  136. package/dist/src/providers/prompt-cache-observability.js +44 -0
  137. package/dist/src/providers/prompt-cache-payload.d.ts +10 -3
  138. package/dist/src/providers/prompt-cache-payload.js +204 -31
  139. package/dist/src/providers/prompt-cache-plan.d.ts +15 -0
  140. package/dist/src/providers/prompt-cache-plan.js +29 -0
  141. package/dist/src/realtime/runtime.js +9 -2
  142. package/dist/src/realtime/writer.js +5 -0
  143. package/package.json +3 -3
  144. package/dist/gateway/static/root/assets/agents-Cke8EF5Z.js +0 -68
  145. package/dist/gateway/static/root/assets/apps-page-B9D-6sJt.js +0 -1
  146. package/dist/gateway/static/root/assets/automations-page-BFS3dH43.js +0 -6
  147. package/dist/gateway/static/root/assets/browser-settings-page-ELV-Y8rz.js +0 -8
  148. package/dist/gateway/static/root/assets/capabilities-settings-panel-Dbx_nPAI.js +0 -7
  149. package/dist/gateway/static/root/assets/capability-presets-settings-panel-DHg_JMUz.js +0 -3
  150. package/dist/gateway/static/root/assets/channels-settings-DjHOlPq-.js +0 -1
  151. package/dist/gateway/static/root/assets/channels-status-swr-DNDvMWpl.js +0 -1
  152. package/dist/gateway/static/root/assets/connectors-page-RY-W6CQt.js +0 -2
  153. package/dist/gateway/static/root/assets/cron-expression-DHY0LrLq.js +0 -2
  154. package/dist/gateway/static/root/assets/dependency-picker-DDECr6Hj.js +0 -1
  155. package/dist/gateway/static/root/assets/desktop-pet-BoEIRgm2.js +0 -1
  156. package/dist/gateway/static/root/assets/home-page-DPhkqxEb.js +0 -3
  157. package/dist/gateway/static/root/assets/index-Bp8I_eX4.css +0 -1
  158. package/dist/gateway/static/root/assets/index-CKSplmFw.js +0 -152
  159. package/dist/gateway/static/root/assets/local-app-workbench-page-D7FmunJF.js +0 -2
  160. package/dist/gateway/static/root/assets/local-apps-page-C7H807zd.js +0 -1
  161. package/dist/gateway/static/root/assets/logs-page-vrjf-JTk.js +0 -2
  162. package/dist/gateway/static/root/assets/messages-D7kOD_o_.js +0 -3
  163. package/dist/gateway/static/root/assets/notes-workbench-Cp1ierS_.js +0 -4
  164. package/dist/gateway/static/root/assets/preview-open-alternatives-CpblfmWk.js +0 -1
  165. package/dist/gateway/static/root/assets/project-detail-page-Cp_jfZJj.js +0 -1
  166. package/dist/gateway/static/root/assets/projects-page-DYbmweGw.js +0 -1
  167. package/dist/gateway/static/root/assets/remote-access-hub-U2qXVyLH.js +0 -1
  168. package/dist/gateway/static/root/assets/sessions-page-CtNKNhCi.js +0 -1
  169. package/dist/gateway/static/root/assets/settings-advanced-gate-B6IMzhWo.js +0 -1
  170. package/dist/gateway/static/root/assets/setup-status-panel-mUu0iDlD.js +0 -1
  171. package/dist/gateway/static/root/assets/share-preview-page-1M79esSO.js +0 -2
  172. package/dist/gateway/static/root/assets/shares-settings-DadcMA8y.js +0 -1
  173. package/dist/gateway/static/root/assets/skill-reload-api-C3ZzYCz0.js +0 -1
  174. package/dist/gateway/static/root/assets/skills-page-B3kqZ-ke.js +0 -2
  175. package/dist/gateway/static/root/assets/task-detail-page-Bnjpkl4v.js +0 -1
  176. package/dist/gateway/static/root/assets/user-context-page-CBgAAZiB.js +0 -1
  177. package/dist/gateway/static/root/assets/work-discovery-overlay-BvlvyVJX.js +0 -1
  178. package/dist/gateway/static/root/assets/work-discovery-page-BBpnnnrs.js +0 -1
  179. package/dist/gateway/static/root/assets/workflows-page-Dd_9p9ji.js +0 -5
@@ -1,16 +1,19 @@
1
1
  import { createLogger } from "../../utils/logger/index.js";
2
2
  import { init_logger } from "../../utils/logger.js";
3
3
  import { resolveAgentTurnTimeoutMs, runAgentTurnWithTimeout } from "../orchestration/run-agent-turn-with-timeout.js";
4
+ import { resolvePromptCachePolicy } from "../../providers/prompt-cache-plan.js";
4
5
  import { wrapStreamFnForXopcExtensions } from "./xopc-stream-bridge.js";
5
6
  import { acquireEmbeddedSessionRunner } from "./session-runner.js";
6
7
  import { getAssistantTurnErrorMessage, isAssistantTurnAborted, isAssistantTurnFailed, maybeRetryTurnAfterTransientLlmFailure, stripTrailingErrorAssistantMessages } from "../orchestration/llm-turn-retry.js";
7
- import { pruneToolResultsToFit } from "../memory/context-budget.js";
8
+ import { projectContextForModel } from "../memory/context-budget.js";
8
9
  import { abortEmbeddedRun, queueEmbeddedSteer, registerEmbeddedRun, unregisterEmbeddedRun } from "./runs.js";
9
10
  import { lastAssistantPlainText, subscribeEmbeddedSessionEvents } from "./subscribe-session.js";
10
11
  import { detectToolLoops } from "../orchestration/loop-guard.js";
11
12
  import { tryApplySessionTranscriptHygiene } from "../transcript/transcript-hygiene.js";
12
13
  import { createSqliteTranscriptRuntime } from "./transcript-runtime.js";
13
14
  import { isContextOverflowError } from "../orchestration/context-overflow.js";
15
+ import { buildPromptCacheSnapshot, observePromptCacheSnapshot } from "../../providers/prompt-cache-observability.js";
16
+ import { isPromptCacheExpired, recordPromptCacheTouch } from "../../providers/prompt-cache-lifecycle.js";
14
17
  //#region src/agent/embedded/run-turn.ts
15
18
  init_logger();
16
19
  const log = createLogger("EmbeddedRun");
@@ -98,6 +101,7 @@ async function runXopcEmbeddedTurn(params) {
98
101
  const { sessionKey, runId, userMessage, model, tools, systemPrompt, thinkingLevel, workspaceDir, sessionStore, onEvent } = params;
99
102
  const timeoutMs = params.timeoutMs || resolveAgentTurnTimeoutMs();
100
103
  const resolvedModel = requireEmbeddedModel(model, params.modelRef);
104
+ const promptCachePolicy = resolvePromptCachePolicy(params.promptCachePolicy);
101
105
  const transcriptRuntime = params.transcriptRuntime ?? (sessionStore ? await createSqliteTranscriptRuntime({
102
106
  sessionKey,
103
107
  sessionStore
@@ -119,7 +123,7 @@ async function runXopcEmbeddedTurn(params) {
119
123
  });
120
124
  const { session, reused } = runner;
121
125
  runner.piSm.setActiveTurnId?.(runId);
122
- const streamFnWithXopcExtensions = wrapStreamFnForXopcExtensions(session.agent.streamFunction);
126
+ const streamFnWithXopcExtensions = wrapStreamFnForXopcExtensions(session.agent.streamFunction, params.promptCachePolicy);
123
127
  const loggingStreamFn = (streamModel, context, options) => {
124
128
  const sourceMessages = [...context.messages];
125
129
  const hygienicMessages = tryApplySessionTranscriptHygiene(sourceMessages, streamModel);
@@ -141,26 +145,35 @@ async function runXopcEmbeddedTurn(params) {
141
145
  tools: contextTools
142
146
  };
143
147
  }
144
- const pruned = pruneToolResultsToFit({
148
+ const projection = projectContextForModel({
145
149
  messages: effectiveContext.messages,
146
150
  contextWindow: streamModel.contextWindow ?? 128e3,
147
151
  systemPrompt: effectiveContext.systemPrompt,
148
152
  tools: effectiveContext.tools,
149
- canCompact: false
153
+ canCompact: false,
154
+ reason: isPromptCacheExpired(sessionKey, streamModel, promptCachePolicy) ? "cache_expired" : "normal"
150
155
  });
151
- if (pruned.prunedToolResults > 0) {
156
+ if (projection.prunedToolResults > 0 || projection.prunedImages > 0) {
152
157
  effectiveContext = {
153
158
  ...effectiveContext,
154
- messages: pruned.messages
159
+ messages: projection.messages
155
160
  };
156
161
  log.warn({
157
162
  sessionKey,
158
163
  runId,
159
- prunedToolResults: pruned.prunedToolResults,
160
- estimatedTokens: pruned.evaluation.estimatedTokens,
161
- hardLimitTokens: pruned.evaluation.hardLimitTokens
162
- }, "Pruned old tool results to fit the model context window");
164
+ prunedToolResults: projection.prunedToolResults,
165
+ prunedImages: projection.prunedImages,
166
+ estimatedTokens: projection.evaluation.estimatedTokens,
167
+ hardLimitTokens: projection.evaluation.hardLimitTokens
168
+ }, "Projected a smaller provider context");
163
169
  }
170
+ const promptCacheSnapshot = buildPromptCacheSnapshot({
171
+ model: streamModel,
172
+ systemPrompt: effectiveContext.systemPrompt,
173
+ tools: effectiveContext.tools,
174
+ reasoning: options?.reasoning
175
+ });
176
+ const promptCacheChanges = observePromptCacheSnapshot(sessionKey, promptCacheSnapshot);
164
177
  log.debug({
165
178
  sessionKey,
166
179
  runId,
@@ -173,20 +186,20 @@ async function runXopcEmbeddedTurn(params) {
173
186
  lastUserMessagePreview: getLastUserMessagePreview(effectiveContext.messages),
174
187
  loopWarningInjected: !!loopGuard.injection,
175
188
  hiddenToolCount: loopGuard.hiddenTools.size,
189
+ promptCache: promptCacheSnapshot,
190
+ promptCacheChanges,
176
191
  ...process.env.XOPC_LOG_LLM_PAYLOAD === "true" ? { effectiveContext: {
177
192
  systemPrompt: effectiveContext.systemPrompt,
178
193
  messages: effectiveContext.messages,
179
194
  tools: effectiveContext.tools
180
195
  } } : {}
181
196
  }, "Sending messages to AI");
182
- return streamFnWithXopcExtensions(streamModel, effectiveContext, {
183
- ...options,
184
- ...params.cacheRetention ? { cacheRetention: params.cacheRetention } : {}
185
- });
197
+ return streamFnWithXopcExtensions(streamModel, effectiveContext, { ...options });
186
198
  };
187
199
  session.agent.streamFunction = loggingStreamFn;
188
- if (onEvent) unsubscribe = subscribeEmbeddedSessionEvents(session, (event) => {
189
- onEvent({
200
+ unsubscribe = subscribeEmbeddedSessionEvents(session, (event) => {
201
+ if (event.type === "message_end" && event.message.role === "assistant") recordPromptCacheTouch(sessionKey, resolvedModel, event.message.usage);
202
+ onEvent?.({
190
203
  ...event,
191
204
  runId
192
205
  });
@@ -1,5 +1,5 @@
1
1
  import type { AgentMessage, AgentToolResult, ThinkingLevel } from '@earendil-works/pi-agent-core';
2
- import type { AssistantMessageEvent, Model, Api, CacheRetention, ImageContent } from '@earendil-works/pi-ai';
2
+ import type { AssistantMessageEvent, Model, Api, ImageContent } from '@earendil-works/pi-ai';
3
3
  import type { SessionStore } from '../../session/store.js';
4
4
  import type { EmbeddedTranscriptRuntime } from './transcript-runtime.js';
5
5
  export type EmbeddedStreamEvent = {
@@ -92,7 +92,7 @@ export type RunXopcEmbeddedTurnParams = {
92
92
  tools: import('@earendil-works/pi-agent-core').AgentTool[];
93
93
  systemPrompt: string;
94
94
  thinkingLevel?: ThinkingLevel;
95
- cacheRetention?: CacheRetention;
95
+ promptCachePolicy?: import('../../providers/prompt-cache-plan.js').PromptCachePolicy;
96
96
  workspaceDir: string;
97
97
  sessionStore?: SessionStore;
98
98
  transcriptRuntime?: EmbeddedTranscriptRuntime;
@@ -1,4 +1,5 @@
1
1
  import type { StreamFn } from '@earendil-works/pi-agent-core';
2
+ import type { PromptCachePolicy } from '../../providers/prompt-cache-plan.js';
2
3
  /**
3
4
  * pi-coding-agent's default {@link createAgentSession} streamFn always routes through
4
5
  * pi-ai's HTTP `streamSimple`, so xopc plugin providers (whose models carry the sentinel
@@ -8,4 +9,4 @@ import type { StreamFn } from '@earendil-works/pi-agent-core';
8
9
  *
9
10
  * Use this by reassigning {@link Agent.streamFunction} after `createAgentSession` returns.
10
11
  */
11
- export declare function wrapStreamFnForXopcExtensions(originalStreamFn: StreamFn): StreamFn;
12
+ export declare function wrapStreamFnForXopcExtensions(originalStreamFn: StreamFn, promptCachePolicy?: PromptCachePolicy): StreamFn;
@@ -13,11 +13,11 @@ init_providers();
13
13
  *
14
14
  * Use this by reassigning {@link Agent.streamFunction} after `createAgentSession` returns.
15
15
  */
16
- function wrapStreamFnForXopcExtensions(originalStreamFn) {
16
+ function wrapStreamFnForXopcExtensions(originalStreamFn, promptCachePolicy) {
17
17
  const extensionStreamFn = createExtensionAwareStreamFn();
18
18
  return ((model, context, options) => {
19
19
  if (model.baseUrl === "extension://provider-plugin") return extensionStreamFn(model, preparePromptCacheContext(model, context), options);
20
- return originalStreamFn(model, preparePromptCacheContext(model, context), withPromptCachePayloadTransform(model, context, options));
20
+ return originalStreamFn(model, preparePromptCacheContext(model, context), withPromptCachePayloadTransform(model, context, options, promptCachePolicy));
21
21
  });
22
22
  }
23
23
  //#endregion
@@ -136,13 +136,29 @@ function planCompactionChunks(messages, maxChunkTokens) {
136
136
  flush();
137
137
  return chunks;
138
138
  }
139
- const IDENTIFIER_PATTERN = /(?:\/?[\w.-]+\/)+(?:[\w.@-]+)|https?:\/\/[^\s)>]+|\b[0-9a-f]{8,64}\b|\b\d{4}-\d{2}-\d{2}\b|\bport\s+\d{2,5}\b/gi;
139
+ const IDENTIFIER_PATTERNS = [
140
+ /https?:\/\/[^\s)>]+/gi,
141
+ /(?<![\w:/])\/(?:[\w.@-]+\/)*[\w.@-]+/g,
142
+ /(?<![\w./-])(?:[\w.@-]+\/)+[\w.@-]+\.[a-z0-9]{1,10}\b/gi,
143
+ /\b(?:release|feature|fix|hotfix|bugfix|chore)\/[\w.@-]+\b/gi,
144
+ /\b[0-9a-f]{8,64}\b/gi,
145
+ /\b\d{4}-\d{2}-\d{2}\b/g,
146
+ /\bport\s+\d{2,5}\b/gi
147
+ ];
140
148
  function extractExactIdentifiers(messages, limit = 12) {
141
149
  const text = messages.map(serializeMessageForCompaction).join("\n");
142
- const found = /* @__PURE__ */ new Set();
143
- for (const match of text.matchAll(IDENTIFIER_PATTERN)) {
150
+ const matches = [];
151
+ for (const pattern of IDENTIFIER_PATTERNS) for (const match of text.matchAll(pattern)) {
144
152
  const identifier = match[0].trim();
145
- if (identifier.length < 4) continue;
153
+ if (identifier.length < 4 || match.index == null) continue;
154
+ matches.push({
155
+ identifier,
156
+ index: match.index
157
+ });
158
+ }
159
+ matches.sort((left, right) => left.index - right.index);
160
+ const found = /* @__PURE__ */ new Set();
161
+ for (const { identifier } of matches) {
146
162
  found.add(identifier);
147
163
  if (found.size >= limit) break;
148
164
  }
@@ -102,6 +102,18 @@ function summaryAudit(summary, identifiers) {
102
102
  if (missingIdentifiers.length > 0) issues.push(`missing exact identifiers: ${missingIdentifiers.join(", ")}`);
103
103
  return issues;
104
104
  }
105
+ function enforceSummaryContract(summary, identifiers) {
106
+ let normalized = summary.trim();
107
+ for (const heading of REQUIRED_SUMMARY_HEADINGS) if (!new RegExp(`^#{1,3}\\s+${heading.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*$`, "im").test(normalized)) normalized = `${normalized}\n\n## ${heading}\nNone`.trim();
108
+ const missingIdentifiers = identifiers.filter((identifier) => !normalized.includes(identifier));
109
+ if (missingIdentifiers.length === 0) return normalized;
110
+ const exactHeading = /^#{1,3}\s+Exact identifiers\s*$/im.exec(normalized);
111
+ if (!exactHeading) return normalized;
112
+ const insertAt = exactHeading.index + exactHeading[0].length;
113
+ const identifierList = missingIdentifiers.map((identifier) => `- \`${identifier}\``).join("\n");
114
+ const suffix = normalized.slice(insertAt).replace(/^\r?\n[ \t]*None[ \t]*(?=\r?\n#{1,3}\s|$)/i, "");
115
+ return `${normalized.slice(0, insertAt)}\n${identifierList}${suffix}`;
116
+ }
105
117
  function createLinkedAbortSignal(parent, timeoutMs) {
106
118
  const controller = new AbortController();
107
119
  let timeoutTriggered = false;
@@ -216,7 +228,7 @@ Do not omit facts already present and do not invent new facts.
216
228
  ${summary}
217
229
  </summary_to_repair>`;
218
230
  const repaired = await this.callSummaryModels(models, repairPrompt, signal);
219
- summary = repaired.summary;
231
+ summary = enforceSummaryContract(repaired.summary, identifiers);
220
232
  summaryModelRef = repaired.modelRef;
221
233
  const remaining = summaryAudit(summary, identifiers);
222
234
  if (remaining.length > 0) throw new Error(`Compaction summary failed quality audit: ${remaining.join("; ")}`);
@@ -233,7 +245,7 @@ ${summary}
233
245
  if (parentSignal?.aborted) throw parentSignal.reason;
234
246
  const linked = createLinkedAbortSignal(parentSignal, this.config.summaryTimeoutMs);
235
247
  try {
236
- const summary = extractSummaryText(await completeWithResolvedCredentials(model, {
248
+ const result = await completeWithResolvedCredentials(model, {
237
249
  systemPrompt: COMPACTION_SYSTEM_PROMPT,
238
250
  messages: [{
239
251
  role: "user",
@@ -243,10 +255,16 @@ ${summary}
243
255
  }, {
244
256
  maxTokens: this.config.summaryMaxTokens,
245
257
  temperature: .2,
258
+ reasoning: "low",
246
259
  signal: linked.signal,
247
260
  sessionId: COMPACTION_CACHE_SESSION_ID
248
- }));
249
- if (!summary) throw new Error("Compaction model returned an empty summary");
261
+ });
262
+ const summary = extractSummaryText(result);
263
+ if (!summary) {
264
+ const response = result;
265
+ const contentTypes = Array.isArray(response.content) ? response.content.map((block) => String(block?.type ?? "unknown")) : [];
266
+ throw new Error(`Compaction model returned an empty summary (stopReason=${String(response.stopReason ?? "unknown")}, rawStopReason=${String(response.rawStopReason ?? "unknown")}, contentTypes=${contentTypes.join(",") || "none"}, outputTokens=${String(response.usage?.output ?? "unknown")}, reasoningTokens=${String(response.usage?.reasoning ?? "unknown")})`);
267
+ }
250
268
  return {
251
269
  summary,
252
270
  modelRef: `${model.provider}/${model.id}`
@@ -16,6 +16,7 @@ export interface ContextBudgetInput {
16
16
  minToolResultKeepChars?: number;
17
17
  canCompact?: boolean;
18
18
  }
19
+ export type ContextProjectionReason = 'normal' | 'cache_expired' | 'hard_limit';
19
20
  export interface ContextBudgetEvaluation {
20
21
  route: ContextBudgetRoute;
21
22
  estimatedTokens: number;
@@ -33,8 +34,12 @@ export declare function estimateTextTokens(text: string): number;
33
34
  export declare function estimateMessageTokens(message: AgentMessage): number;
34
35
  export declare function estimateMessagesTokens(messages: readonly AgentMessage[]): number;
35
36
  export declare function evaluateContextBudget(input: ContextBudgetInput): ContextBudgetEvaluation;
36
- export declare function pruneToolResultsToFit(input: ContextBudgetInput): {
37
+ export declare function projectContextForModel(input: ContextBudgetInput & {
38
+ reason: ContextProjectionReason;
39
+ recentTurns?: number;
40
+ }): {
37
41
  messages: AgentMessage[];
38
42
  evaluation: ContextBudgetEvaluation;
39
43
  prunedToolResults: number;
44
+ prunedImages: number;
40
45
  };
@@ -73,7 +73,7 @@ function evaluateContextBudget(input) {
73
73
  usagePercent: estimatedTokens / contextWindow
74
74
  };
75
75
  }
76
- function pruneToolResultsToFit(input) {
76
+ function truncateToolResultsToFit(input) {
77
77
  const initial = evaluateContextBudget(input);
78
78
  if (initial.estimatedTokens <= initial.hardLimitTokens || initial.reducibleToolResultTokens === 0) return {
79
79
  messages: input.messages,
@@ -101,5 +101,97 @@ function pruneToolResultsToFit(input) {
101
101
  prunedToolResults
102
102
  };
103
103
  }
104
+ function protectedRecentTurnStart(messages, recentTurns) {
105
+ let remaining = recentTurns;
106
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
107
+ if (messages[index].role !== "assistant") continue;
108
+ remaining -= 1;
109
+ if (remaining === 0) return index;
110
+ }
111
+ return 0;
112
+ }
113
+ function replaceOldImages(message) {
114
+ const content = message.content;
115
+ if (!Array.isArray(content)) return {
116
+ message,
117
+ removed: 0
118
+ };
119
+ let removed = 0;
120
+ const nextContent = content.map((block) => {
121
+ if (!block || typeof block !== "object" || block.type !== "image") return block;
122
+ removed += 1;
123
+ return {
124
+ type: "text",
125
+ text: "[Earlier image omitted from the model context.]"
126
+ };
127
+ });
128
+ return removed > 0 ? {
129
+ message: {
130
+ ...message,
131
+ content: nextContent
132
+ },
133
+ removed
134
+ } : {
135
+ message,
136
+ removed: 0
137
+ };
138
+ }
139
+ function projectContextForModel(input) {
140
+ const initial = evaluateContextBudget(input);
141
+ const mustFit = input.reason === "hard_limit" || initial.estimatedTokens > initial.hardLimitTokens;
142
+ if (input.reason !== "cache_expired" && !mustFit) return {
143
+ messages: input.messages,
144
+ evaluation: initial,
145
+ prunedToolResults: 0,
146
+ prunedImages: 0
147
+ };
148
+ if (input.reason === "cache_expired" && initial.usagePercent < .3 && !mustFit) return {
149
+ messages: input.messages,
150
+ evaluation: initial,
151
+ prunedToolResults: 0,
152
+ prunedImages: 0
153
+ };
154
+ const protectedStart = protectedRecentTurnStart(input.messages, input.recentTurns ?? 3);
155
+ let prunedToolResults = 0;
156
+ let prunedImages = 0;
157
+ let messages = input.messages.map((message, index) => {
158
+ if (index >= protectedStart) return message;
159
+ const imageProjection = replaceOldImages(message);
160
+ prunedImages += imageProjection.removed;
161
+ if (getToolResultTextLength(imageProjection.message) <= 3e3) return imageProjection.message;
162
+ prunedToolResults += 1;
163
+ return truncateToolResultMessage(imageProjection.message, 3e3, { minKeepChars: 1500 });
164
+ });
165
+ let evaluation = evaluateContextBudget({
166
+ ...input,
167
+ messages
168
+ });
169
+ if (input.reason === "cache_expired" && evaluation.usagePercent > .5) {
170
+ messages = messages.map((message, index) => {
171
+ if (index >= protectedStart || getToolResultTextLength(message) <= 400) return message;
172
+ prunedToolResults += 1;
173
+ return truncateToolResultMessage(message, 400, { minKeepChars: 200 });
174
+ });
175
+ evaluation = evaluateContextBudget({
176
+ ...input,
177
+ messages
178
+ });
179
+ }
180
+ if (evaluation.estimatedTokens > evaluation.hardLimitTokens) {
181
+ const fitted = truncateToolResultsToFit({
182
+ ...input,
183
+ messages
184
+ });
185
+ messages = fitted.messages;
186
+ prunedToolResults += fitted.prunedToolResults;
187
+ evaluation = fitted.evaluation;
188
+ }
189
+ return {
190
+ messages,
191
+ evaluation,
192
+ prunedToolResults,
193
+ prunedImages
194
+ };
195
+ }
104
196
  //#endregion
105
- export { estimateMessageTokens, estimateMessagesTokens, estimateTextTokens, evaluateContextBudget, pruneToolResultsToFit };
197
+ export { estimateMessageTokens, estimateMessagesTokens, estimateTextTokens, evaluateContextBudget, projectContextForModel };
@@ -1,8 +1,8 @@
1
- import { buildMemoryRuntime } from "../../agent-runtime/memory-runtime.js";
2
1
  import { isMemorySubsystemEnabled } from "./memory-config.js";
3
2
  import { BuiltinMemoryProvider } from "./builtin-provider.js";
4
3
  import { MemoryManager } from "./manager.js";
5
4
  import { loadMemoryPluginProviders } from "./plugin-discovery.js";
5
+ import { buildMemoryRuntime } from "../../agent-runtime/memory-runtime.js";
6
6
  //#region src/agent/memory/create-memory-manager.ts
7
7
  function createMemoryManagerFromConfig(config) {
8
8
  const routing = resolveMemoryManagerOptions(config);
@@ -22,3 +22,5 @@ export declare function splitPromptCacheBoundary(text: string): {
22
22
  * Trims trailing whitespace per line, normalizes line endings, trims overall.
23
23
  */
24
24
  export declare function normalizePromptSection(text: string): string;
25
+ /** Add stable content immediately before the cache boundary. */
26
+ export declare function appendStablePromptSection(systemPrompt: string, section: string): string;
@@ -31,5 +31,14 @@ function splitPromptCacheBoundary(text) {
31
31
  function normalizePromptSection(text) {
32
32
  return text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "").trim();
33
33
  }
34
+ /** Add stable content immediately before the cache boundary. */
35
+ function appendStablePromptSection(systemPrompt, section) {
36
+ const normalizedSection = normalizePromptSection(section);
37
+ if (!normalizedSection) return systemPrompt;
38
+ const split = splitPromptCacheBoundary(systemPrompt);
39
+ if (!split) return [normalizePromptSection(systemPrompt), normalizedSection].filter(Boolean).join("\n\n");
40
+ const stablePrefix = [split.stablePrefix, normalizedSection].filter(Boolean).join("\n\n");
41
+ return split.dynamicSuffix ? `${stablePrefix}${PROMPT_CACHE_BOUNDARY}${split.dynamicSuffix}` : `${stablePrefix}${PROMPT_CACHE_BOUNDARY}`;
42
+ }
34
43
  //#endregion
35
- export { PROMPT_CACHE_BOUNDARY, normalizePromptSection, splitPromptCacheBoundary, stripPromptCacheBoundary };
44
+ export { PROMPT_CACHE_BOUNDARY, appendStablePromptSection, normalizePromptSection, splitPromptCacheBoundary, stripPromptCacheBoundary };
@@ -1,16 +1,13 @@
1
1
  import type { EmbeddedContextFile } from '../../bootstrap/types.js';
2
2
  export declare const CONTEXT_FILE_ORDER: Map<string, number>;
3
- export declare const DYNAMIC_CONTEXT_FILE_BASENAMES: Set<string>;
4
3
  export declare const DEFAULT_HEARTBEAT_PROMPT_CONTEXT_BLOCK = "Default heartbeat prompt:\n`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`";
5
4
  export declare function normalizeContextFilePath(pathValue: string): string;
6
5
  export declare function getContextFileBasename(pathValue: string): string;
7
- export declare function isDynamicContextFile(pathValue: string): boolean;
8
6
  export declare function sanitizeContextFileContentForPrompt(content: string): string;
9
7
  export declare function sortContextFilesForPrompt(contextFiles: EmbeddedContextFile[]): EmbeddedContextFile[];
10
8
  export declare function buildProjectContextSection(params: {
11
9
  files: EmbeddedContextFile[];
12
10
  heading: string;
13
- dynamic: boolean;
14
11
  }): string[];
15
- /** Whether HEARTBEAT.md is injected as dynamic context (vs behavior-only section). */
12
+ /** Heartbeat content belongs to the heartbeat user turn, never the system prompt. */
16
13
  export declare function isHeartbeatContextFile(pathValue: string): boolean;
@@ -9,7 +9,6 @@ const CONTEXT_FILE_ORDER = new Map([
9
9
  ["bootstrap.md", 60],
10
10
  ["memory.md", 70]
11
11
  ]);
12
- const DYNAMIC_CONTEXT_FILE_BASENAMES = new Set(["heartbeat.md"]);
13
12
  const DEFAULT_HEARTBEAT_PROMPT_CONTEXT_BLOCK = "Default heartbeat prompt:\n`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`";
14
13
  function normalizeContextFilePath(pathValue) {
15
14
  return pathValue.trim().replace(/\\/g, "/");
@@ -18,9 +17,6 @@ function getContextFileBasename(pathValue) {
18
17
  const normalizedPath = normalizeContextFilePath(pathValue);
19
18
  return (normalizedPath.split("/").pop() ?? normalizedPath).toLowerCase();
20
19
  }
21
- function isDynamicContextFile(pathValue) {
22
- return DYNAMIC_CONTEXT_FILE_BASENAMES.has(getContextFileBasename(pathValue));
23
- }
24
20
  function sanitizeContextFileContentForPrompt(content) {
25
21
  return content.replaceAll(DEFAULT_HEARTBEAT_PROMPT_CONTEXT_BLOCK, "").replace(/\n{3,}/g, "\n\n");
26
22
  }
@@ -38,19 +34,16 @@ function sortContextFilesForPrompt(contextFiles) {
38
34
  function buildProjectContextSection(params) {
39
35
  if (params.files.length === 0) return [];
40
36
  const lines = [params.heading, ""];
41
- if (params.dynamic) lines.push("The following frequently-changing project context files are kept below the cache boundary when possible:", "");
42
- else {
43
- const hasSoulFile = params.files.some((file) => getContextFileBasename(file.path) === "soul.md");
44
- lines.push("The following project context files have been loaded:");
45
- if (hasSoulFile) lines.push("If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.");
46
- lines.push("");
47
- }
37
+ const hasSoulFile = params.files.some((file) => getContextFileBasename(file.path) === "soul.md");
38
+ lines.push("The following project context files have been loaded:");
39
+ if (hasSoulFile) lines.push("If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.");
40
+ lines.push("");
48
41
  for (const file of params.files) lines.push(`## ${file.path}`, "", sanitizeContextFileContentForPrompt(file.content), "");
49
42
  return lines;
50
43
  }
51
- /** Whether HEARTBEAT.md is injected as dynamic context (vs behavior-only section). */
44
+ /** Heartbeat content belongs to the heartbeat user turn, never the system prompt. */
52
45
  function isHeartbeatContextFile(pathValue) {
53
46
  return getContextFileBasename(pathValue) === DEFAULT_HEARTBEAT_FILENAME.toLowerCase();
54
47
  }
55
48
  //#endregion
56
- export { CONTEXT_FILE_ORDER, DEFAULT_HEARTBEAT_PROMPT_CONTEXT_BLOCK, DYNAMIC_CONTEXT_FILE_BASENAMES, buildProjectContextSection, getContextFileBasename, isDynamicContextFile, isHeartbeatContextFile, normalizeContextFilePath, sanitizeContextFileContentForPrompt, sortContextFilesForPrompt };
49
+ export { CONTEXT_FILE_ORDER, DEFAULT_HEARTBEAT_PROMPT_CONTEXT_BLOCK, buildProjectContextSection, getContextFileBasename, isHeartbeatContextFile, normalizeContextFilePath, sanitizeContextFileContentForPrompt, sortContextFilesForPrompt };
@@ -7,6 +7,7 @@ import { getInteractionState } from "../../storage/sqlite/interaction-state-repo
7
7
  import { DEFAULT_USER_TRUST_LEVEL } from "../../user-context/trust-policy.js";
8
8
  import { getUserTrustPolicy } from "../../storage/sqlite/user-trust-repository.js";
9
9
  import "../../storage/sqlite/index.js";
10
+ import { appendStablePromptSection } from "./cache-boundary.js";
10
11
  import { resolveSystemPromptBuildParams } from "./system-prompt-params.js";
11
12
  import { buildSystemPrompt } from "./system-prompt.js";
12
13
  import { resolveResponseLanguageForSession } from "./response-language.js";
@@ -85,7 +86,7 @@ var SystemPromptBuilder = class {
85
86
  customInstructions: options.customInstructions
86
87
  });
87
88
  const skillPrompt = options.skillPromptText !== void 0 ? options.skillPromptText : this.skillManager.getPromptForSkillAllowlist(options.skillAllowlist, options.registeredToolNames);
88
- const fullPrompt = skillPrompt ? `${basePrompt}\n\n${skillPrompt}` : basePrompt;
89
+ const fullPrompt = skillPrompt ? appendStablePromptSection(basePrompt, skillPrompt) : basePrompt;
89
90
  log.debug({
90
91
  baseLength: basePrompt.length,
91
92
  skillLength: skillPrompt?.length || 0,
@@ -12,7 +12,7 @@ import type { ProviderSystemPromptContribution } from './contribution.js';
12
12
  import { type RuntimeInfoInput } from './sections/workspace-runtime.js';
13
13
  import type { MemoryCitationsMode, PromptMode, SilentReplyPromptMode } from './types.js';
14
14
  export type { MemoryCitationsMode } from './types.js';
15
- export { getContextFileBasename, isDynamicContextFile, sortContextFilesForPrompt, } from './sections/project-context.js';
15
+ export { getContextFileBasename, sortContextFilesForPrompt, } from './sections/project-context.js';
16
16
  export interface SystemPromptOptions {
17
17
  contextFiles?: EmbeddedContextFile[];
18
18
  promptMode?: PromptMode;
@@ -21,8 +21,6 @@ export interface SystemPromptOptions {
21
21
  /** Registered tool names for Tooling section and memory/skills gating. */
22
22
  toolNames?: string[];
23
23
  toolSummaries?: Record<string, string>;
24
- /** @deprecated Prefer toolNames — kept for skill-section gating compatibility. */
25
- availableTools?: string[];
26
24
  memoryCitationsMode?: MemoryCitationsMode;
27
25
  includeMemorySection?: boolean;
28
26
  userTimezone?: string;
@@ -3,7 +3,7 @@ import { PROMPT_CACHE_BOUNDARY, splitPromptCacheBoundary } from "./cache-boundar
3
3
  import { buildAestheticSection, buildExecutionBiasSection, buildHumanCollaborationSection, buildProblemSolvingSection, buildSafetySection, buildToolCallStyleSection, buildWorkContinuitySection } from "./sections/behavior.js";
4
4
  import { buildExternalMemorySection, buildMemorySection, buildSkillsSection } from "./sections/memory-skills.js";
5
5
  import { buildHeartbeatBehaviorSection, buildMessagingSection, buildOutputDirectivesSection, buildSilentRepliesSection, buildTimeSection } from "./sections/messaging-runtime.js";
6
- import { buildProjectContextSection, getContextFileBasename, isDynamicContextFile, sortContextFilesForPrompt } from "./sections/project-context.js";
6
+ import { buildProjectContextSection, getContextFileBasename, isHeartbeatContextFile, sortContextFilesForPrompt } from "./sections/project-context.js";
7
7
  import { buildToolingSection, hasSkillsTools } from "./sections/tooling.js";
8
8
  import { buildRuntimeSection, buildWorkspaceFilesIntroSection, buildWorkspaceSection } from "./sections/workspace-runtime.js";
9
9
  import { buildResponseLanguageSection } from "./sections/response-language.js";
@@ -16,7 +16,7 @@ function joinSections(sections) {
16
16
  * Build system prompt with bootstrap Project Context integration.
17
17
  */
18
18
  function buildSystemPrompt(workspaceDir, options = {}) {
19
- const { contextFiles = [], promptMode = "full", heartbeatEnabled = false, heartbeatPrompt, toolNames: toolNamesOption, toolSummaries, availableTools = [], memoryCitationsMode = "on", includeMemorySection, userTimezone, runtime, agentId, channels = [], externalMemoryInstructions, ttsSystemHint, extraSystemPrompt, activeProjectContext, silentReplyPromptMode = "generic", promptContribution, includeProblemSolving = true, includeToneSection = true, actionTrustLevel, responseLanguage = "auto", customInstructions } = options;
19
+ const { contextFiles = [], promptMode = "full", heartbeatEnabled = false, heartbeatPrompt, toolNames = [], toolSummaries, memoryCitationsMode = "on", includeMemorySection, userTimezone, runtime, agentId, channels = [], externalMemoryInstructions, ttsSystemHint, extraSystemPrompt, activeProjectContext, silentReplyPromptMode = "generic", promptContribution, includeProblemSolving = true, includeToneSection = true, actionTrustLevel, responseLanguage = "auto", customInstructions } = options;
20
20
  if (promptMode === "none") return joinSections([
21
21
  "You are a personal AI assistant running inside xopc.",
22
22
  customInstructions?.trim() ? `<custom_instructions>\n${customInstructions.trim()}\n</custom_instructions>` : void 0,
@@ -24,13 +24,10 @@ function buildSystemPrompt(workspaceDir, options = {}) {
24
24
  activeProjectContext
25
25
  ]);
26
26
  const isMinimal = promptMode === "minimal";
27
- const toolNames = toolNamesOption ?? availableTools;
28
27
  const normalizedTools = new Set(toolNames.map((tool) => tool.toLowerCase()));
29
28
  const effectiveAgentId = agentId ?? runtime?.agentId;
30
29
  const sectionOverrides = promptContribution?.sectionOverrides ?? {};
31
- const orderedContextFiles = sortContextFilesForPrompt(contextFiles.filter((file) => file.path.trim().length > 0));
32
- const stableContextFiles = orderedContextFiles.filter((file) => !isDynamicContextFile(file.path));
33
- const dynamicContextFiles = orderedContextFiles.filter((file) => isDynamicContextFile(file.path));
30
+ const orderedContextFiles = sortContextFilesForPrompt(contextFiles.filter((file) => file.path.trim().length > 0 && !isHeartbeatContextFile(file.path)));
34
31
  const hasProfileMemory = orderedContextFiles.some((file) => getContextFileBasename(file.path) === "memory.md");
35
32
  const stableSections = [
36
33
  "You are a personal AI assistant running inside xopc.",
@@ -79,20 +76,14 @@ function buildSystemPrompt(workspaceDir, options = {}) {
79
76
  }));
80
77
  if (!isMinimal && ttsSystemHint?.trim()) stableSections.push(`## Voice (TTS)\n\n${ttsSystemHint.trim()}`);
81
78
  stableSections.push(joinSections(buildProjectContextSection({
82
- files: stableContextFiles,
83
- heading: "# Project Context",
84
- dynamic: false
79
+ files: orderedContextFiles,
80
+ heading: "# Project Context"
85
81
  })));
86
82
  if (!isMinimal) stableSections.push(buildSilentRepliesSection({
87
83
  isMinimal,
88
84
  silentReplyPromptMode
89
85
  }));
90
86
  const dynamicSections = [];
91
- dynamicSections.push(joinSections(buildProjectContextSection({
92
- files: dynamicContextFiles,
93
- heading: stableContextFiles.length > 0 ? "# Dynamic Project Context" : "# Project Context",
94
- dynamic: true
95
- })));
96
87
  if (extraSystemPrompt?.trim()) {
97
88
  const contextHeader = isMinimal ? "## Subagent Context" : "## Group Chat Context";
98
89
  dynamicSections.push(`${contextHeader}\n\n${extraSystemPrompt.trim()}`);
@@ -129,4 +120,4 @@ function splitBuiltSystemPrompt(text) {
129
120
  return splitPromptCacheBoundary(text);
130
121
  }
131
122
  //#endregion
132
- export { buildSystemPrompt, getContextFileBasename, isDynamicContextFile, sortContextFilesForPrompt, splitBuiltSystemPrompt };
123
+ export { buildSystemPrompt, getContextFileBasename, sortContextFilesForPrompt, splitBuiltSystemPrompt };
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from '@earendil-works/pi-agent-core';
2
+ /** Provider-visible tool order is part of the cached prompt prefix. */
3
+ export declare function sortToolsForPromptCache<T extends Pick<AgentTool, 'name' | 'description'>>(tools: readonly T[]): T[];
@@ -0,0 +1,19 @@
1
+ //#region src/agent/tools/cache-stability.ts
2
+ const TOOL_ORDER = new Map([
3
+ ["xopc_tool_search", 10],
4
+ ["xopc_tool_describe", 20],
5
+ ["xopc_tool_execute", 30]
6
+ ]);
7
+ /** Provider-visible tool order is part of the cached prompt prefix. */
8
+ function sortToolsForPromptCache(tools) {
9
+ return [...tools].sort((left, right) => {
10
+ const leftOrder = TOOL_ORDER.get(left.name) ?? Number.MAX_SAFE_INTEGER;
11
+ const rightOrder = TOOL_ORDER.get(right.name) ?? Number.MAX_SAFE_INTEGER;
12
+ if (leftOrder !== rightOrder) return leftOrder - rightOrder;
13
+ const byName = left.name.localeCompare(right.name);
14
+ if (byName !== 0) return byName;
15
+ return left.description.localeCompare(right.description);
16
+ });
17
+ }
18
+ //#endregion
19
+ export { sortToolsForPromptCache };
@@ -55,6 +55,7 @@ import { mergeTtsConfigFromAppConfig } from "../../voice/tts/merge-config.js";
55
55
  import { createTextToSpeechTool } from "./tts-tool.js";
56
56
  import { getAgentCapabilityToolNames } from "../capabilities/registry.js";
57
57
  import "../capabilities/index.js";
58
+ import { sortToolsForPromptCache } from "./cache-stability.js";
58
59
  //#region src/agent/tools/factory.ts
59
60
  init_agent_scope();
60
61
  init_logger();
@@ -352,7 +353,7 @@ var AgentToolsFactory = class {
352
353
  return ["create_desktop_pet"];
353
354
  }
354
355
  createAllTools(coreOptions) {
355
- return wrapToolsWithProtection(this.createCoreTools(coreOptions), this.deps.toolExecutorConfig);
356
+ return sortToolsForPromptCache(wrapToolsWithProtection(this.createCoreTools(coreOptions), this.deps.toolExecutorConfig));
356
357
  }
357
358
  };
358
359
  function filterToolsByDisabledSet(tools, disabled) {