@inflexa-ai/harness 0.1.1 → 0.2.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/dist/agents/conversation-agent.d.ts +12 -11
- package/dist/agents/conversation-agent.js +30 -33
- package/dist/agents/report-builder.js +2 -1
- package/dist/agents/sandbox/bulk-transcriptomics-agent.js +1 -1
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -19
- package/dist/agents/sandbox/chromatin-agent.js +1 -10
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -10
- package/dist/agents/sandbox/drug-repurposing-agent.js +4 -14
- package/dist/agents/sandbox/enrichment-agent.js +1 -10
- package/dist/agents/sandbox/ephemeral-executor.js +1 -10
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -3
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -13
- package/dist/agents/sandbox/metabolomics-agent.js +1 -1
- package/dist/agents/sandbox/microbiome-agent.js +1 -1
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -12
- package/dist/agents/sandbox/multimodal-sc-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -10
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +2 -8
- package/dist/agents/sandbox/proteomics-agent.js +1 -1
- package/dist/agents/sandbox/scientific-executor.js +1 -10
- package/dist/agents/sandbox/shared.d.ts +17 -6
- package/dist/agents/sandbox/shared.js +49 -38
- package/dist/agents/sandbox/single-cell-agent.js +1 -1
- package/dist/agents/sandbox/spatial-omics-agent.js +1 -1
- package/dist/agents/sandbox/statistical-modeling-agent.js +1 -1
- package/dist/agents/sandbox/translational-safety-agent.js +3 -9
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/agents/system-prompt.d.ts +37 -11
- package/dist/agents/system-prompt.js +35 -12
- package/dist/app/data-profile-orientation.d.ts +34 -0
- package/dist/app/data-profile-orientation.js +117 -0
- package/dist/contracts/schemas/chat-parts.d.ts +9 -9
- package/dist/execution/artifact-metadata.js +2 -1
- package/dist/execution/ephemeral-runner.d.ts +13 -0
- package/dist/execution/ephemeral-runner.js +23 -1
- package/dist/execution/run-synthesis.js +20 -19
- package/dist/execution/step-summary.js +2 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +19 -5
- package/dist/loop/__fixtures__/scripted-provider.d.ts +2 -2
- package/dist/loop/__fixtures__/scripted-provider.js +2 -1
- package/dist/loop/metrics.d.ts +33 -1
- package/dist/loop/metrics.js +56 -2
- package/dist/loop/run-agent.d.ts +13 -1
- package/dist/loop/run-agent.js +19 -4
- package/dist/memory/card-builders.d.ts +8 -1
- package/dist/memory/card-builders.js +20 -3
- package/dist/memory/reconstruct-cards.js +4 -3
- package/dist/memory/thread-history.d.ts +27 -2
- package/dist/memory/thread-history.js +45 -5
- package/dist/memory/working-memory.d.ts +93 -10
- package/dist/memory/working-memory.js +220 -84
- package/dist/prompts/SOUL.d.ts +22 -9
- package/dist/prompts/SOUL.js +56 -37
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +2 -2
- package/dist/prompts/briefing.d.ts +100 -0
- package/dist/prompts/briefing.js +166 -0
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +94 -440
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +10 -10
- package/dist/prompts/planner.js +28 -59
- package/dist/prompts/report-builder.d.ts +1 -1
- package/dist/prompts/report-builder.js +71 -24
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +2 -7
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +11 -15
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +3 -7
- package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
- package/dist/prompts/sandbox/data-profiler.js +13 -8
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +15 -16
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +2 -9
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +2 -6
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +3 -8
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +1 -7
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +2 -7
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +2 -8
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +2 -7
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +2 -7
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +2 -9
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +2 -9
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -6
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +2 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +13 -14
- package/dist/prompts/sandbox-standards.d.ts +22 -14
- package/dist/prompts/sandbox-standards.js +91 -71
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +33 -131
- package/dist/provenance/collector.js +1 -1
- package/dist/providers/ai-sdk.js +31 -4
- package/dist/providers/prompt-cache.d.ts +74 -0
- package/dist/providers/prompt-cache.js +77 -0
- package/dist/providers/types.d.ts +33 -0
- package/dist/reference-data/catalog.d.ts +131 -0
- package/dist/reference-data/catalog.js +348 -0
- package/dist/reference-data/receipt.d.ts +41 -0
- package/dist/reference-data/receipt.js +40 -0
- package/dist/schemas/data-profile-schemas.js +5 -3
- package/dist/schemas/plan-schemas.d.ts +21 -21
- package/dist/schemas/plan-schemas.js +9 -3
- package/dist/schemas/run-synthesis.d.ts +8 -4
- package/dist/schemas/run-synthesis.js +92 -56
- package/dist/schemas/validate-plan.js +1 -1
- package/dist/schemas/workflow-state.d.ts +7 -1
- package/dist/schemas/workflow-state.js +33 -19
- package/dist/state/artifacts.d.ts +13 -0
- package/dist/state/artifacts.js +27 -0
- package/dist/state/data-profile.d.ts +64 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +1 -1
- package/dist/tasks/data-profile.d.ts +20 -2
- package/dist/tasks/data-profile.js +71 -22
- package/dist/tools/bio/chembl.d.ts +38 -0
- package/dist/tools/bio/chembl.js +138 -0
- package/dist/tools/bio/comptox.d.ts +50 -0
- package/dist/tools/bio/comptox.js +149 -0
- package/dist/tools/bio/get-impc-ko-profile.js +9 -6
- package/dist/tools/bio/index.d.ts +5 -17
- package/dist/tools/bio/index.js +5 -17
- package/dist/tools/bio/keys.d.ts +14 -9
- package/dist/tools/bio/keys.js +16 -16
- package/dist/tools/bio/opentargets.d.ts +38 -0
- package/dist/tools/bio/opentargets.js +89 -0
- package/dist/tools/bio/pubchem.d.ts +29 -0
- package/dist/tools/bio/pubchem.js +92 -0
- package/dist/tools/bio/pubmed.d.ts +51 -0
- package/dist/tools/bio/pubmed.js +100 -0
- package/dist/tools/bio/search-bgee-expression.js +12 -6
- package/dist/tools/bio/search-clinical-trials.js +16 -5
- package/dist/tools/bio/search-faers.js +8 -4
- package/dist/tools/bio/search-geo-datasets.js +20 -6
- package/dist/tools/bio/search-gwas-catalog.d.ts +1 -1
- package/dist/tools/bio/search-pharmgkb.js +10 -4
- package/dist/tools/display/index.d.ts +1 -0
- package/dist/tools/display/index.js +1 -0
- package/dist/tools/display/normalize-echart-spec.d.ts +61 -0
- package/dist/tools/display/normalize-echart-spec.js +211 -0
- package/dist/tools/display/show-user.d.ts +6 -1
- package/dist/tools/display/show-user.js +25 -12
- package/dist/tools/execute-plan.js +5 -2
- package/dist/tools/iterate-report.d.ts +168 -118
- package/dist/tools/iterate-report.js +334 -96
- package/dist/tools/lib/chembl-client.d.ts +12 -0
- package/dist/tools/lib/chembl-client.js +127 -0
- package/dist/tools/lib/ctx-ops.d.ts +224 -0
- package/dist/tools/lib/ctx-ops.js +450 -0
- package/dist/tools/lib/iuphar-client.js +10 -1
- package/dist/tools/lib/openfda-client.js +11 -3
- package/dist/tools/lib/pathway-client.js +10 -4
- package/dist/tools/lib/pubchem-ops.d.ts +74 -0
- package/dist/tools/lib/pubchem-ops.js +224 -0
- package/dist/tools/memory/update-working-memory.d.ts +45 -7
- package/dist/tools/memory/update-working-memory.js +111 -41
- package/dist/tools/report/index.d.ts +3 -2
- package/dist/tools/report/index.js +3 -2
- package/dist/tools/research/generate-analogy-report.js +25 -12
- package/dist/tools/research/generate-plan.d.ts +12 -5
- package/dist/tools/research/generate-plan.js +168 -37
- package/dist/tools/research/index.d.ts +1 -0
- package/dist/tools/research/index.js +1 -0
- package/dist/tools/research/inspect-data-profile.d.ts +85 -0
- package/dist/tools/research/inspect-data-profile.js +161 -0
- package/dist/tools/research/literature-reviewer.js +3 -4
- package/dist/tools/sandbox/list-available-packages.d.ts +79 -5
- package/dist/tools/sandbox/list-available-packages.js +152 -10
- package/dist/tools/sandbox/list-available-refs.d.ts +141 -5
- package/dist/tools/sandbox/list-available-refs.js +647 -134
- package/dist/tools/sandbox/report-blocker.d.ts +39 -9
- package/dist/tools/sandbox/report-blocker.js +43 -20
- package/dist/tools/workspace/read-file.js +4 -5
- package/dist/tools/workspace/run-exec.d.ts +4 -4
- package/dist/tools/workspace/run-exec.js +4 -4
- package/dist/tools/workspace/show-file.js +10 -6
- package/dist/tools/workspace/show-plan.js +12 -6
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/tools/workspace/workspace-search.js +11 -5
- package/dist/workflows/execute-analysis.d.ts +43 -4
- package/dist/workflows/execute-analysis.js +107 -6
- package/dist/workflows/target-assessment/lib/citation-validator.js +2 -2
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +2 -1
- package/dist/workspace/paths.d.ts +2 -2
- package/dist/workspace/paths.js +2 -2
- package/package.json +33 -33
- package/dist/execution/filesystem-artifact-registry.d.ts +0 -57
- package/dist/execution/filesystem-artifact-registry.js +0 -92
- package/dist/lib/identifiers.d.ts +0 -8
- package/dist/lib/identifiers.js +0 -10
- package/dist/lib/mime-detect.d.ts +0 -17
- package/dist/lib/mime-detect.js +0 -29
- package/dist/lib/streamed-download.d.ts +0 -28
- package/dist/lib/streamed-download.js +0 -111
- package/dist/lib/trace-context.d.ts +0 -11
- package/dist/lib/trace-context.js +0 -49
- package/dist/providers/llm-capabilities.d.ts +0 -33
- package/dist/providers/llm-capabilities.js +0 -58
- package/dist/schemas/render-step-prompt.d.ts +0 -32
- package/dist/schemas/render-step-prompt.js +0 -76
- package/dist/tools/bio/find-approval-precedent.d.ts +0 -32
- package/dist/tools/bio/find-approval-precedent.js +0 -72
- package/dist/tools/bio/get-article-details.d.ts +0 -11
- package/dist/tools/bio/get-article-details.js +0 -20
- package/dist/tools/bio/get-article-full-text.d.ts +0 -19
- package/dist/tools/bio/get-article-full-text.js +0 -32
- package/dist/tools/bio/get-bioactivity.d.ts +0 -11
- package/dist/tools/bio/get-bioactivity.js +0 -23
- package/dist/tools/bio/get-drug-info.d.ts +0 -9
- package/dist/tools/bio/get-drug-info.js +0 -22
- package/dist/tools/bio/get-mechanism.d.ts +0 -8
- package/dist/tools/bio/get-mechanism.js +0 -18
- package/dist/tools/bio/get-pubchem-assays.d.ts +0 -22
- package/dist/tools/bio/get-pubchem-assays.js +0 -107
- package/dist/tools/bio/get-pubchem-cross-refs.d.ts +0 -15
- package/dist/tools/bio/get-pubchem-cross-refs.js +0 -68
- package/dist/tools/bio/get-target-safety.d.ts +0 -18
- package/dist/tools/bio/get-target-safety.js +0 -26
- package/dist/tools/bio/search-compounds.d.ts +0 -22
- package/dist/tools/bio/search-compounds.js +0 -160
- package/dist/tools/bio/search-ctx-chemical.d.ts +0 -50
- package/dist/tools/bio/search-ctx-chemical.js +0 -130
- package/dist/tools/bio/search-ctx-exposure.d.ts +0 -55
- package/dist/tools/bio/search-ctx-exposure.js +0 -176
- package/dist/tools/bio/search-ctx-hazard.d.ts +0 -111
- package/dist/tools/bio/search-ctx-hazard.js +0 -178
- package/dist/tools/bio/search-opentargets.d.ts +0 -11
- package/dist/tools/bio/search-opentargets.js +0 -25
- package/dist/tools/bio/search-pubchem-compound.d.ts +0 -26
- package/dist/tools/bio/search-pubchem-compound.js +0 -110
- package/dist/tools/bio/search-pubmed.d.ts +0 -17
- package/dist/tools/bio/search-pubmed.js +0 -31
- package/dist/tools/bio/search-targets.d.ts +0 -9
- package/dist/tools/bio/search-targets.js +0 -20
- package/dist/tools/bio/search-toxcast.d.ts +0 -36
- package/dist/tools/bio/search-toxcast.js +0 -141
- package/dist/workflows/register-workflows.d.ts +0 -51
- package/dist/workflows/register-workflows.js +0 -48
- package/dist/workflows/resume-execute-analysis.d.ts +0 -70
- package/dist/workflows/resume-execute-analysis.js +0 -80
- package/dist/workflows/target-assessment/correction-loop.d.ts +0 -45
- package/dist/workflows/target-assessment/correction-loop.js +0 -62
- package/dist/workflows/target-assessment/steps/phase0-resolve.d.ts +0 -71
- package/dist/workflows/target-assessment/steps/phase0-resolve.js +0 -21
- package/dist/workflows/target-assessment/steps/phase1-aggregate.d.ts +0 -1018
- package/dist/workflows/target-assessment/steps/phase1-aggregate.js +0 -25
- package/dist/workspace/mount-strategy.d.ts +0 -50
- package/dist/workspace/mount-strategy.js +0 -67
- package/dist/workspace/provenance-collector.d.ts +0 -34
- package/dist/workspace/provenance-collector.js +0 -16
- package/dist/workspace/workspace-paths.d.ts +0 -26
- package/dist/workspace/workspace-paths.js +0 -31
|
@@ -79,6 +79,28 @@ function getInstruments() {
|
|
|
79
79
|
export function __resetThreadHistoryMetricsForTest() {
|
|
80
80
|
instruments = undefined;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Chunked-eviction block size, in whole turns — the granularity at which
|
|
84
|
+
* `loadRecent`'s retained window may advance its START turn.
|
|
85
|
+
*
|
|
86
|
+
* `loadRecent` snaps its eviction count UP to a multiple of this block, so the
|
|
87
|
+
* window's first turn — hence `messages[0]` and the whole tools+system+history
|
|
88
|
+
* prompt-cache prefix — stays byte-identical across a run of appends and shifts
|
|
89
|
+
* only once per block instead of once per turn. That single shift is the only
|
|
90
|
+
* message-cache miss the block costs.
|
|
91
|
+
*
|
|
92
|
+
* The block is at once (a) the cache-miss cadence — one prefix shift per
|
|
93
|
+
* `EVICTION_BLOCK_TURNS` appends once a thread is over budget — and (b) the most
|
|
94
|
+
* extra context ever sacrificed: snapping up drops up to `EVICTION_BLOCK_TURNS -
|
|
95
|
+
* 1` oldest turns the budget alone would have kept, trading a little history for
|
|
96
|
+
* a still prefix. 4 holds the prefix for ~4 turns at a cost of at most 3 turns of
|
|
97
|
+
* headroom — negligible against a ~120k-token budget that spans far more turns,
|
|
98
|
+
* while cutting cache misses on a long thread roughly fourfold. A turn-count
|
|
99
|
+
* block (not a token block) is deliberate: a cache miss is triggered by the START
|
|
100
|
+
* turn moving, a per-turn event, so counting in turns makes the miss cadence
|
|
101
|
+
* exact and independent of how large any individual turn is.
|
|
102
|
+
*/
|
|
103
|
+
export const EVICTION_BLOCK_TURNS = 4;
|
|
82
104
|
/**
|
|
83
105
|
* Create a `ThreadHistory` bound to a Postgres pool — a factory closure
|
|
84
106
|
* capturing `pool` (dependency injection per the harness-durable-runtime spec). The `messages` table is
|
|
@@ -125,9 +147,22 @@ export function createThreadHistory(pool) {
|
|
|
125
147
|
const turns = groupTurns(parsed, (row) => isGenuineUserStart(row.message));
|
|
126
148
|
const turnTokens = turns.map((turn) => turn.reduce((sum, row) => sum + row.tokens, 0));
|
|
127
149
|
const threadTotal = turnTokens.reduce((sum, n) => sum + n, 0);
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
150
|
+
// Chunked eviction holds the prompt-cache prefix still. The budget alone
|
|
151
|
+
// evicts the OLDEST turns newest-first, so the window START — the stable
|
|
152
|
+
// head of the tools+system+history cache prefix — advances by ~one turn
|
|
153
|
+
// per appended turn; every turn then ships a shifted `messages[0]` and
|
|
154
|
+
// rewrites the whole message cache instead of reading it back (watch
|
|
155
|
+
// `cortex.harness.agent.cache_read_tokens` collapse on long threads).
|
|
156
|
+
// Instead, take the budget-minimal eviction and snap it UP to a whole
|
|
157
|
+
// `EVICTION_BLOCK_TURNS` block: `evicted` stays constant while the minimum
|
|
158
|
+
// sits inside a block and jumps by a block when it crosses, so
|
|
159
|
+
// `messages[0]` is byte-identical for a block of appends (the cache reads
|
|
160
|
+
// survive) and shifts only once per block. Snapping UP retains a subset of
|
|
161
|
+
// the budget-minimal window, so the kept tokens never exceed `tokenBudget`.
|
|
162
|
+
//
|
|
163
|
+
// Budget-minimal fill: walk newest-first, always keeping the most recent
|
|
164
|
+
// turn (a valid over-budget single turn beats an under-budget cut), then
|
|
165
|
+
// add older turns while they fit.
|
|
131
166
|
let included = 0;
|
|
132
167
|
let cumulative = 0;
|
|
133
168
|
for (let t = turns.length - 1; t >= 0; t--) {
|
|
@@ -136,13 +171,18 @@ export function createThreadHistory(pool) {
|
|
|
136
171
|
cumulative += turnTokens[t];
|
|
137
172
|
included++;
|
|
138
173
|
}
|
|
139
|
-
const
|
|
174
|
+
const minimalEvicted = turns.length - included;
|
|
175
|
+
// Snap the eviction count up to a block boundary, clamped below the last
|
|
176
|
+
// turn so the most recent turn always ships. Both terms of the min are
|
|
177
|
+
// >= `minimalEvicted`, so `turnsEvicted >= minimalEvicted`: the retained
|
|
178
|
+
// suffix is a subset of the budget-minimal one and stays within budget.
|
|
179
|
+
const turnsEvicted = turns.length === 0 ? 0 : Math.min(Math.ceil(minimalEvicted / EVICTION_BLOCK_TURNS) * EVICTION_BLOCK_TURNS, turns.length - 1);
|
|
140
180
|
const { totalTokens, turnsEvicted: turnsEvictedHist } = getInstruments();
|
|
141
181
|
const attributes = { eviction: turnsEvicted > 0 };
|
|
142
182
|
totalTokens.record(threadTotal, attributes);
|
|
143
183
|
turnsEvictedHist.record(turnsEvicted, attributes);
|
|
144
184
|
return turns
|
|
145
|
-
.slice(
|
|
185
|
+
.slice(turnsEvicted)
|
|
146
186
|
.flat()
|
|
147
187
|
.map((row) => row.message);
|
|
148
188
|
});
|
|
@@ -6,18 +6,48 @@
|
|
|
6
6
|
* to Markdown only when injected (see the harness-thread-store spec).
|
|
7
7
|
*
|
|
8
8
|
* Four sections. `goal`, `constraints`, `hypotheses` are analysis-flat;
|
|
9
|
-
* `findings` is
|
|
10
|
-
*
|
|
9
|
+
* `findings` is stored keyed by `runId`, so a finding stays attributable to
|
|
10
|
+
* the run that produced it. There is deliberately no `context` section —
|
|
11
11
|
* analysis context lives in `cortex_analysis_state.context` and is injected
|
|
12
12
|
* separately by the chat route.
|
|
13
13
|
*
|
|
14
14
|
* `updateSection` is a section-addressable read-modify-write: the named
|
|
15
15
|
* section is replaced or amended and the other three are left byte-identical.
|
|
16
|
+
*
|
|
17
|
+
* **Working memory is re-injected, in full, into the uncached tail of every
|
|
18
|
+
* turn** — so every stored character is re-paid on every later turn for the
|
|
19
|
+
* life of the analysis. Two consequences shape this module:
|
|
20
|
+
*
|
|
21
|
+
* - **Every section is bounded at write time** (`WORKING_MEMORY_LIMITS`). An
|
|
22
|
+
* over-cap write is *rejected* with a model-actionable message, never
|
|
23
|
+
* silently truncated: the agent must shorten the text or retire a stale
|
|
24
|
+
* entry. Every section — findings included — therefore supports
|
|
25
|
+
* add/revise/retire, so memory can shrink as well as grow.
|
|
26
|
+
* - **Memory holds references to runs, never run content.** A finding cites
|
|
27
|
+
* the `runId` that produced it and renders as one line in a single flat
|
|
28
|
+
* list; the run's detail stays retrievable via `inspect_run`.
|
|
16
29
|
*/
|
|
17
|
-
import type
|
|
30
|
+
import { type ResultAsync } from "neverthrow";
|
|
18
31
|
import type { Pool } from "pg";
|
|
19
32
|
import { z } from "zod";
|
|
20
33
|
import { type DbError } from "../lib/db-result.js";
|
|
34
|
+
/**
|
|
35
|
+
* The write-time bounds on working memory. `updateSection` enforces them, the
|
|
36
|
+
* `update_working_memory` input schema declares them to the model (importing
|
|
37
|
+
* these same numbers), and `renderWorkingMemory` uses them to bound its output.
|
|
38
|
+
*/
|
|
39
|
+
export declare const WORKING_MEMORY_LIMITS: {
|
|
40
|
+
/** Max characters in the goal. */
|
|
41
|
+
readonly goalChars: 500;
|
|
42
|
+
/** Max characters in one constraint / hypothesis / finding entry. */
|
|
43
|
+
readonly entryChars: 300;
|
|
44
|
+
/** Max constraint entries. */
|
|
45
|
+
readonly constraints: 20;
|
|
46
|
+
/** Max hypothesis entries. */
|
|
47
|
+
readonly hypotheses: 10;
|
|
48
|
+
/** Max finding entries, summed across every run. */
|
|
49
|
+
readonly findings: 30;
|
|
50
|
+
};
|
|
21
51
|
/** A binding rule — user-stated or agent-derived. The highest-value section. */
|
|
22
52
|
export declare const constraintSchema: z.ZodObject<{
|
|
23
53
|
id: z.ZodString;
|
|
@@ -43,6 +73,11 @@ export type Finding = z.infer<typeof findingSchema>;
|
|
|
43
73
|
/**
|
|
44
74
|
* The persisted `data` shape — one JSONB object per analysis. `findings` is
|
|
45
75
|
* keyed by `runId`; an analysis with no recorded findings has an empty map.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately unbounded: the caps are a *write-time* rule, not a read-time
|
|
78
|
+
* one. A row written before the caps existed must still `load` — so this
|
|
79
|
+
* schema validates shape only, and `renderWorkingMemory` bounds what an
|
|
80
|
+
* oversized row is allowed to cost.
|
|
46
81
|
*/
|
|
47
82
|
export declare const workingMemorySchema: z.ZodObject<{
|
|
48
83
|
goal: z.ZodString;
|
|
@@ -66,6 +101,20 @@ export declare const workingMemorySchema: z.ZodObject<{
|
|
|
66
101
|
export type WorkingMemory = z.infer<typeof workingMemorySchema>;
|
|
67
102
|
/** The shape `load` returns for an analysis with no working-memory row. */
|
|
68
103
|
export declare function emptyWorkingMemory(): WorkingMemory;
|
|
104
|
+
/**
|
|
105
|
+
* A refused write: a cap was hit, or an id addresses nothing. Not a storage
|
|
106
|
+
* failure — it is an expected outcome the *model* can correct, so `message` is
|
|
107
|
+
* written for the model and says what to do next (shorten this, retire that).
|
|
108
|
+
* The tool surfaces it as an `is_error` tool result; the row is untouched.
|
|
109
|
+
*/
|
|
110
|
+
export interface WorkingMemoryRejection {
|
|
111
|
+
readonly type: "working_memory_rejected";
|
|
112
|
+
readonly message: string;
|
|
113
|
+
}
|
|
114
|
+
/** `updateSection`'s error channel: a storage failure, or a refused write. */
|
|
115
|
+
export type WorkingMemoryError = DbError | WorkingMemoryRejection;
|
|
116
|
+
/** Narrow a `WorkingMemoryError` to the model-correctable branch. */
|
|
117
|
+
export declare function isWorkingMemoryRejection(error: WorkingMemoryError): error is WorkingMemoryRejection;
|
|
69
118
|
/**
|
|
70
119
|
* A list-section amendment: append a new entry, revise an existing one's
|
|
71
120
|
* text, or retire a stale one. `revise`/`retire` address an entry by the
|
|
@@ -95,6 +144,23 @@ export type ConstraintOp = {
|
|
|
95
144
|
readonly op: "retire";
|
|
96
145
|
readonly id: string;
|
|
97
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* As `ListOp`, but `add` also carries the `runId` the finding is attributed
|
|
149
|
+
* to. `revise`/`retire` address the finding by its own id — the run holding it
|
|
150
|
+
* is found from that id, so the agent never has to re-cite the run.
|
|
151
|
+
*/
|
|
152
|
+
export type FindingOp = {
|
|
153
|
+
readonly op: "add";
|
|
154
|
+
readonly runId: string;
|
|
155
|
+
readonly text: string;
|
|
156
|
+
} | {
|
|
157
|
+
readonly op: "revise";
|
|
158
|
+
readonly id: string;
|
|
159
|
+
readonly text: string;
|
|
160
|
+
} | {
|
|
161
|
+
readonly op: "retire";
|
|
162
|
+
readonly id: string;
|
|
163
|
+
};
|
|
98
164
|
/** The `value` argument of `updateSection`, keyed by the `section` name. */
|
|
99
165
|
export interface SectionValue {
|
|
100
166
|
readonly goal: {
|
|
@@ -102,10 +168,7 @@ export interface SectionValue {
|
|
|
102
168
|
};
|
|
103
169
|
readonly constraint: ConstraintOp;
|
|
104
170
|
readonly hypothesis: ListOp;
|
|
105
|
-
readonly finding:
|
|
106
|
-
readonly runId: string;
|
|
107
|
-
readonly text: string;
|
|
108
|
-
};
|
|
171
|
+
readonly finding: FindingOp;
|
|
109
172
|
}
|
|
110
173
|
export type Section = keyof SectionValue;
|
|
111
174
|
/**
|
|
@@ -118,13 +181,33 @@ export interface WorkingMemoryStore {
|
|
|
118
181
|
load(analysisId: string): ResultAsync<WorkingMemory, DbError>;
|
|
119
182
|
/**
|
|
120
183
|
* Atomically read-modify-write one section. The other three sections are
|
|
121
|
-
* left byte-identical; the row is upserted if absent.
|
|
184
|
+
* left byte-identical; the row is upserted if absent. A write that breaks a
|
|
185
|
+
* cap or addresses an unknown id is refused — `err(WorkingMemoryRejection)`,
|
|
186
|
+
* and the row is left exactly as it was.
|
|
122
187
|
*/
|
|
123
|
-
updateSection<S extends Section>(analysisId: string, section: S, value: SectionValue[S]): ResultAsync<void,
|
|
188
|
+
updateSection<S extends Section>(analysisId: string, section: S, value: SectionValue[S]): ResultAsync<void, WorkingMemoryError>;
|
|
124
189
|
/** Serialize working memory to a Markdown document for injection. */
|
|
125
190
|
render(analysisId: string): ResultAsync<string, DbError>;
|
|
126
191
|
}
|
|
127
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Serialize working memory to the Markdown document injected each turn.
|
|
194
|
+
*
|
|
195
|
+
* Renders only what exists: an empty section is omitted entirely — no heading,
|
|
196
|
+
* no placeholder — and an entirely empty memory renders to the **empty
|
|
197
|
+
* string**, costing nothing. The chat route injects the render
|
|
198
|
+
* unconditionally, and the provider's outbound sanitizer drops a message whose
|
|
199
|
+
* content is `""` before the wire call (`providers/ai-sdk.ts`), so an empty
|
|
200
|
+
* memory reaches the model as no message at all.
|
|
201
|
+
*
|
|
202
|
+
* Findings render as ONE flat list, each line citing the run it came from —
|
|
203
|
+
* never a per-run heading block. Memory holds the reference; `inspect_run`
|
|
204
|
+
* holds the run.
|
|
205
|
+
*
|
|
206
|
+
* The output is bounded to `WORKING_MEMORY_LIMITS` no matter what the row
|
|
207
|
+
* holds. Writes are capped, so only a row written before the caps existed can
|
|
208
|
+
* exceed them — and it degrades to its newest entries rather than exploding
|
|
209
|
+
* the context window.
|
|
210
|
+
*/
|
|
128
211
|
export declare function renderWorkingMemory(wm: WorkingMemory): string;
|
|
129
212
|
/**
|
|
130
213
|
* Create a `WorkingMemoryStore` bound to a Postgres pool — a factory closure
|
|
@@ -6,16 +6,47 @@
|
|
|
6
6
|
* to Markdown only when injected (see the harness-thread-store spec).
|
|
7
7
|
*
|
|
8
8
|
* Four sections. `goal`, `constraints`, `hypotheses` are analysis-flat;
|
|
9
|
-
* `findings` is
|
|
10
|
-
*
|
|
9
|
+
* `findings` is stored keyed by `runId`, so a finding stays attributable to
|
|
10
|
+
* the run that produced it. There is deliberately no `context` section —
|
|
11
11
|
* analysis context lives in `cortex_analysis_state.context` and is injected
|
|
12
12
|
* separately by the chat route.
|
|
13
13
|
*
|
|
14
14
|
* `updateSection` is a section-addressable read-modify-write: the named
|
|
15
15
|
* section is replaced or amended and the other three are left byte-identical.
|
|
16
|
+
*
|
|
17
|
+
* **Working memory is re-injected, in full, into the uncached tail of every
|
|
18
|
+
* turn** — so every stored character is re-paid on every later turn for the
|
|
19
|
+
* life of the analysis. Two consequences shape this module:
|
|
20
|
+
*
|
|
21
|
+
* - **Every section is bounded at write time** (`WORKING_MEMORY_LIMITS`). An
|
|
22
|
+
* over-cap write is *rejected* with a model-actionable message, never
|
|
23
|
+
* silently truncated: the agent must shorten the text or retire a stale
|
|
24
|
+
* entry. Every section — findings included — therefore supports
|
|
25
|
+
* add/revise/retire, so memory can shrink as well as grow.
|
|
26
|
+
* - **Memory holds references to runs, never run content.** A finding cites
|
|
27
|
+
* the `runId` that produced it and renders as one line in a single flat
|
|
28
|
+
* list; the run's detail stays retrievable via `inspect_run`.
|
|
16
29
|
*/
|
|
30
|
+
import { err, ok, okAsync } from "neverthrow";
|
|
17
31
|
import { z } from "zod";
|
|
18
32
|
import { tryMutation, tryQuery, withTransaction } from "../lib/db-result.js";
|
|
33
|
+
/**
|
|
34
|
+
* The write-time bounds on working memory. `updateSection` enforces them, the
|
|
35
|
+
* `update_working_memory` input schema declares them to the model (importing
|
|
36
|
+
* these same numbers), and `renderWorkingMemory` uses them to bound its output.
|
|
37
|
+
*/
|
|
38
|
+
export const WORKING_MEMORY_LIMITS = {
|
|
39
|
+
/** Max characters in the goal. */
|
|
40
|
+
goalChars: 500,
|
|
41
|
+
/** Max characters in one constraint / hypothesis / finding entry. */
|
|
42
|
+
entryChars: 300,
|
|
43
|
+
/** Max constraint entries. */
|
|
44
|
+
constraints: 20,
|
|
45
|
+
/** Max hypothesis entries. */
|
|
46
|
+
hypotheses: 10,
|
|
47
|
+
/** Max finding entries, summed across every run. */
|
|
48
|
+
findings: 30,
|
|
49
|
+
};
|
|
19
50
|
/** A binding rule — user-stated or agent-derived. The highest-value section. */
|
|
20
51
|
export const constraintSchema = z.object({
|
|
21
52
|
id: z.string(),
|
|
@@ -35,6 +66,11 @@ export const findingSchema = z.object({
|
|
|
35
66
|
/**
|
|
36
67
|
* The persisted `data` shape — one JSONB object per analysis. `findings` is
|
|
37
68
|
* keyed by `runId`; an analysis with no recorded findings has an empty map.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately unbounded: the caps are a *write-time* rule, not a read-time
|
|
71
|
+
* one. A row written before the caps existed must still `load` — so this
|
|
72
|
+
* schema validates shape only, and `renderWorkingMemory` bounds what an
|
|
73
|
+
* oversized row is allowed to cost.
|
|
38
74
|
*/
|
|
39
75
|
export const workingMemorySchema = z.object({
|
|
40
76
|
goal: z.string(),
|
|
@@ -46,106 +82,198 @@ export const workingMemorySchema = z.object({
|
|
|
46
82
|
export function emptyWorkingMemory() {
|
|
47
83
|
return { goal: "", constraints: [], hypotheses: [], findings: {} };
|
|
48
84
|
}
|
|
85
|
+
/** Narrow a `WorkingMemoryError` to the model-correctable branch. */
|
|
86
|
+
export function isWorkingMemoryRejection(error) {
|
|
87
|
+
return error.type === "working_memory_rejected";
|
|
88
|
+
}
|
|
49
89
|
/** A short, render-friendly entry id. */
|
|
50
90
|
function newId() {
|
|
51
91
|
return crypto.randomUUID().slice(0, 8);
|
|
52
92
|
}
|
|
53
|
-
function
|
|
93
|
+
function reject(message) {
|
|
94
|
+
return err({ type: "working_memory_rejected", message });
|
|
95
|
+
}
|
|
96
|
+
function rejectUnknownId(section, id) {
|
|
97
|
+
return reject(`no ${section} with id "${id}" — nothing was changed. Copy an id verbatim from the [id] shown ` +
|
|
98
|
+
`against each entry in the rendered working memory, or add a new entry instead.`);
|
|
99
|
+
}
|
|
100
|
+
function rejectFull(section, limit) {
|
|
101
|
+
return reject(`${section} is full (${limit}/${limit}) — the new entry was NOT recorded. Working memory is ` +
|
|
102
|
+
`re-injected on every turn, so it is capped. Retire a stale entry first (operation "retire" ` +
|
|
103
|
+
`with its id), then add this one — or drop it, if it earns its place less than everything ` +
|
|
104
|
+
`already there.`);
|
|
105
|
+
}
|
|
106
|
+
/** Validate one entry's text against the shared entry cap. */
|
|
107
|
+
function checkEntryText(section, text) {
|
|
108
|
+
const trimmed = text.trim();
|
|
109
|
+
if (trimmed.length === 0)
|
|
110
|
+
return reject(`${section} text is empty — nothing was recorded.`);
|
|
111
|
+
if (trimmed.length > WORKING_MEMORY_LIMITS.entryChars) {
|
|
112
|
+
return reject(`${section} text is ${trimmed.length} characters — the cap is ${WORKING_MEMORY_LIMITS.entryChars}. It was ` +
|
|
113
|
+
`NOT recorded. Rewrite it as one concise line and retry; keep the detail in the run itself, ` +
|
|
114
|
+
`which inspect_run still serves.`);
|
|
115
|
+
}
|
|
116
|
+
return ok(trimmed);
|
|
117
|
+
}
|
|
118
|
+
/** Amend a flat `{id, text}` list — the shared body of constraints and hypotheses. */
|
|
119
|
+
function applyEntryOp(entries, op, section, limit, create) {
|
|
54
120
|
switch (op.op) {
|
|
55
121
|
case "add":
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
59
|
-
case "
|
|
60
|
-
|
|
122
|
+
if (entries.length >= limit)
|
|
123
|
+
return rejectFull(section, limit);
|
|
124
|
+
return checkEntryText(section, op.text).map((text) => [...entries, create(text)]);
|
|
125
|
+
case "revise": {
|
|
126
|
+
if (!entries.some((e) => e.id === op.id))
|
|
127
|
+
return rejectUnknownId(section, op.id);
|
|
128
|
+
return checkEntryText(section, op.text).map((text) => entries.map((e) => (e.id === op.id ? { ...e, text } : e)));
|
|
129
|
+
}
|
|
130
|
+
case "retire": {
|
|
131
|
+
if (!entries.some((e) => e.id === op.id))
|
|
132
|
+
return rejectUnknownId(section, op.id);
|
|
133
|
+
return ok(entries.filter((e) => e.id !== op.id));
|
|
134
|
+
}
|
|
61
135
|
}
|
|
62
136
|
}
|
|
63
|
-
|
|
137
|
+
/** Findings across every run — the cap governs the section, not one run's bucket. */
|
|
138
|
+
function countFindings(findings) {
|
|
139
|
+
return Object.values(findings).reduce((n, list) => n + list.length, 0);
|
|
140
|
+
}
|
|
141
|
+
/** The run a finding id hangs under, or `undefined` if no run holds it. */
|
|
142
|
+
function runOfFinding(findings, id) {
|
|
143
|
+
return Object.keys(findings).find((runId) => findings[runId].some((f) => f.id === id));
|
|
144
|
+
}
|
|
145
|
+
function applyFindingOp(findings, op) {
|
|
64
146
|
switch (op.op) {
|
|
65
|
-
case "add":
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
147
|
+
case "add": {
|
|
148
|
+
if (countFindings(findings) >= WORKING_MEMORY_LIMITS.findings) {
|
|
149
|
+
return rejectFull("findings", WORKING_MEMORY_LIMITS.findings);
|
|
150
|
+
}
|
|
151
|
+
return checkEntryText("finding", op.text).map((text) => ({
|
|
152
|
+
...findings,
|
|
153
|
+
[op.runId]: [...(findings[op.runId] ?? []), { id: newId(), text }],
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
case "revise": {
|
|
157
|
+
const runId = runOfFinding(findings, op.id);
|
|
158
|
+
if (runId === undefined)
|
|
159
|
+
return rejectUnknownId("finding", op.id);
|
|
160
|
+
return checkEntryText("finding", op.text).map((text) => ({
|
|
161
|
+
...findings,
|
|
162
|
+
[runId]: findings[runId].map((f) => (f.id === op.id ? { ...f, text } : f)),
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
case "retire": {
|
|
166
|
+
const runId = runOfFinding(findings, op.id);
|
|
167
|
+
if (runId === undefined)
|
|
168
|
+
return rejectUnknownId("finding", op.id);
|
|
169
|
+
const kept = findings[runId].filter((f) => f.id !== op.id);
|
|
170
|
+
const next = { ...findings };
|
|
171
|
+
// An emptied run bucket is dropped whole: memory references a run only
|
|
172
|
+
// for as long as it holds a finding from it.
|
|
173
|
+
if (kept.length === 0)
|
|
174
|
+
delete next[runId];
|
|
175
|
+
else
|
|
176
|
+
next[runId] = kept;
|
|
177
|
+
return ok(next);
|
|
178
|
+
}
|
|
71
179
|
}
|
|
72
180
|
}
|
|
73
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Pure section-addressable amendment. Sibling sections are never touched; a
|
|
183
|
+
* refused write returns `err` and the caller writes nothing.
|
|
184
|
+
*/
|
|
74
185
|
function applySectionUpdate(wm, section, value) {
|
|
75
186
|
switch (section) {
|
|
76
|
-
case "goal":
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
findings: {
|
|
94
|
-
...wm.findings,
|
|
95
|
-
[runId]: [...existing, { id: newId(), text }],
|
|
96
|
-
},
|
|
97
|
-
};
|
|
187
|
+
case "goal": {
|
|
188
|
+
const text = value.text.trim();
|
|
189
|
+
if (text.length > WORKING_MEMORY_LIMITS.goalChars) {
|
|
190
|
+
return reject(`the goal is ${text.length} characters — the cap is ${WORKING_MEMORY_LIMITS.goalChars}. It was NOT ` +
|
|
191
|
+
`recorded. State the objective in a sentence or two; the supporting detail belongs in ` +
|
|
192
|
+
`constraints and findings, not in the goal.`);
|
|
193
|
+
}
|
|
194
|
+
return ok({ ...wm, goal: text });
|
|
195
|
+
}
|
|
196
|
+
case "constraint": {
|
|
197
|
+
const op = value;
|
|
198
|
+
const origin = op.op === "add" ? op.origin : "agent";
|
|
199
|
+
return applyEntryOp(wm.constraints, op, "constraints", WORKING_MEMORY_LIMITS.constraints, (text) => ({
|
|
200
|
+
id: newId(),
|
|
201
|
+
text,
|
|
202
|
+
origin,
|
|
203
|
+
})).map((constraints) => ({ ...wm, constraints }));
|
|
98
204
|
}
|
|
205
|
+
case "hypothesis":
|
|
206
|
+
return applyEntryOp(wm.hypotheses, value, "hypotheses", WORKING_MEMORY_LIMITS.hypotheses, (text) => ({
|
|
207
|
+
id: newId(),
|
|
208
|
+
text,
|
|
209
|
+
})).map((hypotheses) => ({ ...wm, hypotheses }));
|
|
210
|
+
case "finding":
|
|
211
|
+
return applyFindingOp(wm.findings, value).map((findings) => ({ ...wm, findings }));
|
|
99
212
|
}
|
|
100
213
|
// Unreachable — `section` is exhaustively a `Section`.
|
|
101
|
-
return wm;
|
|
214
|
+
return ok(wm);
|
|
215
|
+
}
|
|
216
|
+
/** Every finding, in insertion order, each carrying its run reference. */
|
|
217
|
+
function flattenFindings(findings) {
|
|
218
|
+
return Object.entries(findings).flatMap(([runId, list]) => list.map((f) => ({ ...f, runId })));
|
|
219
|
+
}
|
|
220
|
+
/** Keep the newest `limit` entries, reporting how many older ones were dropped. */
|
|
221
|
+
function newest(entries, limit) {
|
|
222
|
+
if (entries.length <= limit)
|
|
223
|
+
return { shown: entries, omitted: 0 };
|
|
224
|
+
return { shown: entries.slice(entries.length - limit), omitted: entries.length - limit };
|
|
102
225
|
}
|
|
103
|
-
/**
|
|
226
|
+
/** Bound one entry's rendered text. Only a legacy over-cap row can hit this. */
|
|
227
|
+
function clamp(text, limit) {
|
|
228
|
+
const trimmed = text.trim();
|
|
229
|
+
return trimmed.length <= limit ? trimmed : `${trimmed.slice(0, limit)}…`;
|
|
230
|
+
}
|
|
231
|
+
function renderSection(heading, lines, omitted, label) {
|
|
232
|
+
const body = omitted > 0 ? [...lines, `- … ${omitted} older ${label} omitted — over cap; retire entries above.`] : lines;
|
|
233
|
+
return `## ${heading}\n\n${body.join("\n")}`;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Serialize working memory to the Markdown document injected each turn.
|
|
237
|
+
*
|
|
238
|
+
* Renders only what exists: an empty section is omitted entirely — no heading,
|
|
239
|
+
* no placeholder — and an entirely empty memory renders to the **empty
|
|
240
|
+
* string**, costing nothing. The chat route injects the render
|
|
241
|
+
* unconditionally, and the provider's outbound sanitizer drops a message whose
|
|
242
|
+
* content is `""` before the wire call (`providers/ai-sdk.ts`), so an empty
|
|
243
|
+
* memory reaches the model as no message at all.
|
|
244
|
+
*
|
|
245
|
+
* Findings render as ONE flat list, each line citing the run it came from —
|
|
246
|
+
* never a per-run heading block. Memory holds the reference; `inspect_run`
|
|
247
|
+
* holds the run.
|
|
248
|
+
*
|
|
249
|
+
* The output is bounded to `WORKING_MEMORY_LIMITS` no matter what the row
|
|
250
|
+
* holds. Writes are capped, so only a row written before the caps existed can
|
|
251
|
+
* exceed them — and it degrades to its newest entries rather than exploding
|
|
252
|
+
* the context window.
|
|
253
|
+
*/
|
|
104
254
|
export function renderWorkingMemory(wm) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
for (const c of wm.constraints) {
|
|
114
|
-
lines.push(`- [${c.id}] (${c.origin}) ${c.text}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
lines.push("");
|
|
118
|
-
lines.push("## Hypotheses", "");
|
|
119
|
-
if (wm.hypotheses.length === 0) {
|
|
120
|
-
lines.push("_none yet_");
|
|
255
|
+
const sections = [];
|
|
256
|
+
const goal = clamp(wm.goal, WORKING_MEMORY_LIMITS.goalChars);
|
|
257
|
+
if (goal.length > 0)
|
|
258
|
+
sections.push(`## Goal\n\n${goal}`);
|
|
259
|
+
const constraints = newest(wm.constraints, WORKING_MEMORY_LIMITS.constraints);
|
|
260
|
+
if (constraints.shown.length > 0) {
|
|
261
|
+
const lines = constraints.shown.map((c) => `- [${c.id}] (${c.origin}) ${clamp(c.text, WORKING_MEMORY_LIMITS.entryChars)}`);
|
|
262
|
+
sections.push(renderSection("Constraints", lines, constraints.omitted, "constraints"));
|
|
121
263
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
lines.push("");
|
|
128
|
-
lines.push("## Findings", "");
|
|
129
|
-
const runIds = Object.keys(wm.findings);
|
|
130
|
-
if (runIds.length === 0) {
|
|
131
|
-
lines.push("_none yet_");
|
|
264
|
+
const hypotheses = newest(wm.hypotheses, WORKING_MEMORY_LIMITS.hypotheses);
|
|
265
|
+
if (hypotheses.shown.length > 0) {
|
|
266
|
+
const lines = hypotheses.shown.map((h) => `- [${h.id}] ${clamp(h.text, WORKING_MEMORY_LIMITS.entryChars)}`);
|
|
267
|
+
sections.push(renderSection("Hypotheses", lines, hypotheses.omitted, "hypotheses"));
|
|
132
268
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (findings.length === 0) {
|
|
138
|
-
lines.push("_none yet_");
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
for (const f of findings) {
|
|
142
|
-
lines.push(`- [${f.id}] ${f.text}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
lines.push("");
|
|
146
|
-
}
|
|
269
|
+
const findings = newest(flattenFindings(wm.findings), WORKING_MEMORY_LIMITS.findings);
|
|
270
|
+
if (findings.shown.length > 0) {
|
|
271
|
+
const lines = findings.shown.map((f) => `- [${f.id}] (${f.runId}) ${clamp(f.text, WORKING_MEMORY_LIMITS.entryChars)}`);
|
|
272
|
+
sections.push(renderSection("Findings", lines, findings.omitted, "findings"));
|
|
147
273
|
}
|
|
148
|
-
|
|
274
|
+
if (sections.length === 0)
|
|
275
|
+
return "";
|
|
276
|
+
return `# Working Memory\n\n${sections.join("\n\n")}\n`;
|
|
149
277
|
}
|
|
150
278
|
/**
|
|
151
279
|
* Create a `WorkingMemoryStore` bound to a Postgres pool — a factory closure
|
|
@@ -164,7 +292,9 @@ export function createWorkingMemory(pool) {
|
|
|
164
292
|
return withTransaction(pool, "working-memory.updateSection", (client) =>
|
|
165
293
|
// Serialize concurrent read-modify-write on this analysis — the lock
|
|
166
294
|
// covers the not-yet-existing-row case an `SELECT ... FOR UPDATE`
|
|
167
|
-
// would miss. Released automatically at COMMIT/ROLLBACK.
|
|
295
|
+
// would miss. Released automatically at COMMIT/ROLLBACK. The cap check
|
|
296
|
+
// runs under it too, so two racing adds cannot both slip past a full
|
|
297
|
+
// section.
|
|
168
298
|
tryQuery("working-memory.updateSection.lock", () => client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [analysisId]))
|
|
169
299
|
.andThen(() => tryQuery("working-memory.updateSection.read", () => client.query("SELECT data FROM cortex_working_memory WHERE analysis_id = $1", [analysisId])))
|
|
170
300
|
.map(({ rows }) => {
|
|
@@ -173,10 +303,16 @@ export function createWorkingMemory(pool) {
|
|
|
173
303
|
const current = rows.length === 0 ? emptyWorkingMemory() : workingMemorySchema.parse(rows[0].data);
|
|
174
304
|
return applySectionUpdate(current, section, value);
|
|
175
305
|
})
|
|
176
|
-
.andThen((
|
|
306
|
+
.andThen((amended) =>
|
|
307
|
+
// A rejection rides the transaction's ok channel: it is a decision, not a
|
|
308
|
+
// storage failure. The transaction commits having written nothing, and the
|
|
309
|
+
// rejection is lifted back into the error channel below.
|
|
310
|
+
amended.isErr()
|
|
311
|
+
? okAsync(err(amended.error))
|
|
312
|
+
: tryMutation("working-memory.updateSection.upsert", () => client.query(`INSERT INTO cortex_working_memory (analysis_id, data, updated_at)
|
|
177
313
|
VALUES ($1, $2::jsonb, NOW())
|
|
178
314
|
ON CONFLICT (analysis_id)
|
|
179
|
-
DO UPDATE SET data = EXCLUDED.data, updated_at = NOW()`, [analysisId, JSON.stringify(
|
|
315
|
+
DO UPDATE SET data = EXCLUDED.data, updated_at = NOW()`, [analysisId, JSON.stringify(amended.value)])).map(() => ok(undefined)))).andThen((written) => written);
|
|
180
316
|
}
|
|
181
317
|
function render(analysisId) {
|
|
182
318
|
return load(analysisId).map(renderWorkingMemory);
|