talon-agent 4.6.1 → 5.0.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.
Files changed (223) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -1
  3. package/prompts/README.md +2 -2
  4. package/prompts/system/memory-recall.md +16 -0
  5. package/src/app.ts +19 -4
  6. package/src/backend/claude-sdk/handler.ts +28 -7
  7. package/src/backend/claude-sdk/one-shot.ts +1 -1
  8. package/src/backend/claude-sdk/options.ts +7 -6
  9. package/src/backend/claude-sdk/stream.ts +30 -3
  10. package/src/backend/claude-sdk/warm.ts +1 -1
  11. package/src/backend/codex/constants.ts +1 -1
  12. package/src/backend/codex/factory.ts +2 -2
  13. package/src/backend/codex/handler/events.ts +1 -1
  14. package/src/backend/codex/handler/message.ts +26 -12
  15. package/src/backend/codex/handler/rollout-accounting.ts +1 -1
  16. package/src/backend/codex/init.ts +1 -1
  17. package/src/backend/codex/mcp-config.ts +1 -1
  18. package/src/backend/codex/one-shot.ts +1 -1
  19. package/src/backend/kilo/handler/message.ts +4 -1
  20. package/src/backend/openai-agents/constants.ts +1 -1
  21. package/src/backend/openai-agents/factory.ts +2 -2
  22. package/src/backend/openai-agents/handler/events.ts +1 -1
  23. package/src/backend/openai-agents/handler/message.ts +8 -4
  24. package/src/backend/openai-agents/init.ts +1 -1
  25. package/src/backend/openai-agents/mcp-pool.ts +1 -1
  26. package/src/backend/opencode/handler/message.ts +4 -1
  27. package/src/backend/remote-server/chat-turn.ts +31 -23
  28. package/src/backend/remote-server/events.ts +3 -3
  29. package/src/backend/remote-server/factory.ts +6 -3
  30. package/src/backend/remote-server/index.ts +1 -1
  31. package/src/backend/remote-server/mcp.ts +1 -1
  32. package/src/backend/remote-server/one-shot.ts +1 -1
  33. package/src/backend/remote-server/server-bindings.ts +1 -1
  34. package/src/backend/remote-server/turn.ts +1 -1
  35. package/src/backend/runtime/cache/cache-metrics.ts +126 -0
  36. package/src/backend/{shared → runtime/cache}/cache-telemetry.ts +22 -2
  37. package/src/backend/{shared → runtime}/index.ts +32 -23
  38. package/src/backend/{shared → runtime/prompt}/delivery-contract.ts +1 -1
  39. package/src/backend/{shared → runtime/prompt}/prompt-format.ts +46 -2
  40. package/src/backend/{shared → runtime/prompt}/system-prompt.ts +4 -4
  41. package/src/backend/{shared → runtime/turn}/delivered-text.ts +1 -1
  42. package/src/backend/{shared → runtime/turn}/delivery.ts +2 -2
  43. package/src/backend/{shared → runtime/turn}/handle-retry.ts +5 -5
  44. package/src/backend/{shared → runtime/turn}/handler-to-events.ts +24 -10
  45. package/src/backend/{shared → runtime/turn}/handler-types.ts +7 -1
  46. package/src/backend/{shared → runtime/turn}/model-retry.ts +2 -2
  47. package/src/backend/{shared → runtime/turn}/result-events.ts +2 -2
  48. package/src/backend/{shared → runtime/turn}/stream-state.ts +2 -2
  49. package/src/backend/{shared → runtime/turn}/turn-interrupt.ts +2 -2
  50. package/src/backend/{shared → runtime/turn}/turn-phases.ts +6 -6
  51. package/src/bootstrap.ts +4 -19
  52. package/src/cli.ts +1 -1
  53. package/src/core/agent-runtime/capabilities.ts +11 -0
  54. package/src/core/agent-runtime/contract-tests.ts +92 -1
  55. package/src/core/agent-runtime/events.ts +1 -1
  56. package/src/core/background/{isolated-agent.ts → cron/isolated-agent.ts} +3 -3
  57. package/src/core/background/{job-health.ts → cron/job-health.ts} +1 -1
  58. package/src/core/background/{job-oneshot.ts → cron/job-oneshot.ts} +5 -5
  59. package/src/core/background/{job-prompt.ts → cron/job-prompt.ts} +1 -1
  60. package/src/core/background/{cron.ts → cron/scheduler.ts} +6 -6
  61. package/src/core/background/{cron-spec.ts → cron/spec.ts} +1 -1
  62. package/src/core/background/{dream.ts → dream/index.ts} +10 -15
  63. package/src/core/background/{plan-alerts.ts → pulse/plan-alerts.ts} +3 -3
  64. package/src/core/background/{pulse.ts → pulse/pulse.ts} +8 -5
  65. package/src/core/background/triggers/command.ts +1 -1
  66. package/src/core/config/index.ts +15 -11
  67. package/src/core/daemon/resource-sampler.ts +121 -0
  68. package/src/core/engine/dispatcher.ts +16 -0
  69. package/src/core/engine/gateway-actions/cron.ts +2 -2
  70. package/src/core/engine/gateway-actions/index.ts +3 -0
  71. package/src/core/engine/gateway-actions/memory.ts +335 -0
  72. package/src/core/engine/gateway.ts +0 -6
  73. package/src/core/memory/import.ts +3 -2
  74. package/src/core/memory/taps.ts +199 -0
  75. package/src/core/memory/turn-retrieval.ts +222 -0
  76. package/src/core/prompt/assemble.ts +2 -12
  77. package/src/core/prompt/index.ts +2 -2
  78. package/src/core/prompt/invalidation.ts +1 -1
  79. package/src/core/tasks/index.ts +1 -1
  80. package/src/core/tools/index.ts +2 -0
  81. package/src/core/tools/memory.ts +128 -0
  82. package/src/core/tools/types.ts +1 -0
  83. package/src/core/weaver/turn-cpu.ts +32 -0
  84. package/src/core/weaver/weaver.ts +36 -0
  85. package/src/frontend/discord/admin.ts +1 -1
  86. package/src/frontend/discord/callbacks/components/backend-select.ts +1 -1
  87. package/src/frontend/discord/callbacks/components/pulse.ts +1 -1
  88. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  89. package/src/frontend/discord/callbacks/modals.ts +4 -1
  90. package/src/frontend/discord/commands/admin.ts +1 -35
  91. package/src/frontend/discord/commands/definitions.ts +0 -11
  92. package/src/frontend/discord/commands/router.ts +0 -3
  93. package/src/frontend/discord/commands/session.ts +6 -0
  94. package/src/frontend/discord/commands/settings.ts +1 -1
  95. package/src/frontend/discord/middleware.ts +1 -25
  96. package/src/frontend/discord/runtime.ts +1 -2
  97. package/src/frontend/native/{auth.ts → bridge/auth.ts} +2 -2
  98. package/src/frontend/native/{discovery.ts → bridge/discovery.ts} +3 -3
  99. package/src/frontend/native/{routes → bridge/routes}/chats.ts +1 -1
  100. package/src/frontend/native/{routes → bridge/routes}/daemon.ts +1 -1
  101. package/src/frontend/native/{routes → bridge/routes}/host.ts +6 -3
  102. package/src/frontend/native/{routes → bridge/routes}/pre-auth.ts +1 -1
  103. package/src/frontend/native/{server.ts → bridge/server.ts} +3 -3
  104. package/src/frontend/native/{tls.ts → bridge/tls.ts} +2 -2
  105. package/src/frontend/native/{chat-lifecycle.ts → chats/chat-lifecycle.ts} +3 -3
  106. package/src/frontend/native/{chat-wire.ts → chats/chat-wire.ts} +4 -4
  107. package/src/frontend/native/{chats.ts → chats/chats.ts} +4 -4
  108. package/src/frontend/native/{empty-chat-sweep.ts → chats/empty-chat-sweep.ts} +4 -4
  109. package/src/frontend/native/{history.ts → chats/history.ts} +7 -7
  110. package/src/frontend/native/{reset.ts → chats/reset.ts} +7 -7
  111. package/src/frontend/native/index.ts +13 -10
  112. package/src/frontend/native/{media.ts → media/media.ts} +3 -3
  113. package/src/frontend/native/runtime.ts +1 -1
  114. package/src/frontend/native/{control.ts → surface/control.ts} +4 -4
  115. package/src/frontend/native/{extensions.ts → surface/extensions.ts} +12 -9
  116. package/src/frontend/native/{handlers.ts → surface/handlers.ts} +19 -14
  117. package/src/frontend/native/{logs.ts → surface/logs.ts} +2 -2
  118. package/src/frontend/native/{memory.ts → surface/memory.ts} +2 -2
  119. package/src/frontend/native/{models.ts → surface/models.ts} +17 -10
  120. package/src/frontend/native/{settings.ts → surface/settings.ts} +9 -9
  121. package/src/frontend/native/{status.ts → surface/status.ts} +3 -3
  122. package/src/frontend/native/{actions.ts → turn/actions.ts} +7 -4
  123. package/src/frontend/native/{context.ts → turn/context.ts} +8 -8
  124. package/src/frontend/native/{emit.ts → turn/emit.ts} +7 -7
  125. package/src/frontend/native/{queue.ts → turn/queue.ts} +3 -3
  126. package/src/frontend/native/{turn-meta.ts → turn/turn-meta.ts} +2 -2
  127. package/src/frontend/native/{turn.ts → turn/turn.ts} +9 -9
  128. package/src/frontend/shared/model-commands.ts +1 -1
  129. package/src/frontend/shared/session-status.ts +35 -8
  130. package/src/frontend/shared/status-context.ts +101 -2
  131. package/src/frontend/telegram/admin/background.ts +1 -1
  132. package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
  133. package/src/frontend/telegram/callbacks/pulse.ts +1 -1
  134. package/src/frontend/telegram/callbacks/settings.ts +1 -1
  135. package/src/frontend/telegram/commands/admin.ts +2 -37
  136. package/src/frontend/telegram/commands/index.ts +1 -1
  137. package/src/frontend/telegram/commands/session.ts +6 -0
  138. package/src/frontend/telegram/commands/settings.ts +1 -1
  139. package/src/frontend/telegram/handlers/messages.ts +0 -10
  140. package/src/frontend/telegram/index.ts +1 -5
  141. package/src/frontend/telegram/middleware.ts +1 -15
  142. package/src/frontend/whatsapp/access.ts +2 -2
  143. package/src/frontend/whatsapp/actions/history.ts +2 -2
  144. package/src/frontend/whatsapp/actions/messaging.ts +2 -2
  145. package/src/frontend/whatsapp/actions/moderation.ts +1 -1
  146. package/src/frontend/whatsapp/actions/send.ts +1 -1
  147. package/src/frontend/whatsapp/commands.ts +8 -2
  148. package/src/frontend/whatsapp/{connection.ts → connection/connection.ts} +5 -5
  149. package/src/frontend/whatsapp/{pairing-service.ts → connection/pairing-service.ts} +3 -3
  150. package/src/frontend/whatsapp/{wa-logger.ts → connection/wa-logger.ts} +2 -2
  151. package/src/frontend/whatsapp/index.ts +4 -4
  152. package/src/frontend/whatsapp/{inbound.ts → messages/inbound.ts} +15 -15
  153. package/src/frontend/whatsapp/{media-store.ts → messages/media-store.ts} +4 -4
  154. package/src/frontend/whatsapp/{message-store.ts → messages/message-store.ts} +1 -1
  155. package/src/frontend/whatsapp/{turn-recovery.ts → messages/turn-recovery.ts} +4 -4
  156. package/src/frontend/whatsapp/registry.ts +1 -1
  157. package/src/frontend/whatsapp/runtime.ts +1 -1
  158. package/src/index.ts +1 -1
  159. package/src/storage/db.ts +6 -1
  160. package/src/storage/memory.ts +59 -8
  161. package/src/storage/metrics.ts +38 -0
  162. package/src/storage/repositories/sessions-repo.ts +6 -0
  163. package/src/storage/session-record.ts +13 -2
  164. package/src/storage/sessions.ts +12 -0
  165. package/src/storage/sql/db.sql +5 -0
  166. package/src/storage/sql/schema.sql +4 -0
  167. package/src/storage/sql/sessions.sql +3 -3
  168. package/src/storage/sql/statements.generated.ts +10 -3
  169. package/src/storage/sql/turn-meta.sql +1 -1
  170. package/src/util/boot-timer.ts +15 -1
  171. package/src/util/chat-id.ts +30 -0
  172. package/src/util/log.ts +1 -1
  173. package/src/util/paths.ts +0 -2
  174. package/src/core/soul/README.md +0 -110
  175. package/src/core/soul/RESEARCH.md +0 -98
  176. package/src/core/soul/associative.ts +0 -98
  177. package/src/core/soul/centrality.ts +0 -98
  178. package/src/core/soul/cluster.ts +0 -83
  179. package/src/core/soul/compiler.ts +0 -207
  180. package/src/core/soul/consolidate.ts +0 -179
  181. package/src/core/soul/critic.ts +0 -162
  182. package/src/core/soul/dag.ts +0 -265
  183. package/src/core/soul/delta.ts +0 -123
  184. package/src/core/soul/drift.ts +0 -99
  185. package/src/core/soul/embedder.ts +0 -129
  186. package/src/core/soul/emergent-critic.ts +0 -96
  187. package/src/core/soul/forgetting.ts +0 -131
  188. package/src/core/soul/governance.ts +0 -93
  189. package/src/core/soul/hash.ts +0 -97
  190. package/src/core/soul/hdc.ts +0 -154
  191. package/src/core/soul/kernel.ts +0 -540
  192. package/src/core/soul/lattice.ts +0 -103
  193. package/src/core/soul/lens.ts +0 -110
  194. package/src/core/soul/projector.ts +0 -240
  195. package/src/core/soul/reflect.ts +0 -170
  196. package/src/core/soul/reflex.ts +0 -164
  197. package/src/core/soul/retrieve.ts +0 -146
  198. package/src/core/soul/salience.ts +0 -146
  199. package/src/core/soul/service.ts +0 -204
  200. package/src/core/soul/settings.ts +0 -47
  201. package/src/core/soul/signals.ts +0 -117
  202. package/src/core/soul/talon-embedder.ts +0 -80
  203. package/src/core/soul/taps.ts +0 -199
  204. package/src/core/soul/types.ts +0 -298
  205. package/src/core/soul/valence.ts +0 -83
  206. /package/src/backend/{shared → runtime}/frontends.ts +0 -0
  207. /package/src/backend/{shared → runtime}/metrics.ts +0 -0
  208. /package/src/backend/{shared → runtime}/sleep.ts +0 -0
  209. /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
  210. /package/src/backend/{shared → runtime}/usage.ts +0 -0
  211. /package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +0 -0
  212. /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
  213. /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
  214. /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
  215. /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
  216. /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
  217. /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
  218. /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
  219. /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
  220. /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
  221. /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
  222. /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
  223. /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
