@graphorin/memory 0.6.1 → 0.7.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/CHANGELOG.md +71 -0
- package/README.md +1 -1
- package/dist/conflict/index.js +0 -1
- package/dist/conflict/pipeline.js +0 -1
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/types.d.ts +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/index.js +2 -0
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +7 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/phases/deep.js +19 -12
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts.map +1 -1
- package/dist/consolidator/phases/induce.js +2 -3
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +35 -8
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +199 -28
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +16 -15
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +129 -47
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +17 -3
- package/dist/consolidator/scheduler.d.ts.map +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/types.d.ts +30 -1
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +10 -0
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.d.ts +1 -0
- package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
- package/dist/context-engine/compaction/compactor.js +32 -8
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
- package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
- package/dist/context-engine/compaction/templates/marker.js +34 -0
- package/dist/context-engine/compaction/templates/marker.js.map +1 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/engine.d.ts +7 -176
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +0 -2
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +8 -49
- package/dist/context-engine/io-types.d.ts +144 -0
- package/dist/context-engine/io-types.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/types.d.ts +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +1 -2
- package/dist/context-engine/privacy-filter.d.ts.map +1 -1
- package/dist/context-engine/privacy-filter.js +1 -2
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +43 -52
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +5 -3
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.js +1 -1
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +16 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/envelope.js +57 -0
- package/dist/internal/envelope.js.map +1 -0
- package/dist/internal/injection-heuristics.js +0 -0
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/llm-json.js +40 -0
- package/dist/internal/llm-json.js.map +1 -0
- package/dist/internal/storage-adapter.d.ts +23 -10
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/memory-interface.d.ts +131 -0
- package/dist/memory-interface.d.ts.map +1 -0
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +2 -3
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts.map +1 -1
- package/dist/search/query-transform.js +2 -3
- package/dist/search/query-transform.js.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -3
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +2 -2
- package/dist/tiers/insight-memory.js +1 -1
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +1 -1
- package/dist/tiers/procedural-memory.js +2 -2
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +58 -4
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +103 -20
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +19 -57
- package/dist/tools/block-tools.d.ts.map +1 -1
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +53 -214
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +3 -2
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +34 -175
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +5 -4
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +15 -58
- package/dist/tools/runbook-tools.d.ts.map +1 -1
- package/dist/tools/runbook-tools.js.map +1 -1
- package/dist/tools/types.d.ts +1 -1
- package/package.json +18 -17
- package/src/conflict/index.ts +29 -0
- package/src/conflict/locale-packs/en.ts +142 -0
- package/src/conflict/locale-packs/index.ts +17 -0
- package/src/conflict/locale-packs/types.ts +138 -0
- package/src/conflict/pipeline.ts +371 -0
- package/src/conflict/stages/helpers.ts +65 -0
- package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
- package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
- package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
- package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
- package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
- package/src/conflict/types.ts +263 -0
- package/src/consolidator/budget.ts +312 -0
- package/src/consolidator/decay.ts +246 -0
- package/src/consolidator/dlq.ts +61 -0
- package/src/consolidator/errors.ts +87 -0
- package/src/consolidator/idempotency.ts +27 -0
- package/src/consolidator/index.ts +269 -0
- package/src/consolidator/lock.ts +141 -0
- package/src/consolidator/noise-filter.ts +235 -0
- package/src/consolidator/phases/deep.ts +308 -0
- package/src/consolidator/phases/induce.ts +423 -0
- package/src/consolidator/phases/learned-context.ts +233 -0
- package/src/consolidator/phases/light.ts +166 -0
- package/src/consolidator/phases/reflect.ts +504 -0
- package/src/consolidator/phases/standard.ts +1002 -0
- package/src/consolidator/reconcile.ts +273 -0
- package/src/consolidator/runtime.ts +1049 -0
- package/src/consolidator/scheduler.ts +182 -0
- package/src/consolidator/triggers.ts +134 -0
- package/src/consolidator/types.ts +622 -0
- package/src/context-engine/annotations.ts +161 -0
- package/src/context-engine/auto-recall.ts +92 -0
- package/src/context-engine/compaction/clear-tool-results.ts +232 -0
- package/src/context-engine/compaction/compactor.ts +403 -0
- package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
- package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
- package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
- package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
- package/src/context-engine/compaction/hooks/types.ts +68 -0
- package/src/context-engine/compaction/index.ts +60 -0
- package/src/context-engine/compaction/templates/marker.ts +33 -0
- package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
- package/src/context-engine/compaction/thresholds.ts +107 -0
- package/src/context-engine/compaction/types.ts +255 -0
- package/src/context-engine/engine.ts +970 -0
- package/src/context-engine/index.ts +118 -0
- package/src/context-engine/io-types.ts +162 -0
- package/src/context-engine/locale-packs/en.ts +107 -0
- package/src/context-engine/locale-packs/index.ts +19 -0
- package/src/context-engine/locale-packs/resolver.ts +197 -0
- package/src/context-engine/locale-packs/types.ts +177 -0
- package/src/context-engine/metadata.ts +114 -0
- package/src/context-engine/preambles/inbound-en.ts +19 -0
- package/src/context-engine/privacy-filter.ts +190 -0
- package/src/context-engine/templates/base-en.ts +19 -0
- package/src/context-engine/templates/composer.ts +91 -0
- package/src/context-engine/token-budget.ts +319 -0
- package/src/context-engine/token-counter.ts +157 -0
- package/src/errors/index.ts +273 -0
- package/src/facade.ts +750 -0
- package/src/graph/entity-resolver.ts +364 -0
- package/src/graph/index.ts +24 -0
- package/src/index.ts +259 -0
- package/src/internal/contextualize.ts +173 -0
- package/src/internal/embedder-binding.ts +53 -0
- package/src/internal/envelope.ts +70 -0
- package/src/internal/id.ts +23 -0
- package/src/internal/injection-heuristics.ts +0 -0
- package/src/internal/llm-json.ts +38 -0
- package/src/internal/spans.ts +35 -0
- package/src/internal/storage-adapter.ts +963 -0
- package/src/memory-interface.ts +158 -0
- package/src/migration/embedder-migration.ts +237 -0
- package/src/migration/index.ts +13 -0
- package/src/search/explain.ts +123 -0
- package/src/search/fit-weights.ts +109 -0
- package/src/search/graph-ppr.ts +38 -0
- package/src/search/index.ts +55 -0
- package/src/search/iterative.ts +658 -0
- package/src/search/query-transform.ts +285 -0
- package/src/search/rrf.ts +225 -0
- package/src/search/trust.ts +55 -0
- package/src/search/types.ts +50 -0
- package/src/tiers/episodic-memory.ts +463 -0
- package/src/tiers/index.ts +50 -0
- package/src/tiers/insight-memory.ts +208 -0
- package/src/tiers/procedural-memory.ts +482 -0
- package/src/tiers/semantic-memory.ts +1729 -0
- package/src/tiers/session-memory.ts +271 -0
- package/src/tiers/shared-memory.ts +62 -0
- package/src/tiers/working-memory.ts +368 -0
- package/src/tools/block-tools.ts +172 -0
- package/src/tools/fact-tools.ts +473 -0
- package/src/tools/index.ts +109 -0
- package/src/tools/recall-tools.ts +303 -0
- package/src/tools/runbook-tools.ts +107 -0
- package/src/tools/taint.ts +39 -0
- package/src/tools/types.ts +37 -0
- package/dist/conflict/locale-packs/index.js +0 -4
- package/dist/context-engine/compaction/index.js +0 -10
- package/dist/context-engine/index.d.ts.map +0 -1
- package/dist/context-engine/index.js +0 -25
- package/dist/context-engine/locale-packs/index.js +0 -4
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentRegistryEntry,
|
|
3
|
+
MemoryHit,
|
|
4
|
+
Message,
|
|
5
|
+
MessageRef,
|
|
6
|
+
SessionListOptions,
|
|
7
|
+
SessionMessageWithMetadata,
|
|
8
|
+
SessionScope,
|
|
9
|
+
Tracer,
|
|
10
|
+
} from '@graphorin/core';
|
|
11
|
+
import { newMemoryId } from '../internal/id.js';
|
|
12
|
+
import { withMemorySpan } from '../internal/spans.js';
|
|
13
|
+
import type { MemoryStoreAdapter } from '../internal/storage-adapter.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-session compaction policy. The default `0.9` matches DEC-104:
|
|
17
|
+
* compaction kicks in once cached message tokens exceed
|
|
18
|
+
* `0.9 * contextWindow`.
|
|
19
|
+
*
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
export interface SessionCompactionPolicy {
|
|
23
|
+
/** Default `0.9`. */
|
|
24
|
+
readonly compactAtRatio?: number;
|
|
25
|
+
/** Default `8192`. */
|
|
26
|
+
readonly contextWindowTokens?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Snapshot returned by {@link SessionMemory.compact}. The
|
|
31
|
+
* minimum-viable rendering simply reports counts; the full
|
|
32
|
+
* LLM-summarized cutoff (Phase 10c Consolidator) replaces this
|
|
33
|
+
* implementation later.
|
|
34
|
+
*
|
|
35
|
+
* @stable
|
|
36
|
+
*/
|
|
37
|
+
export interface SessionCompactionResult {
|
|
38
|
+
readonly removed: number;
|
|
39
|
+
readonly summarized: number;
|
|
40
|
+
readonly summary?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `SessionMemory` - append-only message log per session. Owns the
|
|
45
|
+
* `session_messages` storage by single-source-of-truth (DEC-147); the
|
|
46
|
+
* `@graphorin/sessions` package wraps this surface in Phase 11.
|
|
47
|
+
*
|
|
48
|
+
* @stable
|
|
49
|
+
*/
|
|
50
|
+
export class SessionMemory {
|
|
51
|
+
readonly #store: MemoryStoreAdapter;
|
|
52
|
+
readonly #tracer: Tracer;
|
|
53
|
+
readonly #compactionPolicy: SessionCompactionPolicy;
|
|
54
|
+
|
|
55
|
+
constructor(args: {
|
|
56
|
+
store: MemoryStoreAdapter;
|
|
57
|
+
tracer: Tracer;
|
|
58
|
+
compactionPolicy?: SessionCompactionPolicy;
|
|
59
|
+
}) {
|
|
60
|
+
this.#store = args.store;
|
|
61
|
+
this.#tracer = args.tracer;
|
|
62
|
+
this.#compactionPolicy = args.compactionPolicy ?? {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Persist a message. Returns the storage reference. */
|
|
66
|
+
async push(scope: SessionScope, message: Message): Promise<MessageRef> {
|
|
67
|
+
return withMemorySpan(
|
|
68
|
+
this.#tracer,
|
|
69
|
+
'memory.write.session',
|
|
70
|
+
scope,
|
|
71
|
+
{
|
|
72
|
+
'memory.session.message_role': message.role,
|
|
73
|
+
...(message.role === 'assistant' && message.agentId !== undefined
|
|
74
|
+
? { 'memory.session.agent_id': message.agentId }
|
|
75
|
+
: {}),
|
|
76
|
+
},
|
|
77
|
+
async (span) => {
|
|
78
|
+
const ref = await this.#store.session.push(scope, message);
|
|
79
|
+
span.setAttributes({
|
|
80
|
+
'memory.session.sequence': ref.sequence,
|
|
81
|
+
'memory.session.message_id': ref.messageId,
|
|
82
|
+
});
|
|
83
|
+
return ref;
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** List messages for the supplied scope. */
|
|
89
|
+
async list(scope: SessionScope, opts: SessionListOptions = {}): Promise<ReadonlyArray<Message>> {
|
|
90
|
+
return withMemorySpan(
|
|
91
|
+
this.#tracer,
|
|
92
|
+
'memory.read.session',
|
|
93
|
+
scope,
|
|
94
|
+
{
|
|
95
|
+
...(opts.lastN !== undefined ? { 'memory.session.last_n': opts.lastN } : {}),
|
|
96
|
+
...(opts.agentId !== undefined ? { 'memory.session.agent_id': opts.agentId } : {}),
|
|
97
|
+
...(opts.role !== undefined ? { 'memory.session.role': opts.role } : {}),
|
|
98
|
+
},
|
|
99
|
+
async (span) => {
|
|
100
|
+
const out = await this.#store.session.list(scope, opts);
|
|
101
|
+
span.setAttributes({ 'memory.read.session.count': out.length });
|
|
102
|
+
return out;
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* RP-5: list messages with their persisted identity (stored id / sequence /
|
|
109
|
+
* `createdAt`) so an exporter preserves message identity + chronology.
|
|
110
|
+
* Delegates to the store when it supports the richer read.
|
|
111
|
+
*/
|
|
112
|
+
async listWithMetadata(
|
|
113
|
+
scope: SessionScope,
|
|
114
|
+
opts: SessionListOptions = {},
|
|
115
|
+
): Promise<ReadonlyArray<SessionMessageWithMetadata>> {
|
|
116
|
+
const store = this.#store.session;
|
|
117
|
+
if (store.listWithMetadata !== undefined) {
|
|
118
|
+
return store.listWithMetadata(scope, opts);
|
|
119
|
+
}
|
|
120
|
+
// A store without the richer read: fabricate a unique id + the current time
|
|
121
|
+
// (the pre-RP-5 behaviour, just centralised here).
|
|
122
|
+
const messages = await store.list(scope, opts);
|
|
123
|
+
return messages.map((message, i) => ({
|
|
124
|
+
message,
|
|
125
|
+
messageId: newMemoryId('msg'),
|
|
126
|
+
sequence: i + 1,
|
|
127
|
+
createdAt: new Date().toISOString(),
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Hybrid (FTS5) search over the session messages. */
|
|
132
|
+
async search(
|
|
133
|
+
scope: SessionScope,
|
|
134
|
+
query: string,
|
|
135
|
+
opts: { topK?: number; signal?: AbortSignal } = {},
|
|
136
|
+
): Promise<ReadonlyArray<MemoryHit>> {
|
|
137
|
+
return withMemorySpan(
|
|
138
|
+
this.#tracer,
|
|
139
|
+
'memory.search.session',
|
|
140
|
+
scope,
|
|
141
|
+
{ 'memory.search.query_length': query.length },
|
|
142
|
+
async (span) => {
|
|
143
|
+
const out = await this.#store.session.search(scope, query, {
|
|
144
|
+
query,
|
|
145
|
+
...(opts.topK !== undefined ? { topK: opts.topK } : {}),
|
|
146
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
147
|
+
});
|
|
148
|
+
span.setAttributes({ 'memory.search.session.count': out.length });
|
|
149
|
+
return out;
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves `{ flushed: 0 }` and
|
|
156
|
+
* performs no work. The consolidator pipeline (extraction → facts /
|
|
157
|
+
* episodes) superseded the planned "silent flush turn"; this method
|
|
158
|
+
* remains only for contract stability. Do not branch on its counter.
|
|
159
|
+
*/
|
|
160
|
+
async flushImportant(
|
|
161
|
+
scope: SessionScope,
|
|
162
|
+
opts: { silent?: boolean } = {},
|
|
163
|
+
): Promise<{ flushed: number }> {
|
|
164
|
+
void opts;
|
|
165
|
+
return withMemorySpan(
|
|
166
|
+
this.#tracer,
|
|
167
|
+
'memory.write.session',
|
|
168
|
+
scope,
|
|
169
|
+
{ 'memory.session.action': 'flush-important', 'memory.session.implemented': false },
|
|
170
|
+
async () => ({ flushed: 0 }),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves
|
|
176
|
+
* `{ removed: 0, summarized: 0 }` and deletes / summarizes nothing.
|
|
177
|
+
* Session-context compaction is owned by the context engine
|
|
178
|
+
* (`memory.contextEngine.compactNow`, driven by the agent runtime);
|
|
179
|
+
* this tier-level method previously FABRICATED counts (it reported
|
|
180
|
+
* `total - keepLastN` as "removed" while removing nothing - with the
|
|
181
|
+
* default `keepLastN: 0` it claimed to have compacted the whole
|
|
182
|
+
* session). It now reports the truth until a real message splice
|
|
183
|
+
* exists at this layer.
|
|
184
|
+
*/
|
|
185
|
+
async compact(
|
|
186
|
+
scope: SessionScope,
|
|
187
|
+
opts: { keepLastN?: number } = {},
|
|
188
|
+
): Promise<SessionCompactionResult> {
|
|
189
|
+
void opts;
|
|
190
|
+
return withMemorySpan(
|
|
191
|
+
this.#tracer,
|
|
192
|
+
'memory.write.session',
|
|
193
|
+
scope,
|
|
194
|
+
{ 'memory.session.action': 'compact', 'memory.session.implemented': false },
|
|
195
|
+
async (span) => {
|
|
196
|
+
span.setAttributes({
|
|
197
|
+
'memory.session.compact.removed': 0,
|
|
198
|
+
'memory.session.compact.summarized': 0,
|
|
199
|
+
});
|
|
200
|
+
return { removed: 0, summarized: 0 };
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Returns `true` when the cached message tokens exceed
|
|
207
|
+
* `compactAtRatio * contextWindow` (default `0.9` per DEC-104). The
|
|
208
|
+
* second argument can be either:
|
|
209
|
+
*
|
|
210
|
+
* - a `number` - interpreted as the live `contextWindow` size in
|
|
211
|
+
* tokens (matches the documented memory-system spec signature);
|
|
212
|
+
* - an options bag - `{ usedTokens?, contextWindow? }`. When
|
|
213
|
+
* `usedTokens` is supplied the call is purely arithmetic; when
|
|
214
|
+
* omitted, the storage adapter's per-message `token_count`
|
|
215
|
+
* cache (DEC-131) is consulted via `totalCachedTokens(scope)`,
|
|
216
|
+
* falling back to a heuristic (~4 chars/token) for cache
|
|
217
|
+
* misses.
|
|
218
|
+
*
|
|
219
|
+
* @stable
|
|
220
|
+
*/
|
|
221
|
+
async shouldCompact(
|
|
222
|
+
scope: SessionScope,
|
|
223
|
+
contextWindowOrOptions: number | { usedTokens?: number; contextWindow?: number } = {},
|
|
224
|
+
): Promise<boolean> {
|
|
225
|
+
const options =
|
|
226
|
+
typeof contextWindowOrOptions === 'number'
|
|
227
|
+
? { contextWindow: contextWindowOrOptions }
|
|
228
|
+
: contextWindowOrOptions;
|
|
229
|
+
const ratio = this.#compactionPolicy.compactAtRatio ?? 0.9;
|
|
230
|
+
const window = options.contextWindow ?? this.#compactionPolicy.contextWindowTokens ?? 8192;
|
|
231
|
+
if (options.usedTokens !== undefined) {
|
|
232
|
+
return options.usedTokens / window > ratio;
|
|
233
|
+
}
|
|
234
|
+
if (typeof this.#store.session.totalCachedTokens === 'function') {
|
|
235
|
+
const cached = await this.#store.session.totalCachedTokens(scope);
|
|
236
|
+
if (cached !== null) {
|
|
237
|
+
return cached / window > ratio;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const messages = await this.#store.session.list(scope, {});
|
|
241
|
+
let used = 0;
|
|
242
|
+
for (const m of messages) used += approximateTokenLength(m);
|
|
243
|
+
return used / window > ratio;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves `[]`. The agent
|
|
248
|
+
* registry lives in `@graphorin/sessions` and has never been
|
|
249
|
+
* threaded into this tier; the previous JSDoc claimed the default
|
|
250
|
+
* sqlite adapter "resolves" registry rows here, which was false. Use
|
|
251
|
+
* the sessions facade for participant attribution.
|
|
252
|
+
*/
|
|
253
|
+
async attributedFor(scope: SessionScope): Promise<ReadonlyArray<AgentRegistryEntry>> {
|
|
254
|
+
void scope;
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function approximateTokenLength(message: Message): number {
|
|
260
|
+
if (message.role === 'system') {
|
|
261
|
+
return Math.ceil(message.content.length / 4);
|
|
262
|
+
}
|
|
263
|
+
if (typeof message.content === 'string') return Math.ceil(message.content.length / 4);
|
|
264
|
+
let total = 0;
|
|
265
|
+
for (const part of message.content) {
|
|
266
|
+
if (part.type === 'text' || part.type === 'reasoning') {
|
|
267
|
+
total += Math.ceil(part.text.length / 4);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return total;
|
|
271
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MemoryRecord, Tracer } from '@graphorin/core';
|
|
2
|
+
import { withMemorySpan } from '../internal/spans.js';
|
|
3
|
+
import type { MemoryStoreAdapter } from '../internal/storage-adapter.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `SharedMemory` - many-to-many attach mode for blocks / facts /
|
|
7
|
+
* rules across multiple agents. Storage adapters represent
|
|
8
|
+
* attachments as a join table (`shared_attachments` in
|
|
9
|
+
* `@graphorin/store-sqlite`).
|
|
10
|
+
*
|
|
11
|
+
* @stable
|
|
12
|
+
*/
|
|
13
|
+
export class SharedMemory {
|
|
14
|
+
readonly #store: MemoryStoreAdapter;
|
|
15
|
+
readonly #tracer: Tracer;
|
|
16
|
+
|
|
17
|
+
constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {
|
|
18
|
+
this.#store = args.store;
|
|
19
|
+
this.#tracer = args.tracer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Attach `recordId` to `agentId`. Idempotent. */
|
|
23
|
+
async attach(recordId: string, agentId: string, userId: string): Promise<void> {
|
|
24
|
+
await withMemorySpan(
|
|
25
|
+
this.#tracer,
|
|
26
|
+
'memory.write.shared',
|
|
27
|
+
{ userId, agentId },
|
|
28
|
+
{ 'memory.shared.action': 'attach', 'memory.shared.record_id': recordId },
|
|
29
|
+
async () => {
|
|
30
|
+
await this.#store.shared.attach(recordId, agentId);
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Detach `recordId` from `agentId`. Idempotent. */
|
|
36
|
+
async detach(recordId: string, agentId: string, userId: string): Promise<void> {
|
|
37
|
+
await withMemorySpan(
|
|
38
|
+
this.#tracer,
|
|
39
|
+
'memory.write.shared',
|
|
40
|
+
{ userId, agentId },
|
|
41
|
+
{ 'memory.shared.action': 'detach', 'memory.shared.record_id': recordId },
|
|
42
|
+
async () => {
|
|
43
|
+
await this.#store.shared.detach(recordId, agentId);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** List every attachment for `agentId`. */
|
|
49
|
+
async listFor(agentId: string, userId: string): Promise<ReadonlyArray<MemoryRecord>> {
|
|
50
|
+
return withMemorySpan(
|
|
51
|
+
this.#tracer,
|
|
52
|
+
'memory.read.shared',
|
|
53
|
+
{ userId, agentId },
|
|
54
|
+
{},
|
|
55
|
+
async (span) => {
|
|
56
|
+
const out = await this.#store.shared.listFor(agentId);
|
|
57
|
+
span.setAttributes({ 'memory.read.shared.count': out.length });
|
|
58
|
+
return out;
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { Block, Sensitivity, SessionScope, Tracer, ZodLikeSchema } from '@graphorin/core';
|
|
2
|
+
import {
|
|
3
|
+
WorkingBlockOverflowError,
|
|
4
|
+
WorkingBlockReadOnlyError,
|
|
5
|
+
WorkingBlockReplaceMismatchError,
|
|
6
|
+
} from '../errors/index.js';
|
|
7
|
+
import { newMemoryId } from '../internal/id.js';
|
|
8
|
+
import { withMemorySpan } from '../internal/spans.js';
|
|
9
|
+
import type { MemoryStoreAdapter } from '../internal/storage-adapter.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Author-time block specification accepted by
|
|
13
|
+
* {@link defineBlock} and {@link WorkingMemory.define}.
|
|
14
|
+
*
|
|
15
|
+
* @stable
|
|
16
|
+
*/
|
|
17
|
+
export interface BlockSpec {
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly description?: string;
|
|
20
|
+
readonly schema?: ZodLikeSchema<unknown>;
|
|
21
|
+
/** Maximum length of the rendered value in characters. */
|
|
22
|
+
readonly charLimit: number;
|
|
23
|
+
/** Default `'internal'`. */
|
|
24
|
+
readonly sensitivity?: Sensitivity;
|
|
25
|
+
/** Default `false`. */
|
|
26
|
+
readonly readOnly?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Per-block default value. Applied on first definition only; later
|
|
29
|
+
* runs preserve any value already in storage.
|
|
30
|
+
*/
|
|
31
|
+
readonly defaultValue?: string;
|
|
32
|
+
/** Default `'truncate'` - `'reject'` opt-in for strict use cases. */
|
|
33
|
+
readonly overflowPolicy?: 'truncate' | 'reject';
|
|
34
|
+
/** Optional free-form labels surfaced through `tags`. */
|
|
35
|
+
readonly tags?: ReadonlyArray<string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Frozen block definition returned by {@link defineBlock}. Re-used by
|
|
40
|
+
* the agent runtime / context engine to know which blocks should be
|
|
41
|
+
* compiled into the system prompt every step.
|
|
42
|
+
*
|
|
43
|
+
* @stable
|
|
44
|
+
*/
|
|
45
|
+
export interface BlockDefinition {
|
|
46
|
+
readonly label: string;
|
|
47
|
+
readonly description?: string;
|
|
48
|
+
readonly schema?: ZodLikeSchema<unknown>;
|
|
49
|
+
readonly charLimit: number;
|
|
50
|
+
readonly sensitivity: Sensitivity;
|
|
51
|
+
readonly readOnly: boolean;
|
|
52
|
+
readonly defaultValue?: string;
|
|
53
|
+
readonly overflowPolicy: 'truncate' | 'reject';
|
|
54
|
+
readonly tags?: ReadonlyArray<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build a frozen {@link BlockDefinition}. Surfaced as `blocks.define(...)`
|
|
59
|
+
* for ergonomic call-sites; the underlying object is the same shape
|
|
60
|
+
* the {@link WorkingMemory.define} method accepts.
|
|
61
|
+
*
|
|
62
|
+
* @stable
|
|
63
|
+
*/
|
|
64
|
+
export function defineBlock(spec: BlockSpec): BlockDefinition {
|
|
65
|
+
if (!spec.label || spec.label.length === 0) {
|
|
66
|
+
throw new TypeError('[graphorin/memory] BlockSpec.label must be a non-empty string.');
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isInteger(spec.charLimit) || spec.charLimit <= 0) {
|
|
69
|
+
throw new TypeError(
|
|
70
|
+
`[graphorin/memory] BlockSpec.charLimit must be a positive integer, got ${String(spec.charLimit)}.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
label: spec.label,
|
|
75
|
+
...(spec.description !== undefined ? { description: spec.description } : {}),
|
|
76
|
+
...(spec.schema !== undefined ? { schema: spec.schema } : {}),
|
|
77
|
+
charLimit: spec.charLimit,
|
|
78
|
+
sensitivity: spec.sensitivity ?? 'internal',
|
|
79
|
+
readOnly: spec.readOnly ?? false,
|
|
80
|
+
...(spec.defaultValue !== undefined ? { defaultValue: spec.defaultValue } : {}),
|
|
81
|
+
overflowPolicy: spec.overflowPolicy ?? 'truncate',
|
|
82
|
+
...(spec.tags !== undefined ? { tags: Object.freeze([...spec.tags]) } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `WorkingMemory` - labeled, character-bounded blocks rendered into
|
|
88
|
+
* every system prompt. Operations:
|
|
89
|
+
*
|
|
90
|
+
* - `define(spec)`: idempotently registers a block definition; the
|
|
91
|
+
* block's row is created in storage on the next `write` call so
|
|
92
|
+
* operators can change `description` between runs without
|
|
93
|
+
* triggering a write.
|
|
94
|
+
* - `read(scope, label)` / `list(scope)`: surface the current block
|
|
95
|
+
* contents.
|
|
96
|
+
* - `write(scope, label, value)`: full replace.
|
|
97
|
+
* - `append(scope, label, content)`: append-with-newline.
|
|
98
|
+
* - `replace(scope, label, oldUnique, newText)`: targeted replace
|
|
99
|
+
* with a uniqueness check.
|
|
100
|
+
* - `compile(scope)`: render the active blocks for the context
|
|
101
|
+
* engine (used by Phase 10d).
|
|
102
|
+
*
|
|
103
|
+
* @stable
|
|
104
|
+
*/
|
|
105
|
+
export class WorkingMemory {
|
|
106
|
+
readonly #store: MemoryStoreAdapter;
|
|
107
|
+
readonly #tracer: Tracer;
|
|
108
|
+
readonly #defs: Map<string, BlockDefinition>;
|
|
109
|
+
|
|
110
|
+
constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {
|
|
111
|
+
this.#store = args.store;
|
|
112
|
+
this.#tracer = args.tracer;
|
|
113
|
+
this.#defs = new Map();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Register a block definition. Returns the same definition object. */
|
|
117
|
+
define(definition: BlockDefinition): BlockDefinition {
|
|
118
|
+
this.#defs.set(definition.label, definition);
|
|
119
|
+
return definition;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Snapshot of every registered definition. */
|
|
123
|
+
definitions(): ReadonlyArray<BlockDefinition> {
|
|
124
|
+
return Array.from(this.#defs.values());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Lookup a definition by label. */
|
|
128
|
+
definitionFor(label: string): BlockDefinition | undefined {
|
|
129
|
+
return this.#defs.get(label);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** List active (non-deleted) blocks for the supplied scope. */
|
|
133
|
+
async list(scope: SessionScope): Promise<ReadonlyArray<Block>> {
|
|
134
|
+
return withMemorySpan(this.#tracer, 'memory.read.working', scope, {}, async (span) => {
|
|
135
|
+
const out = await this.#store.working.list(scope);
|
|
136
|
+
span.setAttributes({ 'memory.read.working.count': out.length });
|
|
137
|
+
return out;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Read a single block's value (or `null` when absent). */
|
|
142
|
+
async read(scope: SessionScope, label: string): Promise<string | null> {
|
|
143
|
+
return withMemorySpan(
|
|
144
|
+
this.#tracer,
|
|
145
|
+
'memory.read.working',
|
|
146
|
+
scope,
|
|
147
|
+
{ 'memory.block.label': label },
|
|
148
|
+
async (span) => {
|
|
149
|
+
const block = await this.#store.working.get(scope, label);
|
|
150
|
+
span.setAttributes({ 'memory.read.working.found': block !== null });
|
|
151
|
+
if (block !== null) return block.value;
|
|
152
|
+
// MST-8: a defined-but-unwritten block answers with its declared
|
|
153
|
+
// defaultValue (previously advertised, copied into the
|
|
154
|
+
// definition, and then never read by anything).
|
|
155
|
+
const definition = this.#defs.get(label);
|
|
156
|
+
return definition?.defaultValue ?? null;
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Replace a block's value entirely. Honours overflow policy. */
|
|
162
|
+
async write(scope: SessionScope, label: string, value: string): Promise<Block> {
|
|
163
|
+
return this.#mutate(scope, label, async () => value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Append `content` to a block (with a newline separator). */
|
|
167
|
+
async append(scope: SessionScope, label: string, content: string): Promise<Block> {
|
|
168
|
+
return this.#mutate(scope, label, async (current) => {
|
|
169
|
+
if (current === '' || current === undefined) return content;
|
|
170
|
+
return `${current}\n${content}`;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Replace the unique substring `oldUnique` inside the block's value
|
|
176
|
+
* with `newText`. Throws `WorkingBlockReplaceMismatchError` when
|
|
177
|
+
* the substring is missing or appears more than once.
|
|
178
|
+
*/
|
|
179
|
+
async replace(
|
|
180
|
+
scope: SessionScope,
|
|
181
|
+
label: string,
|
|
182
|
+
oldUnique: string,
|
|
183
|
+
newText: string,
|
|
184
|
+
): Promise<Block> {
|
|
185
|
+
return this.#mutate(scope, label, async (current) => {
|
|
186
|
+
const haystack = current ?? '';
|
|
187
|
+
const occurrences = countOccurrences(haystack, oldUnique);
|
|
188
|
+
if (occurrences !== 1) {
|
|
189
|
+
throw new WorkingBlockReplaceMismatchError(label, occurrences);
|
|
190
|
+
}
|
|
191
|
+
return haystack.replace(oldUnique, newText);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Run `mutator(current) => next` and persist the result. */
|
|
196
|
+
async rethink(
|
|
197
|
+
scope: SessionScope,
|
|
198
|
+
label: string,
|
|
199
|
+
mutator: (current: string) => string | Promise<string>,
|
|
200
|
+
): Promise<Block> {
|
|
201
|
+
return this.#mutate(scope, label, async (current) => mutator(current ?? ''));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Soft-delete a block. */
|
|
205
|
+
async forget(scope: SessionScope, label: string, reason?: string): Promise<void> {
|
|
206
|
+
await withMemorySpan(
|
|
207
|
+
this.#tracer,
|
|
208
|
+
'memory.write.working',
|
|
209
|
+
scope,
|
|
210
|
+
{ 'memory.block.label': label, 'memory.write.working.action': 'forget' },
|
|
211
|
+
async () => {
|
|
212
|
+
await this.#store.working.delete(scope, label, reason);
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Attach a working block to an additional agent. Backed by the
|
|
219
|
+
* adapter's `shared.attach(...)` join table so multi-agent crews
|
|
220
|
+
* can share the same block without duplicating storage.
|
|
221
|
+
*/
|
|
222
|
+
async attach(scope: SessionScope, blockId: string, agentId: string): Promise<void> {
|
|
223
|
+
await withMemorySpan(
|
|
224
|
+
this.#tracer,
|
|
225
|
+
'memory.write.working',
|
|
226
|
+
scope,
|
|
227
|
+
{
|
|
228
|
+
'memory.write.working.action': 'attach',
|
|
229
|
+
'memory.block.id': blockId,
|
|
230
|
+
'memory.write.working.target_agent_id': agentId,
|
|
231
|
+
},
|
|
232
|
+
async () => {
|
|
233
|
+
await this.#store.shared.attach(blockId, agentId);
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Detach a working block from an agent. */
|
|
239
|
+
async detach(scope: SessionScope, blockId: string, agentId: string): Promise<void> {
|
|
240
|
+
await withMemorySpan(
|
|
241
|
+
this.#tracer,
|
|
242
|
+
'memory.write.working',
|
|
243
|
+
scope,
|
|
244
|
+
{
|
|
245
|
+
'memory.write.working.action': 'detach',
|
|
246
|
+
'memory.block.id': blockId,
|
|
247
|
+
'memory.write.working.target_agent_id': agentId,
|
|
248
|
+
},
|
|
249
|
+
async () => {
|
|
250
|
+
await this.#store.shared.detach(blockId, agentId);
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Render a deterministic `<memory_blocks>` XML fragment for the
|
|
257
|
+
* supplied scope. The full layered system prompt (six layers) is
|
|
258
|
+
* produced by Phase 10d's ContextEngine; this method ships the
|
|
259
|
+
* minimum-viable rendering used by the smoke acceptance criteria.
|
|
260
|
+
*
|
|
261
|
+
* The optional `agentId` argument is reserved for the per-agent
|
|
262
|
+
* filtering Phase 10d wires through. In Phase 10a the argument is
|
|
263
|
+
* accepted but ignored - the rendering is scope-wide.
|
|
264
|
+
*/
|
|
265
|
+
async compile(scope: SessionScope, agentId?: string): Promise<string> {
|
|
266
|
+
void agentId;
|
|
267
|
+
const blocks = await this.list(scope);
|
|
268
|
+
if (blocks.length === 0) return '';
|
|
269
|
+
const lines = ['<memory_blocks>'];
|
|
270
|
+
for (const block of blocks) {
|
|
271
|
+
const description =
|
|
272
|
+
block.description !== undefined ? ` description="${escapeXml(block.description)}"` : '';
|
|
273
|
+
lines.push(` <block label="${escapeXml(block.label)}"${description}>`);
|
|
274
|
+
lines.push(` ${escapeXml(block.value)}`);
|
|
275
|
+
lines.push(' </block>');
|
|
276
|
+
}
|
|
277
|
+
lines.push('</memory_blocks>');
|
|
278
|
+
return lines.join('\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async #mutate(
|
|
282
|
+
scope: SessionScope,
|
|
283
|
+
label: string,
|
|
284
|
+
nextValueFn: (current: string | undefined) => Promise<string>,
|
|
285
|
+
): Promise<Block> {
|
|
286
|
+
return withMemorySpan(
|
|
287
|
+
this.#tracer,
|
|
288
|
+
'memory.write.working',
|
|
289
|
+
scope,
|
|
290
|
+
{ 'memory.block.label': label },
|
|
291
|
+
async (span) => {
|
|
292
|
+
const definition = this.#requireDefinition(label);
|
|
293
|
+
if (definition.readOnly === true) {
|
|
294
|
+
// MRET-14: a dedicated kind - the old
|
|
295
|
+
// WorkingBlockReplaceMismatchError(label, 0) read as "substring
|
|
296
|
+
// matched 0 times" and misled replace-retry callers.
|
|
297
|
+
throw new WorkingBlockReadOnlyError(label);
|
|
298
|
+
}
|
|
299
|
+
const existing = await this.#store.working.get(scope, label);
|
|
300
|
+
// MST-8: first materialization starts from the declared default,
|
|
301
|
+
// so an append/replace on an unwritten block composes with it.
|
|
302
|
+
const candidate = await nextValueFn(existing?.value ?? definition.defaultValue);
|
|
303
|
+
const enforcedValue = enforceCharLimit(candidate, definition);
|
|
304
|
+
const now = new Date().toISOString();
|
|
305
|
+
const id = existing?.id ?? newMemoryId('block');
|
|
306
|
+
const block: Block = {
|
|
307
|
+
id,
|
|
308
|
+
kind: 'working',
|
|
309
|
+
userId: scope.userId,
|
|
310
|
+
...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),
|
|
311
|
+
...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),
|
|
312
|
+
sensitivity: definition.sensitivity,
|
|
313
|
+
label,
|
|
314
|
+
...(definition.description !== undefined ? { description: definition.description } : {}),
|
|
315
|
+
value: enforcedValue,
|
|
316
|
+
charLimit: definition.charLimit,
|
|
317
|
+
readOnly: definition.readOnly,
|
|
318
|
+
...(definition.tags !== undefined ? { tags: definition.tags } : {}),
|
|
319
|
+
createdAt: existing?.createdAt ?? now,
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
};
|
|
322
|
+
await this.#store.working.upsert(scope, block);
|
|
323
|
+
span.setAttributes({
|
|
324
|
+
'memory.write.working.action': existing === null ? 'create' : 'update',
|
|
325
|
+
'memory.write.working.length': enforcedValue.length,
|
|
326
|
+
});
|
|
327
|
+
return block;
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#requireDefinition(label: string): BlockDefinition {
|
|
333
|
+
const def = this.#defs.get(label);
|
|
334
|
+
if (def !== undefined) return def;
|
|
335
|
+
throw new TypeError(
|
|
336
|
+
`[graphorin/memory] working block '${label}' was not defined. ` +
|
|
337
|
+
'Call memory.working.define(blocks.define({...})) before writing to it.',
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function enforceCharLimit(value: string, definition: BlockDefinition): string {
|
|
343
|
+
if (value.length <= definition.charLimit) return value;
|
|
344
|
+
if (definition.overflowPolicy === 'reject') {
|
|
345
|
+
throw new WorkingBlockOverflowError(definition.label, value.length, definition.charLimit);
|
|
346
|
+
}
|
|
347
|
+
return value.slice(0, definition.charLimit);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
351
|
+
if (needle.length === 0) return 0;
|
|
352
|
+
let count = 0;
|
|
353
|
+
let idx = 0;
|
|
354
|
+
for (;;) {
|
|
355
|
+
const next = haystack.indexOf(needle, idx);
|
|
356
|
+
if (next === -1) return count;
|
|
357
|
+
count += 1;
|
|
358
|
+
idx = next + needle.length;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function escapeXml(value: string): string {
|
|
363
|
+
return value
|
|
364
|
+
.replace(/&/g, '&')
|
|
365
|
+
.replace(/</g, '<')
|
|
366
|
+
.replace(/>/g, '>')
|
|
367
|
+
.replace(/"/g, '"');
|
|
368
|
+
}
|