feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { UIMessage, UIMessageChunk } from "ai";
|
||||
import { createResponseRequest, responsesToUiStream } from "./responses-transport";
|
||||
|
||||
const bytes = (events: Record<string, unknown>[], partial = false) => {
|
||||
const text = events.map((event) => `event: ${String(event.type)}\r\ndata: ${JSON.stringify(event)}\r\n\r\n`).join("");
|
||||
const encoded = new TextEncoder().encode(text);
|
||||
return new ReadableStream<Uint8Array>({ start(controller) {
|
||||
if (partial) for (let i = 0; i < encoded.length; i += 7) controller.enqueue(encoded.slice(i, i + 7));
|
||||
else controller.enqueue(encoded);
|
||||
controller.close();
|
||||
} });
|
||||
};
|
||||
const collect = async (stream: ReadableStream<UIMessageChunk>) => {
|
||||
const parts: UIMessageChunk[] = [];
|
||||
const reader = stream.getReader();
|
||||
while (true) { const next = await reader.read(); if (next.done) break; parts.push(next.value); }
|
||||
return parts;
|
||||
};
|
||||
const events = (): Record<string, unknown>[] => [
|
||||
{ type: "response.created", sequence_number: 0, response: { id: "resp_1" } },
|
||||
{ type: "response.output_item.added", sequence_number: 1, item: { type: "message", id: "msg_1", role: "assistant", content: [] } },
|
||||
{ type: "response.content_part.added", sequence_number: 2, item_id: "msg_1", content_index: 0, part: { type: "output_text", text: "" } },
|
||||
{ type: "response.output_text.delta", sequence_number: 3, item_id: "msg_1", content_index: 0, delta: "压力待核验" },
|
||||
{ type: "tjwater.permission_request", sequence_number: 4, response_id: "resp_1", data: { request_id: "permission_1" } },
|
||||
{ type: "response.output_text.done", sequence_number: 5, item_id: "msg_1", content_index: 0, text: "压力待核验" },
|
||||
{ type: "response.completed", sequence_number: 6, response: { id: "resp_1", status: "completed" } },
|
||||
];
|
||||
|
||||
describe("Responses transport", () => {
|
||||
it("sends only the latest user input and keeps business options explicit", () => {
|
||||
const messages = [{ id: "old", role: "assistant", parts: [{ type: "text", text: "旧回答" }] }, { id: "new", role: "user", parts: [{ type: "text", text: "继续核查" }] }] as UIMessage[];
|
||||
expect(createResponseRequest(messages, { conversation: "owned", approvalMode: "always" })).toEqual({
|
||||
input: [{ role: "user", content: [{ type: "input_text", text: "继续核查" }] }], stream: true, conversation: "owned",
|
||||
tjwater: { approval_mode: "always", capabilities: [] },
|
||||
});
|
||||
});
|
||||
it("decodes split UTF-8 and CRLF while retaining approval events", async () => {
|
||||
expect(await collect(responsesToUiStream(bytes(events(), true)))).toEqual([
|
||||
{ type: "start", messageId: "msg_1" }, { type: "text-start", id: "msg_1:0" },
|
||||
{ type: "text-delta", id: "msg_1:0", delta: "压力待核验" },
|
||||
{ type: "data-permission_request", data: { request_id: "permission_1" } },
|
||||
{ type: "text-end", id: "msg_1:0" }, { type: "finish", finishReason: "stop" },
|
||||
]);
|
||||
});
|
||||
it("reports premature disconnects, mismatched owners and invalid event order", async () => {
|
||||
await expect(collect(responsesToUiStream(bytes(events().slice(0, 5))))).rejects.toThrow("断开");
|
||||
const wrong = events(); wrong[4] = { ...wrong[4], response_id: "other" };
|
||||
await expect(collect(responsesToUiStream(bytes(wrong)))).rejects.toThrow("归属");
|
||||
await expect(collect(responsesToUiStream(bytes([events()[0]!, events()[3]!])))).rejects.toThrow();
|
||||
});
|
||||
it("turns failed and cancelled Responses into visible errors", async () => {
|
||||
const failed = events();
|
||||
failed[6] = { type: "response.failed", sequence_number: 6, response: { id: "resp_1", error: { message: "数据源不可用" } } };
|
||||
expect((await collect(responsesToUiStream(bytes(failed)))).at(-2)).toEqual({ type: "error", errorText: "数据源不可用" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user