Replies: 1 comment 1 reply
|
Thanks for writing this up. I'd been circling the same question from the outside, whether DICE could answer "what did memory look like at T", and having the concepts named and ordered made it much easier to work through. I ended up implementing all six against DICE as an external observer: a DiceEventListener plus a wrapper over PropositionRepository, without changing DICE itself. Five of the six came out of what DICE already emits. Two gave me trouble, and I thought the details might be useful here. Where does a commit boundary come from? Step 2, group revisions into memory commits, is the one I couldn't work out from the outside. As far as I can tell there isn't anything in the event stream to group by:
That leaves an observer with one shared "current batch" holder, rotated whenever a marker shows up. It works while ingestion is sequential and stops working as soon as it isn't. I ran it to see how bad it gets: 8 concurrent ingestion runs, 6 facts each, ground truth known, repeated 5 times. Inference recovered between 2 and 6 of the 8 boundaries depending on scheduling, and in each of the 5 runs one commit ended up holding facts from all eight. It's quite possible I'm missing a path here. If there's a way to recover the grouping that I overlooked, I'd like to know. persist() might already be the boundary If it isn't recoverable from the events, the next question is where it would naturally live. PersistablePropositions.persist() looks like a good candidate: one call, one set of propositions written together, which is close to how you define a commit. The pattern also seems established in the codebase. CollectorRun groups many CollectorRecords under a shared runId with startedAt/finishedAt, and GraphProjectionService mints one per projection run. Extraction/persistence looks like the one write path without something equivalent. I tried the smallest version I could think of: a reentrant, thread-bound commit scope opened by persist(), which a listener reads inside onEvent (this works because listeners run inline on the emitting thread). The same measurement came out at 8 commits with none mixed, across all 5 runs. It's one new file plus a wrap in persist(), with no public signatures changed, no changes to any event, and the module's 1193 tests still passing. Making it reentrant seemed worth it for the responsibility split you describe: a caller can open a wider scope with an id it owns, a case id or a conversation id, around extraction and persistence, and persist() joins that instead of splitting it in two. On naming I'd defer to you. I used the proposal's vocabulary (MemoryCommit), though runICollectorRun already establishes. Memory Use Record was the one I couldn't reach cleanly All 16 DiceEvent types look write-side to me, and I couldn't find anything emitted on retrieval, so a DiceEventListener never sees what a query returned, which is the substance of a Memory Use Record. I fell back on a dynamic proxy around PropositionRepository, recording what came back in interfaces, so implementing it by hand wasn't practical). It works, but it's aworkaround, and one a consumer shouldn't have to build. With a retrieval-side event this becomes ordinary listener work, and DICE could record its own memory use instead of depending on someone wrapping the repository. Commit over clock, under a 30-second skew I moved a single event's timestamp 30 seconds backwards, roughly what an NTP correction or a second writer on another node would do, and changed nothing else. Timestamp-ordered lookup returned the wrong What none of this touches The thing you identify as the missing concept, historical versions of propositions, isn't addressed by any of the above. save still replaces by id (propositions[proposition.id] = proposition), so the previous revision is gone. Deriving revisions from an event log works well enough for an outside observer, but keeping them inside DICE is a storage-model question (retention, GC, indexing) and a much bigger one than either item above. Two smaller things I wasn't sure about Both came up while building, and I suspect they're undecided rather than wrong:
The listener and the experiments are at https://github.com/hornosj/dice-chronicle if any of it is useful. The commit-scope patch is a small branch against main. Happy to open a PR, or to leave it as input if you'd rather shape it differently. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
DICE Revision Memory: Short Concept Proposal
The Core Example
Assume DICE has three propositions after the first memory change:
Meaning:
Later, DICE changes
P2and addsP4:Now the full memory state after
C2is:So the audit delta from
C1toC2is:What DICE Already Has
DICE already has a proposition-first memory model.
It has:
These concepts provide the base for temporal memory.
But it mostly represents the current proposition state plus timestamps and lineage around some operations.
What Is Missing
The missing concept is:
If proposition
P2changes fromR2toR4, DICE needs to retain both:Without that, DICE cannot reliably answer:
because the old version may have been overwritten.
Proposed Concepts
Proposition
The logical memory fact.
Example:
This is the stable identity of the memory fact.
Proposition Revision
One historical version of a proposition.
Example:
P2is the same logical proposition.R2andR4are different versions of it.Memory Commit
One logical change to memory.
Example:
A commit is not the whole memory.
A commit is only what changed together.
Memory View at Commit
The full memory state after a commit.
Example:
This is what lets DICE answer:
Memory Delta
The difference between two memory views.
Example:
The delta says what changed.
The collector, projection, ingestion, or agent audit explains why it changed.
What About Time?
Commit is the primary coordinate.
Use:
not primarily:
Time lookup resolves to a commit:
This avoids ambiguity around clocks, concurrent writes, and transaction boundaries.
What About Snapshots?
There are two different concepts:
Historical Memory View
This is the full memory state after a commit:
This is built from proposition revisions.
Memory Use Record
This records which proposition revisions were supplied to a reasoning step.
Example:
rankmeans position in the returned retrieval result.For example,
rank 1is the first proposition revision supplied to the reasoning step,rank 2is the second, and so on. Rank preserves retrieval order for replay and audit. It does not mean confidence rank globally across all memory.Replay has two levels:
DICE is responsible for memory replay, not full execution replay.
For memory replay, DICE can answer:
That output can then be used by the Embabel Agent audit layer to reconstruct the surrounding execution context.
The result is audit replay, not guaranteed deterministic re-execution of an LLM. To re-run the same LLM call deterministically, the framework would also need the original prompt, model/options, tool responses, and output record.
Proposition revisions and commits come first.
Responsibility Split
DICE Should Own
DICE owns memory history:
DICE answers:
Embabel Agent Framework Layer
DICE uses Embabel Agent as the agent execution, LLM, and tool framework.
Execution history belongs at that framework layer rather than inside the DICE proposition store:
DICE memory operations can attach DICE-owned identifiers to the surrounding Embabel Agent execution context:
`
Application Should Own
The application owns business context:
Recommended Order
Build in this order:
Do not start with memory use records.
They depend on the revision/commit model.
Final Concept
The proposed DICE memory history model is:
Audit example:
Audit at Framework level
Considering employing observability for implementing an audit at the framework level, such as
changes in object state between actions.
Disclaimer: employ codex for brainstorming
All reactions