Session 恢复是 Aster 框架提供的一个重要特性,允许 Agent 在用户刷新页面或重新进入应用时智能地恢复之前的工作上下文。
在实际应用中,用户可能会:
此时 Agent 实例会被重新创建,虽然历史消息被持久化保存,但 Agent 可能不知道:
Aster 采用 框架 + 钩子 的设计模式:
graph TB
subgraph Framework["Aster 框架(通用能力)"]
SessionState[SessionState<br/>会话状态检测]
ProcessRecovery[ProcessRecovery<br/>恢复处理入口]
RecoveryHook[RecoveryHook<br/>钩子接口]
end
subgraph Application["应用层(业务逻辑)"]
CustomHook[自定义 RecoveryHook<br/>实现具体恢复策略]
ProjectFiles[检查项目文件<br/>AGENTS.md, README.md 等]
EnhanceMsg[构建增强消息<br/>恢复指令 + 用户消息]
end
ProcessRecovery --> SessionState
ProcessRecovery --> RecoveryHook
RecoveryHook -.->|应用注入| CustomHook
CustomHook --> ProjectFiles
CustomHook --> EnhanceMsg
style Framework fill:#3b82f6,color:#fff
style Application fill:#10b981,color:#fff
| 层级 | 职责 | 说明 |
|---|---|---|
| Aster 框架 | 提供基础设施 | 会话状态检测、恢复处理入口、类型定义 |
| 应用层 | 实现业务逻辑 | 检查哪些文件、如何构建恢复指令、恢复触发条件 |
// SessionState 会话状态信息
type SessionState struct {
// HasHistory 是否有历史消息
HasHistory bool `json:"has_history"`
// MessageCount 历史消息数量
MessageCount int `json:"message_count"`
// IsResumed 是否是恢复的会话(Agent 实例是新创建的,但有历史数据)
IsResumed bool `json:"is_resumed"`
// LastMessageTime 最后一条消息的时间
LastMessageTime *string `json:"last_message_time,omitempty"`
}
// RecoveryContext 恢复上下文(传递给 RecoveryHook)
type RecoveryContext struct {
// AgentID Agent 标识
AgentID string `json:"agent_id"`
// SessionState 会话状态
SessionState *SessionState `json:"session_state"`
// OriginalMessage 用户原始消息
OriginalMessage string `json:"original_message"`
// WorkDir 工作目录(如果有)
WorkDir string `json:"work_dir,omitempty"`
// Metadata 自定义元数据(应用层可以传入额外信息)
Metadata map[string]any `json:"metadata,omitempty"`
}
// RecoveryResult 恢复结果
type RecoveryResult struct {
// ShouldRecover 是否应该执行恢复
ShouldRecover bool `json:"should_recover"`
// EnhancedMessage 增强后的消息(如果需要恢复)
EnhancedMessage string `json:"enhanced_message,omitempty"`
// Instructions 恢复指令(可选,会被添加到消息前)
Instructions string `json:"instructions,omitempty"`
}
// RecoveryHook 恢复钩子函数类型
// 应用层实现这个函数来定义自己的恢复逻辑
type RecoveryHook func(ctx *RecoveryContext) *RecoveryResult
在创建 Agent Dependencies 时注入自定义的恢复钩子:
deps := &agent.Dependencies{
Store: jsonStore,
SandboxFactory: sandboxFactory,
ToolRegistry: toolRegistry,
ProviderFactory: providerFactory,
TemplateRegistry: templateRegistry,
// 注入自定义恢复钩子
RecoveryHook: createMyRecoveryHook(),
}
func createMyRecoveryHook() types.RecoveryHook {
return func(ctx *types.RecoveryContext) *types.RecoveryResult {
// 1. 检查是否应该触发恢复
if !shouldTriggerRecovery(ctx.OriginalMessage) {
return &types.RecoveryResult{ShouldRecover: false}
}
// 2. 检查项目状态
hasProjectFile := checkProjectFile(ctx.WorkDir)
if !ctx.SessionState.HasHistory && !hasProjectFile {
return &types.RecoveryResult{ShouldRecover: false}
}
// 3. 构建恢复消息
enhancedMsg := buildRecoveryMessage(ctx)
return &types.RecoveryResult{
ShouldRecover: true,
EnhancedMessage: enhancedMsg,
}
}
}
在发送消息前调用 Agent 的 ProcessRecovery 方法:
// 发送消息时处理恢复
func sendMessage(ctx context.Context, ag *agent.Agent, message string) error {
// ProcessRecovery 会检测是否需要恢复,并调用注册的 RecoveryHook
finalMessage, recovered := ag.ProcessRecovery(ctx, message, workDir, nil)
if recovered {
log.Printf("Session recovery triggered")
}
return ag.Send(ctx, finalMessage)
}
以下是一个写作助手应用的完整恢复实现:
package writing
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/astercloud/aster/pkg/types"
)
// createRecoveryHook 创建写作助手的恢复钩子
func createRecoveryHook() types.RecoveryHook {
return func(ctx *types.RecoveryContext) *types.RecoveryResult {
// 检查是否应该触发恢复
// 短消息如"继续"、"好的"通常意味着用户想继续之前的工作
if !isRecoveryTriggerMessage(ctx.OriginalMessage) {
return &types.RecoveryResult{ShouldRecover: false}
}
// 检查项目文件
hasProjectFile := false
if ctx.WorkDir != "" {
agentsFile := filepath.Join(ctx.WorkDir, "AGENTS.md")
if _, err := os.Stat(agentsFile); err == nil {
hasProjectFile = true
}
}
// 需要恢复的条件:有历史消息 或 有项目文件
if !ctx.SessionState.HasHistory && !hasProjectFile {
return &types.RecoveryResult{ShouldRecover: false}
}
// 构建恢复指令
var sb strings.Builder
sb.WriteString("【会话恢复】这是一个恢复的会话,你需要先理解当前项目状态再响应用户。\n\n")
sb.WriteString("请按以下步骤操作:\n")
sb.WriteString("1. 首先使用 Read 工具读取 AGENTS.md 文件,了解项目信息和当前步骤\n")
sb.WriteString("2. 如果存在 _briefs 目录,读取其中的文件了解已收集的需求\n")
sb.WriteString("3. 根据上下文理解用户的意图,然后继续之前的工作\n\n")
if ctx.WorkDir != "" {
sb.WriteString(fmt.Sprintf("工作目录: %s\n\n", ctx.WorkDir))
}
sb.WriteString("---\n\n")
sb.WriteString("用户消息: ")
sb.WriteString(ctx.OriginalMessage)
return &types.RecoveryResult{
ShouldRecover: true,
EnhancedMessage: sb.String(),
}
}
}
// isRecoveryTriggerMessage 判断消息是否触发恢复
func isRecoveryTriggerMessage(message string) bool {
message = strings.TrimSpace(message)
if len(message) > 20 {
return false // 长消息通常是新的指令
}
triggers := []string{
"继续", "好", "好的", "可以", "下一步", "然后呢", "接下来",
"continue", "ok", "okay", "yes", "next", "go on",
}
messageLower := strings.ToLower(message)
for _, trigger := range triggers {
if strings.Contains(messageLower, trigger) {
return true
}
}
return false
}
不同类型的应用可以实现不同的恢复策略:
// 检查 AGENTS.md 和 _briefs 目录
func writingRecoveryHook(ctx *types.RecoveryContext) *types.RecoveryResult {
// 读取 AGENTS.md 了解写作进度
// 读取 _briefs 目录了解需求信息
}
// 检查 README.md 和 git 状态
func codingRecoveryHook(ctx *types.RecoveryContext) *types.RecoveryResult {
// 读取 README.md 了解项目信息
// 检查 git status 了解代码改动
// 检查 TODO 或 FIXME 注释
}
// 检查数据文件和分析报告
func analysisRecoveryHook(ctx *types.RecoveryContext) *types.RecoveryResult {
// 检查 data/ 目录的数据文件
// 检查 reports/ 目录的已有报告
// 读取 analysis_plan.md 了解分析计划
}
sequenceDiagram
participant User as 用户
participant App as 应用
participant Agent
participant Hook as RecoveryHook
User->>App: 刷新页面后发送"继续"
App->>Agent: SendMessage("继续")
Agent->>Agent: GetSessionState()
Note over Agent: HasHistory=true<br/>IsResumed=true
Agent->>Hook: RecoveryHook(ctx)
Hook->>Hook: 检查触发条件
Hook->>Hook: 检查项目文件
Hook->>Hook: 构建增强消息
Hook-->>Agent: RecoveryResult{ShouldRecover: true, EnhancedMessage: "..."}
Agent->>Agent: Send(enhancedMessage)
Agent->>Agent: 读取 AGENTS.md
Agent->>Agent: 理解项目状态
Agent-->>User: "我已理解上下文,继续..."
获取当前会话状态:
func (a *Agent) GetSessionState(ctx context.Context) *types.SessionState
处理会话恢复,返回可能增强后的消息:
func (a *Agent) ProcessRecovery(
ctx context.Context,
message string,
workDir string,
metadata map[string]any,
) (enhancedMessage string, recovered bool)
检查是否是恢复的会话:
func (a *Agent) IsResumedSession() bool
// ✅ 好:短消息触发恢复
if len(message) > 20 {
return &types.RecoveryResult{ShouldRecover: false}
}
// ❌ 不好:任何消息都触发恢复
// 会导致用户的新指令也被当作"继续"
// ✅ 好:给 Agent 明确的步骤
sb.WriteString("请按以下步骤操作:\n")
sb.WriteString("1. 首先读取 AGENTS.md\n")
sb.WriteString("2. 然后检查 _briefs 目录\n")
sb.WriteString("3. 最后继续之前的工作\n")
// ❌ 不好:模糊的指令
sb.WriteString("请恢复上下文")
// ✅ 好:将原始消息放在最后
sb.WriteString("---\n\n")
sb.WriteString("用户消息: ")
sb.WriteString(ctx.OriginalMessage)
// ✅ 好:根据应用类型检查相关文件
if _, err := os.Stat(filepath.Join(workDir, "AGENTS.md")); err == nil {
hasProjectFile = true
}
不设置 RecoveryHook 即可:
deps := &agent.Dependencies{
// 不设置 RecoveryHook,恢复功能自动禁用
}
目前不支持,RecoveryHook 在创建 Dependencies 时设置。如果需要动态切换,可以在 Hook 内部实现逻辑分支。
取决于实现。框架每次调用 ProcessRecovery 都会检查,但通常应用层会根据 IsResumed 状态只在第一次消息时触发。
Session 持久化保存消息历史,Session 恢复利用这些历史来判断是否需要恢复上下文,两者是互补的关系。