@@ -5,7 +5,7 @@
5
5
  * Resolves the active model against the server's catalog, makes sure the
6
6
  * server, session, and this chat's MCP servers exist, builds the prompt
7
7
  * pair, drives the turn (`./turn.ts`), then runs the shared post-turn
8
- * phases (`backend/shared/turn-phases.ts`) with the one family-specific
8
+ * phases (`backend/runtime/turn/turn-phases.ts`) with the one family-specific
9
9
  * step in between: the usage fallback from the session summary.
10
10
  *
11
11
  * Kilo and OpenCode ran byte-for-byte copies of this (modulo the backend
@@ -18,8 +18,14 @@ import { getSession, incrementTurns } from "../../storage/sessions.js";
18
18
  import { getChatSettings } from "../../storage/chat-settings.js";
19
19
  import { log, logError } from "../../util/log.js";
20
20
  import { traceMessage } from "../../util/trace.js";
21
- import type { QueryParams, QueryResult } from "../shared/handler-types.js";
22
- import { frontendsForChat, nonTerminalFrontends } from "../shared/frontends.js";
21
+ import type {
22
+ QueryParams,
23
+ QueryResult,
24
+ } from "../runtime/turn/handler-types.js";
25
+ import {
26
+ frontendsForChat,
27
+ nonTerminalFrontends,
28
+ } from "../runtime/frontends.js";
23
29
  import {
24
30
  createStreamState,
25
31
  recordTokens,
@@ -33,7 +39,7 @@ import {
33
39
  accountFailedTurn,
34
40
  nameSessionFromFirstMessage,
35
41
  finishCallbackTurn,
36
- } from "../shared/index.js";
42
+ } from "../runtime/index.js";
37
43
  import type { RemoteAgentClient } from "./client.js";
38
44
  import type { RemoteServerBindings } from "./server-bindings.js";
39
45
  import { getTurnSummary, type RemoteSessionClient } from "./session-helpers.js";
@@ -68,16 +74,7 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
68
74
  const config = bindings.getConfig();
69
75
  if (!config) throw new Error(`${label} agent not initialized`);
70
76
 
71
- const {
72
- chatId,
73
- text,
74
- senderName,
75
- senderHandle,
76
- isGroup,
77
- messageId,
78
- onTextBlock,
79
- onToolUse,
80
- } = params;
77
+ const { chatId, text, senderName, onTextBlock, onToolUse } = params;
81
78
  const t0 = Date.now();
82
79
  const session = getSession(chatId);
83
80
  const previousTurns = session.turns;
@@ -108,14 +105,8 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
108
105
  pluginMcpServerNames,
109
106
  );
