Skip to content

Show streamed Expert tool calls, reasoning and progress messages - #8895

Merged
andypalmi merged 23 commits into
mainfrom
feat/expert-streamed-activity
Oct 16, 2026
Merged

andypalmi merged 23 commits into
mainfrom
feat/expert-streamed-activity

Conversation

@andypalmi

@andypalmi andypalmi commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes FlowFuse/engineering#547
Closes FlowFuse/engineering#549
Closes FlowFuse/engineering#551
Closes FlowFuse/engineering#553
Closes FlowFuse/engineering#554
Closes FlowFuse/engineering#556

Part of FlowFuse/engineering#545.

The Expert chat shows a turn's tool calls, reasoning and progress messages as they happen, inside the reply bubble, instead of only a tool-call count with the final answer. Streamed events arrive on the existing expert:status-message inflight topic with a kind field (tool-record, reasoning or message), so no new MQTT topics or ACL changes are needed.

Adds

  1. supportsStreamedActivity in the chat context. Expert only streams to clients that send it; older Expert versions ignore it and keep today's behaviour.
  2. Store handling for streamed events (product-expert.js): each event is placed in the turn's reply bubble in seq order, an event with a known id updates its item in place, and the final answer merges into the same bubble by id, so nothing is duplicated.
  3. A guard that drops a late update older than the one shown, or one that would reopen a finished item.
  4. TimelineItem.vue: one collapsible card per tool call. The header is the reason for the call on one line, and expanding it shows the full reason, the tool, its input and output.
  5. ReasoningBlock.vue: a collapsed "Thinking" / "Thought" block with one line of the reasoning under it, the latest sentence while streaming and the first once done.
  6. AgentMessage.vue: progress messages from the Expert as assistant bubbles between the cards.
  7. streamedActivity.js: the shared list of streamed and activity kinds.

Changes

  1. AiMessage.vue renders tool records, reasoning and messages in order above the answer. Insights answers and saved conversations use the same tool cards; Insights does not stream.
  2. The loading indicator keeps its three dots while a turn streams but drops the status lines under them. Turns that do not stream keep both.
  3. CollapsibleSection.vue gains a summary slot under the header.

Removes

  1. ToolCalls.vue and ToolCallItem.vue, the old "N tool calls" strip, replaced by the cards.

Tests

  1. Store: streamed items are inserted by seq, updated by id, merged with the final answer, and late updates are ignored.
  2. Components: tool cards render for streamed, Insights and saved answers; the reasoning preview shows one line; the loading indicator hides its status lines when asked.

@andypalmi
andypalmi requested review from Steve-Mcl and cstns October 13, 2026 13:03
@andypalmi andypalmi self-assigned this Oct 13, 2026
@codecov

codecov Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.29%. Comparing base (111f500) to head (e8883b7).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8895      +/-   ##
==========================================
+ Coverage   79.08%   79.29%   +0.20%     
==========================================
  Files         476      476              
  Lines       25890    25912      +22     
  Branches     6886     6896      +10     
