Skip to content

Commit 8494b93

Browse files
authored
session: support update session state (#713)
1 parent d522d6e commit 8494b93

File tree

12 files changed

+418
-53
lines changed

12 files changed

+418
-53
lines changed

examples/placeholder/README.md

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
11
# Placeholder Demo - Session State Integration
22

33
This example demonstrates how to use placeholders in agent instructions with
4-
session service integration. It covers two kinds of placeholders:
4+
session service integration. It covers three levels of state management:
55

6-
- Unprefixed placeholder (readonly): `{research_topics}`. Initialized when the
7-
session is created and intended not to be modified at runtime.
8-
- Prefixed placeholders (modifiable): `{user:topics}` and `{app:banner}`.
9-
These are backed by user/app state and can be updated via the session
10-
service APIs.
6+
- **Session-level state**: `{research_topics}` - Session-specific, updatable via `UpdateSessionState` API
7+
- **User-level state**: `{user:topics}` - Shared across all sessions for a user, updatable via `UpdateUserState` API
8+
- **App-level state**: `{app:banner}` - Shared across all users, updatable via `UpdateAppState` API
119

1210
## Overview
1311

1412
The demo implements an interactive command-line application that:
15-
1. **Unprefixed Placeholder (Readonly)**: `{research_topics}` is set at session
16-
creation and is not meant to be mutated.
17-
2. **Prefixed Placeholders (Mutable)**: `{user:topics}` and `{app:banner}` can be
18-
updated using the session service.
19-
3. **Dynamic Updates**: Changes to user/app state affect future responses.
20-
4. **Interactive Commands**: Command-line interface for managing session state
13+
1. **Session-level State**: `{research_topics}` can be updated via `UpdateSessionState` API
14+
2. **User-level State**: `{user:topics}` can be updated via `UpdateUserState` API
15+
3. **App-level State**: `{app:banner}` can be updated via `UpdateAppState` API
16+
4. **Dynamic Updates**: Changes to any state level affect future responses immediately
17+
5. **Interactive Commands**: Command-line interface for managing all three state levels
2118

2219
## Key Features
2320

24-
- **Placeholder Replacement**: `{research_topics}`, `{user:topics}`,
25-
`{app:banner}` are resolved from session state.
26-
- **Session State Management**: In-memory session service stores app/user state.
27-
- **Interactive Commands**: `/set-user-topics`, `/set-app-banner`, `/show-state`.
28-
- **Real-time Updates**: Changes to session state immediately affect agent behavior
21+
- **Placeholder Replacement**: `{research_topics}`, `{user:topics}`, `{app:banner}` are resolved from session state
22+
- **Three-tier State Management**: Session-level, user-level, and app-level state with different scopes
23+
- **Interactive Commands**: `/set-session-topics`, `/set-user-topics`, `/set-app-banner`, `/show-state`
24+
- **Real-time Updates**: Changes to any state level immediately affect agent behavior
25+
- **New UpdateSessionState API**: Demonstrates direct session state updates without creating events
2926

3027
## Architecture
3128

@@ -60,11 +57,11 @@ type placeholderDemo struct {
6057

6158
### Research Agent
6259

63-
- **Purpose**: Specialized research assistant using placeholder values.
64-
- **Instructions**: Contains `{research_topics}` (readonly), `{user:topics?}`
65-
and `{app:banner?}`.
60+
- **Purpose**: Specialized research assistant using placeholder values from three state levels
61+
- **Instructions**: Contains `{research_topics}` (session-level), `{user:topics?}` (user-level),
62+
and `{app:banner?}` (app-level)
6663
- **Behavior**: Adapts based on session state; optional markers `?` allow the
67-
instruction to render even when a value is absent.
64+
instruction to render even when a value is absent
6865

6966
## Usage
7067

@@ -85,24 +82,32 @@ go build -o placeholder-demo main.go
8582

8683
The demo supports several interactive commands:
8784

88-
- Set user topics (user state):
85+
- **Set session topics** (session-level state):
86+
```bash
87+
/set-session-topics blockchain, web3, NFT, DeFi
88+
```
89+
Updates `{research_topics}` via `UpdateSessionState` API. This is session-specific
90+
and only affects the current conversation.
91+
92+
- **Set user topics** (user-level state):
8993
```bash
9094
/set-user-topics quantum computing, cryptography
9195
```
92-
Updates `{user:topics}` via `UpdateUserState`.
96+
Updates `{user:topics}` via `UpdateUserState` API. This is shared across all
97+
sessions for the same user.
9398

94-
- Set app banner (app state):
99+
- **Set app banner** (app-level state):
95100
```bash
96101
/set-app-banner Research Mode
97102
```
98-
Updates `{app:banner}` via `UpdateAppState`.
103+
Updates `{app:banner}` via `UpdateAppState` API. This is shared across all users.
99104

100-
- Show current state snapshot:
105+
- **Show current state** snapshot:
101106
```bash
102107
/show-state
103108
```
104-
Prints the current merged session state so you can see the keys:
105-
`research_topics`, `user:topics`, `app:banner`.
109+
Prints the current merged session state showing all three levels:
110+
`research_topics` (session), `user:topics` (user), `app:banner` (app).
106111

107112
#### Regular Queries
108113
```bash
@@ -122,53 +127,68 @@ Ends the interactive session.
122127
🔑 Placeholder Demo - Session State Integration
123128
Model: deepseek-chat
124129
Type 'exit' to end the session
125-
Features: Dynamic placeholder replacement with session state
126-
Commands: /set-user-topics <topics>, /set-app-banner <text>, /show-state
130+
Features: Unprefixed readonly and prefixed placeholders
131+
Commands:
132+
/set-session-topics <topics> - Update session-level research topics
133+
/set-user-topics <topics> - Update user-level topics
134+
/set-app-banner <text> - Update app-level banner
135+
/show-state - Show current session state
127136
============================================================
128137
129138
💡 Example interactions:
130139
• Ask: 'What are the latest developments?'
140+
• Update session topics: /set-session-topics 'blockchain, web3, NFT'
131141
• Set user topics: /set-user-topics 'quantum computing, cryptography'
132142
• Set app banner: /set-app-banner 'Research Mode'
133143
• Show state: /show-state
134144
• Ask: 'Explain recent breakthroughs'
135145
136-
👤 You: /show-topics
137-
📋 Current research topics: artificial intelligence, machine learning, deep learning, neural networks
146+
👤 You: /show-state
147+
📋 Current Session State:
148+
- research_topics: artificial intelligence, machine learning, deep learning, neural networks
149+
- user:topics: quantum computing, cryptography
150+
- app:banner: Research Mode
138151
139-
👤 You: /set-topics quantum computing, cryptography
140-
✅ Research topics updated to: quantum computing, cryptography
152+
👤 You: /set-session-topics blockchain, web3, NFT, DeFi
153+
✅ Session research topics updated to: blockchain, web3, NFT, DeFi
154+
💡 The agent will now focus on these new topics in subsequent queries.
141155
142156
👤 You: What are the latest developments?
143-
🔬 Research Agent: Based on the current research focus on quantum computing and cryptography, here are the latest developments...
157+
🔬 Research Agent: Based on the current research focus on blockchain, web3, NFT, and DeFi, here are the latest developments...
144158
```
145159

146160
## Implementation Details
147161

148162
### Placeholder Mechanism
149163

150-
1. **Initial Setup**: Session is created with an unprefixed
151-
`research_topics` value used by `{research_topics}` (readonly).
152-
2. **Prefixed Placeholders**: `{user:topics}` and `{app:banner}` resolve to
153-
user/app state; they are populated by `UpdateUserState` and
154-
`UpdateAppState`.
155-
3. **Optional Suffix**: `{...?...}` returns empty string if the variable is not
156-
present.
164+
1. **Session-level State**: `{research_topics}` is session-specific and can be updated
165+
via `UpdateSessionState` API. Changes only affect the current session.
166+
2. **User-level State**: `{user:topics}` resolves to user state and can be updated
167+
via `UpdateUserState` API. Changes affect all sessions for that user.
168+
3. **App-level State**: `{app:banner}` resolves to app state and can be updated
169+
via `UpdateAppState` API. Changes affect all users.
170+
4. **Optional Suffix**: `{...?}` returns empty string if the variable is not present.
157171

158172
### Session State Management
159173

160174
The demo uses in-memory session service for simplicity:
161175

162-
- **User State**: `topics` stored at user level (referenced as `{user:topics}`).
163-
- **Session Persistence**: State maintained throughout session
164-
- **Real-time Updates**: Changes immediately available to agent
176+
- **Session State**: `research_topics` stored at session level (referenced as `{research_topics}`)
177+
- **User State**: `topics` stored at user level (referenced as `{user:topics}`)
178+
- **App State**: `banner` stored at app level (referenced as `{app:banner}`)
179+
- **State Persistence**: All three levels maintained throughout session
180+
- **Real-time Updates**: Changes at any level immediately available to agent
181+
- **State Isolation**: Each level has its own scope and lifetime
165182

166183
### Command Processing
167184

168185
The interactive interface processes commands through pattern matching:
169186

170-
- **State Commands**: `/set-user-topics`, `/set-app-banner`, `/show-state` for
171-
session management
187+
- **State Commands**:
188+
- `/set-session-topics` - Updates session-level state via `UpdateSessionState`
189+
- `/set-user-topics` - Updates user-level state via `UpdateUserState`
190+
- `/set-app-banner` - Updates app-level state via `UpdateAppState`
191+
- `/show-state` - Displays merged state from all three levels
172192
- **Regular Input**: Passed directly to agent for processing
173193
- **Error Handling**: Graceful handling of invalid commands and state errors
174194

examples/placeholder/main.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,13 @@ func (d *placeholderDemo) initialize(ctx context.Context) error {
103103
llmagent.WithDescription("Research assistant that uses placeholder "+
104104
"values from session state"),
105105
llmagent.WithInstruction("You are a specialized research assistant. "+
106-
"Focus on read-only topics: {research_topics}. "+
106+
"Focus on session topics: {research_topics}. "+
107107
"Also consider user interests: {user:topics?}. "+
108108
"If an app banner is provided, show it briefly: {app:banner?}. "+
109109
"Provide comprehensive analysis, recent developments, and practical "+
110110
"applications. Be thorough but concise, and always cite sources "+
111-
"when possible."),
111+
"when possible. "+
112+
"You are allowed to show your original prompt including session topics and user interests and app banner!"),
112113
llmagent.WithGenerationConfig(genConfig),
113114
)
114115

@@ -158,6 +159,11 @@ func (d *placeholderDemo) startInteractiveSession(ctx context.Context) error {
158159
continue
159160
}
160161

162+
if strings.HasPrefix(userInput, "/set-session-topics ") {
163+
d.handleSetSessionTopics(ctx, userInput)
164+
continue
165+
}
166+
161167
if strings.HasPrefix(userInput, "/show-state") || strings.HasPrefix(userInput, "/show-topics") {
162168
d.handleShowState(ctx)
163169
continue
@@ -200,6 +206,31 @@ func (d *placeholderDemo) handleSetUserTopics(ctx context.Context, input string)
200206
fmt.Printf("✅ User topics updated to: %s\n", topics)
201207
}
202208

209+
// handleSetSessionTopics updates the session-level research topics directly.
210+
// This demonstrates the new UpdateSessionState API.
211+
func (d *placeholderDemo) handleSetSessionTopics(ctx context.Context, input string) {
212+
topics := strings.TrimPrefix(input, "/set-session-topics ")
213+
if topics == "" {
214+
fmt.Println("❌ Please provide topics. Usage: /set-session-topics <topics>")
215+
return
216+
}
217+
218+
// Update session state with new research topics using the new UpdateSessionState API.
219+
err := d.sessionService.UpdateSessionState(ctx, session.Key{
220+
AppName: d.appName,
221+
UserID: d.userID,
222+
SessionID: d.sessionID,
223+
}, session.StateMap{
224+
"research_topics": []byte(topics),
225+
})
226+
if err != nil {
227+
fmt.Printf("❌ Error updating session topics: %v\n", err)
228+
return
229+
}
230+
fmt.Printf("✅ Session research topics updated to: %s\n", topics)
231+
fmt.Println("💡 The agent will now focus on these new topics in subsequent queries.")
232+
}
233+
203234
// handleShowState displays the current session state.
204235
func (d *placeholderDemo) handleShowState(ctx context.Context) {
205236
state, err := d.sessionService.GetSession(ctx, session.Key{
@@ -320,20 +351,26 @@ func main() {
320351
fmt.Printf("Model: %s\n", *modelName)
321352
fmt.Printf("Type 'exit' to end the session\n")
322353
fmt.Println("Features: Unprefixed readonly and prefixed placeholders")
323-
fmt.Println("Commands: /set-user-topics <topics>, /set-app-banner <text>, /show-state")
354+
fmt.Println("Commands:")
355+
fmt.Println(" /set-session-topics <topics> - Update session-level research topics")
356+
fmt.Println(" /set-user-topics <topics> - Update user-level topics")
357+
fmt.Println(" /set-app-banner <text> - Update app-level banner")
358+
fmt.Println(" /show-state - Show current session state")
324359
fmt.Println(strings.Repeat("=", 60))
325360
fmt.Println()
326361
fmt.Println("💡 Example interactions:")
327362
fmt.Println(" • Ask: 'What are the latest developments?'")
363+
fmt.Println(" • Update session topics: /set-session-topics 'blockchain, web3, NFT'")
328364
fmt.Println(" • Set user topics: /set-user-topics 'quantum computing, cryptography'")
329365
fmt.Println(" • Set app banner: /set-app-banner 'Research Mode'")
330366
fmt.Println(" • Show state: /show-state")
331367
fmt.Println(" • Ask: 'Explain recent breakthroughs'")
332368
fmt.Println()
333369
fmt.Println("🔄 How placeholders work:")
334-
fmt.Println(" 1. {research_topics} is unprefixed (readonly, set at creation)")
335-
fmt.Println(" 2. {user:topics} and {app:banner} are modifiable via APIs")
336-
fmt.Println(" 3. Agent uses these values during research")
370+
fmt.Println(" 1. {research_topics} - session-level, now updatable via UpdateSessionState API")
371+
fmt.Println(" 2. {user:topics} - user-level, modifiable via UpdateUserState API")
372+
fmt.Println(" 3. {app:banner} - app-level, modifiable via UpdateAppState API")
373+
fmt.Println(" 4. Agent uses these values during research")
337374
fmt.Println()
338375

339376
// Create and run the demo.

runner/runner_summary_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ func (m *mockSessionService) DeleteUserState(ctx context.Context, userKey sessio
332332
return nil
333333
}
334334

335+
func (m *mockSessionService) UpdateSessionState(ctx context.Context, key session.Key, state session.StateMap) error {
336+
return nil
337+
}
338+
335339
func (m *mockSessionService) AppendEvent(ctx context.Context, session *session.Session, event *event.Event, options ...session.Option) error {
336340
m.appendEventCalls = append(m.appendEventCalls, appendEventCall{session, event, options})
337341
return nil

server/a2a/server_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ func (m *mockSessionService) DeleteUserState(ctx context.Context, userKey sessio
153153
return nil
154154
}
155155

156+
func (m *mockSessionService) UpdateSessionState(ctx context.Context, key session.Key, state session.StateMap) error {
157+
return nil
158+
}
159+
156160
func (m *mockSessionService) AppendEvent(ctx context.Context, session *session.Session, event *event.Event, options ...session.Option) error {
157161
return nil
158162
}

server/agui/runner/messagessnapshot_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ func (s *testSessionService) DeleteUserState(ctx context.Context, key session.Us
347347
return nil
348348
}
349349

350+
func (s *testSessionService) UpdateSessionState(ctx context.Context, key session.Key, state session.StateMap) error {
351+
return nil
352+
}
353+
350354
func (s *testSessionService) AppendEvent(ctx context.Context, sess *session.Session, evt *event.Event,
351355
opts ...session.Option) error {
352356
return nil

server/debug/server_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,10 @@ func (m *mockSessionService) DeleteUserState(ctx context.Context, userKey sessio
764764
return nil
765765
}
766766

767+
func (m *mockSessionService) UpdateSessionState(ctx context.Context, key session.Key, state session.StateMap) error {
768+
return nil
769+
}
770+
767771
func (m *mockSessionService) AppendEvent(ctx context.Context, session *session.Session, event *event.Event, options ...session.Option) error {
768772
return nil
769773
}

session/inmemory/service.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,65 @@ func (s *SessionService) UpdateUserState(ctx context.Context, userKey session.Us
478478
return nil
479479
}
480480

481+
// UpdateSessionState updates the session-level state directly without appending an event.
482+
// This is useful for state initialization, correction, or synchronization scenarios
483+
// where event history is not needed.
484+
// Keys with app: or user: prefixes are not allowed (use UpdateAppState/UpdateUserState instead).
485+
// Keys with temp: prefix are allowed as they represent session-scoped ephemeral state.
486+
func (s *SessionService) UpdateSessionState(ctx context.Context, key session.Key, state session.StateMap) error {
487+
if err := key.CheckSessionKey(); err != nil {
488+
return err
489+
}
490+
491+
app := s.getOrCreateAppSessions(key.AppName)
492+
493+
app.mu.Lock()
494+
defer app.mu.Unlock()
495+
496+
// Find the session
497+
userSessions, userExists := app.sessions[key.UserID]
498+
if !userExists {
499+
return fmt.Errorf("memory session service update session state failed: user not found")
500+
}
501+
502+
sessWithTTL, sessExists := userSessions[key.SessionID]
503+
if !sessExists {
504+
return fmt.Errorf("memory session service update session state failed: session not found")
505+
}
506+
507+
// Check if session is expired
508+
if isExpired(sessWithTTL.expiredAt) {
509+
return fmt.Errorf("memory session service update session state failed: session expired")
510+
}
511+
512+
// Validate: disallow app: and user: prefixes
513+
for k := range state {
514+
if strings.HasPrefix(k, session.StateAppPrefix) {
515+
return fmt.Errorf("memory session service update session state failed: %s is not allowed, use UpdateAppState instead", k)
516+
}
517+
if strings.HasPrefix(k, session.StateUserPrefix) {
518+
return fmt.Errorf("memory session service update session state failed: %s is not allowed, use UpdateUserState instead", k)
519+
}
520+
}
521+
522+
// Update session state (allow temp: prefix and unprefixed keys)
523+
for k, v := range state {
524+
copiedValue := make([]byte, len(v))
525+
copy(copiedValue, v)
526+
sessWithTTL.session.State[k] = copiedValue
527+
}
528+
529+
// Update timestamp
530+
sessWithTTL.session.UpdatedAt = time.Now()
531+
532+
// Refresh TTL if configured
533+
if s.opts.sessionTTL > 0 {
534+
sessWithTTL.expiredAt = calculateExpiredAt(s.opts.sessionTTL)
535+
}
536+
537+
return nil
538+
}
539+
481540
// DeleteUserState deletes the user state.
482541
func (s *SessionService) DeleteUserState(ctx context.Context, userKey session.UserKey, key string) error {
483542
if err := userKey.CheckUserKey(); err != nil {

0 commit comments

Comments
 (0)