feat(ai): session harness contract — incomplete, run_id, confirm bind
Add docs/16 for session state machine, incomplete/resume, queue semantics, and agent-loop/harness/MCP gap review. Wire RunOutcome on max-rounds, run_id through audit, TrimMessages/Redact hooks, ConfirmBindHash on write confirm path; session file skeleton without HTTP yet. No push.
This commit is contained in:
1 parent
0aeee2b692
commit
5416e86379
17 files changed
+1041
-61
No files matched your search
@@ -0,0 +1,89 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session 桌面本地会话(目标态落盘;HTTP 路由可后挂)。
|
||||
type Session struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Status string `json:"status"` // idle|running|incomplete|completed|failed|cancelled
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Messages []Message `json:"messages"`
|
||||
Runs []SessionRun `json:"runs,omitempty"`
|
||||
}
|
||||
|
||||
// SessionRun 一次运行摘要。
|
||||
type SessionRun struct {
|
||||
RunID string `json:"runId"`
|
||||
Outcome string `json:"outcome"`
|
||||
IncompleteReason string `json:"incompleteReason,omitempty"`
|
||||
Resumable bool `json:"resumable"`
|
||||
StepsUsed int `json:"stepsUsed"`
|
||||
StepsBudget int `json:"stepsBudget"`
|
||||
FinishedAt string `json:"finishedAt"`
|
||||
}
|
||||
|
||||
func sessionsDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".opsxdb", "ai-sessions"), nil
|
||||
}
|
||||
|
||||
// SessionPath 返回会话文件路径。
|
||||
func SessionPath(id string) (string, error) {
|
||||
dir, err := sessionsDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, id+".json"), nil
|
||||
}
|
||||
|
||||
// SaveSession 脱敏后写入磁盘。
|
||||
func SaveSession(s Session) error {
|
||||
if s.ID == "" {
|
||||
return fmt.Errorf("session id required")
|
||||
}
|
||||
s.Messages = RedactMessages(s.Messages)
|
||||
s.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
if s.CreatedAt == "" {
|
||||
s.CreatedAt = s.UpdatedAt
|
||||
}
|
||||
path, err := SessionPath(s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, b, 0600)
|
||||
}
|
||||
|
||||
// LoadSession 读会话;不存在返回 os.ErrNotExist。
|
||||
func LoadSession(id string) (Session, error) {
|
||||
path, err := SessionPath(id)
|
||||
if err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
var s Session
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
Reference in new issue
Block a user