docs/INGEST_ROUTER.md
Rama master · réplica de lectura del repositorio original.
# Ingest Router
Input-stage smart orchestrator. Classifies incoming content and picks an ingest strategy: `raw-chunks`, `summarized`, `observational`, `fact-graph`, `hybrid`, or `skip`. The first stage of the [Cognitive Pipeline](./COGNITIVE_PIPELINE.md).
## What it actually does
Every piece of content entering memory (a new conversation turn, a long article, a code file, a CSV) goes through one classifier call:
1. A `gpt-5-mini`-style classifier reads the content (truncated to ~1k chars for the kind detection) and emits an [`IngestContentKind`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts) (short-conversation, long-conversation, long-article, code, structured-data, multimodal).
2. The routing table picks an ingest strategy for that kind.
3. The dispatcher invokes a registered executor that actually writes to memory in the chosen format.
Why this matters: a 3-turn chat snippet doesn't justify the LLM cost of observation extraction; a 50-turn customer support thread does. A long-form article benefits from session-summarized contextual retrieval; structured CSV data doesn't. Picking the wrong strategy at ingest costs accuracy or money downstream.
## Six content kinds
| Kind | Examples |
|---|---|
| `short-conversation` | 1-3 turn chats, brief Q&A |
| `long-conversation` | extended chat sessions, support threads |
| `long-article` | blog posts, paper sections, long emails |
| `code` | source files, configs, schemas |
| `structured-data` | CSV, JSON record lists, table dumps |
| `multimodal` | content with images, video frames, audio |
## Six ingest strategies
| Strategy | What it writes | Cost (illustrative) |
|---|---|---:|
| `raw-chunks` | turn/chunk traces with embeddings | $0.0001/ingest |
| `summarized` | session/document summary prefixed to every chunk | $0.005/ingest |
| `observational` | structured observation log replacing raw turns | $0.020/ingest |
| `fact-graph` | extracted fact triples + entity-relation graph | $0.015/ingest |
| `hybrid` | parallel raw + summarized + observational | $0.030/ingest |
| `skip` | content discarded; nothing written | $0 |
The `summarized` strategy implements Anthropic's "contextual retrieval" pattern (every chunk prepended with a dense session/document summary before embedding). The `observational` strategy implements Mastra's Observational Memory pattern (LLM-extracted observation log). The `fact-graph` strategy stores extracted typed facts plus an entity-relation graph at retrieval time — used by Hindsight (typed network of World, Experience, Opinion, Observation) and the original Mem0 v2 design. Mem0 v3 (Mar 2026) dropped its graph store in favor of single-pass ADD-only extraction with multi-signal hybrid search; the `fact-graph` ID here remains valid for systems that still want a queryable fact graph.
## Four shipping presets
| Preset | Strategy mix | When to use |
|---|---|---|
| `raw-chunks` (default) | every kind → raw-chunks | high-volume / cost-sensitive workloads; retrieval does the work |
| `summarized` | long-* and code → summarized; short stays raw | documents/conversations with global context that aids recall |
| `observational` | long-conversation → observational; long-article → summarized | conversational workloads with multi-session synthesis questions |
| `hybrid` | long-* → hybrid; code → summarized; the rest raw-chunks | cost-tolerant workloads with heterogeneous retrieval needs |
## Quickstart
```ts
import {
LLMIngestClassifier,
IngestRouter,
FunctionIngestDispatcher,
} from '@framers/agentos/ingest-router';
// Stand-ins for the host-side dispatchers and the LLM adapter the
// classifier delegates to. Replace `openaiAdapter` with whatever LLM
// adapter your runtime exposes (`OpenAIProviderAdapter`, etc.) and
// implement each dispatch fn against your real ingest pipelines.
declare const openaiAdapter: any;
declare const content: string;
async function rawIngest(_c: string) { return 0; }
async function summarizedIngest(_c: string) { return 0; }
async function omIngest(_c: string) { return 0; }
async function factGraphIngest(_c: string) { return 0; }
async function hybridIngest(_c: string) { return 0; }
const router = new IngestRouter({
classifier: new LLMIngestClassifier({ llm: openaiAdapter }),
preset: 'summarized',
budget: { perIngestUsd: 0.01, mode: 'cheapest-fallback' },
dispatcher: new FunctionIngestDispatcher<{ writtenTraces: number }>({
'raw-chunks': async (content) => ({ writtenTraces: await rawIngest(content) }),
summarized: async (content) => ({ writtenTraces: await summarizedIngest(content) }),
observational: async (content) => ({ writtenTraces: await omIngest(content) }),
'fact-graph': async (content) => ({ writtenTraces: await factGraphIngest(content) }),
hybrid: async (content) => ({ writtenTraces: await hybridIngest(content) }),
skip: async () => ({ writtenTraces: 0 }),
}),
});
const { decision, outcome } = await router.decideAndDispatch(content);
console.log(decision.classifier.kind); // 'long-conversation'
console.log(decision.routing.chosenStrategy); // 'summarized' (the preset's pick for long-conversation)
console.log(decision.routing.estimatedCostUsd); // 0.005
console.log(outcome.writtenTraces); // 47
```
## Decision-only flow
```ts
const { classifier, routing } = await router.decide(content);
if (routing.chosenStrategy === 'summarized') {
await mySummarizeAndStore(content);
} else if (routing.chosenStrategy === 'skip') {
return; // content not worth storing
}
```
## Manual kind override
When the caller already knows the content kind (file extension determines code, payload metadata says it's structured data), skip the classifier:
```ts
const decision = await router.decide(content, {
manualKind: 'code',
});
// classifier is not invoked; routing table consulted with 'code' directly.
```
## Budget-aware dispatch
```ts
const router = new IngestRouter({
classifier,
preset: 'observational',
budget: {
perIngestUsd: 0.005,
mode: 'cheapest-fallback', // over budget: the cheapest strategy that fits
},
});
```
The mode decides what happens when the routed strategy costs more than `perIngestUsd`. `hard` throws `IngestRouterBudgetExceededError` and returns no decision. `soft` keeps the routed strategy and sets `budgetExceeded: true`. `cheapest-fallback` (the default) picks the cheapest strategy whose cost fits. In `soft` and `cheapest-fallback` mode, when no strategy fits, the router picks the cheapest strategy overall and sets `budgetExceeded: true`. With the default costs `skip` costs 0, so it always fits, and `cheapest-fallback` turns every over-budget ingest into `skip`: the content is not stored. To store it, use `soft`, or pass a `strategyCosts` map (it replaces the defaults, so give every strategy a cost) in which the cheapest strategy that fits the budget stores content.
## Few-shot classifier prompt
For ambiguous content (a long email vs a long article; structured data vs code), use the few-shot variant:
```ts
const router = new IngestRouter({
classifier,
preset: 'observational',
useFewShotPrompt: true,
});
```
## API surface
- [`IngestContentKind`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts), [`IngestStrategyId`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts), [`IngestRouterPreset`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts), [`IngestRoutingTable`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts)
- [`INGEST_CONTENT_KINDS`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts)
- [`RAW_CHUNKS_TABLE`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts), [`SUMMARIZED_TABLE`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts), [`OBSERVATIONAL_TABLE`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts), [`HYBRID_TABLE`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts), [`PRESET_INGEST_TABLES`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/routing-tables.ts)
- [`IngestStrategyCostPoint`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/costs.ts), [`DEFAULT_INGEST_COSTS`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/costs.ts), plus per-strategy constants
- [`selectIngestStrategy`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/select-strategy.ts) (pure function)
- [`IngestRoutingDecision`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/select-strategy.ts), [`IngestRouterConfig`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/select-strategy.ts), [`IngestBudgetMode`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/select-strategy.ts)
- [`IIngestClassifier`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/classifier.ts), [`IIngestClassifierLLM`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/classifier.ts), [`LLMIngestClassifier`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/classifier.ts)
- [`INGEST_CLASSIFIER_SYSTEM_PROMPT`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/classifier.ts), [`INGEST_CLASSIFIER_SYSTEM_PROMPT_FEWSHOT`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/classifier.ts)
- [`IIngestDispatcher`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/dispatcher.ts), [`FunctionIngestDispatcher`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/dispatcher.ts)
- [`IngestRouter`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/IngestRouter.ts), [`IngestRouterOptions`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/IngestRouter.ts), [`IngestRouterDecideOptions`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/IngestRouter.ts), [`IngestRouterDispatchedResult`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/IngestRouter.ts)
- Errors: [`IngestRouterUnknownKindError`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/select-strategy.ts), [`IngestRouterBudgetExceededError`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/select-strategy.ts), [`UnsupportedIngestStrategyError`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/dispatcher.ts), [`IngestRouterDispatcherMissingError`](https://github.com/framerslab/agentos/blob/master/src/orchestration/pipeline/ingest/IngestRouter.ts)
## Calibration
The shipping cost-points are illustrative averages on a typical OpenAI stack. For workloads with very different ingest profiles (heavy observation extraction, custom triple-extraction LLM), supply your own `strategyCosts` map at construction.
## Related
- [Cognitive Pipeline](./COGNITIVE_PIPELINE.md) — composition primitive
- [Memory Router](./MEMORY_ROUTER.md) — recall stage sibling
- [Read Router](./READ_ROUTER.md) — read stage sibling
- [Memory Operations](./MEMORY_OPERATIONS.md#auto-ingest-pipeline) — the underlying auto-ingest primitives this router orchestrates
- [Memory Document Ingestion](./memory/MEMORY_DOCUMENT_INGESTION.md) — document-mode ingest pipeline