loop: auto-infer TaskState from actual tool results each round

This commit is contained in:
Levi Neely 2026-05-17 11:04:55 +02:00
parent bfc7f15254
commit d1ac24fb1d
1 changed files with 45 additions and 0 deletions

View File

@ -451,6 +451,12 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
cfg.SaveSession()
}
// Auto-update TaskState from what actually happened this round.
if ts := state.taskState(); ts != nil {
inferTaskStateUpdate(ts, msg, results)
state.updateTaskState(*ts)
}
if interrupted {
return ctx.Err()
}
@ -540,6 +546,45 @@ func contextBudget(ctx context.Context, b backend.Backend) int {
return ctxLen / 2
}
// inferTaskStateUpdate updates TaskState fields from the actual tool round
// without requiring model cooperation. Keeps the state current even if the
// model ignores re-injection nudges.
func inferTaskStateUpdate(ts *TaskState, msg backend.Message, results []toolResult) {
// LastAction: summarize what tools ran and whether they succeeded.
if len(msg.ToolCalls) > 0 {
names := make([]string, 0, len(msg.ToolCalls))
for _, tc := range msg.ToolCalls {
names = append(names, tc.Name)
}
errCount := 0
for _, r := range results {
if r.IsError {
errCount++
}
}
action := strings.Join(names, ", ")
if errCount > 0 {
action += fmt.Sprintf(" (%d/%d failed)", errCount, len(results))
}
ts.LastAction = action
}
// PlanStep: extract from PLAN: block if present in assistant text.
if idx := strings.Index(msg.Content, "PLAN:"); idx >= 0 {
lines := strings.Split(msg.Content[idx:], "\n")
for _, line := range lines[1:] {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "- [ ]") {
ts.PlanStep = strings.TrimSpace(trimmed[5:])
break
}
if trimmed == "" || (!strings.HasPrefix(trimmed, "- [") && trimmed != "") {
break
}
}
}
}
// hasTextToolCall returns true when the assistant text contains a registered
// tool name immediately followed by ':' or '(', indicating the model tried to
// invoke a tool by writing the call as plain text rather than via the API.