adk-go icon indicating copy to clipboard operation
adk-go copied to clipboard

Deleting State Keys does not seem possible today

Open verdverm opened this issue 1 month ago • 7 comments

This does not seem possible today from what I can tell. In particular, being able to remove user/app state, because I can always start a new session to nuke the session state.

The following change enables one to send empty strings to delete them

diff --git a/session/database/service.go b/session/database/service.go
index 56e808a..c9e5173 100644
--- a/session/database/service.go
+++ b/session/database/service.go
@@ -67,6 +67,29 @@ func AutoMigrate(service session.Service) error {
        return nil
 }
 
+func filterMap(m map[string]any) map[string]any {
+       result := make(map[string]any)
+       for k, v := range m {
+               if v == nil {
+                       continue
+               }
+               switch val := v.(type) {
+               case string:
+                       if val != "" {
+                               result[k] = v
+                       }
+               case *string:
+                       if val != nil && *val != "" {
+                               result[k] = v
+                       }
+               default:
+                       // Keep all other types
+                       result[k] = v
+               }
+       }
+       return result
+}
+
 // Create generates a session and inserts it to the db, implements session.Service
 func (s *databaseService) Create(ctx context.Context, req *session.CreateRequest) (*session.CreateResponse, error) {
        if req.AppName == "" || req.UserID == "" {
@@ -109,19 +132,22 @@ func (s *databaseService) Create(ctx context.Context, req *session.CreateRequest
                // apply state delta
                if len(appDelta) > 0 {
                        maps.Copy(storageApp.State, appDelta)
+                       storageApp.State = filterMap(storageApp.State)
                        if err := tx.Save(&storageApp).Error; err != nil {
                                return fmt.Errorf("failed to save app state: %w", err)
                        }
                }
                if len(userDelta) > 0 {
                        maps.Copy(storageUser.State, userDelta)
+                       storageUser.State = filterMap(storageUser.State)
                        if err := tx.Save(&storageUser).Error; err != nil {
                                return fmt.Errorf("failed to save user state: %w", err)
                        }
                }
                createdSession.State = sessionState
+               createdSession.State = filterMap(createdSession.State) 

verdverm avatar Nov 20 '25 07:11 verdverm

Hello, can I work on this?

cs168898 avatar Nov 25 '25 03:11 cs168898

I don't think this is the right place to fix this issue. I believe it's closer to this other issue I found: https://github.com/google/adk-go/issues/354

Delete is still missing, but the state management should be done within the event system rather than around the database calls, otherwise it will not be realized between tool calls and such

hmm, maybe it does go there, but needs to be handled in way more places, and/or the same filtering still needed in the main base_flow.go to handle deletes between function calls but before the commit?

verdverm avatar Nov 25 '25 06:11 verdverm

sorry if I am misunderstanding something, as I am still relatively new to adk-go.

Would implementing something similar to the code you provided above into the applyEvent method (found in session/database/service.go) work? Since it is the method that applies the state changes from an event and saves it, we can apply your filterMap function right before it saves into the db.

Then we probably also have to define to the agent that setting StateDelta values to empty strings ( "" ) OR nil, it will be deleting it that key.

maybe something like this in the applyEvents method:


		// Merge state deltas and update the storage objects.
		// GORM's .Save() method will correctly perform an INSERT or UPDATE.
		if len(appDelta) > 0 {
			// Loop through only the keys of the map
			for key, value := range appDelta {
				if value == nil {
					// Delete the entry if the key is marked for deletion by the empty string format
					delete(appDelta, key)
				}
			}
			maps.Copy(storageApp.State, appDelta)
			if err := tx.Save(&storageApp).Error; err != nil {
				return fmt.Errorf("failed to save app state: %w", err)
			}
		}
		if len(userDelta) > 0 {
			for key, value := range userDelta {
				if value == nil {
					// Delete the entry if the key is marked for deletion by the empty string format
					delete(userDelta, key)
				}
			}
			maps.Copy(storageUser.State, userDelta)
			if err := tx.Save(&storageUser).Error; err != nil {
				return fmt.Errorf("failed to save user state: %w", err)
			}
		}
		if len(sessionDelta) > 0 {
			for key, value := range sessionDelta {
				if value == nil {
					// Delete the entry if the key is marked for deletion by the empty string format
					delete(sessionDelta, key)
				}
			}
			maps.Copy(storageSess.State, sessionDelta)
			// The session state update will be saved along with the event timestamp update.
		}

... other code to create new event record and save the session etc...

cs168898 avatar Nov 25 '25 07:11 cs168898

yeah, I had that thought too, but only implementing it in session/database.go means none of the other systems will have the same feature (in-memory, within commit changes)

btw, I think this is a great file to start in to understand the core flow of the agentic / llm / function calling / commit loop

https://github.com/google/adk-go/blob/main/internal/llminternal/base_flow.go

I also think that the change probably goes around there, similar collection and filtering operations, but I too am new to the project (almost 2 weeks now!)

verdverm avatar Nov 25 '25 08:11 verdverm

hmm, what if... for in-memory, the agent sets nil values for the state keys it wants to delete, that way the next tool can receive the updated stateMap where each value that is nil indicates a deleted state.

Then for the persistent portion, when we are about to commit it to the db , we run the code I pasted above?

I just thought this out briefly, not sure if the idea has any issues

also, hello fellow newcomer, i'm 2 days in to this project 😃

cs168898 avatar Nov 25 '25 10:11 cs168898

this is what the python implementation probably does, the docs say "set to None" to delete from the state

this is more or less how our examples for Go work nil or "", but perhaps just nil makes sense, given the value type is any

I thought about it some more today, and thought maybe the session implementation is where this should happen, and all session service implementations have choice in how they do this? One of the reasons is that state updates are finalized on commit, which really isn't true until the database transaction finalizes, conceptually.

Also, sequences of tool usage as parts of the same message do see the changes to state, but probably still see the null, I haven't tried deleting from the map directly there yet, probably should try that to see what happens

Still, I'd prefer to lean towards the cleaning steps being outside the database or implementation details, mainly so that we can have consistent semantics and implementation of the state changing code. The current database implementation is doing more than it should imo

verdverm avatar Nov 26 '25 02:11 verdverm

When you say that the sequences of tool usage sees the changes to state, are you referring to AFTER you implemented the maps.Copy to accumulate changes from your other Issue?

also, maybe we could just create a util package method to implement the cleaning steps, which satisfies separation of concerns? then call those methods in their respective areas. Not sure if this is too naive of an approach.

cs168898 avatar Nov 26 '25 06:11 cs168898