110
107
 
111
- // Build the prompt (time tag + sender + msg_id reference).
112
- const prompt = formatUserPrompt({
113
- text,
114
- senderName: senderName ?? "user",
115
- senderHandle,
116
- isGroup,
117
- messageId,
118
- });
108
+ // Build the prompt (time tag + sender + msg_id + recalled memory).
109
+ const prompt = buildTurnPrompt(params);
119
110
 
120
111
  // Per-session frozen prompt + this backend's delivery suffix.
121
112
  const { text: systemPrompt } = prepareSystemPrompt({
@@ -130,7 +121,7 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
130
121
  });
131
122
 
132
123
  log("agent", `[${chatId}] <- (${text.length} chars)`);
133
- traceMessage(chatId, "in", text, { senderName, isGroup });
124
+ traceMessage(chatId, "in", text, { senderName, isGroup: params.isGroup });
134
125
 
135
126
  // Bind the stream state to the chat so token mutators mirror counts
136
127
  // into the live-turn overlay — /status updates while the turn runs.
@@ -268,6 +259,23 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
268
259
  * session summaries can race on cancellation, so a failure leaves the
269
260
  * counts at zero.
270
261
  */
262
+ /**
263
+ * The turn's user prompt: the shared framing every backend emits, plus
264
+ * whatever this turn's memory retrieval produced. `formatUserPrompt` is
265
+ * the one place `retrievedMemory` is rendered — see
266
+ * `backend/runtime/prompt/prompt-format.ts`.
267
+ */
268
+ function buildTurnPrompt(params: QueryParams): string {
269
+ return formatUserPrompt({
270
+ text: params.text,
271
+ senderName: params.senderName ?? "user",
272
+ senderHandle: params.senderHandle,
273
+ isGroup: params.isGroup,
274
+ messageId: params.messageId,
275
+ retrievedMemory: params.retrievedMemory,
276
+ });
277
+ }
278
+
271
279
  async function fillUsageFromSummary(
272
280
  oc: RemoteSessionClient,
273
281
  sessionId: string,
@@ -33,7 +33,7 @@ import {
33
33
  recordToolUse,
34
34
  recordToolCall,
35
35
  type StreamState,
36
- } from "../shared/index.js";
36
+ } from "../runtime/index.js";
37
37
  import {
38
38
  extractAssistantUsage,
39
39
  type RemoteAssistantInfo,
@@ -68,7 +68,7 @@ export interface EventProcessingContext {
68
68
  chatId: string;
69
69
  /** Session id we're scoped to — events for other sessions are dropped. */
70
70
  sessionId: string;
71
- /** Stream state accumulator (shared/). */
71
+ /** Stream state accumulator (runtime/turn/). */
72
72
  state: StreamState;
73
73
  /** Tool-call ids we've already fired callbacks for. */
74
74
  seenToolCallIds: Set<string>;
@@ -164,7 +164,7 @@ export async function processStreamEvent(
164
164
  * lands so token/context stats are live mid-turn instead of appearing
165
165
  * only at the post-loop accounting. `recordTokens` also mirrors the
166
166
  * counts into the chat's live-turn overlay when the state is bound to a
167
- * chat (see `shared/stream-state.ts: pushLiveUsage`).
167
+ * chat (see `runtime/turn/stream-state.ts: pushLiveUsage`).
168
168
  *
169
169
  * Values are the message's totals (not deltas), matching what the
170
170
  * post-loop `extractAssistantUsage(lastAssistant.info)` reads — so the
@@ -28,9 +28,12 @@ import type { OneShotAgentParams, OneShotUsage } from "../../core/types.js";
28
28
  import type { TalonConfig } from "../../core/config/index.js";
29
29
  import { binaryOnPath } from "../../util/binary-on-path.js";
30
30
  import { log } from "../../util/log.js";
31
- import { handlerToEvents } from "../shared/handler-to-events.js";
32
- import type { QueryParams, QueryResult } from "../shared/handler-types.js";
33
- import { interruptChatTurn } from "../shared/turn-interrupt.js";
31
+ import { handlerToEvents } from "../runtime/turn/handler-to-events.js";
32
+ import type {
33
+ QueryParams,
34
+ QueryResult,
35
+ } from "../runtime/turn/handler-types.js";
36
+ import { interruptChatTurn } from "../runtime/turn/turn-interrupt.js";
34
37
  import type { RemoteModelProvider } from "./model-catalog/provider.js";
35
38
  import type { RemoteSessionSnapshot } from "./session-helpers.js";
36
39
 
@@ -28,7 +28,7 @@
28
28
  * What's NOT here (intentionally):
29
29
  *
30
30
  * - Tool definitions, frontend prompt format — those are backend-
31
- * agnostic and live in `core/` and `backend/shared/`.
31
+ * agnostic and live in `core/` and `backend/runtime/`.
32
32
  *
33
33
  * This barrel exposes the helper layer for tests and the conformance
34
34
  * suite; the bindings modules import from the concrete files directly.
@@ -50,7 +50,7 @@ import { ALL_TOOLS, nativeTools } from "../../core/tools/index.js";
50
50
  import {
51
51
  frontendForChatId,
52
52
  nonTerminalFrontends,
53
- } from "../shared/frontends.js";
53
+ } from "../runtime/frontends.js";
54
54
 
55
55
  // ── Constants ───────────────────────────────────────────────────────────────
56
56
 
@@ -41,7 +41,7 @@ import {
41
41
  type RemoteSessionClient,
42
42
  type RemoteAssistantInfo,
43
43
  } from "./session-helpers.js";
44
- import { appendBackendSuffix, sleep } from "../shared/index.js";
44
+ import { appendBackendSuffix, sleep } from "../runtime/index.js";
45
45
  import { buildPermissionRuleset } from "./sessions.js";
46
46
 
47
47
  // ── Client surface ──────────────────────────────────────────────────────────
@@ -25,7 +25,7 @@ import type { FrontendName } from "../../core/agent-runtime/backend-registry.js"
25
25
  import {
26
26
  buildDeliveryContract,
27
27
  type DeliveryMode,
28
- } from "../shared/delivery-contract.js";
28
+ } from "../runtime/prompt/delivery-contract.js";
29
29
  import type { RemoteAgentClient } from "./client.js";
30
30
  import { createRemoteServerState, errMsg } from "./state.js";
31
31
  import { ensureRemoteServer, stopRemoteServer } from "./lifecycle.js";
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { logWarn } from "../../util/log.js";
16
- import { createStreamState, recordTokens, sleep } from "../shared/index.js";
16
+ import { createStreamState, recordTokens, sleep } from "../runtime/index.js";
17
17
  import { processStreamEvent, finalizePartsIntoState } from "./events.js";
18
18
  import { findLastAssistantMessage } from "./messages.js";
19
19
  import {
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Prompt-cache rollups — the per-turn verdict, the tool fingerprint and the
3
+ * SDK's compaction boundaries, folded into counters an operator can read.
4
+ *
5
+ * `cache-telemetry.ts` next door decides WHAT a turn did (verdict,
6
+ * fingerprint, lookback risk) and is pure apart from its own per-chat maps.
7
+ * This module is the side-effecting half: it writes to the metrics store and
8
+ * logs, so the telemetry stays testable without a store.
9
+ *
10
+ * Why these numbers exist (docs/cache-economics.md, PR A): every decision in
11
+ * PR B ("one prefix per frontend") and PR C ("compact when the cache has gone
12
+ * cold") needs a before/after figure, and nothing here changes a prompt byte
13
+ * or a turn's behaviour — measurement only.
14
+ *
15
+ * - `cache.first_request.{hit,miss,none}` — did the previous turn's prefix
16
+ * survive? The turn's first request is the only one that can say, and it
17
+ * is the number that tracks cost.
18
+ * - `cache.first_request.{read,write}_tokens` — how big the prefix is, so a
19
+ * miss can be priced.
20
+ * - `cache.session_start.{hit,miss,none}` — the same verdict restricted to a
21
+ * chat's FIRST turn. A hit there means another chat had already warmed an
22
+ * identical prefix: the cross-chat sharing signal PR B is aiming at.
23
+ * - `cache.tool_fingerprint.changed` — tools render before the system
24
+ * prompt, so a change invalidates everything after it.
25
+ * - `session.compacted.{manual,auto}` + `session.compact.{pre,post}_tokens`
26
+ * — how often the SDK compacts and how much it reclaims, the baseline
27
+ * PR C's idle-compaction policy is measured against.
28
+ */
29
+
30
+ import { log } from "../../../util/log.js";
31
+ import {
32
+ incrementCounter,
33
+ noteCacheVerdict,
34
+ recordHistogram,
35
+ } from "../../../storage/metrics.js";
36
+ import {
37
+ crossTurnVerdict,
38
+ fingerprintHash,
39
+ hasToolFingerprint,
40
+ noteToolFingerprint,
41
+ type CrossTurnVerdict,
42
+ type TurnCacheStats,
43
+ } from "./cache-telemetry.js";
44
+
45
+ /**
46
+ * Roll one finished turn's cache behaviour up into the metrics store and
47
+ * remember the verdict for `/status`.
48
+ *
49
+ * `turnsIncludingThis` is the chat's turn count with this turn already
50
+ * counted, so 1 means the turn was the session's first — the only turn whose
51
+ * verdict says anything about cross-chat prefix sharing.
52
+ */
53
+ export function rollUpTurnCache(
54
+ chatId: string,
55
+ stats: TurnCacheStats,
56
+ turnsIncludingThis: number,
57
+ ): CrossTurnVerdict {
58
+ const verdict = crossTurnVerdict(stats);
59
+ incrementCounter(`cache.first_request.${verdict}`);
60
+ // Recorded on every turn, zeros included: the average is then "tokens per
61
+ // turn", which is what a miss costs, rather than an average over the
62
+ // turns that happened to be interesting.
63
+ recordHistogram("cache.first_request.read_tokens", stats.firstRead);
64
+ recordHistogram("cache.first_request.write_tokens", stats.firstWrite);
65
+ if (turnsIncludingThis <= 1) {
66
+ incrementCounter(`cache.session_start.${verdict}`);
67
+ }
68
+ noteCacheVerdict(chatId, verdict);
69
+ return verdict;
70
+ }
71
+
72
+ /**
73
+ * Record a chat's tool set: one info line the first time a chat is seen (so
74
+ * two chats on the same frontend can be compared by eye — identical hash +
75
+ * identical static prompt ⇒ shared cache), and a counter whenever it changes
76
+ * mid-process. `noteToolFingerprint` still owns the warning it already logs
77
+ * for a mid-session change.
78
+ */
79
+ export function reportToolFingerprint(
80
+ chatId: string,
81
+ fingerprint: readonly string[],
82
+ ): void {
83
+ const seen = hasToolFingerprint(chatId);
84
+ const changed = noteToolFingerprint(chatId, fingerprint);
85
+ if (!seen) {
86
+ log(
87
+ "agent",
88
+ `[${chatId}] tool fingerprint ${fingerprintHash(fingerprint)} ` +
89
+ `(${fingerprint.length} tools)`,
90
+ );
91
+ }
92
+ if (changed) incrementCounter("cache.tool_fingerprint.changed");
93
+ }
94
+
95
+ /** The SDK's compaction metadata, narrowed to the fields that are measured. */
96
+ export type CompactBoundary = {
97
+ trigger: "manual" | "auto";
98
+ pre_tokens: number;
99
+ post_tokens?: number;
100
+ };
101
+
102
+ /**
103
+ * Record a compaction boundary the SDK reported mid-stream. Purely
104
+ * observational — the turn carries on exactly as it did before, this just
105
+ * stops the event being dropped on the floor.
106
+ */
107
+ export function recordCompactBoundary(
108
+ chatId: string,
109
+ meta: CompactBoundary,
110
+ ): void {
111
+ const trigger = meta.trigger === "manual" ? "manual" : "auto";
112
+ incrementCounter(`session.compacted.${trigger}`);
113
+ const pre = Number.isFinite(meta.pre_tokens) ? meta.pre_tokens : 0;
114
+ const post =
115
+ typeof meta.post_tokens === "number" && Number.isFinite(meta.post_tokens)
116
+ ? meta.post_tokens
117
+ : undefined;
118
+ recordHistogram("session.compact.pre_tokens", pre);
119
+ if (post !== undefined) recordHistogram("session.compact.post_tokens", post);
120
+ log(
121
+ "agent",
122
+ `[${chatId}] context compacted (${trigger}): ${pre} tokens` +
123
+ (post !== undefined ? ` -> ${post}` : "") +
124
+ ` — the prefix after the boundary is new, so the next turn re-writes it`,
125
+ );
126
+ }
@@ -36,7 +36,9 @@
36
36
  * did.
37
37
  */
38
38
 
39
- import { logWarn } from "../../util/log.js";
39
+ import { createHash } from "node:crypto";
40
+
41
+ import { logWarn } from "../../../util/log.js";
40
42
 
41
43
  // ── Per-turn cache stats ────────────────────────────────────────────────────
42
44
 
@@ -244,7 +246,7 @@ export function cacheMinimumTokens(model: string): number | undefined {
244
246
  return best?.min;
245
247
  }
246
248
 
247
- /** Cheap tokenizer-free estimate (~4 chars/token), matching soul/projector. */
249
+ /** Cheap tokenizer-free estimate (~4 chars/token), the house heuristic. */
248
250
  function estimateTokens(text: string): number {
249
251
  return Math.ceil(text.length / 4);
250
252
  }
@@ -307,6 +309,24 @@ export function toolFingerprint(
307
309
  ].sort();
308
310
  }
309
311
 
312
+ /**
313
+ * Short, stable digest of a tool set — the thing to compare between two
314
+ * chats. Two chats whose fingerprints (and static prompt) match render a
315
+ * byte-identical prefix and therefore share the cache; two that don't,
316
+ * can't, however similar they look (docs/cache-economics.md, PR B).
317
+ */
318
+ export function fingerprintHash(fingerprint: readonly string[]): string {
319
+ return createHash("sha256")
320
+ .update(fingerprint.join("\n"))
321
+ .digest("hex")
322
+ .slice(0, 12);
323
+ }
324
+
325
+ /** True once a fingerprint has been recorded for this chat in this process. */
326
+ export function hasToolFingerprint(chatId: string): boolean {
327
+ return lastToolSets.has(chatId);
328
+ }
329
+
310
330
  /**
311
331
  * Record this turn's tool set for a chat and warn when it differs from the
312
332
  * previous turn's. Returns true when a change was detected.
@@ -1,30 +1,36 @@
1
1
  /**
2
- * Shared backend framework — barrel re-export.
2
+ * Backend runtime library — barrel re-export.
3
3
  *
4
4
  * Helpers used by every concrete backend (`claude-sdk`, `codex`,
5
5
  * `kilo`, `opencode`, `openai-agents`) to keep behaviour aligned and
6
- * avoid copy-paste drift.
6
+ * avoid copy-paste drift. The modules sit in three groups — `turn/`
7
+ * (what a turn does once the SDK loop is running), `prompt/` (the text
8
+ * handed to the model) and `cache/` (prompt-cache telemetry) — with the
9
+ * cross-cutting vocabulary (`usage`, `metrics`, `frontends`, `sleep`)
10
+ * at the root beside this barrel.
7
11
  *
8
12
  * What's here:
9
- * - `delivered-text` — scratchpad/dedup primitives.
10
- * - `delivery-contract` — per-backend response-flow contract
13
+ * - `turn/delivered-text` — scratchpad/dedup primitives.
14
+ * - `prompt/delivery-contract` — per-backend response-flow contract
11
15
  * (rendered from prompts/system templates), frontend-aware
12
16
  * flow-violation reminder, first-turn nudge.
13
- * - `flow-violation` — flow-violation detection + reminder text.
17
+ * - `turn/flow-violation` — flow-violation detection + reminder text.
14
18
  * - `metrics` — the shared metric vocabulary (tool calls, per-turn
15
19
  * rollups, flow violations) with backend dimensions.
16
- * - `prompt-format` — user-prompt formatter ([time] [Name] [msg_id:N]).
20
+ * - `prompt/prompt-format` — user-prompt formatter
21
+ * ([time] [Name] [msg_id:N]).
17
22
  * - `frontends` — config `frontend` → messaging-frontend list.
18
23
  * - `extractSessionName` — re-exported from `util/session-name` so the
19
24
  * backends keep one import site; the helper itself is frontend-neutral.
20
25
  * - `usage` — cache-hit % + log summarisers.
21
- * - `system-prompt` — per-session prompt snapshots + backend suffix
22
- * (assembly itself lives in `core/prompt/`).
23
- * - `model-retry` — session-expiry / context-overflow / fallback decisions.
24
- * - `stream-state` — backend-agnostic accumulator for stream loops.
25
- * - `turn-interrupt` — user-driven mid-turn interrupt registry (the
26
+ * - `prompt/system-prompt` — per-session prompt snapshots + backend
27
+ * suffix (assembly itself lives in `core/prompt/`).
28
+ * - `turn/model-retry` — session-expiry / context-overflow / fallback
29
+ * decisions.
30
+ * - `turn/stream-state` — backend-agnostic accumulator for stream loops.
31
+ * - `turn/turn-interrupt` — user-driven mid-turn interrupt registry (the
26
32
  * shared `ChatBackend.interruptChatTurn` for callback backends).
27
- * - `turn-phases` — the post-stream phases (accounting, session name,
33
+ * - `turn/turn-phases` — the post-stream phases (accounting, session name,
28
34
  * trailing-prose contract, result tail) every handler runs.
29
35
  *
30
36
  * What's NOT here (intentionally):
@@ -34,17 +40,17 @@
34
40
  * (the spawn/env contract they share is `core/tools/mcp-env.ts`).
35
41
  */
36
42
 
37
- export { captureDeliveredText } from "./delivered-text.js";
43
+ export { captureDeliveredText } from "./turn/delivered-text.js";
38
44
 
39
- export { registerTurnInterrupt } from "./turn-interrupt.js";
45
+ export { registerTurnInterrupt } from "./turn/turn-interrupt.js";
40
46
 
41
- export { formatUserPrompt } from "./prompt-format.js";
47
+ export { formatUserPrompt } from "./prompt/prompt-format.js";
42
48
 
43
49
  export {
44
50
  buildDeliveryContract,
45
51
  buildFlowViolationReminder,
46
52
  buildFirstTurnReminder,
47
- } from "./delivery-contract.js";
53
+ } from "./prompt/delivery-contract.js";
48
54
 
49
55
  export { summarizeUsage } from "./usage.js";
50
56
 
@@ -57,9 +63,12 @@ export {
57
63
  priorLookbackOverflow,
58
64
  noteLookbackRisk,
59
65
  CACHE_LOOKBACK_BLOCKS,
60
- } from "./cache-telemetry.js";
66
+ } from "./cache/cache-telemetry.js";
61
67
 
