auggy 0.3.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 +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
Augment,
|
|
5
|
+
CostResult,
|
|
6
|
+
InternalTurnContext,
|
|
7
|
+
MemoryEntry,
|
|
8
|
+
MemoryQueryOpts,
|
|
9
|
+
MemoryWriteOpts,
|
|
10
|
+
SchedulerContext,
|
|
11
|
+
Transcript,
|
|
12
|
+
TurnResult,
|
|
13
|
+
TurnTrigger,
|
|
14
|
+
} from "../../types";
|
|
15
|
+
import { createBuffer, type ExtractionBuffer } from "./extractor/buffer";
|
|
16
|
+
import { type ExtractionFrequencyConfig, shouldExtract } from "./extractor/frequency";
|
|
17
|
+
import { type ExtractionEngine, handleExtractionTurn } from "./extractor/inject-handler";
|
|
18
|
+
import { createSqliteStore } from "./storage/sqlite-store";
|
|
19
|
+
import { createSupabaseStore, type LayeredSupabaseClient } from "./storage/supabase-store";
|
|
20
|
+
import type { MemoryStore, StoreEntry } from "./storage/types";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Optional auto-save block (PR β / ADR-018 Phase 2). When `enabled` is
|
|
24
|
+
* true (the default), the augment registers two hooks per ADR-027:
|
|
25
|
+
*
|
|
26
|
+
* - `scheduleAfterTurn`: fires after every user-facing turn, runs the
|
|
27
|
+
* per-trust-level frequency dispatcher, and either skips, buffers,
|
|
28
|
+
* or admits an internal extraction turn via `ctx.inject(...)`.
|
|
29
|
+
* - `handleInternalTurn`: claims triggers whose `source ===
|
|
30
|
+
* "layered-memory.autoSave"` and runs the extraction LLM call inside
|
|
31
|
+
* the admitted turn, so cost flows through the standard turn-loop
|
|
32
|
+
* machinery (turn-gate prepare/confirm + commit) — closing the
|
|
33
|
+
* cost-attribution gap Codex Critical-2 flagged. The handler returns
|
|
34
|
+
* a TurnResult whose `trace.inferenceSteps[]` carries the priced
|
|
35
|
+
* cost the engine reported; `runCostCommit` aggregates and the
|
|
36
|
+
* budgets augment commits identically to a user-facing turn.
|
|
37
|
+
*/
|
|
38
|
+
export interface LayeredMemoryAutoSaveOptions {
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
extractionFrequency?: ExtractionFrequencyConfig;
|
|
41
|
+
everyNTurns?: number;
|
|
42
|
+
confidenceThreshold?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Operator-supplied path to a custom extraction prompt template. The
|
|
45
|
+
* template must contain a `{{TRANSCRIPT}}` token the handler replaces
|
|
46
|
+
* with the rendered transcript. When absent, the bundled
|
|
47
|
+
* `extractor/prompt.md` ships as the default.
|
|
48
|
+
*/
|
|
49
|
+
promptTemplate?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Dedicated extraction engine. Required for auto-save to perform
|
|
52
|
+
* extraction. Memorist Decision 6 anticipates this engine being a
|
|
53
|
+
* cheaper Haiku-priced adapter while the user-facing agent runs on
|
|
54
|
+
* Sonnet — keep this knob explicit so operators don't accidentally
|
|
55
|
+
* route extraction through the (more expensive) primary engine.
|
|
56
|
+
*/
|
|
57
|
+
engine?: ExtractionEngine;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface LayeredMemoryOptions {
|
|
61
|
+
backend: "sqlite" | "supabase";
|
|
62
|
+
namespace: string;
|
|
63
|
+
retentionDays?: number;
|
|
64
|
+
// SQLite-specific
|
|
65
|
+
dbPath?: string;
|
|
66
|
+
// Supabase-specific
|
|
67
|
+
client?: LayeredSupabaseClient;
|
|
68
|
+
table?: string;
|
|
69
|
+
// PR β
|
|
70
|
+
autoSave?: LayeredMemoryAutoSaveOptions;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Trigger source string used by both the auto-save scheduler and the
|
|
75
|
+
* internal-turn handler. Kept as a constant so the dispatch routing
|
|
76
|
+
* key has exactly one definition site.
|
|
77
|
+
*/
|
|
78
|
+
const AUTO_SAVE_TRIGGER_SOURCE = "layered-memory.autoSave";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default per-trust-level frequency config — Decision 3 of the memorist
|
|
82
|
+
* design with Codex 2nd-pass Important-2 calibration applied (`agent`
|
|
83
|
+
* defaults to `every-N-turns` rather than `every-turn`).
|
|
84
|
+
*/
|
|
85
|
+
const DEFAULT_FREQUENCY_CONFIG: ExtractionFrequencyConfig = {
|
|
86
|
+
creator: "every-turn",
|
|
87
|
+
agent: "every-N-turns",
|
|
88
|
+
public: { recognized: "every-turn", anonymous: "session-end-only" },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const DEFAULT_EVERY_N_TURNS = 3;
|
|
92
|
+
const DEFAULT_CONFIDENCE_THRESHOLD = 0.5;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Synthetic model label that surfaces in the trace's inference-step
|
|
96
|
+
* record for an extraction turn. Distinct from the user-facing agent's
|
|
97
|
+
* model so trace consumers can differentiate extraction spend from
|
|
98
|
+
* primary spend.
|
|
99
|
+
*/
|
|
100
|
+
const EXTRACTION_MODEL_LABEL = "layered-memory.extraction-engine";
|
|
101
|
+
|
|
102
|
+
function storeEntryToMemoryEntry(e: StoreEntry): MemoryEntry {
|
|
103
|
+
return {
|
|
104
|
+
label: e.label,
|
|
105
|
+
content: e.content,
|
|
106
|
+
peerId: e.peerId ?? undefined,
|
|
107
|
+
trustLevel: e.trustLevel ?? undefined,
|
|
108
|
+
createdAt: e.createdAt,
|
|
109
|
+
supersededBy: e.supersededBy ?? undefined,
|
|
110
|
+
retentionClass: e.retentionClass,
|
|
111
|
+
isVerbatim: e.isVerbatim,
|
|
112
|
+
origin: e.origin,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Load the extraction prompt template — operator override path takes
|
|
118
|
+
* precedence; otherwise the augment's bundled `extractor/prompt.md`.
|
|
119
|
+
* Returns null if neither is readable; auto-save then logs and stays
|
|
120
|
+
* disabled at boot rather than failing the whole augment.
|
|
121
|
+
*/
|
|
122
|
+
function loadPromptTemplate(overridePath?: string): string | null {
|
|
123
|
+
if (overridePath && existsSync(overridePath)) {
|
|
124
|
+
try {
|
|
125
|
+
return readFileSync(overridePath, "utf-8");
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`[layered-memory.autoSave] failed to read promptTemplate "${overridePath}": ${(err as Error).message}`,
|
|
129
|
+
);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Bundled default — sibling file inside the extractor folder.
|
|
134
|
+
const bundled = join(import.meta.dir, "extractor", "prompt.md");
|
|
135
|
+
if (existsSync(bundled)) {
|
|
136
|
+
try {
|
|
137
|
+
return readFileSync(bundled, "utf-8");
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.warn(
|
|
140
|
+
`[layered-memory.autoSave] failed to read bundled prompt template: ${(err as Error).message}`,
|
|
141
|
+
);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Detect that a TurnResult corresponds to an auto-save extraction turn
|
|
150
|
+
* the augment itself injected. Used as the recursion guard so the
|
|
151
|
+
* scheduleAfterTurn hook (which fires on EVERY turn, including the
|
|
152
|
+
* extraction turn the augment injected) does not re-enter and trigger
|
|
153
|
+
* extraction-on-extraction loops.
|
|
154
|
+
*/
|
|
155
|
+
function isAutoSaveTurn(result: TurnResult): boolean {
|
|
156
|
+
return (
|
|
157
|
+
result.trace.trigger.type === "internal" &&
|
|
158
|
+
result.trace.trigger.sourceAugment === AUTO_SAVE_TRIGGER_SOURCE
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build the per-peer namespaced label used for an extracted fact. The
|
|
164
|
+
* shape mirrors PR α's peer-scoped discipline: `<prefix><peerId>:<turnId>:<idx>`.
|
|
165
|
+
* Each fact gets a distinct suffix so they don't collide within a turn.
|
|
166
|
+
*/
|
|
167
|
+
function buildAutoSaveLabel(
|
|
168
|
+
prefix: string,
|
|
169
|
+
peerId: string,
|
|
170
|
+
sourceTurnId: string,
|
|
171
|
+
factIndex: number,
|
|
172
|
+
): string {
|
|
173
|
+
return `${prefix}${peerId}:${sourceTurnId}:${factIndex}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Payload shape carried on the internal trigger from `scheduleAfterTurn`
|
|
178
|
+
* to `handleInternalTurn`. Kept structural (no class) so the kernel's
|
|
179
|
+
* generic `TurnTrigger.payload: Record<string, unknown>` type accepts it.
|
|
180
|
+
*/
|
|
181
|
+
interface AutoSaveTriggerPayload extends Record<string, unknown> {
|
|
182
|
+
transcript: Transcript;
|
|
183
|
+
sourceTurnId: string;
|
|
184
|
+
promptTemplate: string;
|
|
185
|
+
confidenceThreshold: number;
|
|
186
|
+
prefix: string;
|
|
187
|
+
peerId: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isAutoSaveTriggerPayload(
|
|
191
|
+
payload: TurnTrigger["payload"],
|
|
192
|
+
): payload is AutoSaveTriggerPayload {
|
|
193
|
+
if (!payload || typeof payload !== "object") return false;
|
|
194
|
+
const p = payload as Record<string, unknown>;
|
|
195
|
+
return (
|
|
196
|
+
typeof p.sourceTurnId === "string" &&
|
|
197
|
+
typeof p.promptTemplate === "string" &&
|
|
198
|
+
typeof p.confidenceThreshold === "number" &&
|
|
199
|
+
typeof p.prefix === "string" &&
|
|
200
|
+
typeof p.peerId === "string" &&
|
|
201
|
+
p.transcript !== undefined
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Build the TurnResult the kernel turn-loop folds into the kernel
|
|
207
|
+
* trace. The `trace.inferenceSteps[]` carries the priced (or unpriced)
|
|
208
|
+
* extraction cost; the kernel's runCostCommit aggregates this and
|
|
209
|
+
* turnGate.commit observes it (this is the cost-flow path that closes
|
|
210
|
+
* Codex Critical-2).
|
|
211
|
+
*
|
|
212
|
+
* The trace is a stub — the kernel preserves its own trace fields
|
|
213
|
+
* (turnId, threadId, trigger metadata, timestamps) and only consumes
|
|
214
|
+
* `inferenceSteps[]` from the handler-returned trace. We populate the
|
|
215
|
+
* other trace fields with zero/empty defaults so the type checks.
|
|
216
|
+
*/
|
|
217
|
+
function buildExtractionTurnResult(args: {
|
|
218
|
+
trigger: TurnTrigger;
|
|
219
|
+
status: "completed" | "failed";
|
|
220
|
+
cost: CostResult;
|
|
221
|
+
inferenceDurationMs: number;
|
|
222
|
+
inputTokens?: number;
|
|
223
|
+
outputTokens?: number;
|
|
224
|
+
errorMessage?: string;
|
|
225
|
+
responseText?: string;
|
|
226
|
+
}): TurnResult {
|
|
227
|
+
const turnId = args.trigger.turnId;
|
|
228
|
+
const threadId = args.trigger.threadId ?? turnId;
|
|
229
|
+
return {
|
|
230
|
+
turnId,
|
|
231
|
+
success: args.status === "completed",
|
|
232
|
+
status: args.status,
|
|
233
|
+
response: args.responseText
|
|
234
|
+
? { parts: [{ kind: "text", text: args.responseText }] }
|
|
235
|
+
: undefined,
|
|
236
|
+
toolCalls: [],
|
|
237
|
+
error: args.errorMessage
|
|
238
|
+
? { message: args.errorMessage, source: AUTO_SAVE_TRIGGER_SOURCE }
|
|
239
|
+
: undefined,
|
|
240
|
+
trace: {
|
|
241
|
+
turnId,
|
|
242
|
+
threadId,
|
|
243
|
+
timestamp: Date.now(),
|
|
244
|
+
duration: 0,
|
|
245
|
+
trigger: {
|
|
246
|
+
type: "internal",
|
|
247
|
+
sourceAugment: AUTO_SAVE_TRIGGER_SOURCE,
|
|
248
|
+
},
|
|
249
|
+
contextAssembly: {
|
|
250
|
+
augmentBlocks: [],
|
|
251
|
+
preambleTokens: 0,
|
|
252
|
+
toolSchemaTokens: 0,
|
|
253
|
+
historyTokens: 0,
|
|
254
|
+
totalTokens: 0,
|
|
255
|
+
budgetUsed: 0,
|
|
256
|
+
},
|
|
257
|
+
toolSelection: {
|
|
258
|
+
totalTools: 0,
|
|
259
|
+
phase1Used: false,
|
|
260
|
+
mountedTools: [],
|
|
261
|
+
withheldTools: [],
|
|
262
|
+
},
|
|
263
|
+
inferenceSteps: [
|
|
264
|
+
{
|
|
265
|
+
model: EXTRACTION_MODEL_LABEL,
|
|
266
|
+
inputTokens: args.inputTokens ?? 0,
|
|
267
|
+
outputTokens: args.outputTokens ?? 0,
|
|
268
|
+
durationMs: args.inferenceDurationMs,
|
|
269
|
+
toolCalls: [],
|
|
270
|
+
cost: args.cost,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
capabilityChecks: [],
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Run the extraction body for a single internal turn. Calls the engine,
|
|
280
|
+
* parses the response, writes facts via the store's writeAutoSavedEntry,
|
|
281
|
+
* and returns a TurnResult whose trace.inferenceSteps[] carries the
|
|
282
|
+
* priced cost the kernel turn-loop's runCostCommit will surface to
|
|
283
|
+
* turnGate.commit (cost-flow contract per ADR-027 Decision 5).
|
|
284
|
+
*
|
|
285
|
+
* Best-effort. Engine errors and parse errors map to a failed
|
|
286
|
+
* TurnResult that STILL carries the engine's reported cost (when an
|
|
287
|
+
* engine billed for a malformed response, suppressing it in budgets
|
|
288
|
+
* would silently break daily-cap accounting). Per-fact write failures
|
|
289
|
+
* are logged and swallowed.
|
|
290
|
+
*/
|
|
291
|
+
async function runExtractionInsideTurn(args: {
|
|
292
|
+
trigger: TurnTrigger;
|
|
293
|
+
transcript: Transcript;
|
|
294
|
+
engine: ExtractionEngine;
|
|
295
|
+
promptTemplate: string;
|
|
296
|
+
store: MemoryStore;
|
|
297
|
+
prefix: string;
|
|
298
|
+
peerId: string;
|
|
299
|
+
confidenceThreshold: number;
|
|
300
|
+
sourceTurnId: string;
|
|
301
|
+
}): Promise<TurnResult> {
|
|
302
|
+
const inferenceStart = Date.now();
|
|
303
|
+
const result = await handleExtractionTurn({
|
|
304
|
+
transcript: args.transcript,
|
|
305
|
+
engine: args.engine,
|
|
306
|
+
promptTemplate: args.promptTemplate,
|
|
307
|
+
});
|
|
308
|
+
const inferenceDurationMs = Date.now() - inferenceStart;
|
|
309
|
+
const cost: CostResult =
|
|
310
|
+
result.costUsd > 0
|
|
311
|
+
? { priced: true, costUsd: result.costUsd }
|
|
312
|
+
: { priced: false, reason: "extraction engine reported zero cost" };
|
|
313
|
+
|
|
314
|
+
if (!result.success) {
|
|
315
|
+
console.warn(
|
|
316
|
+
`[layered-memory.autoSave] extraction failed (sourceTurn=${args.sourceTurnId}): ${result.error}`,
|
|
317
|
+
);
|
|
318
|
+
return buildExtractionTurnResult({
|
|
319
|
+
trigger: args.trigger,
|
|
320
|
+
status: "failed",
|
|
321
|
+
cost,
|
|
322
|
+
inferenceDurationMs,
|
|
323
|
+
errorMessage: result.error,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let written = 0;
|
|
328
|
+
for (const [idx, fact] of result.facts.entries()) {
|
|
329
|
+
if (fact.confidence < args.confidenceThreshold) {
|
|
330
|
+
// Below threshold: skip rather than write a low-signal entry.
|
|
331
|
+
// Spec Decision 7 leaves a knob for "write but flag" — at v1.0
|
|
332
|
+
// we err on the side of fewer writes; future calibration can
|
|
333
|
+
// revisit once eval data exists.
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await args.store.writeAutoSavedEntry({
|
|
338
|
+
peerId: args.peerId,
|
|
339
|
+
label: buildAutoSaveLabel(args.prefix, args.peerId, args.sourceTurnId, idx),
|
|
340
|
+
content: fact.object,
|
|
341
|
+
subject: fact.subject,
|
|
342
|
+
predicate: fact.predicate,
|
|
343
|
+
object: fact.object,
|
|
344
|
+
confidence: fact.confidence,
|
|
345
|
+
retentionClass: "operational",
|
|
346
|
+
isVerbatim: fact.isVerbatim,
|
|
347
|
+
sourceTurnId: args.sourceTurnId,
|
|
348
|
+
model: EXTRACTION_MODEL_LABEL,
|
|
349
|
+
});
|
|
350
|
+
written++;
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.warn(
|
|
353
|
+
`[layered-memory.autoSave] writeAutoSavedEntry failed for fact ${idx}: ${(err as Error).message}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return buildExtractionTurnResult({
|
|
359
|
+
trigger: args.trigger,
|
|
360
|
+
status: "completed",
|
|
361
|
+
cost,
|
|
362
|
+
inferenceDurationMs,
|
|
363
|
+
responseText: `extracted ${written} fact(s) from turn ${args.sourceTurnId}`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function layeredMemory(opts: LayeredMemoryOptions): Promise<Augment> {
|
|
368
|
+
const prefix = opts.namespace.endsWith(":") ? opts.namespace : `${opts.namespace}:`;
|
|
369
|
+
const retentionDays = opts.retentionDays ?? 90;
|
|
370
|
+
|
|
371
|
+
let store: MemoryStore;
|
|
372
|
+
if (opts.backend === "sqlite") {
|
|
373
|
+
if (!opts.dbPath) throw new Error("layeredMemory: sqlite backend requires dbPath");
|
|
374
|
+
store = createSqliteStore({
|
|
375
|
+
dbPath: opts.dbPath,
|
|
376
|
+
retentionDays,
|
|
377
|
+
namespace: opts.namespace,
|
|
378
|
+
});
|
|
379
|
+
} else if (opts.backend === "supabase") {
|
|
380
|
+
if (!opts.client || !opts.table) {
|
|
381
|
+
throw new Error("layeredMemory: supabase backend requires client and table");
|
|
382
|
+
}
|
|
383
|
+
store = createSupabaseStore({
|
|
384
|
+
client: opts.client,
|
|
385
|
+
table: opts.table,
|
|
386
|
+
retentionDays,
|
|
387
|
+
namespace: opts.namespace,
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
throw new Error(`layeredMemory: unknown backend "${opts.backend}"`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
await store.initialize();
|
|
394
|
+
|
|
395
|
+
// Auto-save state (per-augment-instance — process-local). The buffer
|
|
396
|
+
// accumulates session-end-only transcripts; turnIndexes drives the
|
|
397
|
+
// every-N-turns dispatcher; threadPeerHistory tracks the most-recent
|
|
398
|
+
// peerId observed on each threadId so the scheduler can detect an
|
|
399
|
+
// anonymous→recognized promotion (Decision 5 of the memorist design).
|
|
400
|
+
const autoSaveEnabled = opts.autoSave?.enabled ?? true;
|
|
401
|
+
const buffer: ExtractionBuffer = createBuffer();
|
|
402
|
+
const turnIndexes = new Map<string, number>();
|
|
403
|
+
const threadPeerHistory = new Map<string, string>();
|
|
404
|
+
const promptTemplate = autoSaveEnabled ? loadPromptTemplate(opts.autoSave?.promptTemplate) : null;
|
|
405
|
+
if (autoSaveEnabled && promptTemplate === null) {
|
|
406
|
+
console.warn(
|
|
407
|
+
"[layered-memory.autoSave] no prompt template available; auto-save disabled until promptTemplate is configured",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const frequencyConfig = opts.autoSave?.extractionFrequency ?? DEFAULT_FREQUENCY_CONFIG;
|
|
411
|
+
const everyNTurns = opts.autoSave?.everyNTurns ?? DEFAULT_EVERY_N_TURNS;
|
|
412
|
+
const confidenceThreshold = opts.autoSave?.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD;
|
|
413
|
+
const extractionEngine = opts.autoSave?.engine;
|
|
414
|
+
|
|
415
|
+
const search = async (query: string, queryOpts?: MemoryQueryOpts): Promise<MemoryEntry[]> => {
|
|
416
|
+
const results = await store.search(query, queryOpts?.peerId);
|
|
417
|
+
return results.map(storeEntryToMemoryEntry);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const write = async (
|
|
421
|
+
label: string,
|
|
422
|
+
content: string,
|
|
423
|
+
writeOpts?: MemoryWriteOpts,
|
|
424
|
+
): Promise<void> => {
|
|
425
|
+
if (!label.startsWith(prefix)) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`layeredMemory: label "${label}" does not start with namespace prefix "${prefix}"`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
// Structural peer-binding: if a peerId is provided, the label MUST be
|
|
431
|
+
// scoped to that peer (format: <prefix><peerId> or <prefix><peerId>:<rest>).
|
|
432
|
+
// This prevents peer A from writing to a label like "ep:vis_b:1" — even if
|
|
433
|
+
// they guessed it — by storing a row whose label segment claims another
|
|
434
|
+
// peer. Without this, search remains peer-isolated (rows are stored with
|
|
435
|
+
// the caller's peer_id, not the label's), but the database accumulates
|
|
436
|
+
// misleading rows that could surface in audit/forget paths.
|
|
437
|
+
const peerId = writeOpts?.peerId;
|
|
438
|
+
if (peerId) {
|
|
439
|
+
const peerScopedPrefix = `${prefix}${peerId}`;
|
|
440
|
+
if (label !== peerScopedPrefix && !label.startsWith(`${peerScopedPrefix}:`)) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`layeredMemory: peer "${peerId}" cannot write to label "${label}" — labels must be scoped as "${peerScopedPrefix}" or "${peerScopedPrefix}:<topic>"`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
await store.write({
|
|
448
|
+
label,
|
|
449
|
+
content,
|
|
450
|
+
peerId: peerId ?? null,
|
|
451
|
+
trustLevel: writeOpts?.trustLevel ?? null,
|
|
452
|
+
createdAt: Date.now(),
|
|
453
|
+
supersededBy: null,
|
|
454
|
+
retentionClass: "operational",
|
|
455
|
+
isVerbatim: false,
|
|
456
|
+
expiresAt: null,
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const forget = async (peerId: string): Promise<number> => {
|
|
461
|
+
return store.forget(peerId);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// NOTE: read() is intentionally NOT exposed on this NamespaceMemoryProvider.
|
|
465
|
+
// Episodic memory is peer-scoped — direct label reads bypass that scoping
|
|
466
|
+
// because the generic memory_read tool only checks origin, not peer
|
|
467
|
+
// ownership of the label. Callers must use search (peer-scoped via
|
|
468
|
+
// ToolExecuteContext) instead. memory_read on an "ep:" label will return
|
|
469
|
+
// "does not support reading by label", which is the desired behavior.
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Detect that an anonymous→recognized promotion has just happened on
|
|
473
|
+
* `threadId` and, if so, flush the buffered anonymous-bound transcripts
|
|
474
|
+
* by injecting an extraction trigger. Per the post-PR-1 fix, the flush
|
|
475
|
+
* targets the NEW recognized peer-id (currentPeer), NOT the prior
|
|
476
|
+
* anonymous peer-id — this preserves visitorAuth's verify-time peer-id
|
|
477
|
+
* migration. Pragmatic deviation from the original "Decision 5" of the
|
|
478
|
+
* memorist design (which scoped facts to their original identity);
|
|
479
|
+
* once a visitor verifies, they own the conversation history they
|
|
480
|
+
* participated in. See inline comment at the payload construction
|
|
481
|
+
* site for full rationale.
|
|
482
|
+
*
|
|
483
|
+
* The trigger's `peer` field is also set to `currentPeer` (not the old
|
|
484
|
+
* anon peer) so that budget caps and turn gates apply to the recognized
|
|
485
|
+
* identity — preventing the anonymous peer's (possibly exhausted) caps
|
|
486
|
+
* from blocking the flush.
|
|
487
|
+
*
|
|
488
|
+
* The detection rule is unchanged:
|
|
489
|
+
* - the previously-observed peerId for this threadId is `anon-<threadId>`,
|
|
490
|
+
* - the current peerId is different, AND
|
|
491
|
+
* - there are buffered transcripts under the prior anonymous peerId.
|
|
492
|
+
*
|
|
493
|
+
* Best-effort. ctx.inject failures are caught and logged; the buffered
|
|
494
|
+
* transcripts are dropped on failure.
|
|
495
|
+
*/
|
|
496
|
+
async function maybeFlushOnPromotion(
|
|
497
|
+
threadId: string,
|
|
498
|
+
currentPeer: import("../../types").PeerIdentity,
|
|
499
|
+
ctx: SchedulerContext,
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
const currentPeerId = currentPeer.id;
|
|
502
|
+
const priorPeerId = threadPeerHistory.get(threadId);
|
|
503
|
+
if (!priorPeerId) return; // first turn on this thread
|
|
504
|
+
if (priorPeerId === currentPeerId) return; // same peer, no promotion
|
|
505
|
+
if (priorPeerId !== `anon-${threadId}`) return; // not an anonymous→other transition
|
|
506
|
+
const buffered = buffer.flush(priorPeerId);
|
|
507
|
+
if (buffered.length === 0) return; // nothing to flush
|
|
508
|
+
if (!extractionEngine || !promptTemplate) return; // can't extract
|
|
509
|
+
|
|
510
|
+
// Synthesize a single combined transcript from the buffered turns
|
|
511
|
+
// so one extraction call covers the whole anonymous batch (per
|
|
512
|
+
// session-end-only semantics). The flush's source turnId is the
|
|
513
|
+
// last buffered turn's id — that's the most recent context the
|
|
514
|
+
// anonymous peer contributed before promotion.
|
|
515
|
+
const last = buffered[buffered.length - 1];
|
|
516
|
+
if (!last) return;
|
|
517
|
+
const combinedParts = buffered.flatMap((t) => t.parts);
|
|
518
|
+
// The buffered transcripts were recorded under the OLD anonymous identity —
|
|
519
|
+
// preserve that in the combined transcript (historical record of what
|
|
520
|
+
// the peer said while anonymous).
|
|
521
|
+
const combinedTranscript: Transcript = {
|
|
522
|
+
turnId: last.turnId,
|
|
523
|
+
threadId: last.threadId,
|
|
524
|
+
peer: last.peer,
|
|
525
|
+
parts: combinedParts,
|
|
526
|
+
toolCalls: buffered.flatMap((t) => t.toolCalls),
|
|
527
|
+
startedAt: buffered[0]?.startedAt ?? last.startedAt,
|
|
528
|
+
endedAt: last.endedAt,
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const flushSourceTurnId = last.turnId;
|
|
532
|
+
const payload: AutoSaveTriggerPayload = {
|
|
533
|
+
transcript: combinedTranscript,
|
|
534
|
+
sourceTurnId: flushSourceTurnId,
|
|
535
|
+
promptTemplate,
|
|
536
|
+
confidenceThreshold,
|
|
537
|
+
prefix,
|
|
538
|
+
// Use the NEW recognized peer-id, not the old anonymous one. By the time
|
|
539
|
+
// this flush fires, visitorAuth's verify-route has already migrated
|
|
540
|
+
// existing memory rows from anon-<threadId> to vis_<uuid> via
|
|
541
|
+
// migratePeerIdOnVerify. If we wrote new facts under priorPeerId, we'd
|
|
542
|
+
// recreate the orphaned-history regression that migration was designed
|
|
543
|
+
// to prevent. Pragmatic deviation from "Decision 5" of the memorist
|
|
544
|
+
// design (which said anonymous facts should remain anonymous): once a
|
|
545
|
+
// visitor proves identity, they own the conversation history they
|
|
546
|
+
// participated in.
|
|
547
|
+
peerId: currentPeerId,
|
|
548
|
+
};
|
|
549
|
+
const trigger: TurnTrigger = {
|
|
550
|
+
type: "internal",
|
|
551
|
+
turnId: `auto-save-flush-${priorPeerId}-${flushSourceTurnId}`,
|
|
552
|
+
threadId,
|
|
553
|
+
timestamp: Date.now(),
|
|
554
|
+
source: AUTO_SAVE_TRIGGER_SOURCE,
|
|
555
|
+
// Use the NEW recognized peer identity, not the old anon peer.
|
|
556
|
+
// Budget caps and turn gates key off trigger.peer, so the flush
|
|
557
|
+
// must target the recognized peer to get correct accounting.
|
|
558
|
+
peer: currentPeer,
|
|
559
|
+
payload,
|
|
560
|
+
};
|
|
561
|
+
try {
|
|
562
|
+
await ctx.inject(trigger);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
console.warn(
|
|
565
|
+
`[layered-memory.autoSave] promotion-flush ctx.inject failed for prior peer=${priorPeerId}: ${(err as Error).message}`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Post-turn auto-save dispatcher. ADR-027 delivers `result` + `ctx`
|
|
572
|
+
* after every turn (including the extraction turns this augment
|
|
573
|
+
* itself injects — those are skipped by `isAutoSaveTurn` to prevent
|
|
574
|
+
* recursion).
|
|
575
|
+
*
|
|
576
|
+
* For decision === "extract", this hook calls `ctx.inject(...)` with
|
|
577
|
+
* an internal trigger; the kernel routes that trigger to this same
|
|
578
|
+
* augment's `handleInternalTurn`, which runs the extraction LLM call
|
|
579
|
+
* inside the admitted turn so cost flows through `turnGate.commit`.
|
|
580
|
+
* Errors during inject are caught and logged — best-effort per
|
|
581
|
+
* ADR-027 Decision 2.
|
|
582
|
+
*
|
|
583
|
+
* Promotion flush (post-PR-1 behavior): before applying the standard
|
|
584
|
+
* frequency dispatch, check whether the just-completed turn's peerId
|
|
585
|
+
* differs from the prior peerId for the same threadId AND the prior
|
|
586
|
+
* peerId was the anonymous form (`anon-<threadId>`). If so, inject a
|
|
587
|
+
* one-off extraction-flush trigger targeting the NEW recognized peer
|
|
588
|
+
* (see `maybeFlushOnPromotion` JSDoc for why this deviates from the
|
|
589
|
+
* original "Decision 5" of the memorist design).
|
|
590
|
+
*/
|
|
591
|
+
async function scheduleAfterTurn(result: TurnResult, ctx: SchedulerContext): Promise<void> {
|
|
592
|
+
if (!autoSaveEnabled) return;
|
|
593
|
+
if (!promptTemplate) return;
|
|
594
|
+
// Recursion guard: skip extraction-initiated turns. Without this,
|
|
595
|
+
// every injected extraction turn would itself fire scheduleAfterTurn
|
|
596
|
+
// and synthesize another extraction trigger ad infinitum.
|
|
597
|
+
if (isAutoSaveTurn(result)) return;
|
|
598
|
+
|
|
599
|
+
const transcript = await ctx.getCompletedTranscript();
|
|
600
|
+
if (!transcript) return; // turn was compacted before the hook ran
|
|
601
|
+
if (!transcript.peer) return; // no peer, no scoped namespace to write under
|
|
602
|
+
|
|
603
|
+
const peerId = transcript.peer.id;
|
|
604
|
+
const threadId = transcript.threadId;
|
|
605
|
+
|
|
606
|
+
// Decision 5: detect anonymous→recognized promotion and flush
|
|
607
|
+
// anonymous-bound buffer BEFORE we apply the current peer's cadence.
|
|
608
|
+
// Pass the full peer object so trigger.peer targets the recognized
|
|
609
|
+
// identity (budget caps and turn gates key off trigger.peer).
|
|
610
|
+
await maybeFlushOnPromotion(threadId, transcript.peer, ctx);
|
|
611
|
+
|
|
612
|
+
// Update thread→peer history AFTER promotion detection so the
|
|
613
|
+
// detector compares against the prior turn's identity.
|
|
614
|
+
threadPeerHistory.set(threadId, peerId);
|
|
615
|
+
|
|
616
|
+
const turnIndex = turnIndexes.get(peerId) ?? 0;
|
|
617
|
+
turnIndexes.set(peerId, turnIndex + 1);
|
|
618
|
+
|
|
619
|
+
const decision = shouldExtract(
|
|
620
|
+
{
|
|
621
|
+
trustLevel: transcript.peer.trustLevel,
|
|
622
|
+
publicSubstate: transcript.peer.publicSubstate,
|
|
623
|
+
},
|
|
624
|
+
turnIndex,
|
|
625
|
+
frequencyConfig,
|
|
626
|
+
everyNTurns,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
if (decision === "skip") return;
|
|
630
|
+
if (decision === "buffer") {
|
|
631
|
+
buffer.append(peerId, transcript);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// decision === "extract"
|
|
636
|
+
if (!extractionEngine) {
|
|
637
|
+
// No engine configured — log once-per-turn and skip. The frequency
|
|
638
|
+
// dispatcher already advanced the turnIndex above so subsequent
|
|
639
|
+
// turns still honor the cadence even when extraction itself is a
|
|
640
|
+
// no-op.
|
|
641
|
+
console.warn(
|
|
642
|
+
`[layered-memory.autoSave] no extraction engine configured; skipping extraction for turn ${transcript.turnId}`,
|
|
643
|
+
);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Inject an internal trigger; the kernel routes back to this
|
|
648
|
+
// augment's handleInternalTurn (option a — extraction admits as its
|
|
649
|
+
// own turn, cost flows through turnGate.commit).
|
|
650
|
+
const sourceTurnId = transcript.turnId;
|
|
651
|
+
const payload: AutoSaveTriggerPayload = {
|
|
652
|
+
transcript,
|
|
653
|
+
sourceTurnId,
|
|
654
|
+
promptTemplate,
|
|
655
|
+
confidenceThreshold,
|
|
656
|
+
prefix,
|
|
657
|
+
peerId,
|
|
658
|
+
};
|
|
659
|
+
const trigger: TurnTrigger = {
|
|
660
|
+
type: "internal",
|
|
661
|
+
turnId: `auto-save-${sourceTurnId}`,
|
|
662
|
+
threadId: transcript.threadId,
|
|
663
|
+
timestamp: Date.now(),
|
|
664
|
+
source: AUTO_SAVE_TRIGGER_SOURCE,
|
|
665
|
+
peer: transcript.peer,
|
|
666
|
+
payload,
|
|
667
|
+
};
|
|
668
|
+
try {
|
|
669
|
+
await ctx.inject(trigger);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
// ctx.inject failures are best-effort per ADR-027 — log and move
|
|
672
|
+
// on. The user-facing turn already succeeded; extraction loss is
|
|
673
|
+
// operationally low-stakes (next turn will retry on cadence).
|
|
674
|
+
console.warn(
|
|
675
|
+
`[layered-memory.autoSave] ctx.inject failed for sourceTurn=${sourceTurnId}: ${(err as Error).message}`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* ADR-027 Decision 5 internal-trigger handler. Claims triggers whose
|
|
682
|
+
* `source === "layered-memory.autoSave"` and runs the extraction LLM
|
|
683
|
+
* call inside the kernel's admitted turn. The returned TurnResult's
|
|
684
|
+
* `trace.inferenceSteps[]` carries the priced cost; the kernel
|
|
685
|
+
* turn-loop's `runCostCommit` aggregates and `turnGate.commit`
|
|
686
|
+
* observes — closing Codex Critical-2.
|
|
687
|
+
*
|
|
688
|
+
* Returns null for any trigger this augment does not own; the kernel
|
|
689
|
+
* then offers the trigger to the next augment's handleInternalTurn
|
|
690
|
+
* (or falls through to the standard inference loop if no augment
|
|
691
|
+
* claims).
|
|
692
|
+
*/
|
|
693
|
+
async function handleInternalTurn(
|
|
694
|
+
trigger: TurnTrigger,
|
|
695
|
+
_ctx: InternalTurnContext,
|
|
696
|
+
): Promise<TurnResult | null> {
|
|
697
|
+
if (trigger.source !== AUTO_SAVE_TRIGGER_SOURCE) return null;
|
|
698
|
+
if (!isAutoSaveTriggerPayload(trigger.payload)) {
|
|
699
|
+
// Defensive — a stray internal trigger named auto-save without
|
|
700
|
+
// the expected payload shape. Don't try to extract; surface as a
|
|
701
|
+
// failed turn so the misuse is visible in trace.
|
|
702
|
+
return buildExtractionTurnResult({
|
|
703
|
+
trigger,
|
|
704
|
+
status: "failed",
|
|
705
|
+
cost: { priced: false, reason: "auto-save trigger missing required payload fields" },
|
|
706
|
+
inferenceDurationMs: 0,
|
|
707
|
+
errorMessage: "auto-save trigger missing required payload fields",
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (!extractionEngine) {
|
|
711
|
+
// Configuration drifted between scheduleAfterTurn-time and here
|
|
712
|
+
// (e.g. operator hot-reloaded the engine to undefined). Best-effort
|
|
713
|
+
// — surface as a failed turn so the trace shows it.
|
|
714
|
+
return buildExtractionTurnResult({
|
|
715
|
+
trigger,
|
|
716
|
+
status: "failed",
|
|
717
|
+
cost: { priced: false, reason: "no extraction engine configured" },
|
|
718
|
+
inferenceDurationMs: 0,
|
|
719
|
+
errorMessage: "no extraction engine configured",
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return runExtractionInsideTurn({
|
|
723
|
+
trigger,
|
|
724
|
+
transcript: trigger.payload.transcript as Transcript,
|
|
725
|
+
engine: extractionEngine,
|
|
726
|
+
promptTemplate: trigger.payload.promptTemplate,
|
|
727
|
+
store,
|
|
728
|
+
prefix: trigger.payload.prefix,
|
|
729
|
+
peerId: trigger.payload.peerId,
|
|
730
|
+
confidenceThreshold: trigger.payload.confidenceThreshold,
|
|
731
|
+
sourceTurnId: trigger.payload.sourceTurnId,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
name: `layered-memory-${opts.namespace}`,
|
|
737
|
+
capabilities: ["context", "tools"],
|
|
738
|
+
memory: {
|
|
739
|
+
owns: { kind: "namespace", prefix },
|
|
740
|
+
defaults: {
|
|
741
|
+
mutable: true,
|
|
742
|
+
origin: "peer-derived",
|
|
743
|
+
priority: "normal",
|
|
744
|
+
placement: "preamble",
|
|
745
|
+
eviction: "drop",
|
|
746
|
+
ttl: "session",
|
|
747
|
+
},
|
|
748
|
+
search,
|
|
749
|
+
write,
|
|
750
|
+
forget,
|
|
751
|
+
},
|
|
752
|
+
...(autoSaveEnabled ? { scheduleAfterTurn, handleInternalTurn } : {}),
|
|
753
|
+
onShutdown: async () => {
|
|
754
|
+
await store.close();
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|