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.
- package/README.md +3 -3
- package/package.json +3 -2
- package/prompts/README.md +2 -2
- package/prompts/system/memory-recall.md +16 -0
- package/src/app.ts +19 -4
- package/src/backend/claude-sdk/handler.ts +28 -7
- package/src/backend/claude-sdk/one-shot.ts +1 -1
- package/src/backend/claude-sdk/options.ts +7 -6
- package/src/backend/claude-sdk/stream.ts +30 -3
- package/src/backend/claude-sdk/warm.ts +1 -1
- package/src/backend/codex/constants.ts +1 -1
- package/src/backend/codex/factory.ts +2 -2
- package/src/backend/codex/handler/events.ts +1 -1
- package/src/backend/codex/handler/message.ts +26 -12
- package/src/backend/codex/handler/rollout-accounting.ts +1 -1
- package/src/backend/codex/init.ts +1 -1
- package/src/backend/codex/mcp-config.ts +1 -1
- package/src/backend/codex/one-shot.ts +1 -1
- package/src/backend/kilo/handler/message.ts +4 -1
- package/src/backend/openai-agents/constants.ts +1 -1
- package/src/backend/openai-agents/factory.ts +2 -2
- package/src/backend/openai-agents/handler/events.ts +1 -1
- package/src/backend/openai-agents/handler/message.ts +8 -4
- package/src/backend/openai-agents/init.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +1 -1
- package/src/backend/opencode/handler/message.ts +4 -1
- package/src/backend/remote-server/chat-turn.ts +31 -24
- package/src/backend/remote-server/events.ts +3 -3
- package/src/backend/remote-server/factory.ts +6 -3
- package/src/backend/remote-server/index.ts +1 -1
- package/src/backend/remote-server/mcp.ts +1 -1
- package/src/backend/remote-server/one-shot.ts +1 -1
- package/src/backend/remote-server/server-bindings.ts +1 -1
- package/src/backend/remote-server/turn.ts +1 -1
- package/src/backend/runtime/cache/cache-metrics.ts +126 -0
- package/src/backend/{shared → runtime/cache}/cache-telemetry.ts +22 -2
- package/src/backend/{shared → runtime}/index.ts +32 -23
- package/src/backend/{shared → runtime/prompt}/delivery-contract.ts +1 -1
- package/src/backend/{shared → runtime/prompt}/prompt-format.ts +46 -2
- package/src/backend/{shared → runtime/prompt}/system-prompt.ts +4 -4
- package/src/backend/{shared → runtime/turn}/delivered-text.ts +1 -1
- package/src/backend/{shared → runtime/turn}/delivery.ts +2 -2
- package/src/backend/{shared → runtime/turn}/handle-retry.ts +5 -5
- package/src/backend/{shared → runtime/turn}/handler-to-events.ts +24 -10
- package/src/backend/{shared → runtime/turn}/handler-types.ts +7 -1
- package/src/backend/{shared → runtime/turn}/model-retry.ts +2 -2
- package/src/backend/{shared → runtime/turn}/result-events.ts +2 -2
- package/src/backend/{shared → runtime/turn}/stream-state.ts +2 -2
- package/src/backend/{shared → runtime/turn}/turn-interrupt.ts +2 -2
- package/src/backend/{shared → runtime/turn}/turn-phases.ts +6 -6
- package/src/bootstrap.ts +4 -19
- package/src/cli.ts +1 -1
- package/src/core/agent-runtime/capabilities.ts +11 -0
- package/src/core/agent-runtime/contract-tests.ts +92 -1
- package/src/core/agent-runtime/events.ts +1 -1
- package/src/core/background/{isolated-agent.ts → cron/isolated-agent.ts} +3 -3
- package/src/core/background/{job-health.ts → cron/job-health.ts} +1 -1
- package/src/core/background/{job-oneshot.ts → cron/job-oneshot.ts} +5 -5
- package/src/core/background/{job-prompt.ts → cron/job-prompt.ts} +1 -1
- package/src/core/background/{cron.ts → cron/scheduler.ts} +6 -6
- package/src/core/background/{cron-spec.ts → cron/spec.ts} +1 -1
- package/src/core/background/{dream.ts → dream/index.ts} +10 -15
- package/src/core/background/{plan-alerts.ts → pulse/plan-alerts.ts} +3 -3
- package/src/core/background/{pulse.ts → pulse/pulse.ts} +8 -5
- package/src/core/background/triggers/command.ts +1 -1
- package/src/core/config/index.ts +15 -11
- package/src/core/daemon/resource-sampler.ts +121 -0
- package/src/core/engine/dispatcher.ts +16 -0
- package/src/core/engine/gateway-actions/cron.ts +2 -2
- package/src/core/engine/gateway-actions/index.ts +3 -0
- package/src/core/engine/gateway-actions/memory.ts +335 -0
- package/src/core/engine/gateway.ts +0 -6
- package/src/core/memory/import.ts +3 -2
- package/src/core/memory/taps.ts +199 -0
- package/src/core/memory/turn-retrieval.ts +222 -0
- package/src/core/prompt/assemble.ts +2 -12
- package/src/core/prompt/index.ts +2 -2
- package/src/core/prompt/invalidation.ts +1 -1
- package/src/core/tasks/index.ts +1 -1
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/memory.ts +128 -0
- package/src/core/tools/types.ts +1 -0
- package/src/core/weaver/turn-cpu.ts +32 -0
- package/src/core/weaver/weaver.ts +36 -0
- package/src/frontend/discord/admin.ts +1 -1
- package/src/frontend/discord/callbacks/components/backend-select.ts +1 -1
- package/src/frontend/discord/callbacks/components/pulse.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/callbacks/modals.ts +4 -1
- package/src/frontend/discord/commands/admin.ts +1 -35
- package/src/frontend/discord/commands/definitions.ts +0 -11
- package/src/frontend/discord/commands/router.ts +0 -3
- package/src/frontend/discord/commands/session.ts +6 -0
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/discord/middleware.ts +1 -25
- package/src/frontend/discord/runtime.ts +1 -2
- package/src/frontend/native/{auth.ts → bridge/auth.ts} +2 -2
- package/src/frontend/native/{discovery.ts → bridge/discovery.ts} +3 -3
- package/src/frontend/native/{routes → bridge/routes}/chats.ts +1 -1
- package/src/frontend/native/{routes → bridge/routes}/daemon.ts +1 -1
- package/src/frontend/native/{routes → bridge/routes}/host.ts +6 -3
- package/src/frontend/native/{routes → bridge/routes}/pre-auth.ts +1 -1
- package/src/frontend/native/{server.ts → bridge/server.ts} +3 -3
- package/src/frontend/native/{tls.ts → bridge/tls.ts} +2 -2
- package/src/frontend/native/{chat-lifecycle.ts → chats/chat-lifecycle.ts} +3 -3
- package/src/frontend/native/{chat-wire.ts → chats/chat-wire.ts} +4 -4
- package/src/frontend/native/{chats.ts → chats/chats.ts} +4 -4
- package/src/frontend/native/{empty-chat-sweep.ts → chats/empty-chat-sweep.ts} +4 -4
- package/src/frontend/native/{history.ts → chats/history.ts} +7 -7
- package/src/frontend/native/{reset.ts → chats/reset.ts} +7 -7
- package/src/frontend/native/index.ts +13 -10
- package/src/frontend/native/{media.ts → media/media.ts} +3 -3
- package/src/frontend/native/runtime.ts +1 -1
- package/src/frontend/native/{control.ts → surface/control.ts} +4 -4
- package/src/frontend/native/{extensions.ts → surface/extensions.ts} +12 -9
- package/src/frontend/native/{handlers.ts → surface/handlers.ts} +19 -14
- package/src/frontend/native/{logs.ts → surface/logs.ts} +2 -2
- package/src/frontend/native/{memory.ts → surface/memory.ts} +2 -2
- package/src/frontend/native/{models.ts → surface/models.ts} +17 -10
- package/src/frontend/native/{settings.ts → surface/settings.ts} +9 -9
- package/src/frontend/native/{status.ts → surface/status.ts} +3 -3
- package/src/frontend/native/{actions.ts → turn/actions.ts} +7 -4
- package/src/frontend/native/{context.ts → turn/context.ts} +8 -8
- package/src/frontend/native/{emit.ts → turn/emit.ts} +7 -7
- package/src/frontend/native/{queue.ts → turn/queue.ts} +3 -3
- package/src/frontend/native/{turn-meta.ts → turn/turn-meta.ts} +2 -2
- package/src/frontend/native/{turn.ts → turn/turn.ts} +9 -9
- package/src/frontend/shared/model-commands.ts +1 -1
- package/src/frontend/shared/session-status.ts +35 -8
- package/src/frontend/shared/status-context.ts +101 -2
- package/src/frontend/telegram/admin/background.ts +1 -1
- package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
- package/src/frontend/telegram/callbacks/pulse.ts +1 -1
- package/src/frontend/telegram/callbacks/settings.ts +1 -1
- package/src/frontend/telegram/commands/admin.ts +2 -37
- package/src/frontend/telegram/commands/index.ts +1 -1
- package/src/frontend/telegram/commands/session.ts +6 -0
- package/src/frontend/telegram/commands/settings.ts +1 -1
- package/src/frontend/telegram/handlers/messages.ts +0 -10
- package/src/frontend/telegram/index.ts +1 -5
- package/src/frontend/telegram/middleware.ts +1 -15
- package/src/frontend/whatsapp/access.ts +2 -2
- package/src/frontend/whatsapp/actions/history.ts +2 -2
- package/src/frontend/whatsapp/actions/messaging.ts +2 -2
- package/src/frontend/whatsapp/actions/moderation.ts +1 -1
- package/src/frontend/whatsapp/actions/send.ts +1 -1
- package/src/frontend/whatsapp/commands.ts +8 -2
- package/src/frontend/whatsapp/{connection.ts → connection/connection.ts} +5 -5
- package/src/frontend/whatsapp/{pairing-service.ts → connection/pairing-service.ts} +3 -3
- package/src/frontend/whatsapp/{wa-logger.ts → connection/wa-logger.ts} +2 -2
- package/src/frontend/whatsapp/index.ts +4 -4
- package/src/frontend/whatsapp/{inbound.ts → messages/inbound.ts} +15 -15
- package/src/frontend/whatsapp/{media-store.ts → messages/media-store.ts} +4 -4
- package/src/frontend/whatsapp/{message-store.ts → messages/message-store.ts} +1 -1
- package/src/frontend/whatsapp/{turn-recovery.ts → messages/turn-recovery.ts} +4 -4
- package/src/frontend/whatsapp/registry.ts +1 -1
- package/src/frontend/whatsapp/runtime.ts +1 -1
- package/src/index.ts +1 -1
- package/src/storage/db.ts +6 -1
- package/src/storage/memory.ts +59 -8
- package/src/storage/metrics.ts +38 -0
- package/src/storage/repositories/goals-repo.ts +2 -1
- package/src/storage/repositories/sessions-repo.ts +6 -0
- package/src/storage/session-record.ts +13 -2
- package/src/storage/sessions.ts +12 -0
- package/src/storage/sql/db.sql +5 -0
- package/src/storage/sql/schema.sql +4 -0
- package/src/storage/sql/sessions.sql +3 -3
- package/src/storage/sql/statements.generated.ts +10 -3
- package/src/storage/sql/turn-meta.sql +1 -1
- package/src/util/boot-timer.ts +15 -1
- package/src/util/chat-id.ts +30 -0
- package/src/util/concurrency.ts +1 -1
- package/src/util/log.ts +1 -1
- package/src/util/paths.ts +0 -2
- package/src/core/soul/README.md +0 -110
- package/src/core/soul/RESEARCH.md +0 -98
- package/src/core/soul/associative.ts +0 -98
- package/src/core/soul/centrality.ts +0 -98
- package/src/core/soul/cluster.ts +0 -83
- package/src/core/soul/compiler.ts +0 -207
- package/src/core/soul/consolidate.ts +0 -179
- package/src/core/soul/critic.ts +0 -162
- package/src/core/soul/dag.ts +0 -265
- package/src/core/soul/delta.ts +0 -123
- package/src/core/soul/drift.ts +0 -99
- package/src/core/soul/embedder.ts +0 -129
- package/src/core/soul/emergent-critic.ts +0 -96
- package/src/core/soul/forgetting.ts +0 -131
- package/src/core/soul/governance.ts +0 -93
- package/src/core/soul/hash.ts +0 -97
- package/src/core/soul/hdc.ts +0 -154
- package/src/core/soul/kernel.ts +0 -540
- package/src/core/soul/lattice.ts +0 -103
- package/src/core/soul/lens.ts +0 -110
- package/src/core/soul/projector.ts +0 -240
- package/src/core/soul/reflect.ts +0 -170
- package/src/core/soul/reflex.ts +0 -164
- package/src/core/soul/retrieve.ts +0 -146
- package/src/core/soul/salience.ts +0 -146
- package/src/core/soul/service.ts +0 -204
- package/src/core/soul/settings.ts +0 -47
- package/src/core/soul/signals.ts +0 -117
- package/src/core/soul/talon-embedder.ts +0 -80
- package/src/core/soul/taps.ts +0 -199
- package/src/core/soul/types.ts +0 -298
- package/src/core/soul/valence.ts +0 -83
- /package/src/backend/{shared → runtime}/frontends.ts +0 -0
- /package/src/backend/{shared → runtime}/metrics.ts +0 -0
- /package/src/backend/{shared → runtime}/sleep.ts +0 -0
- /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
- /package/src/backend/{shared → runtime}/usage.ts +0 -0
- /package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
- /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
- /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
- /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
- /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
- /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
- /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — the embedder boundary and the vector math.
|
|
3
|
-
*
|
|
4
|
-
* The embedder is the ONLY neural component the kernel may touch, and it is a
|
|
5
|
-
* frozen fixed function, not a reasoning agent — so it stays inside the
|
|
6
|
-
* model-free principle. It is injected: production wires MemPalace's local
|
|
7
|
-
* embedding model; tests and offline operation use the deterministic
|
|
8
|
-
* `HashingEmbedder` below.
|
|
9
|
-
*
|
|
10
|
-
* Everything the kernel does *with* embeddings — clustering evidence into values,
|
|
11
|
-
* picking a medoid label, measuring contradiction/drift — is plain vector
|
|
12
|
-
* arithmetic implemented here. No model decides what relates to what; geometry
|
|
13
|
-
* does.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { createHash } from "node:crypto";
|
|
17
|
-
|
|
18
|
-
/** A pluggable text→vector function. Vectors should be L2-normalized. */
|
|
19
|
-
export interface Embedder {
|
|
20
|
-
readonly dim: number;
|
|
21
|
-
embed(texts: readonly string[]): Promise<number[][]>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ── Vector math ──────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
function dot(a: readonly number[], b: readonly number[]): number {
|
|
27
|
-
let s = 0;
|
|
28
|
-
for (let i = 0; i < a.length; i++) s += a[i]! * b[i]!;
|
|
29
|
-
return s;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function norm(a: readonly number[]): number {
|
|
33
|
-
return Math.sqrt(dot(a, a));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** L2-normalize a vector; a zero vector is returned unchanged. */
|
|
37
|
-
export function normalize(a: readonly number[]): number[] {
|
|
38
|
-
const n = norm(a);
|
|
39
|
-
if (n === 0) return [...a];
|
|
40
|
-
return a.map((x) => x / n);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Cosine similarity in [-1, 1] (assumes finite inputs). */
|
|
44
|
-
export function cosineSimilarity(
|
|
45
|
-
a: readonly number[],
|
|
46
|
-
b: readonly number[],
|
|
47
|
-
): number {
|
|
48
|
-
const na = norm(a);
|
|
49
|
-
const nb = norm(b);
|
|
50
|
-
if (na === 0 || nb === 0) return 0;
|
|
51
|
-
return dot(a, b) / (na * nb);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Cosine distance in [0, 2]. 0 = identical direction. */
|
|
55
|
-
export function cosineDistance(
|
|
56
|
-
a: readonly number[],
|
|
57
|
-
b: readonly number[],
|
|
58
|
-
): number {
|
|
59
|
-
return 1 - cosineSimilarity(a, b);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Mean vector of a set, L2-normalized. The cluster centroid. */
|
|
63
|
-
export function centroid(vectors: readonly (readonly number[])[]): number[] {
|
|
64
|
-
if (vectors.length === 0) return [];
|
|
65
|
-
const dim = vectors[0]!.length;
|
|
66
|
-
const acc = Array.from({ length: dim }, () => 0);
|
|
67
|
-
for (const v of vectors) {
|
|
68
|
-
for (let i = 0; i < dim; i++) acc[i]! += v[i]!;
|
|
69
|
-
}
|
|
70
|
-
for (let i = 0; i < dim; i++) acc[i]! /= vectors.length;
|
|
71
|
-
return normalize(acc);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Index of the medoid: the member with minimum total cosine distance to all
|
|
76
|
-
* others — the single most central *real* item. This is what labels a value:
|
|
77
|
-
* never generated prose, always a genuine fragment.
|
|
78
|
-
*/
|
|
79
|
-
export function medoidIndex(vectors: readonly (readonly number[])[]): number {
|
|
80
|
-
if (vectors.length === 0) return -1;
|
|
81
|
-
let best = 0;
|
|
82
|
-
let bestSum = Infinity;
|
|
83
|
-
for (let i = 0; i < vectors.length; i++) {
|
|
84
|
-
let sum = 0;
|
|
85
|
-
for (let j = 0; j < vectors.length; j++) {
|
|
86
|
-
if (i !== j) sum += cosineDistance(vectors[i]!, vectors[j]!);
|
|
87
|
-
}
|
|
88
|
-
if (sum < bestSum) {
|
|
89
|
-
bestSum = sum;
|
|
90
|
-
best = i;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return best;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Deterministic offline embedder ───────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* A frozen, dependency-free embedder via feature hashing over character n-grams
|
|
100
|
-
* and word tokens. Deterministic and model-free: similar strings share n-grams
|
|
101
|
-
* and therefore land near each other in cosine space. Crude next to a real
|
|
102
|
-
* sentence encoder, but it is a genuine fixed function — suitable for tests,
|
|
103
|
-
* offline operation, and as a fallback when the local model is unavailable.
|
|
104
|
-
*/
|
|
105
|
-
export class HashingEmbedder implements Embedder {
|
|
106
|
-
constructor(readonly dim = 256) {}
|
|
107
|
-
|
|
108
|
-
async embed(texts: readonly string[]): Promise<number[][]> {
|
|
109
|
-
return texts.map((t) => this.embedOne(t));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private embedOne(text: string): number[] {
|
|
113
|
-
const vec = Array.from({ length: this.dim }, () => 0);
|
|
114
|
-
const lower = text.toLowerCase();
|
|
115
|
-
const add = (feature: string): void => {
|
|
116
|
-
const h = createHash("md5").update(feature).digest();
|
|
117
|
-
const idx = ((h[0]! << 8) | h[1]!) % this.dim;
|
|
118
|
-
const sign = (h[2]! & 1) === 0 ? 1 : -1; // sign hashing reduces bias
|
|
119
|
-
vec[idx]! += sign;
|
|
120
|
-
};
|
|
121
|
-
// word tokens
|
|
122
|
-
for (const w of lower.split(/[^a-z0-9]+/).filter(Boolean)) add(`w:${w}`);
|
|
123
|
-
// character 3-grams over the normalized stream
|
|
124
|
-
const stream = lower.replace(/\s+/g, " ");
|
|
125
|
-
for (let i = 0; i + 3 <= stream.length; i++)
|
|
126
|
-
add(`g:${stream.slice(i, i + 3)}`);
|
|
127
|
-
return normalize(vec);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — the emergent critic.
|
|
3
|
-
*
|
|
4
|
-
* The lexical Critic (critic.ts) hardcodes three failure modes and their word
|
|
5
|
-
* lists. That is a useful bootstrap, but it is exactly the kind of "overly
|
|
6
|
-
* programmed and specific" thing the soul should grow past. The emergent critic
|
|
7
|
-
* derives failure modes from data: corrections carry negative valence, and
|
|
8
|
-
* clusters of corrections ARE the failure modes — discovered, not declared.
|
|
9
|
-
*
|
|
10
|
-
* To assess a candidate reply, embed it and measure its proximity to those
|
|
11
|
-
* correction clusters. If it lands near a cluster of past corrections, it is at
|
|
12
|
-
* risk of repeating that mistake — and the warning is a real past correction
|
|
13
|
-
* (the cluster's medoid), not an invented category. As Dylan corrects Talon, new
|
|
14
|
-
* failure modes appear on their own; nothing here is hand-authored.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { clusterEvidence } from "./cluster.js";
|
|
18
|
-
import type { SoulDag } from "./dag.js";
|
|
19
|
-
import { cosineDistance, type Embedder } from "./embedder.js";
|
|
20
|
-
import type { EvidencePayload, Hash } from "./types.js";
|
|
21
|
-
|
|
22
|
-
export interface EmergentFailureMode {
|
|
23
|
-
/** A real past correction representing the cluster — never generated. */
|
|
24
|
-
readonly label: string;
|
|
25
|
-
readonly medoid: Hash;
|
|
26
|
-
readonly centroid: readonly number[];
|
|
27
|
-
/** Number of corrections in this mode. */
|
|
28
|
-
readonly size: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface FailureRisk {
|
|
32
|
-
readonly label: string;
|
|
33
|
-
readonly distance: number;
|
|
34
|
-
readonly atRisk: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function correctionEvidence(dag: SoulDag): { hash: Hash; text: string }[] {
|
|
38
|
-
const out: { hash: Hash; text: string }[] = [];
|
|
39
|
-
for (const node of dag.nodesOfKind("evidence")) {
|
|
40
|
-
const p = node.payload as EvidencePayload;
|
|
41
|
-
if (p.source.origin === "correction")
|
|
42
|
-
out.push({ hash: node.hash, text: p.text });
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Derive failure modes by clustering Talon's actual corrections. `threshold` is
|
|
49
|
-
* the cluster tightness; each resulting cluster is one emergent failure mode,
|
|
50
|
-
* labeled by its medoid (a representative real correction).
|
|
51
|
-
*/
|
|
52
|
-
export async function deriveFailureModes(
|
|
53
|
-
dag: SoulDag,
|
|
54
|
-
embedder: Embedder,
|
|
55
|
-
threshold: number,
|
|
56
|
-
): Promise<EmergentFailureMode[]> {
|
|
57
|
-
const corrections = correctionEvidence(dag);
|
|
58
|
-
if (corrections.length === 0) return [];
|
|
59
|
-
|
|
60
|
-
const vectors = await embedder.embed(corrections.map((c) => c.text));
|
|
61
|
-
const embedded = corrections.map((c, i) => ({
|
|
62
|
-
hash: c.hash,
|
|
63
|
-
vector: vectors[i]!,
|
|
64
|
-
}));
|
|
65
|
-
const clusters = clusterEvidence(embedded, threshold);
|
|
66
|
-
|
|
67
|
-
const textByHash = new Map(corrections.map((c) => [c.hash, c.text]));
|
|
68
|
-
return clusters.map((cl) => ({
|
|
69
|
-
label: textByHash.get(cl.medoid) ?? "",
|
|
70
|
-
medoid: cl.medoid,
|
|
71
|
-
centroid: cl.centroid,
|
|
72
|
-
size: cl.members.length,
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Assess a candidate reply against the emergent failure modes. Returns the risk
|
|
78
|
-
* per mode (sorted nearest first); `atRisk` when the candidate sits within
|
|
79
|
-
* `riskDistance` of a correction cluster — i.e. it resembles something Talon was
|
|
80
|
-
* already corrected for.
|
|
81
|
-
*/
|
|
82
|
-
export async function assessText(
|
|
83
|
-
text: string,
|
|
84
|
-
modes: readonly EmergentFailureMode[],
|
|
85
|
-
embedder: Embedder,
|
|
86
|
-
riskDistance: number,
|
|
87
|
-
): Promise<FailureRisk[]> {
|
|
88
|
-
if (modes.length === 0) return [];
|
|
89
|
-
const [vec] = await embedder.embed([text]);
|
|
90
|
-
return modes
|
|
91
|
-
.map((m) => {
|
|
92
|
-
const distance = cosineDistance(vec!, m.centroid);
|
|
93
|
-
return { label: m.label, distance, atRisk: distance <= riskDistance };
|
|
94
|
-
})
|
|
95
|
-
.sort((a, b) => a.distance - b.distance);
|
|
96
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — adaptive forgetting (FSRS / DSR-inspired).
|
|
3
|
-
*
|
|
4
|
-
* Fixed-half-life exponential decay treats a trait recalled a hundred times the
|
|
5
|
-
* same as one recalled once. Human memory does not work that way, and neither
|
|
6
|
-
* should the soul. This module implements a Difficulty-Stability-Retrievability
|
|
7
|
-
* style model (Wozniak's DSR; the basis of FSRS, the algorithm modern Anki uses),
|
|
8
|
-
* adapted to continuous time:
|
|
9
|
-
*
|
|
10
|
-
* - Each node carries a STABILITY S (a time constant). Retrievability follows a
|
|
11
|
-
* POWER LAW, R(t) = (1 + FACTOR·t/S)^DECAY, which fits human forgetting far
|
|
12
|
-
* better than an exponential (Wickelgren; FSRS). At t = S, R ≈ 0.9.
|
|
13
|
-
* - Successful recall GROWS stability, and grows it MORE when the recall
|
|
14
|
-
* happened at low retrievability (the spacing effect) — so a trait that keeps
|
|
15
|
-
* proving itself across time becomes durable. A correction (negative valence)
|
|
16
|
-
* SHRINKS stability, so a discredited trait becomes easy to forget.
|
|
17
|
-
*
|
|
18
|
-
* The effective strength used for projection is salience × retrievability. The
|
|
19
|
-
* net effect: core identity persists, fads evaporate — emergently, from the
|
|
20
|
-
* timing and outcome of activations, not a hand-set constant.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { ActivationState, NodeKind, SoulConfig } from "./types.js";
|
|
24
|
-
import { halfLifeForKind } from "./types.js";
|
|
25
|
-
|
|
26
|
-
// FSRS power-law constants: R = (1 + FACTOR·t/S)^DECAY, with R(S) ≈ 0.9.
|
|
27
|
-
const DECAY = -0.5;
|
|
28
|
-
const FACTOR = 19 / 81;
|
|
29
|
-
|
|
30
|
-
const MIN_STABILITY = 1; // never let a node become un-forgettable-fast to 0
|
|
31
|
-
const GROWTH = 2.0; // spacing-effect stability gain scale
|
|
32
|
-
const LAPSE = 0.5; // stability multiplier on a correction
|
|
33
|
-
|
|
34
|
-
/** Power-law retrievability in (0, 1]. */
|
|
35
|
-
export function retrievability(elapsedMs: number, stability: number): number {
|
|
36
|
-
if (elapsedMs <= 0) return 1;
|
|
37
|
-
const s = Math.max(MIN_STABILITY, stability);
|
|
38
|
-
return Math.pow(1 + (FACTOR * elapsedMs) / s, DECAY);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Initial stability for a node's first exposure, honoring the per-kind
|
|
43
|
-
* half-life when the kind is known. No-decay kinds (infinite half-life) never
|
|
44
|
-
* reach the FSRS path, but fall back to the base rate defensively so an
|
|
45
|
-
* Infinity is never stored into state (it would not survive JSON persistence).
|
|
46
|
-
*/
|
|
47
|
-
export function initialStability(cfg: SoulConfig, kind?: NodeKind): number {
|
|
48
|
-
const hl = halfLifeForKind(cfg, kind);
|
|
49
|
-
return Number.isFinite(hl) ? hl : cfg.decayHalfLifeMs;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Stability after a recall. Positive valence grows it (more when retrievability
|
|
54
|
-
* was low — the spacing effect); negative valence (a correction) shrinks it.
|
|
55
|
-
*/
|
|
56
|
-
export function nextStability(
|
|
57
|
-
stability: number,
|
|
58
|
-
r: number,
|
|
59
|
-
valence: number,
|
|
60
|
-
): number {
|
|
61
|
-
const s = Math.max(MIN_STABILITY, stability);
|
|
62
|
-
if (valence < 0) return Math.max(MIN_STABILITY, s * LAPSE);
|
|
63
|
-
// grow more when r is low (recall that "shouldn't" have succeeded teaches most)
|
|
64
|
-
const gain = 1 + GROWTH * (1 - r) * Math.max(0.25, Math.min(1, valence));
|
|
65
|
-
return s * gain;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Salience after lazy power-law decay to `now`, using the node's stability when
|
|
70
|
-
* present. Falls back to the caller's exponential decay when stability is unset.
|
|
71
|
-
* A no-decay kind (infinite half-life, e.g. reflexes) returns the stored
|
|
72
|
-
* salience untouched regardless of stability.
|
|
73
|
-
*/
|
|
74
|
-
export function effectiveStrength(
|
|
75
|
-
state: ActivationState,
|
|
76
|
-
now: number,
|
|
77
|
-
cfg: SoulConfig,
|
|
78
|
-
kind?: NodeKind,
|
|
79
|
-
): number {
|
|
80
|
-
const halfLifeMs = halfLifeForKind(cfg, kind);
|
|
81
|
-
if (!Number.isFinite(halfLifeMs)) return state.salience;
|
|
82
|
-
if (state.stability === undefined) {
|
|
83
|
-
// exponential fallback (mirrors salience.effectiveSalience)
|
|
84
|
-
const f = Math.pow(
|
|
85
|
-
0.5,
|
|
86
|
-
Math.max(0, now - state.lastActivatedAt) / halfLifeMs,
|
|
87
|
-
);
|
|
88
|
-
return state.salience * f;
|
|
89
|
-
}
|
|
90
|
-
return (
|
|
91
|
-
state.salience *
|
|
92
|
-
retrievability(now - state.lastActivatedAt, state.stability)
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface FsrsReinforceOptions {
|
|
97
|
-
readonly now: number;
|
|
98
|
-
readonly cfg: SoulConfig;
|
|
99
|
-
readonly amount: number;
|
|
100
|
-
readonly valence: number;
|
|
101
|
-
/** Node kind, when known — selects the per-kind decay half-life. */
|
|
102
|
-
readonly kind?: NodeKind;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Reinforcement under adaptive forgetting: decays salience by retrievability,
|
|
107
|
-
* adds the increment, accumulates evidence, and updates stability per the
|
|
108
|
-
* spacing/lapse rule. Mutates and returns the state. No-decay kinds (infinite
|
|
109
|
-
* half-life) accumulate undecayed and skip stability bookkeeping entirely —
|
|
110
|
-
* their strength is time-invariant by construction.
|
|
111
|
-
*/
|
|
112
|
-
export function reinforceFsrs(
|
|
113
|
-
state: ActivationState,
|
|
114
|
-
opts: FsrsReinforceOptions,
|
|
115
|
-
): ActivationState {
|
|
116
|
-
if (!Number.isFinite(halfLifeForKind(opts.cfg, opts.kind))) {
|
|
117
|
-
state.salience += opts.amount;
|
|
118
|
-
state.evidence += opts.valence;
|
|
119
|
-
state.activations += 1;
|
|
120
|
-
state.lastActivatedAt = opts.now;
|
|
121
|
-
return state;
|
|
122
|
-
}
|
|
123
|
-
const stability = state.stability ?? initialStability(opts.cfg, opts.kind);
|
|
124
|
-
const r = retrievability(opts.now - state.lastActivatedAt, stability);
|
|
125
|
-
state.salience = state.salience * r + opts.amount;
|
|
126
|
-
state.evidence += opts.valence;
|
|
127
|
-
state.stability = nextStability(stability, r, opts.valence);
|
|
128
|
-
state.activations += 1;
|
|
129
|
-
state.lastActivatedAt = opts.now;
|
|
130
|
-
return state;
|
|
131
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — governance (the protected-node approval queue).
|
|
3
|
-
*
|
|
4
|
-
* Most of the soul grows autonomously; that is the point. But some facets are
|
|
5
|
-
* load-bearing enough that drift must never happen silently — the reflexes that
|
|
6
|
-
* the harness enforces, and any change to core identity. The mechanical compiler
|
|
7
|
-
* may *propose* such mutations, but they do not take effect until a human
|
|
8
|
-
* approves them. This is the concrete machinery behind "identity cannot drift
|
|
9
|
-
* behind your back".
|
|
10
|
-
*
|
|
11
|
-
* The queue is pure data (persisted with the kernel). Proposing enqueues a
|
|
12
|
-
* payload + reason; approving returns the payload to the kernel to materialize;
|
|
13
|
-
* rejecting drops it. Nothing here writes to the DAG — the kernel owns that — so
|
|
14
|
-
* the queue stays a simple, auditable ledger.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { randomUUID } from "node:crypto";
|
|
18
|
-
import { hashPayload } from "./hash.js";
|
|
19
|
-
import type { NodePayload } from "./types.js";
|
|
20
|
-
|
|
21
|
-
type ProposalStatus = "pending" | "approved" | "rejected";
|
|
22
|
-
|
|
23
|
-
export interface Proposal {
|
|
24
|
-
readonly id: string;
|
|
25
|
-
readonly at: number;
|
|
26
|
-
/** Content hash of the proposed node — stable identity for dedup. */
|
|
27
|
-
readonly target: string;
|
|
28
|
-
readonly payload: NodePayload;
|
|
29
|
-
/** Why the compiler proposed this (templated, never model-written). */
|
|
30
|
-
readonly reason: string;
|
|
31
|
-
status: ProposalStatus;
|
|
32
|
-
resolvedAt?: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ApprovalSnapshot {
|
|
36
|
-
readonly proposals: readonly Proposal[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class ApprovalQueue {
|
|
40
|
-
private proposals: Proposal[] = [];
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Enqueue a proposed protected mutation. Idempotent on payload content: a
|
|
44
|
-
* pending proposal for the same node is returned rather than duplicated.
|
|
45
|
-
*/
|
|
46
|
-
propose(payload: NodePayload, reason: string, at: number): Proposal {
|
|
47
|
-
const target = hashPayload(payload);
|
|
48
|
-
const existing = this.proposals.find(
|
|
49
|
-
(p) => p.target === target && p.status === "pending",
|
|
50
|
-
);
|
|
51
|
-
if (existing) return existing;
|
|
52
|
-
const proposal: Proposal = {
|
|
53
|
-
id: randomUUID(),
|
|
54
|
-
at,
|
|
55
|
-
target,
|
|
56
|
-
payload,
|
|
57
|
-
reason,
|
|
58
|
-
status: "pending",
|
|
59
|
-
};
|
|
60
|
-
this.proposals.push(proposal);
|
|
61
|
-
return proposal;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
pending(): Proposal[] {
|
|
65
|
-
return this.proposals.filter((p) => p.status === "pending");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
get(id: string): Proposal | undefined {
|
|
69
|
-
return this.proposals.find((p) => p.id === id);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Resolve a pending proposal. Returns the payload to apply when approved, or
|
|
74
|
-
* undefined otherwise (rejected, missing, or already resolved).
|
|
75
|
-
*/
|
|
76
|
-
resolve(id: string, approved: boolean, at: number): NodePayload | undefined {
|
|
77
|
-
const p = this.proposals.find((x) => x.id === id && x.status === "pending");
|
|
78
|
-
if (!p) return undefined;
|
|
79
|
-
p.status = approved ? "approved" : "rejected";
|
|
80
|
-
p.resolvedAt = at;
|
|
81
|
-
return approved ? p.payload : undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
snapshot(): ApprovalSnapshot {
|
|
85
|
-
return { proposals: this.proposals.map((p) => ({ ...p })) };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static restore(snap: ApprovalSnapshot): ApprovalQueue {
|
|
89
|
-
const q = new ApprovalQueue();
|
|
90
|
-
q.proposals = snap.proposals.map((p) => ({ ...p }));
|
|
91
|
-
return q;
|
|
92
|
-
}
|
|
93
|
-
}
|
package/src/core/soul/hash.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content addressing for the Soul kernel.
|
|
3
|
-
*
|
|
4
|
-
* A node's identity is the sha-256 of its *canonical* serialization. Canonical
|
|
5
|
-
* means: deterministic key ordering, no whitespace, explicit rejection of
|
|
6
|
-
* non-data values. Two nodes with semantically identical content therefore hash
|
|
7
|
-
* identically — which is what makes dedup, provenance, and partial recompilation
|
|
8
|
-
* structural rather than bolted-on.
|
|
9
|
-
*
|
|
10
|
-
* Only CONTENT is ever hashed here. Mutable state (salience, edge weights) is
|
|
11
|
-
* deliberately excluded from node identity (see types.ts).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
15
|
-
import type { Hash, NodePayload } from "./types.js";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Canonical JSON: object keys sorted lexicographically, arrays preserved in
|
|
19
|
-
* order, no insignificant whitespace. Rejects `undefined`, functions, symbols,
|
|
20
|
-
* NaN and ±Infinity so a hash can never depend on an unserializable value.
|
|
21
|
-
*
|
|
22
|
-
* This is intentionally stricter than JSON.stringify: we want a single
|
|
23
|
-
* canonical byte string per logical value, and we want loud failure on anything
|
|
24
|
-
* that could silently vary.
|
|
25
|
-
*/
|
|
26
|
-
export function canonicalize(value: unknown): string {
|
|
27
|
-
return encode(value);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function encode(value: unknown): string {
|
|
31
|
-
if (value === null) return "null";
|
|
32
|
-
|
|
33
|
-
const t = typeof value;
|
|
34
|
-
|
|
35
|
-
if (t === "string") return JSON.stringify(value);
|
|
36
|
-
|
|
37
|
-
if (t === "number") {
|
|
38
|
-
if (!Number.isFinite(value as number)) {
|
|
39
|
-
throw new TypeError(`canonicalize: non-finite number ${String(value)}`);
|
|
40
|
-
}
|
|
41
|
-
return JSON.stringify(value);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (t === "boolean") return value ? "true" : "false";
|
|
45
|
-
|
|
46
|
-
if (t === "bigint") {
|
|
47
|
-
throw new TypeError("canonicalize: bigint is not serializable");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (Array.isArray(value)) {
|
|
51
|
-
return `[${value.map(encode).join(",")}]`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (t === "object") {
|
|
55
|
-
const obj = value as Record<string, unknown>;
|
|
56
|
-
const keys = Object.keys(obj).sort();
|
|
57
|
-
const parts: string[] = [];
|
|
58
|
-
for (const key of keys) {
|
|
59
|
-
const v = obj[key];
|
|
60
|
-
// Drop keys whose value is undefined, mirroring JSON semantics, but only
|
|
61
|
-
// for objects — undefined inside arrays is rejected below.
|
|
62
|
-
if (v === undefined) continue;
|
|
63
|
-
parts.push(`${JSON.stringify(key)}:${encode(v)}`);
|
|
64
|
-
}
|
|
65
|
-
return `{${parts.join(",")}}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// undefined, function, symbol
|
|
69
|
-
throw new TypeError(`canonicalize: cannot serialize ${t}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** sha-256 of an arbitrary canonicalized value, prefixed "sha256:". */
|
|
73
|
-
export function hashContent(value: unknown): Hash {
|
|
74
|
-
const canonical = canonicalize(value);
|
|
75
|
-
const digest = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
76
|
-
return `sha256:${digest}` as Hash;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** The content address of a node payload. This is the node's identity. */
|
|
80
|
-
export function hashPayload(payload: NodePayload): Hash {
|
|
81
|
-
return hashContent(payload);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* A Merkle root over a set of child hashes. Order-independent: the children are
|
|
86
|
-
* sorted before hashing, so the root depends only on the *set* of content
|
|
87
|
-
* present, not the order it was inserted. This is the structural version id.
|
|
88
|
-
*/
|
|
89
|
-
export function merkleRoot(childHashes: Iterable<Hash>): Hash {
|
|
90
|
-
const sorted = [...childHashes].sort();
|
|
91
|
-
return hashContent({ merkle: sorted });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** True when a string is a well-formed kernel content address. */
|
|
95
|
-
export function isHash(value: unknown): value is Hash {
|
|
96
|
-
return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
|
|
97
|
-
}
|
package/src/core/soul/hdc.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — hyperdimensional computing (Vector-Symbolic Architecture).
|
|
3
|
-
*
|
|
4
|
-
* Kanerva, "Hyperdimensional Computing" (2009). High-dimensional bipolar vectors
|
|
5
|
-
* (±1, here a few thousand dims) support a small algebra that is the basis of a
|
|
6
|
-
* compositional, model-free associative memory:
|
|
7
|
-
*
|
|
8
|
-
* - bind (⊗) elementwise product. Combines two hypervectors into one
|
|
9
|
-
* DISSIMILAR to both, and is its own inverse (bind twice by the
|
|
10
|
-
* same key recovers the other operand). Used to pair a role with a
|
|
11
|
-
* filler: context ⊗ value.
|
|
12
|
-
* - bundle (+) elementwise majority. Superposes many hypervectors into one
|
|
13
|
-
* SIMILAR to all of them — a set, or a memory of many episodes.
|
|
14
|
-
* - permute (ρ) cyclic shift. Encodes order/role without collision.
|
|
15
|
-
* - cleanup nearest stored item under cosine — denoises a recalled vector
|
|
16
|
-
* back to a known symbol.
|
|
17
|
-
*
|
|
18
|
-
* The soul uses this for episodic recall: bind the current context to the value
|
|
19
|
-
* that worked, bundle those bindings into one memory hypervector, and later query
|
|
20
|
-
* "in a context like this, what do I do?" by unbinding and cleaning up. It is a
|
|
21
|
-
* genuinely compositional memory, and it is pure integer arithmetic — no model.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { createHash } from "node:crypto";
|
|
25
|
-
|
|
26
|
-
export type Hypervector = Int8Array;
|
|
27
|
-
|
|
28
|
-
const DEFAULT_HD_DIM = 4096;
|
|
29
|
-
|
|
30
|
-
/** Deterministic ±1 hypervector for a symbol — the same token always maps here. */
|
|
31
|
-
export function symbolVector(token: string, dim = DEFAULT_HD_DIM): Hypervector {
|
|
32
|
-
const v = new Int8Array(dim);
|
|
33
|
-
// Seed a tiny xorshift PRNG from the token hash; fill with ±1.
|
|
34
|
-
const digest = createHash("sha256").update(token).digest();
|
|
35
|
-
let s0 = digest.readUInt32LE(0) || 1;
|
|
36
|
-
let s1 = digest.readUInt32LE(4) || 2;
|
|
37
|
-
for (let i = 0; i < dim; i++) {
|
|
38
|
-
// xorshift128-ish
|
|
39
|
-
let x = s0;
|
|
40
|
-
const y = s1;
|
|
41
|
-
s0 = y;
|
|
42
|
-
x ^= x << 23;
|
|
43
|
-
x ^= x >>> 17;
|
|
44
|
-
x ^= y ^ (y >>> 26);
|
|
45
|
-
s1 = x >>> 0;
|
|
46
|
-
v[i] = ((s1 >>> (i % 31)) & 1) === 0 ? 1 : -1;
|
|
47
|
-
}
|
|
48
|
-
return v;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Elementwise bind (product). Self-inverse for bipolar vectors. */
|
|
52
|
-
export function bind(a: Hypervector, b: Hypervector): Hypervector {
|
|
53
|
-
const out = new Int8Array(a.length);
|
|
54
|
-
for (let i = 0; i < a.length; i++) out[i] = (a[i]! * b[i]!) as number;
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Majority-rule bundle of several hypervectors; empty ⇒ zero vector. */
|
|
59
|
-
export function bundle(vectors: readonly Hypervector[]): Hypervector {
|
|
60
|
-
const dim = vectors[0]?.length ?? DEFAULT_HD_DIM;
|
|
61
|
-
const acc = new Int32Array(dim);
|
|
62
|
-
for (const v of vectors) for (let i = 0; i < dim; i++) acc[i]! += v[i]!;
|
|
63
|
-
const out = new Int8Array(dim);
|
|
64
|
-
for (let i = 0; i < dim; i++) out[i] = acc[i]! >= 0 ? 1 : -1; // ties → +1
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Cyclic shift by k (encodes role/order). */
|
|
69
|
-
export function permute(v: Hypervector, k = 1): Hypervector {
|
|
70
|
-
const dim = v.length;
|
|
71
|
-
const out = new Int8Array(dim);
|
|
72
|
-
const shift = ((k % dim) + dim) % dim;
|
|
73
|
-
for (let i = 0; i < dim; i++) out[(i + shift) % dim] = v[i]!;
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Cosine similarity of two bipolar vectors (= normalized dot). */
|
|
78
|
-
export function hdCosine(a: Hypervector, b: Hypervector): number {
|
|
79
|
-
let dot = 0;
|
|
80
|
-
for (let i = 0; i < a.length; i++) dot += a[i]! * b[i]!;
|
|
81
|
-
return dot / a.length;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Nearest stored item to a (noisy) query, by cosine. */
|
|
85
|
-
export function cleanup(
|
|
86
|
-
query: Hypervector,
|
|
87
|
-
items: ReadonlyMap<string, Hypervector>,
|
|
88
|
-
): { token: string; score: number } | undefined {
|
|
89
|
-
let best: { token: string; score: number } | undefined;
|
|
90
|
-
for (const [token, v] of items) {
|
|
91
|
-
const score = hdCosine(query, v);
|
|
92
|
-
if (!best || score > best.score) best = { token, score };
|
|
93
|
-
}
|
|
94
|
-
return best;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* A compositional associative memory: a running superposition of role⊗filler
|
|
99
|
-
* bindings. Querying with a role unbinds and returns a noisy filler to be cleaned
|
|
100
|
-
* up against an item memory. Stored as an integer accumulator so many episodes
|
|
101
|
-
* can be bundled without saturating.
|
|
102
|
-
*/
|
|
103
|
-
export class CompositionalMemory {
|
|
104
|
-
private readonly acc: Int32Array;
|
|
105
|
-
private count = 0;
|
|
106
|
-
|
|
107
|
-
constructor(
|
|
108
|
-
readonly dim = DEFAULT_HD_DIM,
|
|
109
|
-
acc?: Int32Array,
|
|
110
|
-
count = 0,
|
|
111
|
-
) {
|
|
112
|
-
this.acc = acc ?? new Int32Array(dim);
|
|
113
|
-
this.count = count;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Superpose one role⊗filler binding into the memory. */
|
|
117
|
-
add(role: Hypervector, filler: Hypervector): void {
|
|
118
|
-
const bound = bind(role, filler);
|
|
119
|
-
for (let i = 0; i < this.dim; i++) this.acc[i]! += bound[i]!;
|
|
120
|
-
this.count += 1;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** The signed memory hypervector. */
|
|
124
|
-
vector(): Hypervector {
|
|
125
|
-
const out = new Int8Array(this.dim);
|
|
126
|
-
for (let i = 0; i < this.dim; i++) out[i] = this.acc[i]! >= 0 ? 1 : -1;
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Unbind a role to recover the (noisy) filler it was paired with. */
|
|
131
|
-
query(role: Hypervector): Hypervector {
|
|
132
|
-
return bind(this.vector(), role);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
get episodes(): number {
|
|
136
|
-
return this.count;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
snapshot(): { dim: number; acc: number[]; count: number } {
|
|
140
|
-
return { dim: this.dim, acc: Array.from(this.acc), count: this.count };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
static restore(snap: {
|
|
144
|
-
dim: number;
|
|
145
|
-
acc: number[];
|
|
146
|
-
count: number;
|
|
147
|
-
}): CompositionalMemory {
|
|
148
|
-
return new CompositionalMemory(
|
|
149
|
-
snap.dim,
|
|
150
|
-
Int32Array.from(snap.acc),
|
|
151
|
-
snap.count,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|