62
- export { prepareSystemPrompt, appendBackendSuffix } from "./system-prompt.js";
68
+ export {
69
+ prepareSystemPrompt,
70
+ appendBackendSuffix,
71
+ } from "./prompt/system-prompt.js";
63
72
 
64
73
  export {
65
74
  createStreamState,
@@ -71,19 +80,19 @@ export {
71
80
  pushLiveUsage,
72
81
  finalizeResponseText,
73
82
  type StreamState,
74
- } from "./stream-state.js";
83
+ } from "./turn/stream-state.js";
75
84
 
76
85
  export {
77
86
  routeDelivery,
78
87
  buildDeliveryFailureReminder,
79
88
  TextBlockDeliveryError,
80
- } from "./delivery.js";
89
+ } from "./turn/delivery.js";
81
90
 
82
91
  export { sleep } from "./sleep.js";
83
92
 
84
93
  export { recordToolCall } from "./metrics.js";
85
94
 
86
- export { applyRetryDecision } from "./handle-retry.js";
95
+ export { applyRetryDecision } from "./turn/handle-retry.js";
87
96
 
88
97
  export {
89
98
  accountTurn,
@@ -92,6 +101,6 @@ export {
92
101
  enforceTrailingProse,
93
102
  finishCallbackTurn,
94
103
  turnUsageSnapshot,
95
- } from "./turn-phases.js";
104
+ } from "./turn/turn-phases.js";
96
105
 
