mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 08:02:38 +00:00
fix: count jobless tool rows and the loaded license in the flow chat
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
dd7ee0cdbb
commit
3895e1d579
+3
-2
@@ -123,8 +123,9 @@ export function Support() {
|
||||
The hook returns the [state](#state) plus the chat's methods. It recreates the chat
|
||||
(fresh state, old one destroyed) when `flowPath`, `baseUrl`, `workspace`, `history`,
|
||||
`storageKey` or the credential change: a different token string, or a switch between
|
||||
no token, a string and a function. A token function is called through a ref, so
|
||||
passing a new closure on every render is fine and never resets the chat; when users
|
||||
no token, a string and a function; and when a `run` callback appears or goes away.
|
||||
A token function is called through a ref, so passing a new closure on every render
|
||||
is fine and never resets the chat, and so are `run` and the callbacks; when users
|
||||
sign in and out behind a token function, change `storageKey` (their id) so local
|
||||
history and state start over with them.
|
||||
|
||||
|
||||
+12
-9
@@ -496,20 +496,23 @@ class ChatImpl implements Chat {
|
||||
}
|
||||
|
||||
/**
|
||||
* The latest row one of the turn's jobs persisted after the turn's user message
|
||||
* is an assistant message. An agent writes each round's text before that round's
|
||||
* tool rows, and a tool row when the tool finishes, so an earlier round's text is
|
||||
* followed by a tool row and only the answer closes the turn. The content is not
|
||||
* compared with the flow result: an image answer, a structured one and a forwarded
|
||||
* agent result are all persisted in a shape the result does not reproduce. Rows
|
||||
* from an earlier turn whose job outlived `stop()` (a token without `jobs:write`
|
||||
* cannot cancel it) can land after this turn's user row and do not count.
|
||||
* The latest row the turn persisted after its user message is an assistant
|
||||
* message. An agent issues each round's text row before that round's tool rows,
|
||||
* and a tool row when the tool finishes, so an earlier round's text is followed
|
||||
* by a tool row and only the answer closes the turn (the inserts are spawned, so
|
||||
* a badly delayed one can invert that order at the cost of the reconcile
|
||||
* retries). The content is not compared with the flow result: an image answer, a
|
||||
* structured one and a forwarded agent result are all persisted in a shape the
|
||||
* result does not reproduce. Rows carrying a job id belong to the turn when the
|
||||
* job is one of the turn's, which leaves out an earlier turn whose job outlived
|
||||
* `stop()` (a token without `jobs:write` cannot cancel it); a tool row without one
|
||||
* (an MCP call runs inside the agent step) belongs to whatever turn is under way.
|
||||
*/
|
||||
#answered(turn: Turn): boolean {
|
||||
const messages = this.#state.messages
|
||||
const from = messages.findIndex((m) => m.id === turn.userMessageId)
|
||||
const ownJob = (m: ChatMessage) =>
|
||||
turn.jobIds === undefined || (m.jobId !== undefined && turn.jobIds.has(m.jobId))
|
||||
turn.jobIds === undefined || (m.jobId === undefined ? m.role === 'tool' : turn.jobIds.has(m.jobId))
|
||||
let latest: ChatMessage | undefined
|
||||
for (let i = from + 1; i < messages.length; i++) {
|
||||
const m = messages[i]
|
||||
|
||||
@@ -558,6 +558,31 @@ describe('createChat with server history', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('a tool row without a job (an MCP call) still separates a round from the answer', async () => {
|
||||
let reads = 0
|
||||
const { fetch } = fetchMock(
|
||||
run,
|
||||
(c) =>
|
||||
c.url.pathname === streamPath
|
||||
? sse([{ type: 'update', completed: true, only_result: { output: 'Final answer', messages: [] } }])
|
||||
: undefined,
|
||||
(c) => (c.url.pathname.endsWith('/jobs_u/get/job-1') ? json({ flow_status: { modules: [{ job: 'step-1', agent_actions: [{ type: 'mcp_tool_call' }, { type: 'message' }] }] } }) : undefined),
|
||||
(c) =>
|
||||
c.url.pathname.endsWith('/messages')
|
||||
? json(
|
||||
++reads === 1
|
||||
? [messageRow(81, 'user', 'hi'), messageRow(82, 'assistant', 'Let me check', { job_id: 'step-1' }), messageRow(83, 'tool', 'Used search tool', { job_id: null })]
|
||||
: [messageRow(84, 'assistant', 'Final answer', { job_id: 'step-1' })]
|
||||
)
|
||||
: undefined,
|
||||
(c) => (c.url.pathname === '/api/w/ws/flow_conversations/list' ? json([]) : undefined)
|
||||
)
|
||||
const chat = createChat(options({}, fetch))
|
||||
await chat.sendMessage('hi')
|
||||
expect(reads).toBe(2)
|
||||
expect(chat.getState().messages.map((m) => m.content)).toEqual(['hi', 'Let me check', 'Used search tool', 'Final answer'])
|
||||
})
|
||||
|
||||
test('the stream asks for a server poll interval only when one is set', async () => {
|
||||
const answer: Route = (c) =>
|
||||
c.url.pathname === streamPath ? sse([{ type: 'update', completed: true, only_result: 'ok' }]) : undefined
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { createChat, type Chat, type ChatState } from 'windmill-chat'
|
||||
import FlowConversationsSidebar from './FlowConversationsSidebar.svelte'
|
||||
import FlowChatInterface from './FlowChatInterface.svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
@@ -49,8 +49,9 @@
|
||||
workspace: ws,
|
||||
baseUrl: window.location.origin,
|
||||
history: 'server',
|
||||
// Only an enterprise server honours it; elsewhere it would just log a warning per poll.
|
||||
pollDelayMs: untrack(() => $enterpriseLicense) ? 50 : undefined,
|
||||
// Only an enterprise server honours it; elsewhere it would just log a warning per
|
||||
// poll. The license loads asynchronously, so a cold load may create the chat twice.
|
||||
pollDelayMs: $enterpriseLicense ? 50 : undefined,
|
||||
run: async ({ user_message, ...inputs }, { conversationId }) => {
|
||||
const jobId = await onRunFlow(String(user_message), conversationId, inputs)
|
||||
if (!jobId) throw new Error('the flow did not start')
|
||||
|
||||
Reference in New Issue
Block a user