Skip to content

Commit a8942f7

Browse files
authored
feat(agent): Add session resumption support (#335)
## Summary Adds session resumption support to the agent command, allowing users to continue work from where they left off. ## Changes ### New Features - **`--session-id` flag**: Resume existing agent sessions by conversation ID - **Session lifecycle events**: Structured JSON status messages for session events - **Conversation loading**: Load and restore full conversation history from database - **Turn counter reset**: Full budget restored when resuming sessions ### Implementation Details 1. **`convertFromConversationEntry`**: Reverse conversion from domain.ConversationEntry to ConversationMessage 2. **`loadExistingSession`**: Load conversation from persistent repository and restore state 3. **`outputStatusMessage`**: Output structured JSON for session lifecycle events 4. **Session ID preservation**: Maintain session ID for continued persistence ### Documentation Updates - Updated README.md with session resumption examples - Enhanced commands-reference.md with detailed usage and JSON examples - Added session resumption to feature list ### Testing - Comprehensive unit tests for `convertFromConversationEntry` - Tests cover all message types: user, assistant, tool, with images, tool calls, and metadata - Edge cases: internal messages, tool execution metadata, empty content ## Usage Examples ```bash # List conversations to find session IDs infer conversations list # Resume an existing session infer agent "continue fixing the authentication bug" --session-id abc-123-def # Resume with additional files infer agent "analyze these new error logs" --session-id abc-123 --files error.log # Resume without saving (testing mode) infer agent "try a different approach" --session-id abc-123 --no-save ``` ## JSON Status Messages ```json // Successful resume {"type":"info","message":"Resumed agent session","session_id":"abc-123","message_count":15,"timestamp":"2025-12-11T..."} // Failed resume (warning) {"type":"warning","message":"Could not load session, starting fresh","session_id":"invalid","error":"failed to load conversation: not found","timestamp":"2025-12-11T..."} // New session {"type":"info","message":"Starting new agent session","session_id":"new-uuid","model":"openai/gpt-4","timestamp":"2025-12-11T..."} ``` ## Benefits - **Continuity**: Continue complex tasks across multiple sessions - **Context preservation**: Maintain full conversation history - **Efficiency**: Avoid repeating setup and context establishment - **Debugging**: Easier to debug and iterate on complex problems - **Integration**: Structured JSON output for automation and monitoring ## Technical Notes - Uses existing persistent conversation repository infrastructure - Maintains backward compatibility (no breaking changes) - All existing tests pass - Follows existing code patterns and conventions
1 parent 95f71cd commit a8942f7

File tree

4 files changed

+482
-5
lines changed

4 files changed

+482
-5
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,18 @@ infer chat
202202
**`infer agent`** - Execute autonomous tasks in background mode
203203

204204
```bash
205+
# Start new agent sessions
205206
infer agent "Please fix the github issue 38"
206207
infer agent --model "openai/gpt-4" "Implement feature from issue #42"
207208
infer agent "Analyze this UI issue" --files screenshot.png
209+
210+
# Resume existing sessions
211+
infer conversations list # Find session IDs
212+
infer agent "continue fixing the bug" --session-id abc-123-def
213+
infer agent "analyze new logs" --session-id abc-123 --files error.log
208214
```
209215

210-
**Features:** Autonomous execution, multimodal support (images/files), parallel tool execution.
216+
**Features:** Autonomous execution, multimodal support (images/files), parallel tool execution, **session resumption**.
211217

212218
### Configuration Commands
213219

cmd/agent.go

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
container "github.com/inference-gateway/cli/internal/container"
1515
domain "github.com/inference-gateway/cli/internal/domain"
1616
logger "github.com/inference-gateway/cli/internal/logger"
17+
services "github.com/inference-gateway/cli/internal/services"
1718
sdk "github.com/inference-gateway/sdk"
1819
cobra "github.com/spf13/cobra"
1920
)
@@ -24,23 +25,34 @@ var agentCmd = &cobra.Command{
2425
Long: `Execute a task using an autonomous agent in background mode. The CLI will work iteratively
2526
until the task is considered complete. Particularly useful for SCM tickets like GitHub issues.
2627
28+
Session Resumption:
29+
Use --session-id to resume a previous agent session and continue work from where it left off.
30+
Find session IDs using: infer conversations list
31+
2732
Examples:
33+
# Start new agent sessions
2834
infer agent "Please fix the github issue 38"
2935
infer agent --model "openai/gpt-4" "Implement the feature described in issue #42"
3036
infer agent "Debug the failing test in PR 15"
3137
infer agent "Analyze this screenshot" --files screenshot.png
3238
infer agent "Compare these images" -f image1.png -f image2.png
33-
infer agent "Review this code and diagram" --files @code.go @diagram.png`,
39+
infer agent "Review this code and diagram" --files @code.go @diagram.png
40+
41+
# Resume existing sessions
42+
infer agent "continue fixing the authentication bug" --session-id abc-123-def
43+
infer agent "analyze these new error logs" --session-id abc-123 --files error.log
44+
infer agent "try a different approach" --session-id abc-123 --no-save`,
3445
Args: cobra.ExactArgs(1),
3546
RunE: func(cmd *cobra.Command, args []string) error {
3647
model, _ := cmd.Flags().GetString("model")
3748
files, _ := cmd.Flags().GetStringSlice("files")
3849
noSave, _ := cmd.Flags().GetBool("no-save")
50+
sessionID, _ := cmd.Flags().GetString("session-id")
3951
cfg, err := getConfigFromViper()
4052
if err != nil {
4153
return fmt.Errorf("failed to load config: %w", err)
4254
}
43-
return RunAgentCommand(cfg, model, args[0], files, noSave)
55+
return RunAgentCommand(cfg, model, args[0], files, noSave, sessionID)
4456
},
4557
}
4658

@@ -75,7 +87,7 @@ type AgentSession struct {
7587
saveEnabled bool
7688
}
7789

78-
func RunAgentCommand(cfg *config.Config, modelFlag, taskDescription string, files []string, noSave bool) error {
90+
func RunAgentCommand(cfg *config.Config, modelFlag, taskDescription string, files []string, noSave bool, sessionID string) error {
7991
services := container.NewServiceContainer(cfg)
8092
defer func() {
8193
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -134,7 +146,33 @@ For more information, visit: https://github.com/inference-gateway/inference-gate
134146
saveEnabled: saveEnabled,
135147
}
136148

137-
logger.Info("Starting agent session", "session_id", session.sessionID, "model", selectedModel)
149+
if sessionID != "" {
150+
if err := session.loadExistingSession(sessionID); err != nil {
151+
logger.Warn("Failed to load session, starting fresh",
152+
"session_id", sessionID,
153+
"error", err)
154+
session.outputStatusMessage("warning", "Could not load session, starting fresh", map[string]any{
155+
"session_id": sessionID,
156+
"error": err.Error(),
157+
})
158+
} else {
159+
logger.Info("Resumed agent session",
160+
"session_id", sessionID,
161+
"messages", len(session.conversation))
162+
session.outputStatusMessage("info", "Resumed agent session", map[string]any{
163+
"session_id": sessionID,
164+
"message_count": len(session.conversation),
165+
})
166+
}
167+
} else {
168+
logger.Info("Starting agent session",
169+
"session_id", session.sessionID,
170+
"model", selectedModel)
171+
session.outputStatusMessage("info", "Starting new agent session", map[string]any{
172+
"session_id": session.sessionID,
173+
"model": selectedModel,
174+
})
175+
}
138176

139177
return session.execute(taskDescription, files)
140178
}
@@ -505,6 +543,35 @@ func (s *AgentSession) convertToConversationEntry(msg ConversationMessage) domai
505543
return entry
506544
}
507545

546+
// convertFromConversationEntry converts domain.ConversationEntry back to ConversationMessage
547+
// This is the reverse of convertToConversationEntry, used for loading saved conversations
548+
func (s *AgentSession) convertFromConversationEntry(entry domain.ConversationEntry) ConversationMessage {
549+
msg := ConversationMessage{
550+
Role: string(entry.Message.Role),
551+
Timestamp: entry.Time,
552+
Images: entry.Images,
553+
Internal: entry.Hidden,
554+
}
555+
556+
if contentStr, err := entry.Message.Content.AsMessageContent0(); err == nil {
557+
msg.Content = contentStr
558+
}
559+
560+
if entry.Message.ToolCalls != nil && len(*entry.Message.ToolCalls) > 0 {
561+
msg.ToolCalls = entry.Message.ToolCalls
562+
}
563+
564+
if entry.Message.ToolCallId != nil {
565+
msg.ToolCallID = *entry.Message.ToolCallId
566+
}
567+
568+
if entry.ToolExecution != nil {
569+
msg.ToolExecution = entry.ToolExecution
570+
}
571+
572+
return msg
573+
}
574+
508575
func (s *AgentSession) addMessage(msg ConversationMessage) {
509576
s.conversation = append(s.conversation, msg)
510577

@@ -519,6 +586,39 @@ func (s *AgentSession) addMessage(msg ConversationMessage) {
519586
}
520587
}
521588

589+
// loadExistingSession loads a conversation from the database and restores session state
590+
func (s *AgentSession) loadExistingSession(conversationID string) error {
591+
ctx := context.Background()
592+
593+
persistentRepo, ok := s.conversationRepo.(*services.PersistentConversationRepository)
594+
if !ok {
595+
return fmt.Errorf("conversation repository does not support loading")
596+
}
597+
598+
if err := persistentRepo.LoadConversation(ctx, conversationID); err != nil {
599+
return fmt.Errorf("failed to load conversation: %w", err)
600+
}
601+
602+
entries := persistentRepo.GetMessages()
603+
if len(entries) == 0 {
604+
return fmt.Errorf("loaded conversation is empty")
605+
}
606+
607+
s.conversation = make([]ConversationMessage, 0, len(entries))
608+
for _, entry := range entries {
609+
msg := s.convertFromConversationEntry(entry)
610+
s.conversation = append(s.conversation, msg)
611+
}
612+
613+
s.sessionID = conversationID
614+
615+
logger.Info("Loaded conversation history",
616+
"session_id", conversationID,
617+
"message_count", len(entries))
618+
619+
return nil
620+
}
621+
522622
func (s *AgentSession) outputMessage(msg ConversationMessage) {
523623
if msg.Role == "system" || msg.Internal {
524624
return
@@ -544,6 +644,27 @@ func (s *AgentSession) outputMessage(msg ConversationMessage) {
544644
fmt.Println(string(output))
545645
}
546646

647+
// outputStatusMessage outputs a structured JSON status message
648+
func (s *AgentSession) outputStatusMessage(messageType, message string, metadata map[string]any) {
649+
statusMsg := map[string]any{
650+
"type": messageType,
651+
"message": message,
652+
"timestamp": time.Now(),
653+
}
654+
655+
for k, v := range metadata {
656+
statusMsg[k] = v
657+
}
658+
659+
output, err := json.Marshal(statusMsg)
660+
if err != nil {
661+
logger.Error("Failed to marshal status message", "error", err)
662+
return
663+
}
664+
665+
fmt.Println(string(output))
666+
}
667+
547668
func (s *AgentSession) lastResponseHadNoToolCalls() bool {
548669
if len(s.conversation) < 2 {
549670
return false
@@ -590,5 +711,6 @@ func init() {
590711
agentCmd.Flags().StringP("model", "m", "", "Model to use for the agent (e.g., openai/gpt-4)")
591712
agentCmd.Flags().StringSliceP("files", "f", []string{}, "Files or images to include (e.g., -f image.png -f code.go)")
592713
agentCmd.Flags().Bool("no-save", false, "Disable saving conversation to database")
714+
agentCmd.Flags().String("session-id", "", "Resume an existing agent session by conversation ID")
593715
rootCmd.AddCommand(agentCmd)
594716
}

0 commit comments

Comments
 (0)