From d1ac24fb1dc0283672e9daeb0e1fd1387e957685 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Sun, 17 May 2026 11:04:55 +0200 Subject: [PATCH] loop: auto-infer TaskState from actual tool results each round --- pkg/agent/loop.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index a1a252e..36b4d52 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -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.