97
- export { buildResultEvents } from "./result-events.js";
106
+ export { buildResultEvents } from "./turn/result-events.js";
@@ -34,7 +34,7 @@
34
34
  * logic-adjacent strings, like log messages.
35
35
  */
36
36
 
37
- import { loadSystemTemplate } from "../../core/prompt/templates.js";
37
+ import { loadSystemTemplate } from "../../../core/prompt/templates.js";
38
38
 
39
39
  // ── Types ───────────────────────────────────────────────────────────────────
40
40
 
@@ -15,7 +15,7 @@
15
15
  * DM (no msg_id): "[2026-05-15 11:01:23] actual text"
16
16
  */
17
17
 
18
- import { formatFullDatetime } from "../../util/time.js";
18
+ import { formatFullDatetime } from "../../../util/time.js";
19
19
 
20
20
  // ── Public API ──────────────────────────────────────────────────────────────
21
21
 
@@ -38,8 +38,33 @@ export type PromptFormatInputs = {
38
38
  messageId?: number | string;
39
39
  /** When true, omit the leading `[YYYY-MM-DD HH:MM:SS]` tag. */
40
40
  omitTimeTag?: boolean;
41
+ /**
42
+ * Memory retrieved for THIS turn (`core/memory/turn-retrieval.ts`),
43
+ * already ranked, trust-filtered and budgeted. Appended after the
44
+ * message text under a verify-first header.
45
+ *
46
+ * This is the **only** place retrieved memory enters a prompt, and
47
+ * every backend reaches it through this one helper — which is what
48
+ * keeps #639's divergence (two backends reading the field, four
49
+ * silently dropping it) from coming back. It belongs to the user
50
+ * turn: it must never reach `prepareSystemPrompt()`, a prompt
51
+ * addition, or a backend `system` field, or it would break the
52
+ * per-session frozen prompt (plan §3.6).
53
+ *
54
+ * Absent or blank → the returned prompt is BYTE-IDENTICAL to what
55
+ * this helper produced before the field existed.
56
+ */
57
+ retrievedMemory?: string;
41
58
  };
