UNPKG

@convex-dev/agent

Version:

A agent component for Convex.

103 lines (90 loc) 3.2 kB
import { describe, expect, test } from "vitest"; import { Agent, createThread } from "./index.js"; import { defineSchema, type DataModelFromSchemaDefinition, type ApiFromModules, type ActionBuilder, actionGeneric, anyApi, } from "convex/server"; import { v } from "convex/values"; import { components, initConvexTest } from "./setup.test.js"; import { mockModel } from "./mockModel.js"; const schema = defineSchema({}); type DataModel = DataModelFromSchemaDefinition<typeof schema>; const action = actionGeneric as ActionBuilder<DataModel, "public">; const FINAL_TEXT = "Hello from the model"; const agent = new Agent(components.agent, { name: "stream-test", languageModel: mockModel({ content: [{ type: "text", text: FINAL_TEXT }], }), }); // Action that exercises streamText with saveStreamDeltas.returnImmediately=true. // It consumes the stream after streamText returns, simulating the HTTP response // path described in issue #265. export const streamTextReturnImmediately = action({ args: { threadId: v.string() }, handler: async (ctx, { threadId }) => { const result = await agent.streamText( ctx, { threadId }, { prompt: "Test" }, { saveStreamDeltas: { returnImmediately: true, chunking: "word", throttleMs: 0, }, }, ); // Drain the stream the way an HTTP response would. This triggers // onStepFinish for every step, including the final one. await result.consumeStream(); return { ok: true }; }, }); const testApi: ApiFromModules<{ fns: { streamTextReturnImmediately: typeof streamTextReturnImmediately }; }>["fns"] = anyApi["streamText.test"] as any; describe("streamText with saveStreamDeltas.returnImmediately (issue #265)", () => { test("persists the final assistant text to the messages table", async () => { const t = initConvexTest(schema); const threadId = await t.run(async (ctx) => createThread(ctx, components.agent, { userId: "u1" }), ); await t.action(testApi.streamTextReturnImmediately, { threadId }); // Allow any background work scheduled by consumeStream to settle. await t.finishAllScheduledFunctions(() => {}); const messages = await t.run(async (ctx) => agent.listMessages(ctx, { threadId, paginationOpts: { cursor: null, numItems: 50 }, }), ); const assistantTextMessages = messages.page.filter( (m) => m.message?.role === "assistant" && typeof m.text === "string" && m.text.length > 0, ); expect( assistantTextMessages.length, "expected at least one persisted assistant message with text", ).toBeGreaterThan(0); const combined = assistantTextMessages.map((m) => m.text).join(""); expect(combined).toContain(FINAL_TEXT); // The stream should be marked finished, not stuck in "streaming". const stillStreaming = await t.run(async (ctx) => ctx.runQuery(components.agent.streams.list, { threadId, statuses: ["streaming"], }), ); expect( stillStreaming, "stream should not be stuck in 'streaming' status", ).toHaveLength(0); }); });