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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message taps — the memory store's mechanical input stream.
|
|
3
|
+
*
|
|
4
|
+
* This is the one part of the soul kernel that was worth keeping
|
|
5
|
+
* (docs/memory-persona-plan.md §2): a pair of high-precision regex sets
|
|
6
|
+
* that recognise, without a model call, the two message shapes that carry
|
|
7
|
+
* durable intent — a *directive* ("from now on, …") and a *correction*
|
|
8
|
+
* ("no, that's wrong"). The kernel that consumed them is gone; the rows
|
|
9
|
+
* now land in the typed memory store, where the core view and the
|
|
10
|
+
* retriever already know how to read them.
|
|
11
|
+
*
|
|
12
|
+
* Three rules govern what gets written, and none of them live in the
|
|
13
|
+
* store:
|
|
14
|
+
*
|
|
15
|
+
* - **Trust comes from the chat.** A DM (or a local terminal/native
|
|
16
|
+
* session) is the operator typing, so the row is `operator` trust —
|
|
17
|
+
* stronger than `remember`'s `agent`, because a tap records what a
|
|
18
|
+
* human literally wrote rather than what the model concluded. A
|
|
19
|
+
* group — or a chat-id grammar that cannot tell, which fails closed —
|
|
20
|
+
* is `group_chat`, and a **directive is not recorded there at all**:
|
|
21
|
+
* durable standing intent only comes from a direct conversation with
|
|
22
|
+
* the operator. Same rule as the `remember` action.
|
|
23
|
+
* - **A repeated phrase is not a second memory.** The FTS near-dupe
|
|
24
|
+
* probe runs before every write, so saying "always use ripgrep" twice
|
|
25
|
+
* leaves one row, not two.
|
|
26
|
+
* - **It fails closed and it is never on the prompt-cache path.** Any
|
|
27
|
+
* store error is one warning and nothing else; the turn proceeds. This
|
|
28
|
+
* module must not import `core/prompt/invalidation.js` — a tap that
|
|
29
|
+
* invalidated every live session's prompt snapshot would turn a
|
|
30
|
+
* ~50-token claim into a 60–90 k cache write (plan §3.6).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
assertMemory,
|
|
35
|
+
findSimilarMemories,
|
|
36
|
+
type MemoryKind,
|
|
37
|
+
type MemorySource,
|
|
38
|
+
type MemoryTrust,
|
|
39
|
+
} from "../../storage/memory.js";
|
|
40
|
+
import { chatScope } from "../../util/chat-id.js";
|
|
41
|
+
import { log, logWarn } from "../../util/log.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* High-precision cues that a message is a *correction* of Talon's behavior. Kept
|
|
45
|
+
* tight on purpose: a false positive stores a claim about how to behave, so we
|
|
46
|
+
* would rather miss a soft correction than mislabel ordinary chat.
|
|
47
|
+
*/
|
|
48
|
+
const CORRECTION_PATTERNS: readonly RegExp[] = [
|
|
49
|
+
/^\s*(no|nope|nah)\b[\s,.!:-]/i,
|
|
50
|
+
/\bthat'?s (wrong|incorrect|not (right|correct)|false)\b/i,
|
|
51
|
+
/\b(you'?re|you are) wrong\b/i,
|
|
52
|
+
/\bnot what i (asked|meant|wanted|said)\b/i,
|
|
53
|
+
/\bnever (do|say) that( again)?\b/i,
|
|
54
|
+
/\b(stop|quit) (doing|saying) that\b/i,
|
|
55
|
+
/\bwrong[\s,.!]/i,
|
|
56
|
+
/\byou (messed|screwed|fucked) (that|this|it)? ?up\b/i,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* High-precision cues that a message is a *directive* about how to be — a
|
|
61
|
+
* standing instruction rather than a one-off request. These are stored verbatim
|
|
62
|
+
* as evidence, so again we favor precision over recall.
|
|
63
|
+
*/
|
|
64
|
+
const DIRECTIVE_PATTERNS: readonly RegExp[] = [
|
|
65
|
+
/\bfrom now on\b/i,
|
|
66
|
+
/\bgoing forward\b/i,
|
|
67
|
+
/\bin (the )?future\b/i,
|
|
68
|
+
/\byou should (always|never)\b/i,
|
|
69
|
+
/\b(always|never) (do|say|reply|respond|answer|use|be|call|check)\b/i,
|
|
70
|
+
/\bi (want|need|'?d like) you to\b/i,
|
|
71
|
+
/\bmake sure (you|to|that)\b/i,
|
|
72
|
+
/\bremember to\b/i,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export type MessageClass = "directive" | "correction" | null;
|
|
76
|
+
|
|
77
|
+
/** `source.actor` on every row this path writes — the ownership marker. */
|
|
78
|
+
const TAP_ACTOR = "tap";
|
|
79
|
+
|
|
80
|
+
/** The subject a tapped directive is filed under: standing operator intent. */
|
|
81
|
+
const DIRECTIVE_SUBJECT = "operator";
|
|
82
|
+
|
|
83
|
+
/** The subject a tapped correction is filed under. */
|
|
84
|
+
const CORRECTION_SUBJECT = "correction";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Classify a single inbound message. Returns "correction" or "directive" when a
|
|
88
|
+
* cue matches, else null. Corrections are checked first because a correction is
|
|
89
|
+
* the more specific (and more consequential) signal. Very long messages are
|
|
90
|
+
* skipped — standing instructions and corrections are terse.
|
|
91
|
+
*/
|
|
92
|
+
export function classifyMessage(text: string): MessageClass {
|
|
93
|
+
const t = text.trim();
|
|
94
|
+
if (t.length === 0 || t.length > 500) return null;
|
|
95
|
+
if (CORRECTION_PATTERNS.some((re) => re.test(t))) return "correction";
|
|
96
|
+
if (DIRECTIVE_PATTERNS.some((re) => re.test(t))) return "directive";
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The trust tier for something the human typed in this chat. A DM is the
|
|
102
|
+
* operator speaking for themselves; anything multi-party — or a grammar that
|
|
103
|
+
* cannot tell, which fails closed — is `group_chat`.
|
|
104
|
+
*/
|
|
105
|
+
function trustForChat(chatKey: string): MemoryTrust {
|
|
106
|
+
return chatScope(chatKey) === "dm" ? "operator" : "group_chat";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* How much of two claims' wording must coincide before the tap calls the
|
|
111
|
+
* second one a restatement of the first. Jaccard over the word sets, so
|
|
112
|
+
* word order and punctuation don't matter.
|
|
113
|
+
*
|
|
114
|
+
* A threshold is needed because the tap files every directive under the
|
|
115
|
+
* *same* subject, and the store's FTS probe ORs a claim's terms: "from now
|
|
116
|
+
* on, always use ripgrep" and "from now on, never use emoji" both match it
|
|
117
|
+
* on `from`/`now`/`use`. Without this, the first directive Talon ever heard
|
|
118
|
+
* would swallow every later one. Set high on purpose — the case this
|
|
119
|
+
* exists for is the same sentence typed twice.
|
|
120
|
+
*/
|
|
121
|
+
const DUPLICATE_OVERLAP = 0.8;
|
|
122
|
+
|
|
123
|
+
/** The word set of a claim, lowercased, punctuation dropped. */
|
|
124
|
+
function wordSet(text: string): Set<string> {
|
|
125
|
+
return new Set(
|
|
126
|
+
text
|
|
127
|
+
.toLowerCase()
|
|
128
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
129
|
+
.filter(Boolean),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Jaccard similarity of two claims' word sets, 0..1. */
|
|
134
|
+
function overlap(a: Set<string>, b: Set<string>): number {
|
|
135
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
136
|
+
let shared = 0;
|
|
137
|
+
for (const w of a) if (b.has(w)) shared += 1;
|
|
138
|
+
return shared / (a.size + b.size - shared);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Write one tapped claim, unless the store already holds a restatement of it
|
|
143
|
+
* under the same kind and subject. Returns true when a row was inserted.
|
|
144
|
+
*
|
|
145
|
+
* The store's FTS probe is the cheap prefilter — live rows only, right kind
|
|
146
|
+
* and subject, best textual match first — and `DUPLICATE_OVERLAP` is the
|
|
147
|
+
* decision.
|
|
148
|
+
*/
|
|
149
|
+
function storeClaim(
|
|
150
|
+
kind: MemoryKind,
|
|
151
|
+
subject: string,
|
|
152
|
+
text: string,
|
|
153
|
+
trust: MemoryTrust,
|
|
154
|
+
source: MemorySource,
|
|
155
|
+
speaker: string,
|
|
156
|
+
): boolean {
|
|
157
|
+
const words = wordSet(text);
|
|
158
|
+
const restated = findSimilarMemories(kind, subject, text).some(
|
|
159
|
+
(row) => overlap(words, wordSet(row.text)) >= DUPLICATE_OVERLAP,
|
|
160
|
+
);
|
|
161
|
+
if (restated) return false;
|
|
162
|
+
const { id } = assertMemory({ kind, subject, text, trust, source });
|
|
163
|
+
log("memory", `tap: [${kind}] ${subject} from ${speaker} #${id}`);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Feed one inbound message to the memory store if it reads as a directive or a
|
|
169
|
+
* correction. Returns the class recognised, whether or not a row was written —
|
|
170
|
+
* a group-chat directive is classified and deliberately dropped.
|
|
171
|
+
*
|
|
172
|
+
* Never throws: the tap runs beside a turn, not inside it, so a store failure
|
|
173
|
+
* costs one warning and nothing else.
|
|
174
|
+
*/
|
|
175
|
+
export function recordMessageSignal(opts: {
|
|
176
|
+
readonly text: string;
|
|
177
|
+
readonly chatKey: string;
|
|
178
|
+
readonly actor?: string;
|
|
179
|
+
}): MessageClass {
|
|
180
|
+
const cls = classifyMessage(opts.text);
|
|
181
|
+
if (cls === null) return null;
|
|
182
|
+
const text = opts.text.trim();
|
|
183
|
+
const trust = trustForChat(opts.chatKey);
|
|
184
|
+
const source: MemorySource = { chat: opts.chatKey, actor: TAP_ACTOR };
|
|
185
|
+
const speaker = opts.actor ?? "user";
|
|
186
|
+
try {
|
|
187
|
+
if (cls === "directive") {
|
|
188
|
+
// A standing instruction only counts when the operator gave it
|
|
189
|
+
// directly — anyone in a group could otherwise plant one.
|
|
190
|
+
if (trust !== "operator") return cls;
|
|
191
|
+
storeClaim("directive", DIRECTIVE_SUBJECT, text, trust, source, speaker);
|
|
192
|
+
} else {
|
|
193
|
+
storeClaim("episode", CORRECTION_SUBJECT, text, trust, source, speaker);
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
logWarn("memory", `tap failed to record ${cls}: ${String(err)}`);
|
|
197
|
+
}
|
|
198
|
+
return cls;
|
|
199
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Turn retrieval** — the per-turn tier of docs/memory-persona-plan.md
|
|
3
|
+
* §3.4, behind `TALON_MEMORY_STORE`.
|
|
4
|
+
*
|
|
5
|
+
* Retrieval has two tiers and the boundary between them is a
|
|
6
|
+
* prompt-cache invariant (plan §3.6):
|
|
7
|
+
*
|
|
8
|
+
* - **Core view** (`core-view.ts`) — computed once per session build,
|
|
9
|
+
* lives in `staticText`, frozen for the session.
|
|
10
|
+
* - **Turn retrieval** (this module) — keyed to the *incoming
|
|
11
|
+
* message*, resolved once per turn by the Weaver and injected into
|
|
12
|
+
* the **user turn** by `formatUserPrompt`. It never enters
|
|
13
|
+
* `prepareSystemPrompt()` and nothing here may ever call
|
|
14
|
+
* `notifyPromptInputsChanged()` — a per-turn invalidation would
|
|
15
|
+
* force a full-prompt cache write on every live session, every
|
|
16
|
+
* turn, which is the single most expensive mistake in the plan.
|
|
17
|
+
*
|
|
18
|
+
* This is the seam #639 deleted, rebuilt without the divergence that
|
|
19
|
+
* justified deleting it. The old `retrievedMemory` field was read by
|
|
20
|
+
* two backends out of six and silently dropped by the rest; now there
|
|
21
|
+
* is exactly ONE consumer — `backend/runtime/prompt/prompt-format.ts` — that
|
|
22
|
+
* every backend already calls, so a backend cannot forget to inject it
|
|
23
|
+
* without also losing its time tag and `msg_id` framing.
|
|
24
|
+
*
|
|
25
|
+
* **Trust policy (plan §5, #373).** Only `operator` and `agent` rows
|
|
26
|
+
* are ever auto-injected. `user_claim` and `group_chat` rows are
|
|
27
|
+
* reachable only through the explicit `recall` tool: an auto-injected
|
|
28
|
+
* low-trust row is a permanent prompt injection that anyone in a group
|
|
29
|
+
* chat can plant. `reflection` rows are excluded too — the diary is the
|
|
30
|
+
* persona layer and is structurally never a fact source (plan §3.5).
|
|
31
|
+
*
|
|
32
|
+
* **Fail closed.** A broken store must never block chat delivery: any
|
|
33
|
+
* error logs once per process and the turn runs with no injected
|
|
34
|
+
* memory, exactly as if the flag were off.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
formatMemory,
|
|
39
|
+
searchMemories,
|
|
40
|
+
touchMemory,
|
|
41
|
+
type MemoryRow,
|
|
42
|
+
type MemoryTrust,
|
|
43
|
+
} from "../../storage/memory.js";
|
|
44
|
+
import { logDebug, logWarn } from "../../util/log.js";
|
|
45
|
+
import { memoryStoreEnabled } from "./flag.js";
|
|
46
|
+
|
|
47
|
+
// ── Tunables ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hard cap on the injected block, header excluded. Whole rows only —
|
|
51
|
+
* a truncated memory is a misquoted memory, and a half-line is worse
|
|
52
|
+
* than a missing one. Paid on *every* turn (unlike the core view,
|
|
53
|
+
* which is paid once per session and then cached), so it stays small.
|
|
54
|
+
*/
|
|
55
|
+
export const TURN_MEMORY_MAX_CHARS = 3_000;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Candidates pulled from FTS before re-ranking. The trust filter runs
|
|
59
|
+
* *after* this cut, so a query whose twenty best matches are all
|
|
60
|
+
* low-trust injects nothing — which is the conservative direction: an
|
|
61
|
+
* untrusted row must never be promoted into the prompt just because
|
|
62
|
+
* nothing trusted matched.
|
|
63
|
+
*/
|
|
64
|
+
const CANDIDATE_LIMIT = 20;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Trust tiers eligible for automatic injection (plan §5). Everything
|
|
68
|
+
* else stays pull-only, through the explicit `recall` tool.
|
|
69
|
+
*/
|
|
70
|
+
const AUTO_INJECT_TRUST: ReadonlySet<MemoryTrust> = new Set([
|
|
71
|
+
"operator",
|
|
72
|
+
"agent",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/** `hitCount` at which the affinity term saturates. */
|
|
76
|
+
const HIT_SATURATION = 20;
|
|
77
|
+
|
|
78
|
+
/** Age at which the recency term reaches zero. */
|
|
79
|
+
const RECENCY_HORIZON_MS = 30 * 24 * 60 * 60 * 1000;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Re-ranking weights. bm25 order (as the store returned it) is the
|
|
83
|
+
* strongest signal — it is the only one that knows what was *asked* —
|
|
84
|
+
* and the three store-side signals refine it rather than overturn it:
|
|
85
|
+
* salience is the curator's judgement, `hitCount` is the feedback loop
|
|
86
|
+
* this module itself feeds, and recency breaks ties towards the live
|
|
87
|
+
* world. They sum to 1 so a score reads as a 0..1 fraction.
|
|
88
|
+
*/
|
|
89
|
+
const W_RELEVANCE = 0.4;
|
|
90
|
+
const W_SALIENCE = 0.3;
|
|
91
|
+
const W_AFFINITY = 0.2;
|
|
92
|
+
const W_RECENCY = 0.1;
|
|
93
|
+
|
|
94
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/** What one turn's retrieval produced. `undefined` means "inject nothing". */
|
|
97
|
+
export type TurnMemory = {
|
|
98
|
+
/** The rendered block, one row per line. Never truncated mid-row. */
|
|
99
|
+
text: string;
|
|
100
|
+
/** How many rows it carries. */
|
|
101
|
+
rows: number;
|
|
102
|
+
/** `text.length` — the per-turn cost, recorded as `turn.memory_chars`. */
|
|
103
|
+
chars: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** One turn's retrieval context: the raw inbound message and its chat. */
|
|
107
|
+
export type TurnRetrievalInput = {
|
|
108
|
+
chatId: string;
|
|
109
|
+
/** The user's text, as the Weaver received it (no prompt framing yet). */
|
|
110
|
+
text: string;
|
|
111
|
+
isGroup: boolean;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ── Ranking ─────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/** Eligible for auto-injection: trusted tier, and not the diary. */
|
|
117
|
+
function injectable(row: MemoryRow): boolean {
|
|
118
|
+
return AUTO_INJECT_TRUST.has(row.trust) && row.kind !== "reflection";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Blend the store's bm25 order with the row's own standing. `index` is
|
|
123
|
+
* the row's position in the bm25 result, best first.
|
|
124
|
+
*/
|
|
125
|
+
function score(row: MemoryRow, index: number, total: number, now: number) {
|
|
126
|
+
const relevance = total > 1 ? 1 - index / (total - 1) : 1;
|
|
127
|
+
const affinity = Math.min(
|
|
128
|
+
1,
|
|
129
|
+
Math.log1p(row.hitCount) / Math.log1p(HIT_SATURATION),
|
|
130
|
+
);
|
|
131
|
+
const age = Math.max(0, now - row.lastSeenAt);
|
|
132
|
+
const recency = Math.max(0, 1 - age / RECENCY_HORIZON_MS);
|
|
133
|
+
return (
|
|
134
|
+
W_RELEVANCE * relevance +
|
|
135
|
+
W_SALIENCE * row.salience +
|
|
136
|
+
W_AFFINITY * affinity +
|
|
137
|
+
W_RECENCY * recency
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Trust-filtered candidates, best first. */
|
|
142
|
+
function rank(hits: MemoryRow[], now: number): MemoryRow[] {
|
|
143
|
+
const scored = hits
|
|
144
|
+
.map((row, index) => ({ row, index }))
|
|
145
|
+
.filter(({ row }) => injectable(row))
|
|
146
|
+
.map(({ row, index }) => ({
|
|
147
|
+
row,
|
|
148
|
+
score: score(row, index, hits.length, now),
|
|
149
|
+
}));
|
|
150
|
+
scored.sort((a, b) => b.score - a.score || a.row.id - b.row.id);
|
|
151
|
+
return scored.map((s) => s.row);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Fill the budget with whole rows, in rank order. */
|
|
155
|
+
function fill(rows: MemoryRow[]): { lines: string[]; taken: MemoryRow[] } {
|
|
156
|
+
const lines: string[] = [];
|
|
157
|
+
const taken: MemoryRow[] = [];
|
|
158
|
+
let used = 0;
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
const line = formatMemory(row);
|
|
161
|
+
// +1 for the newline that joins this line to the previous one.
|
|
162
|
+
const cost = line.length + (lines.length > 0 ? 1 : 0);
|
|
163
|
+
if (used + cost > TURN_MEMORY_MAX_CHARS) break;
|
|
164
|
+
lines.push(line);
|
|
165
|
+
taken.push(row);
|
|
166
|
+
used += cost;
|
|
167
|
+
}
|
|
168
|
+
return { lines, taken };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/** One warning per process — a broken store must not spam every turn. */
|
|
174
|
+
let warned = false;
|
|
175
|
+
|
|
176
|
+
function failClosed(err: unknown): undefined {
|
|
177
|
+
if (!warned) {
|
|
178
|
+
warned = true;
|
|
179
|
+
logWarn(
|
|
180
|
+
"dispatcher",
|
|
181
|
+
"memory turn retrieval failed (turns run without it): " +
|
|
182
|
+
(err instanceof Error ? err.message : String(err)),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Retrieve the memory block for one chat turn, or `undefined` when
|
|
190
|
+
* there is nothing to inject (flag off, no match, or any failure).
|
|
191
|
+
*
|
|
192
|
+
* Injected rows are `touch`ed — that is the ranking feedback loop:
|
|
193
|
+
* a row that keeps getting recalled climbs, one that never matches
|
|
194
|
+
* stays where it is.
|
|
195
|
+
*/
|
|
196
|
+
export function retrieveForTurn(
|
|
197
|
+
input: TurnRetrievalInput,
|
|
198
|
+
): TurnMemory | undefined {
|
|
199
|
+
if (!memoryStoreEnabled()) return undefined;
|
|
200
|
+
try {
|
|
201
|
+
// `match: "any"` — the query is a sentence the user wrote as a
|
|
202
|
+
// message, not as a search; AND-ing its every word would match
|
|
203
|
+
// nothing. bm25 over the OR-ed terms does the ranking.
|
|
204
|
+
const hits = searchMemories(input.text, {
|
|
205
|
+
limit: CANDIDATE_LIMIT,
|
|
206
|
+
match: "any",
|
|
207
|
+
});
|
|
208
|
+
if (hits.length === 0) return undefined;
|
|
209
|
+
const { lines, taken } = fill(rank(hits, Date.now()));
|
|
210
|
+
if (taken.length === 0) return undefined;
|
|
211
|
+
for (const row of taken) touchMemory(row.id);
|
|
212
|
+
const text = lines.join("\n");
|
|
213
|
+
logDebug(
|
|
214
|
+
"dispatcher",
|
|
215
|
+
`memory turn retrieval chat=${input.chatId}${input.isGroup ? " (group)" : ""}: ` +
|
|
216
|
+
`${taken.length}/${hits.length} row(s), ${text.length} chars`,
|
|
217
|
+
);
|
|
218
|
+
return { text, rows: taken.length, chars: text.length };
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return failClosed(err);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* 6. Plugin additions plugin.systemPrompt() contributions
|
|
31
31
|
* (7. Delivery contract — appended by the backend as its suffix,
|
|
32
32
|
* AFTER plugins, so it is the last thing the model reads.
|
|
33
|
-
* See backend/
|
|
33
|
+
* See backend/runtime/prompt/delivery-contract.ts.)
|
|
34
34
|
*
|
|
35
35
|
* DYNAMIC
|
|
36
36
|
* 1. Daily-memory pointer prompts/system/daily-memory.md
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
* ## Deliberate omissions
|
|
53
53
|
*
|
|
54
54
|
* No "Current Date & Time" section: every user message already
|
|
55
|
-
* carries a `[YYYY-MM-DD HH:MM:SS]` tag (see
|
|
55
|
+
* carries a `[YYYY-MM-DD HH:MM:SS]` tag (see backend/runtime/prompt/prompt-format),
|
|
56
56
|
* the daily-memory pointer names today's file, and the `check_time`
|
|
57
57
|
* tool covers timezone queries. A minute-precision timestamp here was
|
|
58
58
|
* the single biggest cache-buster — it guaranteed every rebuild
|
|
@@ -72,7 +72,6 @@ import { renderStickerLibraryPrompt } from "../../storage/stickers.js";
|
|
|
72
72
|
import { recordHistogram } from "../../storage/metrics.js";
|
|
73
73
|
import { renderCoreView } from "../memory/core-view.js";
|
|
74
74
|
import { memoryStoreEnabled } from "../memory/flag.js";
|
|
75
|
-
import { getSoul } from "../soul/service.js";
|
|
76
75
|
|
|
77
76
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
78
77
|
|
|
@@ -213,15 +212,6 @@ export function assembleSystemPrompt(
|
|
|
213
212
|
loaded.push("identity");
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
// 1.5. Soul — the compiled identity surface, when the soul is enabled.
|
|
217
|
-
// Off by default (TALON_SOUL_ENABLED); inert deployments add nothing.
|
|
218
|
-
// Selection-based and verbatim, so it never injects invented self-text.
|
|
219
|
-
const soulSection = getSoul().renderPromptSection();
|
|
220
|
-
if (soulSection) {
|
|
221
|
-
staticParts.push(soulSection);
|
|
222
|
-
loaded.push("soul");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
215
|
// 2. Core behaviour — custom.md replaces base.md wholesale when present.
|
|
226
216
|
const custom = readOptionalFile(resolve(promptDir, "custom.md"));
|
|
227
217
|
const basePrompt = readOptionalFile(resolve(promptDir, "base.md"));
|
package/src/core/prompt/index.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* - `workspace-listing` — lazy workspace tree for the dynamic tail
|
|
9
9
|
*
|
|
10
10
|
* The per-session freezing/caching of assembled prompts lives in
|
|
11
|
-
* `backend/
|
|
11
|
+
* `backend/runtime/prompt/system-prompt.ts` (it is a backend concern); the
|
|
12
12
|
* delivery-contract suffix builders live in
|
|
13
|
-
* `backend/
|
|
13
|
+
* `backend/runtime/prompt/delivery-contract.ts`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export { loadSystemTemplate } from "./templates.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* skill toggle).
|
|
5
5
|
*
|
|
6
6
|
* Whoever caches assembled prompts registers a hook here (today: the
|
|
7
|
-
* per-session snapshot store in backend/
|
|
7
|
+
* per-session snapshot store in backend/runtime/prompt/system-prompt.ts, at its
|
|
8
8
|
* module load). Core and frontends call `notifyPromptInputsChanged()`
|
|
9
9
|
* and never import the cache — the dependency points backend → core,
|
|
10
10
|
* as the layer rule requires.
|
package/src/core/tasks/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Task table — public surface.
|
|
3
3
|
*
|
|
4
4
|
* See table.ts for the registry and types.ts for the vocabulary. Wiring
|
|
5
|
-
* points: weaver (turns), heartbeat/agent, dream, background/job-oneshot
|
|
5
|
+
* points: weaver (turns), heartbeat/agent, dream, background/cron/job-oneshot
|
|
6
6
|
* (isolated cron/trigger jobs); read surfaces: gateway `GET /tasks` +
|
|
7
7
|
* `POST /tasks/kill`, CLI `talon ps` / `talon kill`.
|
|
8
8
|
*/
|
package/src/core/tools/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { stickerTools } from "./stickers.js";
|
|
|
16
16
|
import { schedulingTools } from "./scheduling.js";
|
|
17
17
|
import { triggerTools } from "./triggers.js";
|
|
18
18
|
import { goalTools } from "./goals.js";
|
|
19
|
+
import { memoryTools } from "./memory.js";
|
|
19
20
|
import { scriptTools } from "./scripts.js";
|
|
20
21
|
import { skillTools } from "./skills.js";
|
|
21
22
|
import { webTools } from "./web.js";
|
|
@@ -38,6 +39,7 @@ export const ALL_TOOLS: readonly ToolDefinition[] = [
|
|
|
38
39
|
...schedulingTools,
|
|
39
40
|
...triggerTools,
|
|
40
41
|
...goalTools,
|
|
42
|
+
...memoryTools,
|
|
41
43
|
...scriptTools,
|
|
42
44
|
...skillTools,
|
|
43
45
|
...webTools,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory tools — one typed claim at a time, in band.
|
|
3
|
+
*
|
|
4
|
+
* `remember` / `recall` / `forget` are the write path of
|
|
5
|
+
* docs/memory-persona-plan.md §3.2: a claim costs ~50 tokens and a
|
|
6
|
+
* transaction instead of a 23 KB file rewrite mid-conversation, and a
|
|
7
|
+
* restatement is offered back as a supersede candidate rather than
|
|
8
|
+
* silently becoming a second row.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import type { ToolDefinition } from "./types.js";
|
|
13
|
+
import type { MemoryKind } from "../../storage/memory.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The store's kinds, spelled out here rather than imported.
|
|
17
|
+
* `core/tools/` holds pure definitions; importing `storage/memory.js`
|
|
18
|
+
* for one constant would pull SQLite (and `db.ts`'s top-level await)
|
|
19
|
+
* into every backend that imports the tool registry. The `MemoryKind`
|
|
20
|
+
* annotation is the compile-time link, and `memory-actions.test.ts`
|
|
21
|
+
* asserts this list still equals `MEMORY_KINDS` exactly.
|
|
22
|
+
*/
|
|
23
|
+
const MEMORY_KIND_NAMES: readonly [MemoryKind, ...MemoryKind[]] = [
|
|
24
|
+
"directive",
|
|
25
|
+
"fact",
|
|
26
|
+
"state",
|
|
27
|
+
"episode",
|
|
28
|
+
"relationship",
|
|
29
|
+
"reflection",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const kindSchema = z
|
|
33
|
+
.enum(MEMORY_KIND_NAMES)
|
|
34
|
+
.describe(
|
|
35
|
+
"directive = standing instruction; fact = durable and supersedable; state = keyed, replaced on write; episode = dated, decays; relationship = how a person works; reflection = your own diary, never a fact source",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export const memoryTools: ToolDefinition[] = [
|
|
39
|
+
{
|
|
40
|
+
name: "remember",
|
|
41
|
+
description: `Store one durable claim in long-term memory. Cheap — prefer it to editing memory files.
|
|
42
|
+
|
|
43
|
+
One claim per call, written as a standalone sentence that will still parse months from now.
|
|
44
|
+
If a near-duplicate already exists the call is REFUSED and lists the matching rows: re-issue with replace_id=<that id> to fold your wording into it (the old row stays readable), or force=true only when the new claim genuinely stands beside the old one.
|
|
45
|
+
subject is who/what the claim is about; it defaults to this chat for episode and relationship. state also needs a key (e.g. "release.status") and replaces that key's live row.`,
|
|
46
|
+
schema: {
|
|
47
|
+
kind: kindSchema,
|
|
48
|
+
text: z
|
|
49
|
+
.string()
|
|
50
|
+
.describe(
|
|
51
|
+
"The claim itself, self-contained and specific (max 4000 chars)",
|
|
52
|
+
),
|
|
53
|
+
subject: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe(
|
|
57
|
+
"Who or what this is about — a person, a project, a topic. Required except for episode/relationship, which default to this chat.",
|
|
58
|
+
),
|
|
59
|
+
key: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
'Required for kind="state": lowercase dotted namespace (e.g. "heartbeat.health"). Writing a key replaces its live row.',
|
|
64
|
+
),
|
|
65
|
+
confidence: z
|
|
66
|
+
.number()
|
|
67
|
+
.min(0)
|
|
68
|
+
.max(1)
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("How sure you are, 0–1 (default 1)"),
|
|
71
|
+
replace_id: z
|
|
72
|
+
.number()
|
|
73
|
+
.int()
|
|
74
|
+
.positive()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe(
|
|
77
|
+
"Supersede this existing memory with the new text instead of adding a row — the answer to a near-duplicate refusal",
|
|
78
|
+
),
|
|
79
|
+
force: z
|
|
80
|
+
.boolean()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe(
|
|
83
|
+
"Store anyway despite a near-duplicate. Only when the claims really are distinct.",
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
execute: (params, bridge) => bridge("remember", params),
|
|
87
|
+
tag: "memory",
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
name: "recall",
|
|
92
|
+
description: `Search long-term memory for stored claims — full-text, best match first.
|
|
93
|
+
|
|
94
|
+
Use it when you need more than the memory already in this prompt, before asking the user to repeat something. Returns lines prefixed with an id you can pass to remember(replace_id) or forget.`,
|
|
95
|
+
schema: {
|
|
96
|
+
query: z
|
|
97
|
+
.string()
|
|
98
|
+
.describe("Words to search for — names, topics, distinctive phrases"),
|
|
99
|
+
kind: kindSchema.optional().describe("Restrict to one kind"),
|
|
100
|
+
limit: z
|
|
101
|
+
.number()
|
|
102
|
+
.int()
|
|
103
|
+
.positive()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe("Max rows to return (default and max 20)"),
|
|
106
|
+
},
|
|
107
|
+
execute: (params, bridge) => bridge("recall", params),
|
|
108
|
+
tag: "memory",
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
name: "forget",
|
|
113
|
+
description: `Retire a stored memory by id. It goes to the graveyard — readable and revertible, not erased.
|
|
114
|
+
|
|
115
|
+
The reason is required and is kept with the row. Use this for claims that are wrong or that the user asked you to drop; to correct a claim that is merely out of date, use remember with replace_id so the correction supersedes it.
|
|
116
|
+
A claim can only be retired from a context at least as trusted as the one that recorded it: operator memories are never droppable this way, and a group chat cannot drop what was learned in a DM.`,
|
|
117
|
+
schema: {
|
|
118
|
+
id: z.number().int().positive().describe("Memory id, as shown by recall"),
|
|
119
|
+
reason: z
|
|
120
|
+
.string()
|
|
121
|
+
.describe(
|
|
122
|
+
"Why it is being dropped — required, stored in the audit log",
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
execute: (params, bridge) => bridge("forget", params),
|
|
126
|
+
tag: "memory",
|
|
127
|
+
},
|
|
128
|
+
];
|
package/src/core/tools/types.ts
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn CPU accounting — how much of a turn the daemon itself spends
|
|
3
|
+
* on a CPU, as opposed to waiting on a model or a chat platform.
|
|
4
|
+
*
|
|
5
|
+
* This is the number Phase 0's kill criterion is written against
|
|
6
|
+
* (docs/ts-migration-plan.md): the TS control plane's share of turn
|
|
7
|
+
* latency is `turn.cpu_ms / response_latency_ms`, and if that share is
|
|
8
|
+
* small then a port buys latency nothing. `turn.stream_ms` is the wall
|
|
9
|
+
* clock over the identical bracket, so the two divide cleanly.
|
|
10
|
+
*
|
|
11
|
+
* `process.cpuUsage()` is process-wide, not per-turn: with concurrent
|
|
12
|
+
* turns each one's delta includes the others' work. That is the honest
|
|
13
|
+
* upper bound for "what the daemon costs while a turn is in flight", and
|
|
14
|
+
* it is the direction that matters — a small number stays small.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { recordHistogram } from "../../storage/metrics.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bracket a turn's backend stream. Returns the stop function; calling it
|
|
21
|
+
* records `turn.cpu_ms` (user + system, milliseconds).
|
|
22
|
+
*/
|
|
23
|
+
export function startTurnCpu(): () => void {
|
|
24
|
+
const startedAt = process.cpuUsage();
|
|
25
|
+
return () => {
|
|
26
|
+
const used = process.cpuUsage(startedAt);
|
|
27
|
+
recordHistogram(
|
|
28
|
+
"turn.cpu_ms",
|
|
29
|
+
Math.round((used.user + used.system) / 1000),
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
}
|