talon-agent 4.6.0 → 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 (225) hide show
  1. package/README.md +3 -3
  2. package/package.json +3 -2
  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 -24
  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/goals-repo.ts +2 -1
  163. package/src/storage/repositories/sessions-repo.ts +6 -0
  164. package/src/storage/session-record.ts +13 -2
  165. package/src/storage/sessions.ts +12 -0
  166. package/src/storage/sql/db.sql +5 -0
  167. package/src/storage/sql/schema.sql +4 -0
  168. package/src/storage/sql/sessions.sql +3 -3
  169. package/src/storage/sql/statements.generated.ts +10 -3
  170. package/src/storage/sql/turn-meta.sql +1 -1
  171. package/src/util/boot-timer.ts +15 -1
  172. package/src/util/chat-id.ts +30 -0
  173. package/src/util/concurrency.ts +1 -1
  174. package/src/util/log.ts +1 -1
  175. package/src/util/paths.ts +0 -2
  176. package/src/core/soul/README.md +0 -110
  177. package/src/core/soul/RESEARCH.md +0 -98
  178. package/src/core/soul/associative.ts +0 -98
  179. package/src/core/soul/centrality.ts +0 -98
  180. package/src/core/soul/cluster.ts +0 -83
  181. package/src/core/soul/compiler.ts +0 -207
  182. package/src/core/soul/consolidate.ts +0 -179
  183. package/src/core/soul/critic.ts +0 -162
  184. package/src/core/soul/dag.ts +0 -265
  185. package/src/core/soul/delta.ts +0 -123
  186. package/src/core/soul/drift.ts +0 -99
  187. package/src/core/soul/embedder.ts +0 -129
  188. package/src/core/soul/emergent-critic.ts +0 -96
  189. package/src/core/soul/forgetting.ts +0 -131
  190. package/src/core/soul/governance.ts +0 -93
  191. package/src/core/soul/hash.ts +0 -97
  192. package/src/core/soul/hdc.ts +0 -154
  193. package/src/core/soul/kernel.ts +0 -540
  194. package/src/core/soul/lattice.ts +0 -103
  195. package/src/core/soul/lens.ts +0 -110
  196. package/src/core/soul/projector.ts +0 -240
  197. package/src/core/soul/reflect.ts +0 -170
  198. package/src/core/soul/reflex.ts +0 -164
  199. package/src/core/soul/retrieve.ts +0 -146
  200. package/src/core/soul/salience.ts +0 -146
  201. package/src/core/soul/service.ts +0 -204
  202. package/src/core/soul/settings.ts +0 -47
  203. package/src/core/soul/signals.ts +0 -117
  204. package/src/core/soul/talon-embedder.ts +0 -80
  205. package/src/core/soul/taps.ts +0 -199
  206. package/src/core/soul/types.ts +0 -298
  207. package/src/core/soul/valence.ts +0 -83
  208. /package/src/backend/{shared → runtime}/frontends.ts +0 -0
  209. /package/src/backend/{shared → runtime}/metrics.ts +0 -0
  210. /package/src/backend/{shared → runtime}/sleep.ts +0 -0
  211. /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
  212. /package/src/backend/{shared → runtime}/usage.ts +0 -0
  213. /package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +0 -0
  214. /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
  215. /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
  216. /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
  217. /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
  218. /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
  219. /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
  220. /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
  221. /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
  222. /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
  223. /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
  224. /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
  225. /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
