[Discussion] Memory Observation Capability Design #876
rosemarYuan
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Memory Observability Design
Introduction
This doc introduces Memory Observability for Flink Agents — the ability to observe how an agent's memory changes as the agent runs.
The framework already provides event-based observability (
EventLogger/EventListener), but memory reads and writes do not produce events today, so memory operations are invisible to it.This doc proposes to close that gap by reusing the existing event-based observability: the framework records each memory operation as it happens, and at the action execution boundary converts the records into events, so that all existing tooling (logging, listening, routing, action triggering) works for memory out of the box.
The sections below will cover:
valuepayload is encoded.1. Motivation
Background
Flink Agents provides an event-based observability stack: users can record and export framework events with
EventLogger/EventListener. Meanwhile, the framework defines three types of memory with the following operations:Problem
Users can observe an agent's run results through events. But for agents that use memory, there is currently no way to observe how memory evolves inside the agent.
Goal
Use the existing event-based observability and turn memory operation into a memory-event. Concretely: record each memory operation synchronously as it happens, then converting those records into events at the action boundary. Existing observability tooling then applies to memory unchanged.
Target usage:
2. API Design
2.1 Configuration Options
By default, memory observability generates a set of commonly useful events. Without any configuration, users can observe the high-value memory operations: short-term writes, sensory writes, long-term updates, long-term gets, and long-term searches.
Users who need to adjust the observation scope can control whether each type of memory operation generates events via
AgentConfiguration. The framework provides a master switch and per-operation switches:memory.generate-event: the master switch. It has no default value; when a per-operation switch is not configured, the resolution falls back to the master switch's configured value.memory.generate-event.<scope>-<operation>: per-operation switches, each controlling whether one specific kind of memory operation generates events.memory.generate-eventmemory.generate-event.short-term-writememory.generate-event.short-term-readmemory.generate-event.sensory-writememory.generate-event.sensory-readmemory.generate-event.long-term-updatememory.generate-event.long-term-getmemory.generate-event.long-term-searchPer-operation switches take precedence over the master switch. The effective behavior is resolved in the following order:
memory.generate-eventis explicitly configured, use the master switch's configured value.Usage examples:
2.2 Memory Events
A memory event is a regular event that can be routed and can trigger actions like any other event. It contains the following fields:
_short_term_write_event: a write to short-term memory_short_term_read_event: a read from short-term memory_sensory_write_event: a write to sensory memory_sensory_read_event: a read from sensory memory_long_term_update_event: an update to long-term memory, covering both add and delete_long_term_get_event: a get from long-term memory by id_long_term_search_event: a semantic search over long-term memory3. Design
3.1 Event Generation
At runtime the framework turns every memory operation into an event in two steps: it records the operation synchronously when it happens, then converts all records into events at the action boundary.
Recording — synchronous, at the operation site. Recording is done inside the framework's memory operation implementations. The records are written into the action state and persisted with checkpoints. Users do not notice it, and no API changes.
Emission — at the action boundary. When an action finishes executing, all memory records produced by that execution are converted into multiple observation events, grouped by "scope-operation":
{ "id": "<UUID>", // UUID of the event "type": "_short_term_write_event", // memory operation type "key": "key1", // the keyed-state key of this operation "value": { ... } // content of the memory operation } { "id": "<UUID>", "type": "_long_term_search_event", "key": "key1", "value": { ... } }When an action performs multiple operations of the same kind, the records are merged by the following rules:
{k1: v1, k2: v2, ...}.In addition, memory operations performed inside an action that was itself triggered by a memory event do not generate new observation events. This prevents infinite feedback loops.
3.2 The
valueFieldA
MemoryEventcarries the fields**id**,**type**,**key**, and**value**:{ "id": "<UUID>", // UUID of the event "type": "_<scope>_<operation>_event", // memory operation type "key": "<keyed-state key>", // the Flink keyed-state key of this operation "value": { ... } // updates / reads }For
**value**, three internal encodings were considered. Take recording an STM write ofuser.tier = "gold"anduser.address.city = "SF"as an example:{"tier":"gold","city":"SF"}has(tier)user.idvsaddress.id){"user.tier":"gold",``"user.address.city":"SF"}"user.tier" in value{"user":{"tier":"gold","address":``"city":"SF"}}}has(value.user.tier)2. Shared prefixes are merged, saving space
{a: {b: 1, c: 2}}, the following two operation sequences are indistinguishable:1.set(a.b, 1); set(a.c, 2)2.set(a, {b: 1, c: 2})Since the nested-map encoding cannot distinguish writing multiple leaf values field by field from writing a whole map at once, we choose the dotted-key encoding:
4. Rebuilding Short-Term / Sensory Memory State from the Event Log
Besides observing memory at runtime and triggering new actions, there is a second class of demand: during log-based troubleshooting, reconstructing the memory state at a given moment from the event log.
The basic idea: since the framework records every memory update from job start, applying the updates in order on top of a known initial state restores the state at any given moment. Feasibility differs per memory type, though:
writerecords of the current run is sufficient.The rest of this section discusses when to record the full snapshots and how the rebuild works.
4.1 When to Record Full Memory Snapshots
The rebuild needs periodic full STM snapshots as starting points, i.e. recording the complete STM state per key every so often.
Chosen: record at agent-run begin. Whenever an input for some key arrives and an agent run is about to start, record a full STM snapshot for that key. Concretely, when an
InputEventarrives, the framework emits anAgentRunBeginEventmarking the start of the agent run. The event carries the full STM as of the input's arrival, to be used for state reconstruction from the event log. The memory payload format is the same as thevaluestructure in §3.2.Rejected alternative: record at checkpoint time. On checkpoint completion (
notifyCheckpointComplete), iterate over all keys held by the subtask viaapplyToAllKeysand record a full STM snapshot per key. This was rejected because the checkpointing path already carries a lot of responsibilities.Usage:
Configuration:
4.2 How the Rebuild Works
Since every agent run starts with a full STM snapshot, the rebuild needs neither a standalone command-line tool nor replaying long changelogs merged across runs. To rebuild the state of a key at time t:
AgentRunBeginEventof the run containing t)._short_term_write_events of that run before t in order — this yields the state at t. If t is exactly a run boundary, the snapshot itself is the answer.The rebuild cost is proportional to the number of operations within a single run and does not grow with job age.
Likewise, since Sensory Memory always starts empty, applying the
writerecords of the current run reconstructs its state at any moment.Future Works
valuepayloads (e.g. size limits / truncation).AgentRunEndEventcomplementingAgentRunBeginEvent.Beta Was this translation helpful? Give feedback.
All reactions