Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions core/src/sessions/base_session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,37 @@ export function trimTempState(
return filteredState;
}

/**
* Splits a state map into its app-scoped, user-scoped and session-scoped
* parts, stripping the `app:` and `user:` prefixes and dropping `temp:` keys.
*
* The inverse of {@link mergeStates}, which re-applies the prefixes.
*
* @param state The state to split.
* @return The app, user and session buckets.
*/
export function splitStateDelta(state: Record<string, unknown> | undefined): {
app: Record<string, unknown>;
user: Record<string, unknown>;
session: Record<string, unknown>;
} {
const app: Record<string, unknown> = {};
const user: Record<string, unknown> = {};
const session: Record<string, unknown> = {};

for (const [key, value] of Object.entries(state ?? {})) {
if (key.startsWith(State.APP_PREFIX)) {
app[key.slice(State.APP_PREFIX.length)] = value;
} else if (key.startsWith(State.USER_PREFIX)) {
user[key.slice(State.USER_PREFIX.length)] = value;
} else if (!key.startsWith(State.TEMP_PREFIX)) {
session[key] = value;
}
}

return {app, user, session};
}

/**
* Merges app state, user state, and session state.
*
Expand Down
40 changes: 11 additions & 29 deletions core/src/sessions/database_session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ListSessionsRequest,
ListSessionsResponse,
mergeStates,
splitStateDelta,
trimTempDeltaState,
} from './base_session_service.js';
import {
Expand All @@ -37,7 +38,6 @@ import {
StorageUserState,
} from './db/schema.js';
import {createSession, Session} from './session.js';
import {State} from './state.js';

/**
* Checks if a URI is a database connection URI.
Expand Down Expand Up @@ -140,21 +140,11 @@ export class DatabaseSessionService extends BaseSessionService {
em.persist(userStateModel);
}

const appStateDelta: Record<string, unknown> = {};
const userStateDelta: Record<string, unknown> = {};
const sessionState: Record<string, unknown> = {};

if (state) {
for (const [key, value] of Object.entries(state)) {
if (key.startsWith(State.APP_PREFIX)) {
appStateDelta[key.replace(State.APP_PREFIX, '')] = value;
} else if (key.startsWith(State.USER_PREFIX)) {
userStateDelta[key.replace(State.USER_PREFIX, '')] = value;
} else if (!key.startsWith(State.TEMP_PREFIX)) {
sessionState[key] = value;
}
}
}
const {
app: appStateDelta,
user: userStateDelta,
session: sessionState,
} = splitStateDelta(state);

if (Object.keys(appStateDelta).length > 0) {
appStateModel.state = {...appStateModel.state, ...appStateDelta};
Expand Down Expand Up @@ -442,19 +432,11 @@ export class DatabaseSessionService extends BaseSessionService {
}

if (event.actions && event.actions.stateDelta) {
const appDelta: Record<string, unknown> = {};
const userDelta: Record<string, unknown> = {};
const sessionDelta: Record<string, unknown> = {};

for (const [key, value] of Object.entries(event.actions.stateDelta)) {
if (key.startsWith(State.APP_PREFIX)) {
appDelta[key.replace(State.APP_PREFIX, '')] = value;
} else if (key.startsWith(State.USER_PREFIX)) {
userDelta[key.replace(State.USER_PREFIX, '')] = value;
} else if (!key.startsWith(State.TEMP_PREFIX)) {
sessionDelta[key] = value;
}
}
const {
app: appDelta,
user: userDelta,
session: sessionDelta,
} = splitStateDelta(event.actions.stateDelta);

if (Object.keys(appDelta).length > 0) {
appStateModel.state = {...appStateModel.state, ...appDelta};
Expand Down
48 changes: 48 additions & 0 deletions core/test/sessions/base_session_service_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {mergeStates, splitStateDelta} from '@google/adk';
import {describe, expect, it} from 'vitest';

describe('splitStateDelta', () => {
it('routes each key by prefix and drops temporary keys', () => {
expect(
splitStateDelta({
'app:theme': 'dark',
'user:locale': 'en',
'temp:scratch': 1,
turn: 2,
}),
).toEqual({
app: {theme: 'dark'},
user: {locale: 'en'},
session: {turn: 2},
});
});

it('returns empty buckets for undefined state', () => {
expect(splitStateDelta(undefined)).toEqual({
app: {},
user: {},
session: {},
});
});

it('strips only the leading prefix', () => {
expect(splitStateDelta({'app:app:nested': 1})).toEqual({
app: {'app:nested': 1},
user: {},
session: {},
});
});

it('round-trips through mergeStates for non-temporary keys', () => {
const state = {'app:theme': 'dark', 'user:locale': 'en', turn: 2};
const {app, user, session} = splitStateDelta(state);

expect(mergeStates(app, user, session)).toEqual(state);
});
});
1 change: 1 addition & 0 deletions integrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@google-cloud/firestore": "^8.7.0",
"@google/adk": "^1.5.0"
}
}
Loading
Loading