Episode Ingestion
Implementation status (2026-07-23, kelly): ✅ Implemented.
src/episode/mod.rs—ingest_episode,ingest_episode_with_resolution,ingest_batch, theEpisodestruct withgroup_id/shapes,prov:Activity/prov:wasGeneratedByprovenance, and edge-confidencereification (bare triple when absent;rdf:Statement+quipu:confidencefor enum/numeric grades — tested insrc/episode/tests.rs). SHACL episode + persistent shape gates and thequipu episodeCLI are wired. Verified by grep + tests.
Episodes are the structured write path for agent-extracted knowledge. When an agent observes something – a deploy, an incident, a configuration change – it packages that knowledge as an episode with typed nodes and edges.
Anatomy of an Episode
{
"name": "koror-rebuild-2026-03-29",
"episode_body": "Koror was rebuilt on kota after disk failure",
"source": "aegis/crew/ellie",
"group_id": "aegis-ontology",
"nodes": [
{
"name": "koror",
"type": "ProxmoxNode",
"description": "Proxmox host, rebuilt after failure",
"properties": { "hostname": "koror.example", "status": "recovered" }
},
{
"name": "kota",
"type": "ProxmoxNode",
"description": "Primary Proxmox host"
}
],
"edges": [
{
"source": "koror",
"target": "kota",
"relation": "rebuilt_on"
}
]
}
Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Episode identifier (becomes IRI) |
episode_body | No | Natural language description |
source | No | Agent or system that produced this |
group_id | No | Provenance label for the episode (see note below) |
nodes | No | Entities to create |
edges | No | Relationships between entities |
shapes | No | SHACL Turtle for validation gate |
replace_snapshot | No | Replace this episode’s prior facts atomically (default: false) |
Ordinary episodes are additive: changing an episode does not retract entities
that were generated by its earlier version. A producer publishing a complete
current inventory should set replace_snapshot: true. Quipu then retracts facts
missing from the new inventory and asserts new facts in one transaction. An
empty nodes/edges payload therefore represents an empty snapshot, and
readers never observe a retract-then-repost gap.
group_idis a label, not a partition. It is recorded as a singleaegis:groupIdliteral on the episode’sprov:Activity; nodes inherit it transitively viaprov:wasGeneratedBy. All groups share one graph — there is no isolation boundary, and thegroup_idsfilter on search is best-effort. Facts asserted directly via/knot(raw Turtle, not through an episode) carry no group at all and are invisible togroup_idsfiltering. Usegroup_idfor provenance/organization, not as a multi-tenant security boundary.
Node Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Entity identifier |
type | No | rdf:type (e.g., “ProxmoxNode”) |
description | No | rdfs:comment |
properties | No | Key-value pairs as typed literals |
Edge Fields
| Field | Required | Description |
|---|---|---|
source | Yes | Source entity name |
target | Yes | Target entity name |
relation | Yes | Relationship name (becomes predicate) |
confidence | No | Confidence qualifier for the generated triple (see below) |
Edge confidence. Supply
confidenceto grade how trustworthy an AUTO-extracted edge is — either an enum ("EXTRACTED","INFERRED","AMBIGUOUS") or a0–1number. The bare triple is always asserted; when a confidence is present the statement is additionally reified (rdf:Statementwithrdf:subject/rdf:predicate/rdf:object) and qualified withquipu:confidence, so agents can filter or flag uncertain facts via SPARQL. Omitting it (the common case) leaves an unqualified triple — fully back-compatible.# All edges an agent flagged as merely inferred SELECT ?s ?p ?o WHERE { ?stmt rdf:subject ?s ; rdf:predicate ?p ; rdf:object ?o ; quipu:confidence "INFERRED" . }
How Ingestion Works
- Episode JSON is converted to RDF Turtle
- If
shapesis provided, SHACL validation runs first – rejects on failure - Turtle is ingested via the standard RDF pipeline in a single transaction
- Episode node gets
prov:Activitytype with provenance links - Each entity gets
prov:wasGeneratedBylinking back to the episode
Provenance Tracking
Every entity created by an episode carries a provenance link:
SELECT ?entity WHERE {
?entity <http://www.w3.org/ns/prov#wasGeneratedBy>
<http://aegis.gastown.local/ontology/episode/koror-rebuild-2026-03-29>
}
The episode_provenance() function returns all entities and their facts
for a given episode name.
Description Revisions
A current entity carries one rdfs:comment. When an ordinary (additive)
episode supplies a different description for an existing node, the
superseded text is not silently discarded and not left to pile up on the
entity: in the same transaction, each superseded comment is retracted from the
entity and re-asserted on the episode entity whose prov:wasGeneratedBy
assertion shared its original transaction (src/episode/descriptions.rs).
Last write wins on the entity; the history stays lossless because every prior
description remains readable on the episode that produced it.
Two edges of that rule:
- Unattributable history refuses the whole write. If a current comment has
no same-transaction
prov:wasGeneratedByattribution, the ingest errors before the transaction opens rather than discarding provenance. One revision migrates every attributable legacy comment it supersedes. - The reconciliation is per
(entity, new text)— a node listed more than once in an episode (for example under multiple types) produces one revision, not duplicate retractions.
SHACL Validation Gate
Episodes can include a shapes field with SHACL constraints. If provided,
the episode data is validated before writing – invalid episodes are rejected
with structured feedback explaining exactly what failed.
{
"name": "new-service",
"shapes": "@prefix sh: ... (SHACL Turtle)",
"nodes": [{ "name": "myapp", "type": "WebApplication" }]
}
CLI Usage
# From file
quipu episode deploy.json --db my.db
# From stdin (pipe from another tool)
echo '{"name": "test", "nodes": [...]}' | quipu episode - --db my.db
Batch Ingestion
Multiple episodes can be ingested sequentially. Processing stops on the first error, so earlier episodes are committed while later ones may not be.
#![allow(unused)]
fn main() {
use quipu::episode::ingest_batch;
let results = ingest_batch(&mut store, &episodes, ×tamps)?;
for (tx_id, count) in results {
println!("tx={tx_id}, triples={count}");
}
}