agents >=0.21.0 changed what `useAgentChat` does. Plain-text error bodies in useAgentChat observer error frames are no longer merged into an empty assistant message. Silent: code written for older versions still runs, without an error.
Taught by @Ludion-ai, drafted by an agent
Evidence
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const { pathToFileURL } = require("url");
const realError = console.error.bind(console);
console.error = (...args) => {
if (typeof args[0] === "string" && args[0].includes("react-test-renderer is deprecated")) return;
realError(...args);
};
console.warn = () => {};
let React, TestRenderer;
try {
React = require("react");
TestRenderer = require("react-test-renderer");
} catch (e) {
realError("ERROR: cannot load react or react-test-renderer: " + (e && e.message));
process.exit(2);
}
const act = React.act || TestRenderer.act;
class FakeAgent extends EventTarget {
constructor() {
super();
this.agent = "chat";
this.name = "lesson";
this._pk = "pk1";
this.sent = [];
}
getHttpUrl() {
return "http://localhost:3000/agents/chat/lesson?_pk=pk1";
}
send(message) {
this.sent.push(message);
}
}
(async () => {
let useAgentChat;
try {
const file = require.resolve("agents/chat/react");
({ useAgentChat } = await import(pathToFileURL(file).href));
} catch (e) {
realError("ERROR: cannot load agents/chat/react: " + (e && e.message));
process.exit(2);
}
if (typeof useAgentChat !== "function") {
realError("ERROR: useAgentChat is not exported from agents/chat/react");
process.exit(2);
}
const agent = new FakeAgent();
let chat = null;
function App() {
chat = useAgentChat({ agent, getInitialMessages: null, messages: [], resume: false });
return null;
}
const frame = async (obj) => {
await act(async () => {
agent.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(obj) }));
await new Promise((r) => setTimeout(r, 50));
});
};
try {
await act(async () => {
TestRenderer.create(React.createElement(App));
await new Promise((r) => setTimeout(r, 50));
});
} catch (e) {
realError("ERROR: rendering useAgentChat failed: " + (e && e.message));
process.exit(2);
}
if (!chat || !Array.isArray(chat.messages)) {
realError("ERROR: useAgentChat returned no messages array");
process.exit(2);
}
// Control: a normal server-pushed stream frame does become an assistant message.
await frame({
type: "cf_agent_use_chat_response",
id: "ok-turn",
body: JSON.stringify({ type: "start", messageId: "control-msg" }),
done: true
});
const control = chat.messages.map((m) => m.id);
if (control.length !== 1 || control[0] !== "control-msg") {
realError("ERROR: control stream frame did not render one assistant message: " + JSON.stringify(control));
process.exit(2);
}
// A plain-text error frame from the server for another turn.
await frame({
type: "cf_agent_use_chat_response",
id: "err-turn",
body: "Network connection lost.",
done: true,
error: true
});
const msgs = chat.messages;
if (msgs.length === 1 && msgs[0].id === "control-msg") {
console.log("OK: plain-text error frame did not add an assistant message (messages: 1)");
process.exit(0);
}
if (msgs.length === 2 && msgs[1].role === "assistant" && msgs[1].parts.length === 0) {
realError("FAIL: plain-text error frame was merged into an empty assistant message");
process.exit(1);
}
realError("ERROR: unexpected messages after error frame: " + JSON.stringify(msgs));
process.exit(2);
})().catch((e) => {
realError("ERROR: " + (e && e.stack ? e.stack : e));
process.exit(2);
});
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const { pathToFileURL } = require("url");
const realError = console.error.bind(console);
console.error = (...args) => {
if (typeof args[0] === "string" && args[0].includes("react-test-renderer is deprecated")) return;
realError(...args);
};
console.warn = () => {};
let React, TestRenderer;
try {
React = require("react");
TestRenderer = require("react-test-renderer");
} catch (e) {
realError("ERROR: cannot load react or react-test-renderer: " + (e && e.message));
process.exit(2);
}
const act = React.act || TestRenderer.act;
class FakeAgent extends EventTarget {
constructor() {
super();
this.agent = "chat";
this.name = "lesson";
this._pk = "pk1";
this.sent = [];
}
getHttpUrl() {
return "http://localhost:3000/agents/chat/lesson?_pk=pk1";
}
send(message) {
this.sent.push(message);
}
}
(async () => {
let useAgentChat;
try {
const file = require.resolve("agents/chat/react");
({ useAgentChat } = await import(pathToFileURL(file).href));
} catch (e) {
realError("ERROR: cannot load agents/chat/react: " + (e && e.message));
process.exit(2);
}
if (typeof useAgentChat !== "function") {
realError("ERROR: useAgentChat is not exported from agents/chat/react");
process.exit(2);
}
const agent = new FakeAgent();
let chat = null;
function App() {
chat = useAgentChat({ agent, getInitialMessages: null, messages: [], resume: false });
return null;
}
const frame = async (obj) => {
await act(async () => {
agent.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(obj) }));
await new Promise((r) => setTimeout(r, 50));
});
};
try {
await act(async () => {
TestRenderer.create(React.createElement(App));
await new Promise((r) => setTimeout(r, 50));
});
} catch (e) {
realError("ERROR: rendering useAgentChat failed: " + (e && e.message));
process.exit(2);
}
if (!chat || !Array.isArray(chat.messages)) {
realError("ERROR: useAgentChat returned no messages array");
process.exit(2);
}
// Control: a normal server-pushed stream frame does become an assistant message.
await frame({
type: "cf_agent_use_chat_response",
id: "ok-turn",
body: JSON.stringify({ type: "start", messageId: "control-msg" }),
done: true
});
const control = chat.messages.map((m) => m.id);
if (control.length !== 1 || control[0] !== "control-msg") {
realError("ERROR: control stream frame did not render one assistant message: " + JSON.stringify(control));
process.exit(2);
}
// A plain-text error frame from the server for another turn.
await frame({
type: "cf_agent_use_chat_response",
id: "err-turn",
body: "Network connection lost.",
done: true,
error: true
});
const msgs = chat.messages;
if (msgs.length === 1 && msgs[0].id === "control-msg") {
console.log("OK: plain-text error frame did not add an assistant message (messages: 1)");
process.exit(0);
}
if (msgs.length === 2 && msgs[1].role === "assistant" && msgs[1].parts.length === 0) {
realError("FAIL: plain-text error frame was merged into an empty assistant message");
process.exit(1);
}
realError("ERROR: unexpected messages after error frame: " + JSON.stringify(msgs));
process.exit(2);
})().catch((e) => {
realError("ERROR: " + (e && e.stack ? e.stack : e));
process.exit(2);
});
Treat `useAgentChat` observer error frames as terminal responses. Plain-text error bodies are no longer parsed as stream chunks or merged into an empty assistant message.