@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,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedural memory extraction (P2-2) - AWM-style workflow induction. From
|
|
3
|
+
* a **successful** agent trajectory, distil a reusable procedure: a goal, a
|
|
4
|
+
* value-abstracted step sequence (`"search for {product}"`), the variable
|
|
5
|
+
* names that abstraction introduced, and Voyager-style success criteria the
|
|
6
|
+
* reuse can self-verify against. The induced procedure is stored in the
|
|
7
|
+
* procedural tier **quarantined** + `provenance: 'induction'` (P1-4) -
|
|
8
|
+
* procedures drive *actions*, so this is the highest-poisoning-risk write in
|
|
9
|
+
* the system and must never bypass the quarantine gate.
|
|
10
|
+
*
|
|
11
|
+
* Provider-agnostic seam, mirroring the P2-3 / P2-4 retrieval seams: pure
|
|
12
|
+
* request builder ({@link buildInductionRequest}) + tolerant pure parser
|
|
13
|
+
* ({@link parseInducedProcedure}) + a resilient provider-backed inducer
|
|
14
|
+
* ({@link createProviderWorkflowInducer}) + a pure orchestrator
|
|
15
|
+
* ({@link runWorkflowInduction}) that does no I/O of its own. The module
|
|
16
|
+
* imports only `@graphorin/core` types, so the default (no-inducer) path is
|
|
17
|
+
* fully offline - induction only ever calls a model when a provider is wired
|
|
18
|
+
* via `createMemory({ procedureInduction: { provider } })`.
|
|
19
|
+
*
|
|
20
|
+
* "Capture the trajectory + success signal" (the proposal's agent-side
|
|
21
|
+
* dependency) needs **no agent change**: the agent already emits the full,
|
|
22
|
+
* serializable {@link RunState} (`steps[]` + `messages[]` + `status`).
|
|
23
|
+
* {@link trajectoryFromRunState} distils it into the minimal
|
|
24
|
+
* {@link Trajectory} the inducer consumes - `succeeded = status ===
|
|
25
|
+
* 'completed'` is the AWM online-mode success signal (no ground truth
|
|
26
|
+
* needed).
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { Message, Provider, ProviderRequest, RunState } from '@graphorin/core';
|
|
32
|
+
import { stripFence } from '../../internal/llm-json.js';
|
|
33
|
+
|
|
34
|
+
/** Output-token ceiling for one induction call. */
|
|
35
|
+
export const DEFAULT_INDUCTION_MAX_TOKENS = 512;
|
|
36
|
+
|
|
37
|
+
/** Hard cap on steps in an induced procedure (defends against runaway output). */
|
|
38
|
+
export const MAX_PROCEDURE_STEPS = 50;
|
|
39
|
+
|
|
40
|
+
/** Upper bound on trajectory steps rendered into the induction prompt. */
|
|
41
|
+
export const MAX_TRAJECTORY_STEPS_SHOWN = 60;
|
|
42
|
+
|
|
43
|
+
/** Matches `{snake_case}` / `{kebab-case}` variable placeholders in a step. */
|
|
44
|
+
const VARIABLE_PLACEHOLDER = /\{([a-zA-Z][\w-]*)\}/gu;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One distilled step of an agent trajectory - model-agnostic, so the
|
|
48
|
+
* inducer never sees raw provider/tool wire formats.
|
|
49
|
+
*
|
|
50
|
+
* @stable
|
|
51
|
+
*/
|
|
52
|
+
export interface TrajectoryStep {
|
|
53
|
+
/** Tool invoked at this step, when the step was a tool call. */
|
|
54
|
+
readonly tool?: string;
|
|
55
|
+
/** Concrete args / observation rendered to text (the value-abstraction source). */
|
|
56
|
+
readonly detail?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The minimal trajectory an inducer needs: the goal, the ordered steps, and
|
|
61
|
+
* whether the run succeeded. Induction fires on **success only** (AWM online
|
|
62
|
+
* mode) - {@link runWorkflowInduction} returns `null` for a failed run.
|
|
63
|
+
*
|
|
64
|
+
* @stable
|
|
65
|
+
*/
|
|
66
|
+
export interface Trajectory {
|
|
67
|
+
/** What the run set out to do (typically the first user message). */
|
|
68
|
+
readonly goal: string;
|
|
69
|
+
/** Ordered steps the agent took. */
|
|
70
|
+
readonly steps: ReadonlyArray<TrajectoryStep>;
|
|
71
|
+
/** Whether the run succeeded - the induction gate. */
|
|
72
|
+
readonly succeeded: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A reusable workflow distilled from a successful trajectory.
|
|
77
|
+
*
|
|
78
|
+
* @stable
|
|
79
|
+
*/
|
|
80
|
+
export interface InducedProcedure {
|
|
81
|
+
/** Short imperative title / goal of the reusable workflow. */
|
|
82
|
+
readonly title: string;
|
|
83
|
+
/** Ordered, value-abstracted steps (`"search for {product}"`, …). */
|
|
84
|
+
readonly steps: ReadonlyArray<string>;
|
|
85
|
+
/** Variable names abstracted from concrete values (`"product"`, `"day"`). */
|
|
86
|
+
readonly variables: ReadonlyArray<string>;
|
|
87
|
+
/** Voyager-style verifiable success criteria for self-verification on reuse. */
|
|
88
|
+
readonly successCriteria: ReadonlyArray<string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Per-call options for an induction request. */
|
|
92
|
+
export interface WorkflowInductionOptions {
|
|
93
|
+
/** Output-token ceiling. Default {@link DEFAULT_INDUCTION_MAX_TOKENS}. */
|
|
94
|
+
readonly maxTokens?: number;
|
|
95
|
+
readonly signal?: AbortSignal;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Provider-agnostic seam: turn one successful trajectory into a procedure.
|
|
100
|
+
* Returns `null` when nothing inducible (degraded provider, empty output) -
|
|
101
|
+
* never throws.
|
|
102
|
+
*
|
|
103
|
+
* @stable
|
|
104
|
+
*/
|
|
105
|
+
export interface WorkflowInducer {
|
|
106
|
+
induce(
|
|
107
|
+
trajectory: Trajectory,
|
|
108
|
+
options?: WorkflowInductionOptions,
|
|
109
|
+
): Promise<InducedProcedure | null>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Result of self-verifying a reuse against an induced procedure's criteria. */
|
|
113
|
+
export interface VerificationResult {
|
|
114
|
+
/** True only when the procedure has criteria and every one is met. */
|
|
115
|
+
readonly verified: boolean;
|
|
116
|
+
/** Criteria not satisfied by the observed signals. */
|
|
117
|
+
readonly unmet: ReadonlyArray<string>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const INDUCTION_SYSTEM_PROMPT = [
|
|
121
|
+
'You are the workflow-induction step of a long-running agent memory. Given the goal and the',
|
|
122
|
+
'sequence of steps an agent took to SUCCEED at a task, distil a reusable procedure a future',
|
|
123
|
+
'agent could follow for the same kind of task. Abstract concrete values into {snake_case}',
|
|
124
|
+
'variables (e.g. "dry cat food" → {product}, "Monday" → {day}) so the procedure generalizes;',
|
|
125
|
+
'keep the steps short and imperative. Also state a few verifiable success criteria - concrete,',
|
|
126
|
+
'checkable conditions that indicate the task is done. Do NOT invent steps that did not occur.',
|
|
127
|
+
'Return strictly JSON: { "title": string, "steps": string[], "variables": string[],',
|
|
128
|
+
'"successCriteria": string[] }.',
|
|
129
|
+
].join(' ');
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build the (pure) induction request. Renders the trajectory as numbered
|
|
133
|
+
* steps; caps the rendered steps to keep the prompt bounded.
|
|
134
|
+
*/
|
|
135
|
+
export function buildInductionRequest(
|
|
136
|
+
trajectory: Trajectory,
|
|
137
|
+
options: WorkflowInductionOptions = {},
|
|
138
|
+
): ProviderRequest {
|
|
139
|
+
const stepLines = trajectory.steps
|
|
140
|
+
.slice(0, MAX_TRAJECTORY_STEPS_SHOWN)
|
|
141
|
+
.map((step, i) => {
|
|
142
|
+
const head = step.tool !== undefined && step.tool.length > 0 ? step.tool : 'step';
|
|
143
|
+
return step.detail !== undefined && step.detail.length > 0
|
|
144
|
+
? `[${i + 1}] ${head}: ${step.detail}`
|
|
145
|
+
: `[${i + 1}] ${head}`;
|
|
146
|
+
})
|
|
147
|
+
.join('\n');
|
|
148
|
+
const content = [
|
|
149
|
+
`Goal: ${trajectory.goal.length > 0 ? trajectory.goal : '(unstated)'}`,
|
|
150
|
+
'',
|
|
151
|
+
'Trajectory (the steps the agent took to succeed):',
|
|
152
|
+
stepLines.length > 0 ? stepLines : '(none)',
|
|
153
|
+
'',
|
|
154
|
+
'Distil the reusable procedure.',
|
|
155
|
+
].join('\n');
|
|
156
|
+
return {
|
|
157
|
+
messages: [{ role: 'user', content }],
|
|
158
|
+
systemMessage: INDUCTION_SYSTEM_PROMPT,
|
|
159
|
+
temperature: 0,
|
|
160
|
+
maxTokens: options.maxTokens ?? DEFAULT_INDUCTION_MAX_TOKENS,
|
|
161
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
162
|
+
outputType: { kind: 'structured' },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse the induction response into an {@link InducedProcedure}, tolerating
|
|
168
|
+
* chatty / fenced output. Returns `null` when absent / unparseable / it
|
|
169
|
+
* yields no steps. **Never throws on model output** - only normalizes.
|
|
170
|
+
*
|
|
171
|
+
* The returned `variables` are reconciled with the steps: every `{name}`
|
|
172
|
+
* placeholder actually present in the steps is guaranteed to appear, so the
|
|
173
|
+
* abstraction is grounded in the procedure rather than in a model-declared
|
|
174
|
+
* list that may drift from it.
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export function parseInducedProcedure(text: string | undefined): InducedProcedure | null {
|
|
179
|
+
if (text === undefined || text.length === 0) return null;
|
|
180
|
+
const parsed = tryParseObject(stripFence(text).trim());
|
|
181
|
+
if (!isRecord(parsed)) return null;
|
|
182
|
+
return normalizeInducedProcedure({
|
|
183
|
+
title: typeof parsed.title === 'string' ? parsed.title : '',
|
|
184
|
+
steps: toStringArray(parsed.steps),
|
|
185
|
+
variables: toStringArray(parsed.variables),
|
|
186
|
+
successCriteria: toStringArray(parsed.successCriteria ?? parsed.success_criteria),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Normalize a raw induced procedure: trim + drop empty entries, cap step
|
|
192
|
+
* count, and reconcile the variable list with the placeholders actually used
|
|
193
|
+
* in the steps. Returns `null` when no usable steps remain (nothing to
|
|
194
|
+
* induce).
|
|
195
|
+
*
|
|
196
|
+
* @internal
|
|
197
|
+
*/
|
|
198
|
+
export function normalizeInducedProcedure(raw: {
|
|
199
|
+
title: string;
|
|
200
|
+
steps: ReadonlyArray<string>;
|
|
201
|
+
variables: ReadonlyArray<string>;
|
|
202
|
+
successCriteria: ReadonlyArray<string>;
|
|
203
|
+
}): InducedProcedure | null {
|
|
204
|
+
const steps = raw.steps
|
|
205
|
+
.map((s) => s.trim())
|
|
206
|
+
.filter((s) => s.length > 0)
|
|
207
|
+
.slice(0, MAX_PROCEDURE_STEPS);
|
|
208
|
+
if (steps.length === 0) return null;
|
|
209
|
+
|
|
210
|
+
// Variables = placeholders actually in the steps (source of truth, keeps
|
|
211
|
+
// the abstraction grounded), unioned with any extra model-declared names.
|
|
212
|
+
const fromSteps = extractPlaceholders(steps);
|
|
213
|
+
const declared = raw.variables.map((v) => normalizeVariableName(v)).filter((v) => v.length > 0);
|
|
214
|
+
const variables = dedupe([...fromSteps, ...declared]);
|
|
215
|
+
|
|
216
|
+
const successCriteria = dedupe(
|
|
217
|
+
raw.successCriteria.map((c) => c.trim()).filter((c) => c.length > 0),
|
|
218
|
+
);
|
|
219
|
+
const title = raw.title.trim().length > 0 ? raw.title.trim() : (steps[0] ?? 'induced procedure');
|
|
220
|
+
return { title, steps, variables, successCriteria };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resilient provider-backed inducer. A provider throw or unparseable output
|
|
225
|
+
* degrades to `null` (no procedure) - induction never breaks the write path.
|
|
226
|
+
*/
|
|
227
|
+
export function createProviderWorkflowInducer(
|
|
228
|
+
provider: Provider,
|
|
229
|
+
options: {
|
|
230
|
+
readonly maxTokens?: number;
|
|
231
|
+
/**
|
|
232
|
+
* Usage callback (MCON-15) - induction is the framework's highest
|
|
233
|
+
* poisoning-risk LLM spend and previously flowed past every budget
|
|
234
|
+
* envelope. `createMemory` wires this into the consolidator budget
|
|
235
|
+
* when one is enabled; standalone callers can record it themselves.
|
|
236
|
+
* Best-effort: a throwing callback never breaks induction.
|
|
237
|
+
*/
|
|
238
|
+
readonly onUsage?: (usage: {
|
|
239
|
+
readonly promptTokens: number;
|
|
240
|
+
readonly completionTokens: number;
|
|
241
|
+
}) => void;
|
|
242
|
+
} = {},
|
|
243
|
+
): WorkflowInducer {
|
|
244
|
+
return {
|
|
245
|
+
async induce(trajectory, callOptions = {}): Promise<InducedProcedure | null> {
|
|
246
|
+
const merged: WorkflowInductionOptions = {
|
|
247
|
+
maxTokens: callOptions.maxTokens ?? options.maxTokens ?? DEFAULT_INDUCTION_MAX_TOKENS,
|
|
248
|
+
...(callOptions.signal !== undefined ? { signal: callOptions.signal } : {}),
|
|
249
|
+
};
|
|
250
|
+
try {
|
|
251
|
+
const res = await provider.generate(buildInductionRequest(trajectory, merged));
|
|
252
|
+
try {
|
|
253
|
+
options.onUsage?.({
|
|
254
|
+
promptTokens: res.usage.promptTokens,
|
|
255
|
+
completionTokens: res.usage.completionTokens,
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
// Accounting is advisory; never break the induction result.
|
|
259
|
+
}
|
|
260
|
+
return parseInducedProcedure(res.text);
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Pure orchestrator: induce a procedure from a trajectory.
|
|
270
|
+
*
|
|
271
|
+
* **Gate - successful trajectories only** (AWM online mode): a failed /
|
|
272
|
+
* aborted run, or one with no steps, yields `null` without calling the
|
|
273
|
+
* inducer. Otherwise the inducer runs and the result is normalized.
|
|
274
|
+
*/
|
|
275
|
+
export async function runWorkflowInduction(
|
|
276
|
+
trajectory: Trajectory,
|
|
277
|
+
inducer: WorkflowInducer,
|
|
278
|
+
options: WorkflowInductionOptions = {},
|
|
279
|
+
): Promise<InducedProcedure | null> {
|
|
280
|
+
if (!trajectory.succeeded) return null;
|
|
281
|
+
if (trajectory.steps.length === 0) return null;
|
|
282
|
+
const induced = await inducer.induce(trajectory, options);
|
|
283
|
+
if (induced === null) return null;
|
|
284
|
+
return normalizeInducedProcedure({
|
|
285
|
+
title: induced.title,
|
|
286
|
+
steps: induced.steps,
|
|
287
|
+
variables: induced.variables,
|
|
288
|
+
successCriteria: induced.successCriteria,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Distil a {@link RunState} into the minimal {@link Trajectory} the inducer
|
|
294
|
+
* needs. Pure - consumes the agent's already-emitted run state, so capturing
|
|
295
|
+
* a trajectory + its success signal needs no agent-loop change. The success
|
|
296
|
+
* signal is `status === 'completed'`.
|
|
297
|
+
*/
|
|
298
|
+
export function trajectoryFromRunState(run: RunState): Trajectory {
|
|
299
|
+
const steps: TrajectoryStep[] = [];
|
|
300
|
+
for (const step of run.steps) {
|
|
301
|
+
for (const tc of step.toolCalls) {
|
|
302
|
+
const detail = renderArgs(tc.call.args);
|
|
303
|
+
steps.push({
|
|
304
|
+
tool: tc.call.toolName,
|
|
305
|
+
...(detail !== undefined ? { detail } : {}),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
goal: firstUserText(run.messages) ?? '',
|
|
311
|
+
steps,
|
|
312
|
+
succeeded: run.status === 'completed',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Self-verify a reuse against an induced procedure's success criteria. A
|
|
318
|
+
* criterion is met when any observed signal contains it (case-insensitive
|
|
319
|
+
* substring) - a deterministic, offline check the agent runtime can feed
|
|
320
|
+
* actual run observations into on reuse. With **no criteria**, the reuse
|
|
321
|
+
* cannot be self-verified, so `verified` is `false`.
|
|
322
|
+
*/
|
|
323
|
+
export function checkSuccessCriteria(
|
|
324
|
+
procedure: { readonly successCriteria?: ReadonlyArray<string> },
|
|
325
|
+
observed: ReadonlyArray<string>,
|
|
326
|
+
): VerificationResult {
|
|
327
|
+
const criteria = (procedure.successCriteria ?? [])
|
|
328
|
+
.map((c) => c.trim())
|
|
329
|
+
.filter((c) => c.length > 0);
|
|
330
|
+
if (criteria.length === 0) return { verified: false, unmet: [] };
|
|
331
|
+
const haystack = observed.map((o) => o.toLowerCase());
|
|
332
|
+
const unmet = criteria.filter((c) => !haystack.some((o) => o.includes(c.toLowerCase())));
|
|
333
|
+
return { verified: unmet.length === 0, unmet };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
function extractPlaceholders(steps: ReadonlyArray<string>): string[] {
|
|
339
|
+
const out: string[] = [];
|
|
340
|
+
for (const step of steps) {
|
|
341
|
+
for (const match of step.matchAll(VARIABLE_PLACEHOLDER)) {
|
|
342
|
+
const name = match[1];
|
|
343
|
+
if (name !== undefined) out.push(name);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeVariableName(value: string): string {
|
|
350
|
+
// Accept either a bare name or a `{name}` placeholder.
|
|
351
|
+
const inner = /^\{([a-zA-Z][\w-]*)\}$/u.exec(value.trim());
|
|
352
|
+
return (inner?.[1] ?? value).trim();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function dedupe(values: ReadonlyArray<string>): string[] {
|
|
356
|
+
const seen = new Set<string>();
|
|
357
|
+
const out: string[] = [];
|
|
358
|
+
for (const v of values) {
|
|
359
|
+
if (seen.has(v)) continue;
|
|
360
|
+
seen.add(v);
|
|
361
|
+
out.push(v);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderArgs(args: unknown): string | undefined {
|
|
367
|
+
if (args === undefined || args === null) return undefined;
|
|
368
|
+
if (typeof args === 'string') return args.length > 0 ? truncate(args) : undefined;
|
|
369
|
+
try {
|
|
370
|
+
const json = JSON.stringify(args);
|
|
371
|
+
return json !== undefined && json !== '{}' ? truncate(json) : undefined;
|
|
372
|
+
} catch {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function truncate(value: string, max = 200): string {
|
|
378
|
+
return value.length > max ? `${value.slice(0, max)}…` : value;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function firstUserText(messages: ReadonlyArray<Message>): string | undefined {
|
|
382
|
+
for (const message of messages) {
|
|
383
|
+
if (message.role !== 'user') continue;
|
|
384
|
+
const text = messageText(message);
|
|
385
|
+
if (text.length > 0) return text;
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function messageText(message: Message): string {
|
|
391
|
+
const content = message.content;
|
|
392
|
+
if (typeof content === 'string') return content.trim();
|
|
393
|
+
const parts: string[] = [];
|
|
394
|
+
for (const part of content) {
|
|
395
|
+
if (part.type === 'text' && typeof part.text === 'string') parts.push(part.text);
|
|
396
|
+
}
|
|
397
|
+
return parts.join(' ').trim();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function toStringArray(value: unknown): string[] {
|
|
401
|
+
if (!Array.isArray(value)) return [];
|
|
402
|
+
return value.filter((v): v is string => typeof v === 'string');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
406
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function tryParseObject(candidate: string): unknown {
|
|
410
|
+
if (candidate.length === 0) return undefined;
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(candidate);
|
|
413
|
+
} catch {
|
|
414
|
+
const start = candidate.indexOf('{');
|
|
415
|
+
const end = candidate.lastIndexOf('}');
|
|
416
|
+
if (start < 0 || end < start) return undefined;
|
|
417
|
+
try {
|
|
418
|
+
return JSON.parse(candidate.slice(start, end + 1));
|
|
419
|
+
} catch {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learned-context pass (D3) - Letta-style sleep-time maintenance of a
|
|
3
|
+
* persistent, size-bounded, agent-editable digest block. The deep phase
|
|
4
|
+
* rewrites one reserved working-memory block (`learned_context`) from
|
|
5
|
+
* the previous digest + fresh evidence (recent episodes, active
|
|
6
|
+
* insights, active procedures), so every subsequent step's system
|
|
7
|
+
* prompt carries a compact "what I have learned about this user /
|
|
8
|
+
* project" summary inside the stable KV-cache prefix - without
|
|
9
|
+
* retrieval latency and without unbounded growth.
|
|
10
|
+
*
|
|
11
|
+
* The block itself is an ordinary working block: it is spliced into
|
|
12
|
+
* layer 3 of the assembled context automatically, re-anchored after
|
|
13
|
+
* compaction by the persona-block hook pattern, and remains editable by
|
|
14
|
+
* the agent through the existing `block_*` tools (the pass rewrites it
|
|
15
|
+
* on its own cadence, folding any agent edits into the next rewrite).
|
|
16
|
+
*
|
|
17
|
+
* Runs after the deep-phase reflection pass, reusing the same budget /
|
|
18
|
+
* lock / run-audit envelope. Gated off by default at every tier
|
|
19
|
+
* (`learnedContext: false`) - Wave-D trial discipline.
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';
|
|
25
|
+
import { withMemorySpan } from '../../internal/spans.js';
|
|
26
|
+
import type { MemoryStoreAdapter } from '../../internal/storage-adapter.js';
|
|
27
|
+
import type { EpisodicMemory } from '../../tiers/episodic-memory.js';
|
|
28
|
+
import { defineBlock, type WorkingMemory } from '../../tiers/working-memory.js';
|
|
29
|
+
import type { BudgetTracker } from '../budget.js';
|
|
30
|
+
|
|
31
|
+
/** Reserved working-block label maintained by the pass. */
|
|
32
|
+
export const LEARNED_CONTEXT_BLOCK_LABEL = 'learned_context';
|
|
33
|
+
|
|
34
|
+
/** Default character bound for the digest block. */
|
|
35
|
+
export const DEFAULT_LEARNED_CONTEXT_MAX_CHARS = 1200;
|
|
36
|
+
|
|
37
|
+
/** How many recent episode summaries feed one rewrite. */
|
|
38
|
+
const EPISODE_CONTEXT_LIMIT = 15;
|
|
39
|
+
|
|
40
|
+
/** How many active insights / procedures feed one rewrite. */
|
|
41
|
+
const INSIGHT_CONTEXT_LIMIT = 10;
|
|
42
|
+
const PROCEDURE_CONTEXT_LIMIT = 8;
|
|
43
|
+
|
|
44
|
+
const LEARNED_CONTEXT_SYSTEM_PROMPT = [
|
|
45
|
+
'You maintain the learned-context block of a long-running personal-assistant memory:',
|
|
46
|
+
'a compact standing digest of what is durably known about the user and their active',
|
|
47
|
+
'work (profile, preferences, ongoing projects, validated insights, working procedures).',
|
|
48
|
+
'Rewrite the digest by folding the new evidence into the previous digest: keep what is',
|
|
49
|
+
'still true, drop what is stale or superseded, and never invent anything the evidence',
|
|
50
|
+
'does not support. Write plain prose or short dashed lines - no markdown headings, no',
|
|
51
|
+
'code fences, no preamble. Stay strictly under the character budget.',
|
|
52
|
+
].join(' ');
|
|
53
|
+
|
|
54
|
+
/** Inputs accepted by {@link runLearnedContextPass}. */
|
|
55
|
+
export interface LearnedContextDeps {
|
|
56
|
+
readonly provider: Provider;
|
|
57
|
+
readonly tracer: Tracer;
|
|
58
|
+
readonly scope: SessionScope;
|
|
59
|
+
/** Working tier that owns the digest block. */
|
|
60
|
+
readonly working: WorkingMemory;
|
|
61
|
+
/** Recent-episode evidence source. `null` ⇒ episodes are skipped. */
|
|
62
|
+
readonly episodic: EpisodicMemory | null;
|
|
63
|
+
/** Storage adapter - supplies active insights + procedures as evidence. */
|
|
64
|
+
readonly store: MemoryStoreAdapter;
|
|
65
|
+
readonly budget: BudgetTracker;
|
|
66
|
+
/** Character bound enforced on the stored digest. */
|
|
67
|
+
readonly maxChars: number;
|
|
68
|
+
readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;
|
|
69
|
+
/** Override the wall clock - used by tests. */
|
|
70
|
+
readonly now?: () => number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Summary returned by {@link runLearnedContextPass}. */
|
|
74
|
+
export interface LearnedContextOutcome {
|
|
75
|
+
/** True when the block was rewritten this pass. */
|
|
76
|
+
readonly updated: boolean;
|
|
77
|
+
/** Stored digest length after the pass (0 when skipped and absent). */
|
|
78
|
+
readonly chars: number;
|
|
79
|
+
readonly tokens: number;
|
|
80
|
+
readonly costUsd: number;
|
|
81
|
+
/** Why the pass made no LLM call, when it did not. */
|
|
82
|
+
readonly skippedReason?: 'budget' | 'no-evidence' | 'empty-rewrite' | 'provider-error';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Strip an accidental leading code fence from the model's digest and
|
|
87
|
+
* clamp it to the character budget. Returns `null` for an effectively
|
|
88
|
+
* empty rewrite (the previous digest is then left untouched).
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeLearnedContext(raw: string, maxChars: number): string | null {
|
|
91
|
+
let text = raw.trim();
|
|
92
|
+
const fence = /^```[^\n]*\n([\s\S]*?)\n?```\s*$/.exec(text);
|
|
93
|
+
if (fence?.[1] !== undefined) text = fence[1].trim();
|
|
94
|
+
if (text.length === 0) return null;
|
|
95
|
+
return text.length > maxChars ? text.slice(0, maxChars).trimEnd() : text;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build the single rewrite request (pure - testable offline). */
|
|
99
|
+
export function buildLearnedContextRequest(args: {
|
|
100
|
+
readonly previous: string;
|
|
101
|
+
readonly episodes: ReadonlyArray<string>;
|
|
102
|
+
readonly insights: ReadonlyArray<string>;
|
|
103
|
+
readonly procedures: ReadonlyArray<string>;
|
|
104
|
+
readonly maxChars: number;
|
|
105
|
+
}): ProviderRequest {
|
|
106
|
+
const sections: string[] = [
|
|
107
|
+
`Character budget: ${args.maxChars}.`,
|
|
108
|
+
args.previous.length > 0
|
|
109
|
+
? `Previous digest:\n${args.previous}`
|
|
110
|
+
: 'Previous digest: (none yet - write the first one)',
|
|
111
|
+
];
|
|
112
|
+
if (args.episodes.length > 0) {
|
|
113
|
+
sections.push(`Recent episodes:\n${args.episodes.map((s) => `- ${s}`).join('\n')}`);
|
|
114
|
+
}
|
|
115
|
+
if (args.insights.length > 0) {
|
|
116
|
+
sections.push(`Validated insights:\n${args.insights.map((s) => `- ${s}`).join('\n')}`);
|
|
117
|
+
}
|
|
118
|
+
if (args.procedures.length > 0) {
|
|
119
|
+
sections.push(`Active procedures:\n${args.procedures.map((s) => `- ${s}`).join('\n')}`);
|
|
120
|
+
}
|
|
121
|
+
sections.push('Return ONLY the rewritten digest text.');
|
|
122
|
+
return {
|
|
123
|
+
messages: [{ role: 'user', content: sections.join('\n\n') }],
|
|
124
|
+
systemMessage: LEARNED_CONTEXT_SYSTEM_PROMPT,
|
|
125
|
+
temperature: 0,
|
|
126
|
+
maxTokens: Math.max(256, Math.ceil(args.maxChars / 2)),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run the learned-context rewrite. Resilient by construction: a
|
|
132
|
+
* provider failure or empty rewrite leaves the existing block untouched
|
|
133
|
+
* and never throws; only storage errors propagate.
|
|
134
|
+
*/
|
|
135
|
+
export async function runLearnedContextPass(
|
|
136
|
+
deps: LearnedContextDeps,
|
|
137
|
+
): Promise<LearnedContextOutcome> {
|
|
138
|
+
return withMemorySpan(
|
|
139
|
+
deps.tracer,
|
|
140
|
+
'memory.consolidate.learned-context',
|
|
141
|
+
deps.scope,
|
|
142
|
+
{ 'consolidator.phase': 'learned-context' },
|
|
143
|
+
async (span) => {
|
|
144
|
+
const skip = (
|
|
145
|
+
reason: NonNullable<LearnedContextOutcome['skippedReason']>,
|
|
146
|
+
chars = 0,
|
|
147
|
+
): LearnedContextOutcome => {
|
|
148
|
+
span.setAttributes({ 'consolidator.learned_context.skipped': reason });
|
|
149
|
+
return { updated: false, chars, tokens: 0, costUsd: 0, skippedReason: reason };
|
|
150
|
+
};
|
|
151
|
+
if (deps.budget.snapshot().paused) return skip('budget');
|
|
152
|
+
|
|
153
|
+
// Ensure the reserved block is registered so write() can create it.
|
|
154
|
+
if (deps.working.definitionFor(LEARNED_CONTEXT_BLOCK_LABEL) === undefined) {
|
|
155
|
+
deps.working.define(
|
|
156
|
+
defineBlock({
|
|
157
|
+
label: LEARNED_CONTEXT_BLOCK_LABEL,
|
|
158
|
+
description:
|
|
159
|
+
'Standing digest of durable knowledge about the user and active work, ' +
|
|
160
|
+
'maintained by the deep-phase learned-context pass.',
|
|
161
|
+
charLimit: deps.maxChars,
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const previous = (await deps.working.read(deps.scope, LEARNED_CONTEXT_BLOCK_LABEL)) ?? '';
|
|
166
|
+
|
|
167
|
+
// Evidence: recent episodes (incl. quarantined auto-formed ones -
|
|
168
|
+
// they carry the importance signal), ACTIVE insights only (the
|
|
169
|
+
// digest drives behaviour, so quarantined synthesis stays out),
|
|
170
|
+
// and active procedures.
|
|
171
|
+
const episodes =
|
|
172
|
+
deps.episodic === null
|
|
173
|
+
? []
|
|
174
|
+
: (
|
|
175
|
+
await deps.episodic.listRecent(deps.scope, EPISODE_CONTEXT_LIMIT, {
|
|
176
|
+
includeQuarantined: true,
|
|
177
|
+
})
|
|
178
|
+
).map((episode) => episode.summary);
|
|
179
|
+
const insights =
|
|
180
|
+
deps.store.insights === undefined
|
|
181
|
+
? []
|
|
182
|
+
: (await deps.store.insights.list(deps.scope, { limit: INSIGHT_CONTEXT_LIMIT })).map(
|
|
183
|
+
(insight) => insight.text,
|
|
184
|
+
);
|
|
185
|
+
const procedures = (await deps.store.procedural.list(deps.scope))
|
|
186
|
+
.filter((rule) => rule.status !== 'quarantined')
|
|
187
|
+
.slice(0, PROCEDURE_CONTEXT_LIMIT)
|
|
188
|
+
.map((rule) => rule.text.split('\n')[0] ?? rule.text);
|
|
189
|
+
|
|
190
|
+
const evidenceCount = episodes.length + insights.length + procedures.length;
|
|
191
|
+
span.setAttributes({ 'consolidator.learned_context.evidence': evidenceCount });
|
|
192
|
+
// Nothing new to fold and nothing written yet ⇒ no paid call.
|
|
193
|
+
if (evidenceCount === 0) return skip('no-evidence', previous.length);
|
|
194
|
+
|
|
195
|
+
let tokens = 0;
|
|
196
|
+
let costUsd = 0;
|
|
197
|
+
try {
|
|
198
|
+
const res = await deps.provider.generate(
|
|
199
|
+
buildLearnedContextRequest({
|
|
200
|
+
previous,
|
|
201
|
+
episodes,
|
|
202
|
+
insights,
|
|
203
|
+
procedures,
|
|
204
|
+
maxChars: deps.maxChars,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
tokens =
|
|
208
|
+
(res.usage.promptTokens ?? 0) +
|
|
209
|
+
(res.usage.completionTokens ?? 0) +
|
|
210
|
+
(res.usage.reasoningTokens ?? 0);
|
|
211
|
+
costUsd =
|
|
212
|
+
deps.priceUsage?.({
|
|
213
|
+
promptTokens: res.usage.promptTokens ?? 0,
|
|
214
|
+
completionTokens: res.usage.completionTokens ?? 0,
|
|
215
|
+
}) ?? 0;
|
|
216
|
+
deps.budget.record({ phase: 'deep', tokens, costUsd });
|
|
217
|
+
const next = normalizeLearnedContext(res.text ?? '', deps.maxChars);
|
|
218
|
+
if (next === null) {
|
|
219
|
+
return { ...skip('empty-rewrite', previous.length), tokens, costUsd };
|
|
220
|
+
}
|
|
221
|
+
await deps.working.write(deps.scope, LEARNED_CONTEXT_BLOCK_LABEL, next);
|
|
222
|
+
span.setAttributes({
|
|
223
|
+
'consolidator.learned_context.updated': true,
|
|
224
|
+
'consolidator.learned_context.chars': next.length,
|
|
225
|
+
});
|
|
226
|
+
return { updated: true, chars: next.length, tokens, costUsd };
|
|
227
|
+
} catch {
|
|
228
|
+
// Resilient: a provider failure must never fail the deep phase.
|
|
229
|
+
return { ...skip('provider-error', previous.length), tokens, costUsd };
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
}
|