Part 2 of 40 · 8 min read
The company needs a memory that can answer “what changed?”
Experimental · Work in progress · Internal R&D
Typed business objects, state transitions, provenance and historical versions: the memory that holds the company together.
Arunank Sharan · Kuzushi Labs ·
In this article
Suppose a customer says, “We agreed this would include deployment.” The current quote says it does not. A chat summary may recall that deployment was discussed, but that does not tell me which version was accepted. I need the agreement, its versions, the acceptance event and the source of each change.
This is why the platform begins with typed business records. Retrieval helps an agent find relevant material. It does not establish whether a price, approval or commitment is authoritative.
Objects, relationships and evidence
The prototype's contract names object types such as Lead, Opportunity, WorkOrder, Invoice and HumanReviewTask. Relationships join them: an opportunity came from a lead; a work order fulfils a commitment; a source interaction explains why a record was created. Each object belongs to a tenant and carries provenance.
Here is an existing interface, shortened to the operation we care about:
from ontology import OntologyRuntime, Provenance
runtime = OntologyRuntime.bootstrap()
lead = runtime.create(
"core.Lead",
{"email": "[email protected]", "source_channel": "inbound_email"},
tenant_id="studio-demo",
provenance=Provenance(
source="connector:gmail",
actor="lead_capture_agent",
action="create_lead",
),
)
assert lead.state == "CAPTURED"
This low-level runtime API is useful for seeding and internal effects. An agent-facing write goes through the action dispatcher described in the next article. A model should not receive unrestricted access to runtime.update.
State is a constraint, not a suggestion
An opportunity cannot jump directly from “identified” to “paid” because an assistant says the conversation went well. Declared state machines constrain which transitions are legal. The runtime seeds the starting state, checks transitions, and prevents ordinary updates from silently overwriting a state-machine field.
That leaves a useful division of responsibility. The model can interpret “please go ahead with the pilot” as evidence of intent. The sales workflow determines which confirmation is still needed. The dispatcher checks who may accept it. The state machine verifies the proposed transition. No one component is asked to solve all four problems.
Inbound message
→ interpretation: customer intends to proceed
→ proposed action: advance opportunity
→ authority check: may this actor commit the business?
→ state check: is the requested transition legal?
→ recorded change + provenance + event
Two clocks explain late-arriving information
The store has bitemporal concepts: when a fact is valid in the business, and when the platform recorded it. Imagine a payment received on Tuesday but imported on Thursday. A “Tuesday cash position as understood on Wednesday” differs from “Tuesday cash position corrected after Thursday's import.” Both are legitimate questions.
I want these distinctions available to the operator, not hidden in a database administrator's tooling. A useful history view shows the old value, new value, source event, actor and time. An “as of” query must specify which clock it means. Otherwise a corrected historical report can look like an unexplained change to the past.
| Question | Required evidence |
|---|---|
| What is the agreed price now? | Latest valid accepted quote |
| What price did the agent use yesterday? | Version visible at that run's recorded time |
| Why did the customer receive a reminder? | Invoice state and payment evidence visible then |
| Was the reminder wrong? | Compare the decision-time facts with subsequently received evidence |
Identity is not a string match on a person's name
A form and an email can represent the same person. They are still two interactions. The prototype separates interaction records from resolved identity and lead qualification. Its identity resolver supports a deterministic local path; the broader merge policy needs more work before it can handle ambiguous companies, shared mailboxes and changed email addresses.
For a live implementation I would retain source_system, source_account, external_id and tenant_id together as the integration key. “Message 123” in two mailboxes is not one message. A merge should retain both source records and a reversible explanation rather than overwrite one with the other.
Keep long documents outside the transaction record
A contract PDF belongs in object storage with a content hash and version. The business record stores its reference and extracted terms. A retrieved paragraph can explain a candidate term; an accepted structured record establishes the operational term.
A context bundle for an agent might therefore contain:
{
"opportunity_version": 8,
"accepted_scope_version": 3,
"source_document_hash": "sha256:example-content-hash",
"delivery_state": "QA_REVIEW",
"invoice_permission": "WAIT_FOR_ACCEPTANCE",
"retrieved_context": ["Customer requested deployment support"]
}
The final line is supporting context, not permission to invoice. This distinction matters more as memory grows: stale or merely suggested terms should not acquire authority through repeated summarisation.
What still needs hardening
The local stores establish the shape of temporal history and tenant-scoped access. They do not, by themselves, prove transaction isolation for multiple workers. Before using these records to drive live actions, I need conditional writes against expected versions, durable event delivery, consistent relationship checks and recovery tests around multi-object effects.
A practical first test is a stale approval: approve quote version 3, edit the scope to version 4, then try to send under the old approval. The correct outcome is a new review request. If the system sends version 4 anyway, the history may be excellent but the action is still wrong.
A record is not just the latest JSON blob
Consider two agents reading quote version 4. Sales wants to change the fee; delivery wants to attach an implementation estimate. If both write a complete JSON object without checking the version, the second writer can erase the first change. A model cannot resolve that race from the conversation alone. The storage operation needs a precondition.
The public lab uses an optimistic version check. A successful update increments the version, appends history and inserts an outgoing event in the same SQLite transaction:
from company_lab.store import Store, Conflict
store = Store()
store.create("studio", "q17", "quote", {"fee_minor": 600000})
store.change("studio", "q17", 1, {"fee_minor": 650000},
"quote-change-17", "quote.changed")
try:
store.change("studio", "q17", 1, {"fee_minor": 590000},
"quote-change-18", "quote.changed")
except Conflict:
fresh = store.get("studio", "q17")
assert fresh["body"]["fee_minor"] == 650000
assert fresh["version"] == 2
The rejected writer now has a decision to make: reload and recompute its proposal, request a merge, or stop for review. Retrying the old body with the new version would merely disguise a lost update. For an agent-generated quote, I would regenerate the proposed changes from the refreshed facts and show the difference to the reviewer.
Store time and authority separately
The internal ontology has version and temporal concepts. A production schema should make their meanings explicit. occurred_at describes when a source event happened; received_at describes when the platform learned about it; valid_from describes when a business fact applies. A correction received today can change our understanding of last week's delivery without pretending the correction existed last week.
A useful history row contains the changed object, prior version, new version, actor, action, effective time, recorded time and source reference. A mutable current-state table supports ordinary queries; immutable history supports reconstruction. The reference lab implements version history, not a complete bitemporal query engine. That boundary matters when asking an accounting or planning question about a prior date.
For a late delivery confirmation, preserve both the actual acceptance time and the arrival time of the message. Then the founder can distinguish “delivery was late” from “the acknowledgement was entered late.” These lead to different operating fixes.
Model identity at the right level
I would use separate identities for person, organisation, buying process and contract. An email address is an attribute with provenance, not the primary key for all four. A consultant may use one email while representing two customers. A customer may change employers. Two subsidiaries may share a billing mailbox but hold different agreements.
The connector should retain its own (provider, account, external_id) mapping. A CRM contact ID is meaningful only within the connected account. Merge proposals should show the evidence and the affected links. A reversible alias or merge record is safer than deleting one history and moving everything silently to the surviving person.
For document memory, retain object-store keys and content hashes in the business record. The text extraction and embedding index can be rebuilt; the accepted quote and evidence reference must remain stable. Retrieval should filter by tenant, subject and access scope before ranking passages.
Test memory through questions an operator actually asks
Ask “What fee did we approve before the change?”, “Which release did the customer accept?” and “Why was this invoice created twice?” The last answer should ideally be “it was not,” supported by a unique obligation identity. If the system can only produce a plausible summary of recent messages, it has conversational memory but not a dependable operating record.
My implementation exercise is to introduce a version conflict deliberately, replay an event, and inspect both history and outbox. The lab test also forces an outbox insert failure and verifies that the object update rolls back. That checks the storage boundary the later event chapters rely on.