42
59
 
60
+ /**
61
+ * Header on the injected block. "Verify before relying on it" is
62
+ * deliberate: retrieval is a bm25 guess, not a fact, and the model
63
+ * should treat a recalled line as a lead rather than as truth.
64
+ */
65
+ export const RECALLED_MEMORY_HEADER =
66
+ "[Recalled from memory — verify before relying on it]";
67
+
43
68
  /**
44
69
  * Format a user prompt for the AI backend.
45
70
  *
@@ -49,6 +74,13 @@ export type PromptFormatInputs = {
49
74
  * is on the other end.
50
75
  */
51
76
  export function formatUserPrompt(inputs: PromptFormatInputs): string {
77
+ return withRecalledMemory(formatMessageLine(inputs), inputs.retrievedMemory);
78
+ }
79
+
80
+ // ── Helpers ─────────────────────────────────────────────────────────────────
81
+
82
+ /** The message line itself — time tag, sender label, `msg_id`, text. */
83
+ function formatMessageLine(inputs: PromptFormatInputs): string {
52
84
  const timeTag = inputs.omitTimeTag ? "" : `[${formatFullDatetime()}]`;
53
85
  const msgIdHint =
54
86
  inputs.messageId !== undefined ? ` [msg_id:${inputs.messageId}]` : "";
@@ -68,7 +100,19 @@ export function formatUserPrompt(inputs: PromptFormatInputs): string {
68
100
  return joinNonEmpty(timeTag, inputs.text);
69
101
  }
70
102
 
71
- // ── Helpers ─────────────────────────────────────────────────────────────────
103
+ /**
104
+ * Append the retrieved-memory block AFTER the message, never before:
105
+ * the user's own words stay the first thing the model reads, and the
106
+ * recalled lines read as an annotation on them.
107
+ */
108
+ function withRecalledMemory(
109
+ prompt: string,
110
+ memory: string | undefined,
111
+ ): string {
112
+ const block = memory?.trim();
113
+ if (!block) return prompt;
114
+ return `${prompt}\n\n${RECALLED_MEMORY_HEADER}\n${block}`;
115
+ }
72
116
 
73
117
  function joinNonEmpty(...parts: string[]): string {
74
118
  return parts
@@ -39,10 +39,10 @@ import {
39
39
  joinSystemPromptParts,
40
40
  type SystemPromptParts,
41
41
  type TalonConfig,
42
- } from "../../core/config/index.js";
43
- import { getPluginPromptAdditions } from "../../core/plugin/index.js";
44
- import { onPromptInputsChanged } from "../../core/prompt/invalidation.js";
45
- import { frontendForChatId, nonTerminalFrontends } from "./frontends.js";
42
+ } from "../../../core/config/index.js";
43
+ import { getPluginPromptAdditions } from "../../../core/plugin/index.js";
44
+ import { onPromptInputsChanged } from "../../../core/prompt/invalidation.js";
45
+ import { frontendForChatId, nonTerminalFrontends } from "../frontends.js";
46
46
 
47
47
  // ── Types ───────────────────────────────────────────────────────────────────
48
48
 
@@ -25,7 +25,7 @@
25
25
  * whether to surface a flow violation.
26
26
  */
27
27
 
28
- import { stripMcpPrefix } from "../../core/tools/index.js";
28
+ import { stripMcpPrefix } from "../../../core/tools/index.js";
29
29
 
30
30
  // ── Constants ───────────────────────────────────────────────────────────────
31
31
 
@@ -35,8 +35,8 @@
35
35
 
36
36
  import type { StreamState } from "./stream-state.js";
37
37
  import { undeliveredResponseText } from "./stream-state.js";
38
- import { logWarn } from "../../util/log.js";
39
- import { incrementCounter } from "../../storage/metrics.js";
38
+ import { logWarn } from "../../../util/log.js";
39
+ import { incrementCounter } from "../../../storage/metrics.js";
40
40
 
41
41
  /** Route the delivery decision selected. */
42
42
  export type DeliveryRoute =
@@ -23,12 +23,12 @@
23
23
  */
24
24
 
25
25
  import type { QueryParams, QueryResult } from "./handler-types.js";
26
- import { classify, type TalonError } from "../../core/errors.js";
27
- import { logWarn } from "../../util/log.js";
28
- import { incrementCounter } from "../../storage/metrics.js";
29
- import { resetSession } from "../../storage/sessions.js";
26
+ import { classify, type TalonError } from "../../../core/errors.js";
27
+ import { logWarn } from "../../../util/log.js";
28
+ import { incrementCounter } from "../../../storage/metrics.js";
29
+ import { resetSession } from "../../../storage/sessions.js";
30
30
  import { classifyRetry } from "./model-retry.js";
31
- import type { AgentEvent } from "../../core/agent-runtime/events.js";
31
+ import type { AgentEvent } from "../../../core/agent-runtime/events.js";
32
32
 
33
33
  /** Inputs for `applyRetryDecision`. */
34
34
  export interface ApplyRetryDecisionInputs {