mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-27 16:02:43 +00:00
* feat(chat-sdk): chat through a raw app's backend runnable Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(chat-sdk): finish a lost stream and wait out a stopped turn in streamChatTurn Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(chat-sdk): show the answer when a stream is lost before a tool call Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(chat-sdk): send attachments through the raw app runnable Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(chat-sdk): point sandboxed attachments at the runnable section Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: agent runnables for raw apps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: build agent runnables without the parquet feature, cache the agent check query Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review nits on the agent picker and the CLI schema doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: run an agent runnable once when it is given no memory_id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: accept previous_messages on agent runs, read-checking the files it quotes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: refuse an anonymous agent turn while one with the same memory still runs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show the agent's settings in the raw-app runnable panel, size its button like the row Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: offer an agent runnable memory_id or previous_messages after the agent's memory Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: take previous_messages in a memory-off agent's run form, type agent attachments as s3object Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: file an anonymous agent turn as a conversation, so its memory is collected and its turns serialized Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: own anonymous agent conversations under a fixed name that fits created_by Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: own anonymous agent conversations as anonymous and keep them out of conversation lists Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: point file_chat_turn at the anonymous conversation owner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: title the runnable picker after what it lists, in sentence case Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: start a chat with local history listing its stored conversations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: chat through a raw app's agent runnable with { wmill, runnable } Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: key an agent runnable chat's local history by its app as well Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review findings on agent runnables Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: offer memory_id to an agent runnable over an older auto memory Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: drop inherited static inputs from an agent runnable, show the memory-off note only when it is off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
3.1 KiB
TypeScript
69 lines
3.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import { TurnRunningError } from '../src/api'
|
|
import { createChat } from '../src/chat'
|
|
import { memoryStorage, ndjson } from './support'
|
|
|
|
describe('raw app agent runnable', () => {
|
|
test('chats through the runnable without a request of its own', async () => {
|
|
const calls: unknown[] = []
|
|
let refuse = false
|
|
const start = async (args: unknown) => {
|
|
// As the host relays a 409: the server's body behind the error's stack.
|
|
if (refuse) {
|
|
const body = { error: 'still answering', running_turn: { job_id: 'job-1', user_seq: 1 } }
|
|
throw new Error(`Error: ${JSON.stringify(body)}\n at x`)
|
|
}
|
|
calls.push(args)
|
|
return `job-${calls.length}`
|
|
}
|
|
const streamJob = async (jobId: string, onUpdate?: (u: { new_result_stream?: string }) => void) => {
|
|
const stream = ndjson({ type: 'token_delta', content: 'answer ' }, { type: 'token_delta', content: jobId })
|
|
onUpdate?.({ new_result_stream: stream.slice(0, 10) })
|
|
onUpdate?.({ new_result_stream: stream.slice(10) })
|
|
if (jobId === 'job-3') return { error: { name: 'Error', message: 'agent failed' } }
|
|
return { output: `answer ${jobId}`, messages: [] }
|
|
}
|
|
const storage = memoryStorage()
|
|
const wmill = { backendAsync: { chat: start, other: start }, streamJob }
|
|
const chat = createChat({
|
|
wmill,
|
|
runnable: 'chat',
|
|
storage,
|
|
fetch: () => Promise.reject(new Error('no request expected'))
|
|
})
|
|
expect(chat.getState().history).toBe('local')
|
|
await chat.sendMessage('hi')
|
|
await chat.sendMessage('again')
|
|
const conversationId = chat.getState().conversationId
|
|
expect(calls).toEqual([
|
|
{ user_message: 'hi', memory_id: conversationId },
|
|
{ user_message: 'again', memory_id: conversationId }
|
|
])
|
|
expect(chat.getState().messages.map((m) => m.content)).toEqual(['hi', 'answer job-1', 'again', 'answer job-2'])
|
|
|
|
await chat.sendMessage('third')
|
|
const messages = chat.getState().messages
|
|
const last = messages[messages.length - 1]
|
|
expect([last.content, last.success]).toEqual(['agent failed', false])
|
|
|
|
// A turn still running elsewhere takes the message back rather than failing a turn.
|
|
refuse = true
|
|
await expect(chat.sendMessage('fourth')).rejects.toBeInstanceOf(TurnRunningError)
|
|
expect(chat.getState().messages).toEqual(messages)
|
|
|
|
// Each runnable keeps its own history: another agent never sees this conversation.
|
|
expect(createChat({ wmill, runnable: 'chat', storage }).getState().conversations).toHaveLength(1)
|
|
expect(createChat({ wmill, runnable: 'other', storage }).getState().conversations).toEqual([])
|
|
// Nor does the same runnable name in another app, whose path the host puts in `ctx`.
|
|
const g = globalThis as { ctx?: unknown }
|
|
g.ctx = { path: 'u/admin/other_app' }
|
|
try {
|
|
expect(createChat({ wmill, runnable: 'chat', storage }).getState().conversations).toEqual([])
|
|
} finally {
|
|
delete g.ctx
|
|
}
|
|
// @ts-expect-error a name the app's module does not declare
|
|
createChat({ wmill, runnable: 'missing', storage })
|
|
})
|
|
})
|