agent: add TaskState — persisted structured overlay injected at each turn

This commit is contained in:
Levi Neely 2026-05-17 10:06:21 +02:00
parent 45959cb79d
commit b775e5df7a
3 changed files with 65 additions and 8 deletions

View File

@ -969,6 +969,12 @@ func (s *agent) executeTurn(ctx context.Context, input string, handler EventHand
s.cfg.IncrToolCallCount = func() int64 {
return s.toolCallCount.Add(1)
}
s.cfg.TaskState = func() *TaskState {
if s.session == nil {
return nil
}
return s.session.TaskState
}
s.cfg.SaveSession = func() { s.saveSession() }
s.cfg.AutoCompact = func(ctx context.Context) {
if ctx.Err() != nil || s.session == nil {

View File

@ -62,6 +62,8 @@ type agentConfig struct {
PlanFile string
// IncrToolCallCount increments and returns the session-wide tool call counter.
IncrToolCallCount func() int64
// TaskState returns the current task state, or nil if not set.
TaskState func() *TaskState
}
// readPlanFile returns the contents of the plan file, or empty string if
@ -90,6 +92,13 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
if cfg.preamble != "" {
history = append([]backend.Message{{Role: "system", Content: cfg.preamble}}, history...)
}
if cfg.TaskState != nil {
if ts := cfg.TaskState(); ts != nil {
if msg := ts.render(); msg != "" {
history = append([]backend.Message{history[0], {Role: "user", Content: msg}}, history[1:]...)
}
}
}
// Stream the assistant's response, retrying on rate limits, transient
// backend errors (5xx, network), and mid-stream drops.

View File

@ -30,17 +30,58 @@ Here is the summary produced by the other language model, use the information in
// PersistedSession is the on-disk format for a saved session.
type PersistedSession struct {
ID string `json:"id"`
Agent string `json:"agent,omitempty"`
Messages []backend.Message `json:"messages"`
ID string `json:"id"`
Agent string `json:"agent,omitempty"`
Messages []backend.Message `json:"messages"`
TaskState *TaskState `json:"taskState,omitempty"`
}
// TaskState is a compact structured overlay that summarizes the agent's
// current position in a task. Injected at the top of every turn so the
// model doesn't rely on full history recall.
type TaskState struct {
Objective string `json:"objective"`
PlanStep string `json:"plan_step"`
Constraints []string `json:"constraints,omitempty"`
LastAction string `json:"last_action"`
OpenQuestions []string `json:"open_questions,omitempty"`
NextDecision string `json:"next_decision"`
}
// render produces a compact text representation for injection into the message history.
func (ts *TaskState) render() string {
if ts.Objective == "" {
return ""
}
var sb strings.Builder
sb.WriteString("[task state]\n")
sb.WriteString("objective: " + ts.Objective + "\n")
if ts.PlanStep != "" {
sb.WriteString("plan_step: " + ts.PlanStep + "\n")
}
if len(ts.Constraints) > 0 {
sb.WriteString("constraints: " + strings.Join(ts.Constraints, "; ") + "\n")
}
if ts.LastAction != "" {
sb.WriteString("last_action: " + ts.LastAction + "\n")
}
if len(ts.OpenQuestions) > 0 {
sb.WriteString("open_questions: " + strings.Join(ts.OpenQuestions, "; ") + "\n")
}
if ts.NextDecision != "" {
sb.WriteString("next_decision: " + ts.NextDecision + "\n")
}
sb.WriteString("[/task state]")
return sb.String()
}
// SaveTo writes the full message history to path as JSON.
func (s *Session) saveTo(path, id, agentName string) error {
ps := PersistedSession{
ID: id,
Agent: agentName,
Messages: s.messages,
ID: id,
Agent: agentName,
Messages: s.messages,
TaskState: s.TaskState,
}
data, err := json.Marshal(ps)
if err != nil {
@ -63,8 +104,9 @@ func RestoreSession(messages []backend.Message) *Session {
// Session is an ephemeral in-memory state backend.
type Session struct {
goal string
messages []backend.Message
goal string
messages []backend.Message
TaskState *TaskState
// Cumulative usage tracking.
TotalInputTokens int
TotalCachedInputTokens int