Every belief in the world model entered through an observation. Tool results, files, diffs, messages, command output, docs, logs, filings, a piece of evidence the user hands over by hand: all of it crosses this one boundary, and nothing reaches the posterior without crossing it. This is the seam where raw, unstructured input becomes typed beliefs the engine can reason over.
The boundary also fixes what you are on the hook for. You hand the engine the artifact. It reads the artifact, extracts the typed beliefs inside it (claims, goals, gaps, evidence, contradictions), and folds them into the graph by Bayesian fusion. You never construct a pre-scored belief list. You submit the artifact, and the engine decides what it means.
What an Observation Is
An observation is unstructured by default. A paragraph of user text, a tool's stdout, a file diff, a PR description: the engine takes the raw payload and does the work of turning it into structure.
That work is extraction. An LLM reads the artifact in the context of the conversation, emits typed belief events, and fusion merges those events into the existing graph. The split is deliberate. The developer owns the content; the engine owns the extraction and the labeling. You do not decide that a sentence is a "risk" at confidence 0.7. You submit the sentence, and the engine assigns the type, sets the posterior from the balance of evidence, and links it to what is already known.
Two Ways In
Two SDK paths submit an observation. They differ in how much control you want over what enters and where it came from.
after(text, { tool?, source? }) folds an agent turn's output back into the world model. It is the natural close of the agent loop: the agent produced something, and after() hands that result to extraction so the next turn opens against an updated state.
1const context = await beliefs.before(userMessage)
2const result = await runAgent(context.prompt)
3
4const delta = await beliefs.after(result.text, { tool: 'web_search' })observe(envelope) takes a structured artifact when you want explicit control. It carries the engine's provenance vocabulary directly (surface, kind, actor, tags), so a non-agent surface (a UI event, a document edit, a webhook, a hook) can say exactly what entered and where it came from without smuggling metadata through a free-text source string.
1await beliefs.observe({
2 content: 'CI failed on main: auth-middleware.test.ts timed out after 30s.',
3 surface: 'tool',
4 kind: 'ci_result',
5 actor: 'system',
6 tags: ['repo:api', 'suite:auth'],
7})Both run the same extraction pipeline and both fold into the same fused state. after() is the agent loop's verb; observe() is the structured-ingest verb for everything else.
Dedup Is Part of Intake
Raw extraction is noisy in a specific way: an LLM reading one artifact routinely emits several near-duplicate phrasings of the same claim. Left alone, the engine would treat each phrasing as independent support and inflate the posterior. So dedup runs inside intake, before fusion. The engine collapses near-duplicates so one source of evidence produces one update.
The line it draws is the one most systems miss. The same claim restated three ways is one source, not three. Three documents that independently confirm a claim should move the posterior; one document that says the same thing in three sentences should not. Dedup enforces that distinction at intake, which is what keeps the fusion math downstream honest about how much evidence actually arrived.
One source, one update
Submitting the same artifact twice does not double the evidence. Dedup at intake plus idempotent fusion means a repeated observation converges to the same state rather than compounding it.
The Act, Then the Effect
An observation can carry the action the agent just took. That single field is the seam that lets the engine attribute a later belief change back to the action that caused it.
The loop is short. The agent fires an action. The next observation rides in carrying the action that preceded it. When that observation shifts the belief state, the engine holds both halves: the move, and what moved because of it. The agent's own action has become evidence, and over enough turns the model learns how this particular world tends to respond to that particular action.
1// The agent acts, then folds the effect back in.
2const result = await runMigration()
3await beliefs.after(result.log, { source: 'migration-runner' })
4// The action the agent just took rides with this observation,
5// so a later confidence shift can be traced to that migration.This is what puts record-now, forecast-later within reach. Every action and the effect that followed it land as a replayable episode. The forecasting layer runs on exactly that history: before it can project a move's value forward, it has to have watched enough real act-to-effect pairs to know how the world responds. Those pairs get recorded here, at the observation boundary, one turn at a time.
Recorded here, projected in Moves
The act-to-effect episodes the forecasting layer runs on are recorded here, at the observation boundary. The projection itself, and its honesty about a cold start, lives in Moves.
Where This Sits
Observations are the input edge of the world model. They feed the belief state, which the worldview projects into a decision-sized read, which moves rank into next actions. Close the loop, and the action you take becomes the next observation.
World Model
The full frame an agent acts from, and where observations enter it.
Beliefs
What extraction turns an observation into: typed claims with explicit posteriors.
Moves
Ranked next actions; the action you take becomes the next observation.
Core API
The full after() / observe() reference and envelope fields.