==========================================
+ Hits        20475    20546      +71     
+ Misses       5415     5366      -49     
Flag Coverage Δ
backend 79.29% <ø> (+0.20%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andypalmi
andypalmi marked this pull request as ready for review October 14, 2026 07:52
@n-lark
n-lark self-requested a review October 14, 2026 14:53

@n-lark n-lark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey just a couple things good work. Two things that look deliberate but don't match the issues this closes:

  1. #549 asks for the tool name in the header with the reason inside
  2. #551 asks for the reasoning block to be open while text arrives and collapse when it ends.

Fine to keep the implementation as is. Just worth a note or updating the issues.

Comment thread frontend/src/stores/product-expert.js Outdated
andypalmi added 3 commits October 14, 2026 21:55
An update replaced the item with its new seq but left it in place, so the bubble was no longer sorted and a later insert could land above it. The item now keeps its first seq, the newest update's seq is tracked separately for the late-update check, and fields an update leaves out are kept.
The header is now a button with aria-expanded, so it can be focused and toggled from the keyboard. A collapsible prop renders a static header with no chevron or toggle when there is nothing to expand.
Tool cards show input and output in the shared JsonViewer instead of their own highlighter and toggles. A card with no reason, error or data has nothing to expand, so it shows no chevron.
@andypalmi

Copy link
Copy Markdown
Contributor Author

Thanks! Updated FlowFuse/engineering#549 and FlowFuse/engineering#551 to match what shipped: the reason heads the tool card, and the reasoning block stays collapsed with a one-line preview so the chat does not jump while it streams.

@andypalmi
andypalmi requested a review from n-lark October 14, 2026 21:42
Adds an align prop and a plain kind to ff-button so it can render a
left-aligned header with no background or padding.

@Steve-Mcl Steve-Mcl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when a turn leaves _inFlightRequests without a final answer (Stop, handleMqttError, Start over), set done on its bubble's open items, or add a stopped marker.
Alternatively, have ReasoningBlock only show "Thinking" while the turn is live. It's a small store change plus one test. Below is a screenshot of what happens if convo is stopped during thinking (note the partial streamed character "I" at the end:

Image

A few smaller things, none blocking:

  • _streamed now means two things. On streamed items it means "arrived over MQTT", and updateAnswerStreamedState uses it to mean "typewriter finished". hasStreamedActivity relies on the first meaning. It works today, but a separate flag (_live?) would be harder to break.
  • The bubble is placed at its first event. A system or event message pushed mid-turn lands below it, and later cards keep growing the bubble above that notice, so the order on screen stops matching the order things happened.
  • A tool card with no result yet looks the same as a finished one, since ok undefined isn't treated as failed. Does the agent send a start event before the result? If so, a running state on the card would help.

A stopped turn marks its open items done, so a reasoning block no longer shows Thinking forever. Streamed items carry their own _live flag instead of sharing _streamed with the typewriter. Activity that follows a message pushed mid-turn starts a new bubble below it, and final-answer tool records still upgrade their card wherever it is.
@andypalmi

Copy link
Copy Markdown
Contributor Author

Thanks! Done in d7a1553:

  1. When a turn ends without a final answer (Stop or a connection error), its open items are marked done, so a stopped reasoning block shows "Thought" instead of "Thinking". Start over clears the chat, so it needs nothing.
  2. Streamed items now carry their own _live flag, and hasStreamedActivity checks that. _streamed is back to meaning only that the typewriter has finished.
  3. Activity that arrives after a system or event message landed mid-turn now starts a new bubble below that message, so the chat keeps the order things happened in. Updates and final-answer tool records still find their card in whichever bubble it is in.
  4. There is no start event: the Expert sends the tool record once the call returns, so every card already has its result. A running state would need the Expert to send an event when a call starts. That could be a follow-up if we want it.

@Steve-Mcl Steve-Mcl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local testing now good. Approving from an operational perspective - so I am asking that we wait for another tick from one the FE folk before you merge pls.

@cstns

cstns commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Reviewing now

@cstns cstns left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran this on the branch itself against the dev Expert through the FlowFuse central broker. Streamed reasoning, tool records and progress messages render in order, the final answer merges in by id, Stop, reload, keyboard access and Insights cards all behave, and the full frontend suite is green. One thing worth fixing inline (the hover on the Input/Output toggles), the rest are small notes.

</div>
<!-- Input/output are dev-only today, but render whenever present so a future
flag flip needs no UI change. -->
<json-viewer v-if="hasInput" :value="parseJson(item.input)" label="Input" collapsible default-collapsed />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Input/Output toggles come out as full tertiary buttons here, so hovering them fills the row with the accent colour and fades it in over 0.3s. Inside a card that reads like a bug. Might be worth giving the viewer a plain variant for this spot, or scoping the hover away in this component.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e8883b7. JsonViewer takes an opt-in toggleKind, and the tool card passes plain, so the toggles no longer fill on hover. Other viewers are unchanged.

if (typeof value !== 'string') return false
const trimmed = value.trim()
if (trimmed === '' || trimmed === AUTOMATION_PLACEHOLDER) return false
return !trimmed.startsWith('invoke_')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Insights the tool line under the reason comes out as mcp_tool__flowfuse-mcp-gateway-0__invoke_read_tool, since toolTitle is empty and toolName equals toolId. The guard only catches names that start with invoke_. Not a regression, but it defeats the idea of hiding wrappers. Maybe match invoke_ anywhere, or fall back to input.tool_name for gateway calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e8883b7. invoke_ is now rejected after a server prefix too, and a gateway call falls back to input.tool_name.

this._insertBySeq(target, { ...item, _uuid: uuidv4(), _streamed: false })
}
const { answer: _answer, ...fields } = response
Object.assign(target, fields)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small robustness one: if a reasoning block is still done: false when the final answer lands, it stays "Thinking" for good and survives a reload. The Expert does send done before the answer, so I couldn't hit it for real, but calling _closeTurnItems(transactionId) here too would make an Expert error path harmless.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e8883b7. The final answer now marks any open items done as well.

return typeof this.item.error === 'string' ? this.item.error.trim() : ''
},
// First usable name: friendly title, else the tool's own name, else the raw key.
title () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old strip showed per-call and total ms and the records still carry durationMs, but nothing displays it now. Fine if that's on purpose, just checking it wasn't dropped by accident.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On purpose. In support the time per call isn't useful to the user. Insights is where it might help, so we can add it there if people want it.

}

return null
activity () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small UX note, not a bug: a paginated turn gave me ten identical "List Hosted Instances" cards with nothing to expand, ten rows where the old strip was one collapsed line. Could be worth folding consecutive identical cards without a reason into one, or leaving it as a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every tool call now has to carry a reason, in support and Insights alike, so each paged call should get its own card with its reason and something to expand. Cards with nothing to expand would come from a turn on an Expert from before that change, or from an older saved conversation. Could you check which it was? If it still happens on a fresh turn, I'll look into it.

@andypalmi andypalmi Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try with Expert on Cloud, it should be there @cstns

</li>
<li v-if="isWaitingForResponse">
<expert-loading-indicator />
<expert-loading-indicator :show-messages="!hasStreamedActivity" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: after Stop, hasStreamedActivity goes false as soon as the in-flight map clears, so the loader comes back with the generic rotating lines under a bubble that was just streaming. Cosmetic, and the stopping indicator predates this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e8883b7. A turn that is still stopping keeps the loader to its dots.

@n-lark n-lark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM for my fixes but please address Serban's feedback as well.

JsonViewer takes an opt-in toggleKind, and the tool card uses the plain kind so its Input and Output toggles no longer fill with the accent colour on hover. A gateway wrapper call is named after the tool it invoked. The final answer finishes any item still open, and a stopping turn keeps the loader to its dots.
@andypalmi
andypalmi merged commit 87d6371 into main Oct 16, 2026
29 checks passed
@andypalmi
andypalmi deleted the feat/expert-streamed-activity branch October 16, 2026 09:24

This branch was successfully deployed

1 active deployment
staging — e8883b71 Deployed Oct 16, 2026 by andypalmi via Remove application #12514
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants