feat: live visibility into what the agent is running (no more silent waiting)
Operator: "I'd like to be able to see in the chat what the agent is actually running, right now I just wait while nothing happens." Two compounding gaps: 1. The auto-continuation worker (cmd/nomos/continue.go) had zero live push — its result only appeared on a manual page reload, so approving a plan and watching the chat looked completely dead even while the agent was actively working. 2. Even with polling, continueSession only persisted ONE message at the very end of a continuation — a continuation that runs several tool calls before concluding would still show total silence for however long that took. Fixed both: - web/src/lib/stores/chat.ts: polls the current session's messages every 3s between turns (never while a live stream owns the message list) and merges in anything new. Started after a live turn ends and when a session loads; stopped on new-chat/session-switch. - cmd/nomos/store.go: insertMessageReturningID/updateMessage — lets a message be created as a placeholder and updated in place. - cmd/nomos/continue.go: continueSession now inserts a placeholder the instant it starts (renders as the existing "thinking" dots — immediate feedback that something is happening) and updates that SAME row after EVERY tool call, not just at the end. A poll within ~3s of any tool call landing shows it — individual `run` commands appear as the agent issues them, not just the final rolled-up summary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -72,16 +72,49 @@ func (a *agent) processContinuations(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// continueSession re-invokes the agent for one finished execution, persisting
|
||||
// the resulting assistant turn just like handleChat does. The operator sees it
|
||||
// on their next load of the session (live push is a follow-up).
|
||||
// continueSession re-invokes the agent for one finished execution. Persists
|
||||
// progress LIVE — a placeholder row immediately, updated in place as each
|
||||
// tool call completes — instead of only saving once the whole continuation
|
||||
// finishes. The frontend polls (see chat.ts startPolling); without
|
||||
// incremental persistence here, a continuation that runs several tool calls
|
||||
// before concluding would look like total silence in the UI for however long
|
||||
// that takes, which is exactly the "I just wait while nothing happens"
|
||||
// complaint this exists to fix — polling alone only helps if there's
|
||||
// something new to poll for.
|
||||
func (a *agent) continueSession(ctx context.Context, p pendingContinuation) {
|
||||
note := buildContinuationNote(p)
|
||||
slog.Info("nomos: auto-continuing session", "session", p.SessionID, "execution", p.ExecID, "status", p.Status)
|
||||
|
||||
placeholder, _ := json.Marshal(map[string]any{
|
||||
"role": "assistant",
|
||||
"text": "",
|
||||
"auto": true,
|
||||
})
|
||||
msgID, err := a.store.insertMessageReturningID(ctx, p.SessionID, "assistant", placeholder)
|
||||
if err != nil {
|
||||
slog.Error("nomos: continuation placeholder insert failed", "session", p.SessionID, "error", err)
|
||||
}
|
||||
|
||||
var toolCalls []map[string]any
|
||||
var finalText, errText string
|
||||
|
||||
persist := func() {
|
||||
if msgID == uuid.Nil {
|
||||
return
|
||||
}
|
||||
text := finalText
|
||||
if text == "" && errText != "" {
|
||||
text = fmt.Sprintf("(auto-continuation hit an internal error and did not respond: %s — the execution's own result is above; you may need to prompt the agent again)", errText)
|
||||
}
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"role": "assistant",
|
||||
"text": text,
|
||||
"tool_calls": toolCalls,
|
||||
"auto": true, // marks this as an autonomous continuation, not an operator turn
|
||||
})
|
||||
a.store.updateMessage(ctx, msgID, body)
|
||||
}
|
||||
|
||||
// One retry if the LLM call itself produced nothing (transient flake /
|
||||
// empty-response) — the whole point of this mechanism is "don't give up
|
||||
// on the first error," which should apply to the continuation call
|
||||
@@ -99,6 +132,7 @@ func (a *agent) continueSession(ctx context.Context, p pendingContinuation) {
|
||||
m["type"] = ev.Type
|
||||
toolCalls = append(toolCalls, m)
|
||||
}
|
||||
persist() // live: a poller sees this step land within seconds
|
||||
}
|
||||
if ev.Type == "text" {
|
||||
finalText, _ = ev.Data.(string)
|
||||
@@ -116,18 +150,10 @@ func (a *agent) continueSession(ctx context.Context, p pendingContinuation) {
|
||||
}
|
||||
}
|
||||
|
||||
text := finalText
|
||||
if text == "" && errText != "" {
|
||||
text = fmt.Sprintf("(auto-continuation hit an internal error and did not respond: %s — the execution's own result is above; you may need to prompt the agent again)", errText)
|
||||
if errText != "" && finalText == "" {
|
||||
slog.Error("nomos: auto-continuation produced no response after retry", "session", p.SessionID, "execution", p.ExecID, "error", errText)
|
||||
}
|
||||
assistantMsg, _ := json.Marshal(map[string]any{
|
||||
"role": "assistant",
|
||||
"text": text,
|
||||
"tool_calls": toolCalls,
|
||||
"auto": true, // marks this as an autonomous continuation, not an operator turn
|
||||
})
|
||||
a.store.saveMessage(ctx, p.SessionID, "assistant", assistantMsg)
|
||||
persist() // final state — same row, updated one last time with the concluding text
|
||||
}
|
||||
|
||||
// buildContinuationNote frames the finished execution for the model: what
|
||||
|
||||
Reference in New Issue
Block a user