@@ -1,110 +0,0 @@
1
- /**
2
- * Soul Kernel — relational holograph (lens compilation).
3
- *
4
- * Identity is not global; it is refracted per interlocutor. A lens is the
5
- * refraction for one subject: which values to amplify and which evidence is
6
- * specific to that relationship. It is compiled MECHANICALLY — no model decides
7
- * how Talon shows up with Dylan vs. a stranger; the evidence does.
8
- *
9
- * The signal is provenance: evidence carries `source.actor`. A value that is
10
- * disproportionately built from a subject's evidence is amplified for that
11
- * subject, by a factor proportional to how much of the value is "theirs". The
12
- * lens also carries the subject-specific evidence verbatim.
13
- *
14
- * Re-compiling supersedes the prior lens for the same subject (a `supersedes`
15
- * edge), so exactly one live lens per subject survives — and superseded lenses
16
- * fall out of projection like any other superseded node.
17
- */
18
-
19
- import { isSuperseded } from "./consolidate.js";
20
- import type { SoulDag } from "./dag.js";
21
- import type { EvidencePayload, Hash, ValuePayload } from "./types.js";
22
-
23
- export interface LensOptions {
24
- readonly now: number;
25
- /** Max amplification boost for a fully subject-owned value (default 2). */
26
- readonly boost?: number;
27
- /** Cap on the number of subject-evidence fragments carried (default 12). */
28
- readonly maxEvidence?: number;
29
- }
30
-
31
- function actorOf(dag: SoulDag, evidence: Hash): string | undefined {
32
- const node = dag.getNode(evidence);
33
- return node?.payload.kind === "evidence"
34
- ? (node.payload as EvidencePayload).source.actor
35
- : undefined;
36
- }
37
-
38
- /** Live lens node for a subject, if one exists. */
39
- export function liveLens(dag: SoulDag, subject: string): Hash | undefined {
40
- for (const node of dag.nodesOfKind("lens")) {
41
- if (
42
- node.payload.kind === "lens" &&
43
- node.payload.subject === subject &&
44
- !isSuperseded(dag, node.hash)
45
- ) {
46
- return node.hash;
47
- }
48
- }
49
- return undefined;
50
- }
51
-
52
- /**
53
- * Compile (or recompile) the lens for a subject from actor-tagged evidence.
54
- * Returns the lens hash, or undefined if the subject has no evidence. Caller
55
- * commits.
56
- */
57
- export function compileLens(
58
- dag: SoulDag,
59
- subject: string,
60
- opts: LensOptions,
61
- ): Hash | undefined {
62
- const boost = opts.boost ?? 2;
63
- const maxEvidence = opts.maxEvidence ?? 12;
64
-
65
- // Subject-specific evidence.
66
- const subjectEvidence = dag
67
- .nodesOfKind("evidence")
68
- .filter((n) => actorOf(dag, n.hash) === subject)
69
- .map((n) => n.hash);
70
- if (subjectEvidence.length === 0) return undefined;
71
- const subjectSet = new Set(subjectEvidence);
72
-
73
- // Amplify values built disproportionately from the subject's evidence.
74
- const amplify: { node: Hash; factor: number }[] = [];
75
- for (const node of dag.nodesOfKind("value")) {
76
- if (isSuperseded(dag, node.hash)) continue;
77
- const value = node.payload as ValuePayload;
78
- const members = value.members.length ? value.members : [value.medoid];
79
- const owned = members.filter((m) => subjectSet.has(m)).length;
80
- if (owned === 0) continue;
81
- const factor = 1 + boost * (owned / members.length);
82
- amplify.push({ node: node.hash, factor });
83
- }
84
-
85
- const evidence = [...subjectEvidence].sort().slice(0, maxEvidence);
86
- const lens = dag.addNode(
87
- {
88
- kind: "lens",
89
- subject,
90
- amplify: amplify.sort((a, b) => a.node.localeCompare(b.node)),
91
- evidence,
92
- },
93
- opts.now,
94
- );
95
-
96
- // Supersede any prior live lens for this subject.
97
- for (const node of dag.nodesOfKind("lens")) {
98
- if (
99
- node.hash !== lens &&
100
- node.payload.kind === "lens" &&
101
- node.payload.subject === subject &&
102
- !isSuperseded(dag, node.hash)
103
- ) {
104
- const e = dag.edge(lens, "supersedes", node.hash, opts.now);
105
- e.weight = 1;
106
- e.updatedAt = opts.now;
107
- }
108
- }
109
- return lens;
110
- }
@@ -1,240 +0,0 @@
1
- /**
2
- * Soul Kernel — the projector. Compiles the DAG to the runtime surface that is
3
- * injected into the system prompt.
4
- *
5
- * The defining constraint: the projector **selects**, it never **writes**. Every
6
- * substantive line is a verbatim evidence fragment already in the DAG (the
7
- * medoid of a value, a spine event, a lens-scoped quote). The only generated
8
- * text is fixed structural scaffolding — section headers and the action strings
9
- * of reflexes, which are themselves authored data, not claims about Talon. The
10
- * soul therefore cannot hallucinate itself: it can only ever quote ground truth.
11
- *
12
- * Selection is salience-ordered under a token budget, so the surface carries what
13
- * is *currently alive*, not everything ever true. Reflexes are exempt from the
14
- * budget — guarantees are never dropped.
15
- */
16
-
17
- import { SoulDag } from "./dag.js";
18
- import { isSuperseded } from "./consolidate.js";
19
- import { tensionPairs } from "./lattice.js";
20
- import { confidence, effectiveSalience } from "./salience.js";
21
- import type { Hash, LensPayload, SoulConfig, ValuePayload } from "./types.js";
22
- import { halfLifeForKind } from "./types.js";
23
-
24
- export interface ProjectionOptions {
25
- readonly now: number;
26
- readonly config: SoulConfig;
27
- /** If set, refract the projection through this subject's lens, when present. */
28
- readonly lens?: string;
29
- /**
30
- * Optional precomputed value ordering (e.g. from context-conditioned
31
- * retrieval). When present it overrides salience ranking, letting the soul
32
- * surface what is relevant to the current moment. Hashes not listed are
33
- * dropped from the value section.
34
- */
35
- readonly order?: readonly Hash[];
36
- }
37
-
38
- export interface Projection {
39
- readonly text: string;
40
- readonly tokens: number;
41
- readonly includedValues: readonly Hash[];
42
- readonly droppedValues: number;
43
- }
44
-
45
- /** Cheap, tokenizer-free token estimate (~4 chars/token). Deterministic. */
46
- export function estimateTokens(text: string): number {
47
- return Math.ceil(text.length / 4);
48
- }
49
-
50
- function evidenceText(
51
- dag: SoulDag,
52
- hash: Hash | undefined,
53
- ): string | undefined {
54
- if (!hash) return undefined;
55
- const node = dag.getNode(hash);
56
- return node?.payload.kind === "evidence" ? node.payload.text : undefined;
57
- }
58
-
59
- /** The medoid evidence hash of a value node, if it is one. */
60
- function valueMedoid(dag: SoulDag, hash: Hash): Hash | undefined {
61
- const node = dag.getNode(hash);
62
- return node?.payload.kind === "value"
63
- ? (node.payload as ValuePayload).medoid
64
- : undefined;
65
- }
66
-
67
- /** Map of value-hash → amplification factor for a subject's lens. */
68
- function lensFactors(dag: SoulDag, subject?: string): Map<Hash, number> {
69
- const factors = new Map<Hash, number>();
70
- if (!subject) return factors;
71
- for (const node of dag.nodesOfKind("lens")) {
72
- if (isSuperseded(dag, node.hash)) continue;
73
- const lens = node.payload as LensPayload;
74
- if (lens.subject !== subject) continue;
75
- for (const a of lens.amplify) factors.set(a.node, a.factor);
76
- }
77
- return factors;
78
- }
79
-
80
- interface RankedValue {
81
- readonly hash: Hash;
82
- readonly medoidText: string;
83
- readonly conf: number;
84
- readonly weighted: number;
85
- }
86
-
87
- function rankValues(dag: SoulDag, opts: ProjectionOptions): RankedValue[] {
88
- const factors = lensFactors(dag, opts.lens);
89
- const ranked: RankedValue[] = [];
90
- for (const node of dag.nodesOfKind("value")) {
91
- if (isSuperseded(dag, node.hash)) continue; // merged-away values are dead
92
- const value = node.payload as ValuePayload;
93
- const text = evidenceText(dag, value.medoid);
94
- if (!text) continue; // a value with no resolvable medoid is not projectable
95
- const state = dag.stateOf(node.hash);
96
- const base = effectiveSalience(
97
- state,
98
- opts.now,
99
- halfLifeForKind(opts.config, "value"),
100
- );
101
- const factor = factors.get(node.hash) ?? 1;
102
- ranked.push({
103
- hash: node.hash,
104
- medoidText: text,
105
- conf: confidence(state),
106
- weighted: base * factor,
107
- });
108
- }
109
- // Highest weighted salience first; hash tiebreak keeps output deterministic.
110
- ranked.sort(
111
- (a, b) => b.weighted - a.weighted || a.hash.localeCompare(b.hash),
112
- );
113
-
114
- // An explicit retrieval order (context-conditioned) overrides salience rank.
115
- if (opts.order) {
116
- const rank = new Map(opts.order.map((h, i) => [h, i]));
117
- return ranked
118
- .filter((r) => rank.has(r.hash))
119
- .sort((a, b) => rank.get(a.hash)! - rank.get(b.hash)!);
120
- }
121
- return ranked;
122
- }
123
-
124
- function signed(n: number): string {
125
- return `${n >= 0 ? "+" : ""}${n.toFixed(2)}`;
126
- }
127
-
128
- /**
129
- * Project the kernel to its runtime markdown surface. Reflexes are always
130
- * included; values fill the remaining token budget by weighted salience; recent
131
- * spine events provide continuity with whatever budget is left.
132
- */
133
- export function projectRuntime(
134
- dag: SoulDag,
135
- opts: ProjectionOptions,
136
- ): Projection {
137
- const lines: string[] = [];
138
- lines.push("# Talon — compiled identity (soul)");
139
- lines.push(
140
- `<!-- selected from ${dag.size} nodes; root ${dag.root()}; generated, do not edit -->`,
141
- );
142
-
143
- // Reflexes — enforced guarantees, never budgeted away.
144
- const reflexes = dag.nodesOfKind("reflex");
145
- if (reflexes.length) {
146
- lines.push("", "## Reflexes (enforced)");
147
- for (const r of reflexes) {
148
- if (r.payload.kind !== "reflex") continue;
149
- lines.push(`- **${r.payload.name}** — ${r.payload.action}`);
150
- }
151
- }
152
-
153
- let budget = opts.config.runtimeBudgetTokens;
154
- const spend = (s: string): boolean => {
155
- const cost = estimateTokens(s);
156
- if (cost > budget) return false;
157
- budget -= cost;
158
- return true;
159
- };
160
- // Reflexes already spent conceptually fixed cost; charge the header block.
161
- budget -= estimateTokens(lines.join("\n"));
162
-
163
- // Values — salience-ordered, verbatim medoids, until the budget is spent.
164
- const ranked = rankValues(dag, opts);
165
- const included: Hash[] = [];
166
- let dropped = 0;
167
- const valueLines: string[] = [];
168
- for (const v of ranked) {
169
- const line = `- [conf ${signed(v.conf)}] "${v.medoidText}"`;
170
- if (spend(line)) {
171
- valueLines.push(line);
172
- included.push(v.hash);
173
- } else {
174
- dropped++;
175
- }
176
- }
177
- if (valueLines.length) {
178
- const header = opts.lens ? `## Values (with ${opts.lens})` : "## Values";
179
- lines.push("", header, ...valueLines);
180
- }
181
-
182
- // Themes — higher-order reflections over the values.
183
- const themeNodes = dag
184
- .nodesOfKind("theme")
185
- .map((n) => n.payload)
186
- .filter(
187
- (p): p is Extract<typeof p, { kind: "theme" }> => p.kind === "theme",
188
- )
189
- .map((p) => ({
190
- label: p.insight ?? evidenceText(dag, p.medoid) ?? "",
191
- salience: 0,
192
- medoid: p.medoid,
193
- }))
194
- .filter((t) => t.label.length > 0);
195
- const themeLines: string[] = [];
196
- for (const t of themeNodes) {
197
- const line = `- ${t.label}`;
198
- if (spend(line)) themeLines.push(line);
199
- }
200
- if (themeLines.length)
201
- lines.push("", "## Themes (reflections)", ...themeLines);
202
-
203
- // Tensions — opposed values held together, navigated not resolved.
204
- const includedSet = new Set(included);
205
- const tensionLines: string[] = [];
206
- for (const pair of tensionPairs(dag)) {
207
- if (!includedSet.has(pair.a) || !includedSet.has(pair.b)) continue;
208
- const ta = evidenceText(dag, valueMedoid(dag, pair.a));
209
- const tb = evidenceText(dag, valueMedoid(dag, pair.b));
210
- if (!ta || !tb) continue;
211
- const line = `- "${ta}" ⟷ "${tb}"`;
212
- if (spend(line)) tensionLines.push(line);
213
- }
214
- if (tensionLines.length) {
215
- lines.push("", "## Tensions (navigated, not resolved)", ...tensionLines);
216
- }
217
-
218
- // Continuity — most recent spine events first, with remaining budget.
219
- const spine = dag
220
- .nodesOfKind("spine")
221
- .map((n) => n.payload)
222
- .filter(
223
- (p): p is Extract<typeof p, { kind: "spine" }> => p.kind === "spine",
224
- )
225
- .sort((a, b) => b.at - a.at);
226
- const spineLines: string[] = [];
227
- for (const s of spine) {
228
- const line = `- ${s.event}`;
229
- if (spend(line)) spineLines.push(line);
230
- }
231
- if (spineLines.length) lines.push("", "## Continuity (spine)", ...spineLines);
232
-
233
- const text = lines.join("\n");
234
- return {
235
- text,
236
- tokens: estimateTokens(text),
237
- includedValues: included,
238
- droppedValues: dropped,
239
- };
240
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * Soul Kernel — reflection.
3
- *
4
- * Generative Agents (Park et al., 2023) defines reflection as synthesizing
5
- * memories into higher-level inferences over time. Here that synthesis is
6
- * STRUCTURAL and model-free: the soul forms a `theme` over a group of values that
7
- * are both semantically coherent AND frequently co-active. "Coherent and seen
8
- * together" is the mechanical signature of a higher-order trait.
9
- *
10
- * Affinity between two values blends two signals already in the kernel:
11
- *
12
- * affinity(a,b) = w · cos(medoid_a, medoid_b) + (1−w) · normalizedCoactivation
13
- *
14
- * Connected components of the "affinity ≥ threshold" graph become themes. A
15
- * theme's label is its medoid (a real fragment), UNLESS an optional `synthesize`
16
- * callback is supplied — the single, gated place a model may write a natural
17
- * language insight. Critically, that callback can only LABEL; it can never change
18
- * which values are grouped, so the model can't drift identity structure.
19
- */
20
-
21
- import { liveValues } from "./consolidate.js";
22
- import type { SoulDag } from "./dag.js";
23
- import { cosineSimilarity, medoidIndex, type Embedder } from "./embedder.js";
24
- import { effectiveSalience } from "./salience.js";
25
- import type { Hash, SoulConfig, ValuePayload } from "./types.js";
26
- import { halfLifeForKind } from "./types.js";
27
-
28
- export interface ReflectOptions {
29
- readonly now: number;
30
- /** Affinity threshold for grouping values into a theme (0..1). */
31
- readonly affinity?: number;
32
- /** Weight on the semantic term vs. the co-activation term (0..1). */
33
- readonly embeddingWeight?: number;
34
- /**
35
- * Optional gated model label synthesizer. Receives the theme's member
36
- * evidence (verbatim) and may return a short insight; returning undefined
37
- * keeps the medoid label. This is the only model touch in the entire kernel,
38
- * and it is label-only.
39
- */
40
- readonly synthesize?: (
41
- members: readonly { hash: Hash; text: string }[],
42
- ) => string | undefined;
43
- }
44
-
45
- export interface ReflectResult {
46
- readonly created: Hash[];
47
- }
48
-
49
- function medoidText(dag: SoulDag, valueHash: Hash): string {
50
- const v = dag.getNode(valueHash)?.payload as ValuePayload | undefined;
51
- if (!v) return "";
52
- const e = dag.getNode(v.medoid);
53
- return e?.payload.kind === "evidence" ? e.payload.text : "";
54
- }
55
-
56
- function memberEvidence(dag: SoulDag, valueHash: Hash): Hash[] {
57
- const v = dag.getNode(valueHash)?.payload as ValuePayload | undefined;
58
- if (!v) return [];
59
- return [v.medoid, ...v.members];
60
- }
61
-
62
- /**
63
- * Form themes over the value graph. Returns the theme hashes created. Caller
64
- * commits. Async because affinity embeds value medoids.
65
- */
66
- export async function reflect(
67
- dag: SoulDag,
68
- embedder: Embedder,
69
- cfg: SoulConfig,
70
- opts: ReflectOptions,
71
- ): Promise<ReflectResult> {
72
- const threshold = opts.affinity ?? 0.5;
73
- const w = opts.embeddingWeight ?? 0.5;
74
- const values = liveValues(dag);
75
- if (values.length < 2) return { created: [] };
76
-
77
- const reps = await embedder.embed(values.map((h) => medoidText(dag, h)));
78
-
79
- // Normalizer for the co-activation term.
80
- let maxCo = 0;
81
- for (let i = 0; i < values.length; i++) {
82
- for (const e of dag.edgesFrom(values[i]!, "coactivation")) {
83
- maxCo = Math.max(maxCo, e.weight);
84
- }
85
- }
86
- const coWeight = (a: Hash, b: Hash): number => {
87
- if (maxCo === 0) return 0;
88
- const e = dag.getEdge(a, "coactivation", b);
89
- return e ? e.weight / maxCo : 0;
90
- };
91
-
92
- // Union-find over affinity edges.
93
- const parent = values.map((_, i) => i);
94
- const find = (x: number): number => {
95
- while (parent[x] !== x) {
96
- parent[x] = parent[parent[x]!]!;
97
- x = parent[x]!;
98
- }
99
- return x;
100
- };
101
- for (let i = 0; i < values.length; i++) {
102
- for (let j = i + 1; j < values.length; j++) {
103
- const sim = Math.max(0, cosineSimilarity(reps[i]!, reps[j]!));
104
- const affinity = w * sim + (1 - w) * coWeight(values[i]!, values[j]!);
105
- if (affinity >= threshold) parent[find(i)] = find(j);
106
- }
107
- }
108
-
109
- const groups = new Map<number, number[]>();
110
- for (let i = 0; i < values.length; i++) {
111
- const r = find(i);
112
- (groups.get(r) ?? groups.set(r, []).get(r)!).push(i);
113
- }
114
-
115
- const created: Hash[] = [];
116
- for (const idxs of groups.values()) {
117
- if (idxs.length < 2) continue;
118
- const members = idxs.map((i) => values[i]!).sort();
119
-
120
- // Medoid over all evidence across the member values.
121
- const evidence = [
122
- ...new Set(members.flatMap((v) => memberEvidence(dag, v))),
123
- ];
124
- const evVecs = await embedder.embed(
125
- evidence.map((h) => {
126
- const e = dag.getNode(h);
127
- return e?.payload.kind === "evidence" ? e.payload.text : "";
128
- }),
129
- );
130
- const medoid = evidence[medoidIndex(evVecs)]!;
131
-
132
- const insight = opts.synthesize?.(
133
- evidence.map((h) => ({
134
- hash: h,
135
- text:
136
- dag.getNode(h)?.payload.kind === "evidence"
137
- ? (dag.getNode(h)!.payload as { text: string }).text
138
- : "",
139
- })),
140
- );
141
-
142
- const theme = dag.addNode(
143
- {
144
- kind: "theme",
145
- values: members,
146
- medoid,
147
- ...(insight ? { insight } : {}),
148
- },
149
- opts.now,
150
- );
151
- created.push(theme);
152
-
153
- // Theme salience aggregates its members so it can be ranked/projected.
154
- const st = dag.stateOf(theme);
155
- st.salience = members.reduce(
156
- (s, v) =>
157
- s +
158
- effectiveSalience(
159
- dag.stateOf(v),
160
- opts.now,
161
- halfLifeForKind(cfg, "value"),
162
- ),
163
- 0,
164
- );
165
- st.lastActivatedAt = opts.now;
166
- dag.touch(theme);
167
- }
168
-
169
- return { created };
170
- }
@@ -1,164 +0,0 @@
1
- /**
2
- * Soul Kernel — the reflex layer. Behavior the kernel *guarantees*, not behavior
3
- * the prompt merely hopes to elicit.
4
- *
5
- * A reflex is pure data in the DAG (`trigger → guard → action`, content-
6
- * addressed and versioned). The *predicates* it names are tested code in a
7
- * registry here. That separation is deliberate: the data is portable, hashable,
8
- * and rolls back with the rest of identity; the logic is unit-tested TypeScript.
9
- *
10
- * At a decision point the harness builds a ReflexContext — a flat bag of facts
11
- * about the current turn — and asks the enforcer which reflexes fire. A fired
12
- * `block` reflex is an enforceable veto; the model never gets a vote. This is how
13
- * the recurring RULE 0 delivery failure stops being a recurring failure: it
14
- * becomes a structural guard, not a note in memory.
15
- */
16
-
17
- import type { ReflexPayload, ReflexSeverity } from "./types.js";
18
-
19
- /** Flat, serializable facts about a decision point. */
20
- export interface ReflexContext {
21
- readonly facts: Readonly<Record<string, boolean | string | number>>;
22
- }
23
-
24
- /** A named, pure predicate over the context. */
25
- export type ReflexPredicate = (ctx: ReflexContext) => boolean;
26
-
27
- export interface ReflexVerdict {
28
- readonly name: string;
29
- readonly severity: ReflexSeverity;
30
- readonly action: string;
31
- /** True when both trigger and guard held — i.e. the reflex fired. */
32
- readonly fired: boolean;
33
- }
34
-
35
- const bool = (ctx: ReflexContext, key: string): boolean =>
36
- ctx.facts[key] === true;
37
-
38
- const str = (ctx: ReflexContext, key: string): string => {
39
- const v = ctx.facts[key];
40
- return typeof v === "string" ? v : "";
41
- };
42
-
43
- /** Actions taken on a turn after a delivery tool — anything else means silence. */
44
- const DELIVERY_ACTIONS = new Set(["end_turn", "send", "react"]);
45
-
46
- /**
47
- * The built-in predicate registry. These names are referenced by reflex data.
48
- * Keep them small and total; compound conditions get their own named predicate
49
- * rather than being expressed in the data.
50
- */
51
- const BUILTIN_PREDICATES: Readonly<Record<string, ReflexPredicate>> = {
52
- /** Always-on trigger. */
53
- always: () => true,
54
-
55
- /** The turn is ending and a user-facing reply was intended. */
56
- "turn:user-reply-intended": (ctx) =>
57
- bool(ctx, "turn.ending") && bool(ctx, "turn.replyIntended"),
58
-
59
- /** No delivery tool was the last action — the reply would be lost. */
60
- "delivery:not-delivered": (ctx) =>
61
- !DELIVERY_ACTIONS.has(str(ctx, "turn.lastAction")),
62
-
63
- /** About to tell the user a capability/tool is unavailable. */
64
- "claim:tool-unavailable": (ctx) => bool(ctx, "claim.toolUnavailable"),
65
-
66
- /** A deferred-tool search has not yet been performed this turn. */
67
- "search:not-performed": (ctx) => !bool(ctx, "search.performed"),
68
-
69
- /** In a group surface AND about to disclose Dylan-private information. */
70
- "context:group-private-disclosure": (ctx) =>
71
- str(ctx, "surface") === "group" && bool(ctx, "disclosure.dylanPrivate"),
72
-
73
- /** Dylan did not himself raise this fact in this thread. */
74
- "consent:absent": (ctx) => !bool(ctx, "consent.dylanRaisedHere"),
75
- };
76
-
77
- /** Resolve a predicate name, falling back to a registry override map. */
78
- function resolve(
79
- name: string,
80
- overrides?: Readonly<Record<string, ReflexPredicate>>,
81
- ): ReflexPredicate {
82
- const p = overrides?.[name] ?? BUILTIN_PREDICATES[name];
83
- if (!p) throw new Error(`reflex: unknown predicate "${name}"`);
84
- return p;
85
- }
86
-
87
- /** Evaluate one reflex against a context. */
88
- function evaluateReflex(
89
- reflex: ReflexPayload,
90
- ctx: ReflexContext,
91
- overrides?: Readonly<Record<string, ReflexPredicate>>,
92
- ): ReflexVerdict {
93
- const armed = resolve(reflex.trigger, overrides)(ctx);
94
- const tripped = armed && resolve(reflex.guard, overrides)(ctx);
95
- return {
96
- name: reflex.name,
97
- severity: reflex.severity,
98
- action: reflex.action,
99
- fired: tripped,
100
- };
101
- }
102
-
103
- /**
104
- * Evaluate every reflex, returning only those that fired, hardest first. The
105
- * harness blocks the turn if any `block` verdict is present.
106
- */
107
- export function evaluateReflexes(
108
- reflexes: readonly ReflexPayload[],
109
- ctx: ReflexContext,
110
- overrides?: Readonly<Record<string, ReflexPredicate>>,
111
- ): ReflexVerdict[] {
112
- const order: Record<ReflexSeverity, number> = {
113
- block: 0,
114
- warn: 1,
115
- advise: 2,
116
- };
117
- return reflexes
118
- .map((r) => evaluateReflex(r, ctx, overrides))
119
- .filter((v) => v.fired)
120
- .sort((a, b) => order[a.severity] - order[b.severity]);
121
- }
122
-
123
- /** True if any fired verdict is an enforceable block. */
124
- export function isBlocked(verdicts: readonly ReflexVerdict[]): boolean {
125
- return verdicts.some((v) => v.severity === "block");
126
- }
127
-
128
- /**
129
- * The canonical load-bearing reflexes, installed at kernel genesis. These encode
130
- * the three operating scars that recur in Talon's memory: lost replies, claiming
131
- * a deferred tool is unavailable without searching, and leaking Dylan-private
132
- * context into a group.
133
- */
134
- export function seedReflexes(): ReflexPayload[] {
135
- return [
136
- {
137
- kind: "reflex",
138
- name: "RULE-0-DELIVERY",
139
- trigger: "turn:user-reply-intended",
140
- guard: "delivery:not-delivered",
141
- action:
142
- "BLOCK: the reply is private scratchpad until delivered — call end_turn/send/react before ending the turn.",
143
- severity: "block",
144
- },
145
- {
146
- kind: "reflex",
147
- name: "DEFERRED-TOOL-SEARCH",
148
- trigger: "claim:tool-unavailable",
149
- guard: "search:not-performed",
150
- action:
151
- "BLOCK: run tool_search for the capability before telling the user it is unavailable.",
152
- severity: "block",
153
- },
154
- {
155
- kind: "reflex",
156
- name: "PRIVACY-BOUNDARY",
157
- trigger: "context:group-private-disclosure",
158
- guard: "consent:absent",
159
- action:
160
- "BLOCK: do not disclose Dylan-private context in a group unless he raised it here himself.",
161
- severity: "block",
162
- },
163
- ];
164
- }