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
package/src/core/soul/lens.ts
DELETED
|
@@ -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
|
-
}
|
package/src/core/soul/reflect.ts
DELETED
|
@@ -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
|
-
}
|
package/src/core/soul/reflex.ts
DELETED
|
@@ -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
|
-
}
|