assistant-stream
Version:
Streaming utilities for AI assistants
119 lines (110 loc) • 3.89 kB
text/typescript
import type { ReadonlyJSONValue } from "../../utils/json/json-value";
import type {
ToolModelContentPart,
// oxlint-disable-next-line no-unused-vars -- referenced from JSDoc {@link} below
ToolModelOutputFunction,
} from "./tool-types";
const TOOL_RESPONSE_SYMBOL = Symbol.for("aui.tool-response");
/** Stand-in result for a tool that completed without returning a value. */
export const NO_RESULT = "<no result>";
/**
* Shape accepted anywhere a {@link ToolResponse} can be returned.
*/
export type ToolResponseLike<TResult> = {
/** UI-visible tool result value. */
result: TResult;
/**
* Optional UI-only artifact associated with the result.
*
* Artifacts are useful for large or structured data that should be available
* to renderers without necessarily being sent back to the model.
*/
artifact?: ReadonlyJSONValue | undefined;
/** Marks the tool result as an error result. */
isError?: boolean | undefined;
/**
* Marks the result as interim while the tool call keeps running. Honored by
* stream controllers, which can send more responses; a tool's `execute`
* result is always final and ignores it.
*/
isPreliminary?: boolean | undefined;
/**
* Explicit model-visible content to send back after the tool call.
*
* When omitted, assistant-ui derives model output from `result` or a tool's
* {@link ToolModelOutputFunction}.
*/
modelContent?: readonly ToolModelContentPart[] | undefined;
/** Optional provider-specific message payload associated with the tool result. */
messages?: ReadonlyJSONValue | undefined;
};
/**
* Tool result wrapper for separating UI-visible output from model-visible
* output.
*
* Return `ToolResponse` from a tool when you need to attach an artifact, mark
* the result as an error, or control the content sent back to the model.
*
* @example
* ```ts
* return new ToolResponse({
* result: { title: "Report ready" },
* artifact: { reportId },
* modelContent: [{ type: "text", text: "The report is ready." }],
* });
* ```
*/
export class ToolResponse<TResult> {
get [TOOL_RESPONSE_SYMBOL]() {
return true;
}
readonly artifact?: ReadonlyJSONValue;
readonly result: TResult;
readonly isError: boolean;
readonly isPreliminary?: boolean;
readonly modelContent?: readonly ToolModelContentPart[];
readonly messages?: ReadonlyJSONValue;
constructor(options: ToolResponseLike<TResult>) {
if (options.artifact !== undefined) {
this.artifact = options.artifact;
}
// A part written with `state: "result"` is recognized as settled by
// carrying a result, and several writers put a response onto the part
// without ever emitting a chunk, so an absent result is materialized here.
const result = options.result;
this.result =
result === undefined ? (NO_RESULT as unknown as TResult) : result;
this.isError = options.isError ?? false;
if (options.isPreliminary) {
this.isPreliminary = true;
}
if (options.modelContent !== undefined) {
this.modelContent = options.modelContent;
}
if (options.messages !== undefined) {
this.messages = options.messages;
}
}
static [Symbol.hasInstance](
obj: unknown,
): obj is ToolResponse<ReadonlyJSONValue> {
return (
typeof obj === "object" && obj !== null && TOOL_RESPONSE_SYMBOL in obj
);
}
/**
* Converts a plain tool return value into a {@link ToolResponse}.
*
* Existing `ToolResponse` instances are returned unchanged. `undefined`
* becomes the string `"<no result>"` so downstream protocol chunks always
* carry a concrete result.
*/
static toResponse(result: any | ToolResponse<any>): ToolResponse<any> {
if (result instanceof ToolResponse) {
return result;
}
return new ToolResponse({
result: result === undefined ? NO_RESULT : result,
});
}
}