mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-22 11:45:52 +00:00
Mark the post-tool status handoff so silent reasoning never shows a stale tool status
After a tool round, the next model turn can reason server-side for tens
of seconds without streaming a single event. The footer kept the last
tool's status counting up ('Reading inventory. (40s)') while the model
was already composing the answer — observed live on the OpenRouter
reasoning route. OpenCode always shows a thinking state here; Pulse now
emits a model_processing workflow state at the start of every post-tool
turn ('Working on the response with the gathered results.'), which
model_thinking upgrades when reasoning deltas actually arrive.
Pinned by a new interaction-quality corpus scenario.
This commit is contained in:
@@ -2388,6 +2388,14 @@ deriving an older display status from `workflowStatusHistory`.
|
||||
|
||||
## Current State
|
||||
|
||||
Post-tool model turns mark the status handoff: every agentic turn after the
|
||||
first emits a `model_processing` workflow state ("Working on the response
|
||||
with the gathered results.") before the provider call, so a provider that
|
||||
reasons server-side without streaming events can never leave the previous
|
||||
tool's status stale on screen; `model_thinking` still upgrades the status
|
||||
when reasoning deltas actually arrive. Pinned by the interaction-quality
|
||||
corpus scenario "post-tool model turn marks the status handoff".
|
||||
|
||||
Session titles are model-generated after the first exchange
|
||||
(`internal/ai/chat/session_title.go`): the chat runtime upgrades the
|
||||
auto-truncated first-prompt placeholder once the done event has reached the
|
||||
|
||||
@@ -857,6 +857,16 @@ func (a *AgenticLoop) executeWithTools(ctx context.Context, sessionID string, me
|
||||
req = requestSanitizer(req)
|
||||
}
|
||||
|
||||
// Post-tool model turns can reason server-side for a long time without
|
||||
// streaming a single event; without a fresh status the footer keeps
|
||||
// the last tool's message ("Reading inventory. (40s)") while the model
|
||||
// is already composing the answer. Mark the handoff explicitly. The
|
||||
// model_thinking status still upgrades this when reasoning deltas
|
||||
// actually arrive.
|
||||
if turn > 0 {
|
||||
emitWorkflowState(callback, "model_processing", "Working on the response with the gathered results.", sessionFSMState(a.sessionFSM), "")
|
||||
}
|
||||
|
||||
maxProviderAttempts := 2
|
||||
err := error(nil)
|
||||
for attempt := 1; attempt <= maxProviderAttempts; attempt++ {
|
||||
|
||||
@@ -137,6 +137,19 @@ func interactionScenarios() []interactionScenario {
|
||||
answerMustNotContain: []string{"call_a1b2c3d4"},
|
||||
streamMustContain: []string{"pulse_query"},
|
||||
},
|
||||
{
|
||||
name: "post-tool model turn marks the status handoff",
|
||||
promise: "after tools run, the footer status hands off from the last tool to the model composing the answer — a provider that reasons server-side without streaming events never leaves a stale tool status on screen",
|
||||
prompt: "summarize my inventory",
|
||||
calls: []scriptedProviderCall{
|
||||
providerQueryToolCall("call_e5f6a7b8"),
|
||||
providerContentDone("Inventory summarized."),
|
||||
},
|
||||
maxTurns: 4,
|
||||
orderedTypes: []string{"session", "tool_start", "tool_end", "workflow_state", "content", "done"},
|
||||
forbiddenTypes: []string{"error"},
|
||||
streamMustContain: []string{"model_processing", "Working on the response with the gathered results"},
|
||||
},
|
||||
{
|
||||
name: "no-narrative turn falls back to a clean operator sentence",
|
||||
promise: "when the model runs tools but never writes an answer, the user reads one clean sentence — not raw JSON, tool-result dumps, or provider call ids",
|
||||
|
||||
Reference in New Issue
Block a user