diff --git a/docs/concepts/messages.mdx b/docs/concepts/messages.mdx index 4777cbfe1..909737e74 100644 --- a/docs/concepts/messages.mdx +++ b/docs/concepts/messages.mdx @@ -25,6 +25,11 @@ interface BaseMessage { role: string // The role of the sender (user, assistant, system, tool) content?: string // Optional text content of the message name?: string // Optional name of the sender + attachments?: FileAttachment[] // Optional remote files referenced by the message +} + +interface FileAttachment { + url: string // data URL (data:;base64,...) containing the file bytes } ``` @@ -41,9 +46,14 @@ Messages from the end user to the agent: interface UserMessage { id: string role: "user" - content: string // Text input from the user + content?: string // Optional text input from the user + attachments?: FileAttachment[] // Optional remote files shared with the agent name?: string // Optional user identifier } + +> **Validation rule:** a user message must include either non-empty `content` or at least one `attachment`. This allows file-only interactions while keeping the protocol backward compatible with text-only chats. + +> **Implementation note:** in this release, attachments must be provided as `data:` URLs (typically base64-encoded payloads). Remote HTTP(S) links will be supported in a future iteration once upload flows are standardized. ``` ### Assistant Messages diff --git a/sdks/python/ag_ui/core/types.py b/sdks/python/ag_ui/core/types.py index 47b7ae182..b8ce38502 100644 --- a/sdks/python/ag_ui/core/types.py +++ b/sdks/python/ag_ui/core/types.py @@ -4,7 +4,7 @@ from typing import Annotated, Any, List, Literal, Optional, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, model_validator from pydantic.alias_generators import to_camel @@ -44,6 +44,20 @@ class BaseMessage(ConfiguredBaseModel): role: str content: Optional[str] = None name: Optional[str] = None + attachments: Optional[List["FileAttachment"]] = None + + +class FileAttachment(ConfiguredBaseModel): + """ + Remote file metadata associated with a message. + """ + url: str + + @model_validator(mode="after") + def validate_data_url(cls, values: "FileAttachment") -> "FileAttachment": + if not values.url.startswith("data:"): + raise ValueError("Attachment url must be a data URL (data:;base64,...)") + return values class DeveloperMessage(BaseMessage): @@ -75,7 +89,15 @@ class UserMessage(BaseMessage): A user message. """ role: Literal["user"] = "user" # pyright: ignore[reportIncompatibleVariableOverride] - content: str + content: Optional[str] = None + + @model_validator(mode="after") + def ensure_body(cls, values: "UserMessage") -> "UserMessage": + has_content = bool((values.content or "").strip()) + has_attachments = bool(values.attachments) + if not has_content and not has_attachments: + raise ValueError("User messages must include content or at least one attachment.") + return values class ToolMessage(ConfiguredBaseModel): diff --git a/sdks/python/tests/test_events.py b/sdks/python/tests/test_events.py index c73a2537c..85e509c38 100644 --- a/sdks/python/tests/test_events.py +++ b/sdks/python/tests/test_events.py @@ -3,7 +3,7 @@ from datetime import datetime from pydantic import ValidationError, TypeAdapter -from ag_ui.core.types import Message, UserMessage, AssistantMessage, FunctionCall, ToolCall +from ag_ui.core.types import Message, UserMessage, AssistantMessage, FunctionCall, ToolCall, FileAttachment from ag_ui.core.events import ( EventType, BaseEvent, @@ -176,7 +176,10 @@ def test_state_delta(self): def test_messages_snapshot(self): """Test creating and serializing a MessagesSnapshotEvent event""" messages = [ - UserMessage(id="user_1", content="Hello"), + UserMessage( + id="user_1", + attachments=[FileAttachment(url="data:text/plain;base64,ZmFrZQ==")], + ), AssistantMessage(id="asst_1", content="Hi there", tool_calls=[ ToolCall( id="call_1", @@ -194,12 +197,20 @@ def test_messages_snapshot(self): self.assertEqual(len(event.messages), 2) self.assertEqual(event.messages[0].id, "user_1") self.assertEqual(event.messages[1].tool_calls[0].function.name, "get_weather") + self.assertEqual( + str(event.messages[0].attachments[0].url), + "data:text/plain;base64,ZmFrZQ==", + ) # Test serialization serialized = event.model_dump(by_alias=True) self.assertEqual(serialized["type"], "MESSAGES_SNAPSHOT") self.assertEqual(len(serialized["messages"]), 2) self.assertEqual(serialized["messages"][0]["role"], "user") + self.assertEqual( + serialized["messages"][0]["attachments"][0]["url"], + "data:text/plain;base64,ZmFrZQ==", + ) self.assertEqual(serialized["messages"][1]["toolCalls"][0]["function"]["name"], "get_weather") def test_raw_event(self): diff --git a/sdks/python/tests/test_types.py b/sdks/python/tests/test_types.py index e534aa5ab..6440a24fa 100644 --- a/sdks/python/tests/test_types.py +++ b/sdks/python/tests/test_types.py @@ -10,6 +10,7 @@ AssistantMessage, UserMessage, ToolMessage, + FileAttachment, Message, RunAgentInput ) @@ -26,14 +27,15 @@ def test_function_call_creation(self): def test_message_serialization(self): """Test serialization of a basic message""" + attachment = FileAttachment(url="data:text/plain;base64,SGVsbG8=") user_msg = UserMessage( id="msg_123", - content="Hello, world!" + attachments=[attachment] ) serialized = user_msg.model_dump(by_alias=True) self.assertEqual(serialized["id"], "msg_123") self.assertEqual(serialized["role"], "user") - self.assertEqual(serialized["content"], "Hello, world!") + self.assertEqual(serialized["attachments"][0]["url"], str(attachment.url)) def test_tool_call_serialization(self): """Test camel case serialization for ConfiguredBaseModel subclasses""" @@ -143,6 +145,23 @@ def test_user_message(self): self.assertEqual(serialized["role"], "user") self.assertEqual(serialized["content"], "User query") + def test_user_message_with_attachments_only(self): + """User messages should allow attachments without text.""" + attachment = FileAttachment(url="data:image/png;base64,somepngbytes") + msg = UserMessage( + id="user_attachments", + attachments=[attachment] + ) + self.assertIsNone(msg.content) + self.assertEqual(msg.attachments[0].url, attachment.url) + + def test_user_message_requires_body(self): + """User messages must include content or attachments.""" + with self.assertRaises(ValidationError): + UserMessage( + id="user_empty", + ) + def test_message_union_deserialization(self): """Test that the Message union correctly deserializes to the appropriate type""" # Create type adapter for the union @@ -153,7 +172,11 @@ def test_message_union_deserialization(self): {"id": "dev_123", "role": "developer", "content": "Developer note"}, {"id": "sys_456", "role": "system", "content": "System instruction"}, {"id": "asst_789", "role": "assistant", "content": "Assistant response"}, - {"id": "user_101", "role": "user", "content": "User query"}, + { + "id": "user_101", + "role": "user", + "attachments": [{"url": "data:application/json;base64,eyJrZXkiOiAiZGF0YSJ9"}] + }, { "id": "tool_202", "role": "tool", @@ -175,7 +198,7 @@ def test_message_union_deserialization(self): self.assertIsInstance(msg, expected_type) self.assertEqual(msg.id, data["id"]) self.assertEqual(msg.role, data["role"]) - self.assertEqual(msg.content, data["content"]) + self.assertEqual(msg.content, data.get("content")) def test_message_union_with_tool_calls(self): """Test the Message union with an assistant message containing tool calls""" @@ -203,6 +226,25 @@ def test_message_union_with_tool_calls(self): self.assertEqual(len(msg.tool_calls), 1) self.assertEqual(msg.tool_calls[0].function.name, "search_data") + def test_message_union_with_attachments(self): + """Test that attachments are preserved in the message union.""" + message_adapter = TypeAdapter(Message) + + data = { + "id": "user_attachments", + "role": "user", + "attachments": [ + { + "url": "data:image/png;base64,aW1hZ2U=", + } + ] + } + + msg = message_adapter.validate_python(data) + self.assertIsInstance(msg, UserMessage) + self.assertEqual(len(msg.attachments), 1) + self.assertEqual(str(msg.attachments[0].url), "data:image/png;base64,aW1hZ2U=") + def test_run_agent_input_deserialization(self): """Test deserializing RunAgentInput JSON with diverse message types""" # Create JSON data for RunAgentInput with diverse messages @@ -221,7 +263,10 @@ def test_run_agent_input_deserialization(self): { "id": "user_001", "role": "user", - "content": "Can you help me analyze this data?" + "content": "Can you help me analyze this data?", + "attachments": [ + {"url": "data:text/csv;base64,YSxiLGM="} + ] }, # Developer message { @@ -321,6 +366,10 @@ def test_run_agent_input_deserialization(self): # Verify specific message content self.assertEqual(run_agent_input.messages[0].content, "You are a helpful assistant.") self.assertEqual(run_agent_input.messages[1].content, "Can you help me analyze this data?") + self.assertEqual( + str(run_agent_input.messages[1].attachments[0].url), + "data:text/csv;base64,YSxiLGM=", + ) # Verify assistant message with tool call assistant_msg = run_agent_input.messages[3] diff --git a/sdks/typescript/packages/client/src/agent/__tests__/agent-mutations.test.ts b/sdks/typescript/packages/client/src/agent/__tests__/agent-mutations.test.ts index 55d50ddd1..2662ba33d 100644 --- a/sdks/typescript/packages/client/src/agent/__tests__/agent-mutations.test.ts +++ b/sdks/typescript/packages/client/src/agent/__tests__/agent-mutations.test.ts @@ -112,6 +112,33 @@ describe("Agent Mutations", () => { expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); }); + it("should add a user message with attachments", async () => { + const attachment = { + url: "data:image/png;base64,somepngbytes", + }; + + const userMessage: Message = { + id: "user-msg-attachments", + role: "user", + attachments: [attachment], + }; + + agent.addMessage(userMessage); + + expect(agent.messages.at(-1)).toBe(userMessage); + + await waitForAsyncNotifications(); + + expect(mockSubscriber.onNewMessage).toHaveBeenCalledWith({ + message: userMessage, + messages: agent.messages, + state: agent.state, + agent, + }); + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalled(); + expect((agent.messages.at(-1) as Message).attachments).toEqual([attachment]); + }); + it("should add an assistant message without tool calls", async () => { const assistantMessage: Message = { id: "assistant-msg-1", diff --git a/sdks/typescript/packages/client/src/apply/__tests__/default.attachments.test.ts b/sdks/typescript/packages/client/src/apply/__tests__/default.attachments.test.ts new file mode 100644 index 000000000..a782a7b4c --- /dev/null +++ b/sdks/typescript/packages/client/src/apply/__tests__/default.attachments.test.ts @@ -0,0 +1,107 @@ +import { Subject } from "rxjs"; +import { toArray } from "rxjs/operators"; +import { firstValueFrom } from "rxjs"; +import { + BaseEvent, + EventType, + MessagesSnapshotEvent, + RunAgentInput, + Message, + TextMessageContentEvent, +} from "@ag-ui/core"; +import { defaultApplyEvents } from "../default"; +import { AbstractAgent } from "@/agent"; + +const FAKE_AGENT = null as unknown as AbstractAgent; + +describe("defaultApplyEvents attachments", () => { + it("retains attachments from message snapshots", async () => { + const attachments = [ + { + url: "data:image/png;base64,somepngbytes", + }, + ]; + + const snapshotMessages: Message[] = [ + { + id: "msg-1", + role: "user", + attachments, + }, + ]; + + const events$ = new Subject(); + const initialInput: RunAgentInput = { + threadId: "thread", + runId: "run", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }; + + const result$ = defaultApplyEvents(initialInput, events$, FAKE_AGENT, []); + const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); + + events$.next({ + type: EventType.MESSAGES_SNAPSHOT, + messages: snapshotMessages, + } as MessagesSnapshotEvent); + + events$.complete(); + + const stateUpdates = await stateUpdatesPromise; + + expect(stateUpdates.length).toBe(1); + const message = stateUpdates[0].messages?.[0] as Message; + expect(message.attachments).toEqual(attachments); + }); + + it("keeps attachments when message content updates", async () => { + const attachments = [ + { + url: "data:text/csv;base64,YSwxLDM=", + }, + ]; + + const events$ = new Subject(); + const initialInput: RunAgentInput = { + threadId: "thread", + runId: "run", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }; + + const result$ = defaultApplyEvents(initialInput, events$, FAKE_AGENT, []); + const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); + + events$.next({ + type: EventType.MESSAGES_SNAPSHOT, + messages: [ + { + id: "msg-attachment", + role: "user", + content: "Initial", + attachments, + }, + ], + } as MessagesSnapshotEvent); + + events$.next({ + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-attachment", + delta: " update", + } as TextMessageContentEvent); + + events$.complete(); + + const stateUpdates = await stateUpdatesPromise; + const finalMessage = stateUpdates[stateUpdates.length - 1].messages?.[0] as Message; + expect(finalMessage.attachments).toEqual(attachments); + expect(finalMessage.content).toBe("Initial update"); + }); +}); diff --git a/sdks/typescript/packages/client/src/legacy/types.ts b/sdks/typescript/packages/client/src/legacy/types.ts index 1c45012dd..da592e821 100644 --- a/sdks/typescript/packages/client/src/legacy/types.ts +++ b/sdks/typescript/packages/client/src/legacy/types.ts @@ -124,7 +124,7 @@ export type LegacyRunError = z.infer; export const LegacyTextMessageSchema = z.object({ id: z.string(), role: z.string(), - content: z.string(), + content: z.string().optional(), parentMessageId: z.string().optional(), }); diff --git a/sdks/typescript/packages/client/src/transform/__tests__/proto.test.ts b/sdks/typescript/packages/client/src/transform/__tests__/proto.test.ts index 1113424c0..7bef75e18 100644 --- a/sdks/typescript/packages/client/src/transform/__tests__/proto.test.ts +++ b/sdks/typescript/packages/client/src/transform/__tests__/proto.test.ts @@ -375,6 +375,11 @@ describe("parseProtoStream", () => { id: "msg1", role: "user", content: "Hello, can you help me with something?", + attachments: [ + { + url: "data:image/png;base64,somepngbytes", + }, + ], }, { id: "msg2", @@ -432,17 +437,23 @@ describe("parseProtoStream", () => { expect(message.role).toEqual(messagesSnapshotEvent.messages[index].role); expect(message.content).toEqual(messagesSnapshotEvent.messages[index].content); + const originalMessage = messagesSnapshotEvent.messages[index] as any; + if (originalMessage.attachments) { + expect((message as any).attachments?.length).toEqual(originalMessage.attachments.length); + (message as any).attachments?.forEach((attachment: any, attachmentIndex: number) => { + expect(attachment).toMatchObject(originalMessage.attachments[attachmentIndex]); + }); + } else { + expect((message as any).attachments).toBeUndefined(); + } + // Check tool calls if present - if ((messagesSnapshotEvent.messages[index] as any).toolCalls) { + if (originalMessage.toolCalls) { expect((message as any).toolCalls).toBeDefined(); - expect((message as any).toolCalls!.length).toEqual( - (messagesSnapshotEvent.messages[index] as any).toolCalls!.length, - ); + expect((message as any).toolCalls!.length).toEqual(originalMessage.toolCalls!.length); (message as any).toolCalls!.forEach((toolCall: any, toolIndex: number) => { - const originalToolCall = (messagesSnapshotEvent.messages[index] as any).toolCalls![ - toolIndex - ]; + const originalToolCall = originalMessage.toolCalls![toolIndex]; expect(toolCall.id).toEqual(originalToolCall.id); expect(toolCall.type).toEqual(originalToolCall.type); expect(toolCall.function.name).toEqual(originalToolCall.function.name); diff --git a/sdks/typescript/packages/core/src/__tests__/user-message-attachments.test.ts b/sdks/typescript/packages/core/src/__tests__/user-message-attachments.test.ts new file mode 100644 index 000000000..cfb1de8e4 --- /dev/null +++ b/sdks/typescript/packages/core/src/__tests__/user-message-attachments.test.ts @@ -0,0 +1,95 @@ +import { UserMessageSchema, assertUserMessageHasBody, userMessageHasBody } from "../types"; +import { MessagesSnapshotEventSchema, EventType } from "../events"; + +describe("UserMessageSchema attachments", () => { + it("accepts messages with text content only", () => { + const message = UserMessageSchema.parse({ + id: "msg-1", + role: "user", + content: "Hello", + }); + + expect(message.content).toBe("Hello"); + expect(message.attachments).toBeUndefined(); + }); + + it("accepts messages with attachments only", () => { + const message = UserMessageSchema.parse({ + id: "msg-2", + role: "user", + attachments: [ + { + url: "data:image/png;base64,somepngbytes", + }, + ], + }); + + expect(message.content).toBeUndefined(); + expect(message.attachments?.length).toBe(1); + expect(message.attachments?.[0].url).toBe("https://example.com/file.pdf"); + expect(userMessageHasBody(message)).toBe(true); + }); + + it("rejects messages without content or attachments", () => { + expect(() => + UserMessageSchema.parse({ + id: "msg-3", + role: "user", + }), + ).toThrow(/must include content or at least one attachment/); + }); + + it("rejects attachments with invalid URLs", () => { + expect(() => + UserMessageSchema.parse({ + id: "msg-4", + role: "user", + attachments: [ + { + url: "not-a-valid-url", + }, + ], + }), + ).toThrow(); + }); + + it("rejects empty attachment arrays when content missing", () => { + expect(() => + UserMessageSchema.parse({ + id: "msg-5", + role: "user", + attachments: [], + }), + ).toThrow(/must include content or at least one attachment/); + }); + + it("throws via assert helper when body missing", () => { + expect(() => + assertUserMessageHasBody({ + id: "msg-6", + role: "user", + } as any), + ).toThrow(/must include content or at least one attachment/); + }); + + it("parses message snapshots containing attachments", () => { + const parsed = MessagesSnapshotEventSchema.parse({ + type: EventType.MESSAGES_SNAPSHOT, + messages: [ + { + id: "msg-7", + role: "user", + attachments: [ + { + url: "data:application/json;base64,somejsonbytes", + }, + ], + }, + ], + }); + + expect(parsed.messages[0].attachments?.[0].url).toBe( + "data:application/json;base64,somejsonbytes", + ); + }); +}); diff --git a/sdks/typescript/packages/core/src/types.ts b/sdks/typescript/packages/core/src/types.ts index 1abb31a0b..f00aacb33 100644 --- a/sdks/typescript/packages/core/src/types.ts +++ b/sdks/typescript/packages/core/src/types.ts @@ -1,5 +1,13 @@ import { z } from "zod"; +export const FileAttachmentSchema = z.object({ + url: z + .string() + .refine((value) => value.startsWith("data:"), { + message: "Attachment url must be a data URL (data:;base64,...)", + }), +}); + export const FunctionCallSchema = z.object({ name: z.string(), arguments: z.string(), @@ -34,9 +42,23 @@ export const AssistantMessageSchema = BaseMessageSchema.extend({ toolCalls: z.array(ToolCallSchema).optional(), }); -export const UserMessageSchema = BaseMessageSchema.extend({ +const RawUserMessageSchema = BaseMessageSchema.extend({ role: z.literal("user"), - content: z.string(), + content: z.string().optional(), + attachments: z.array(FileAttachmentSchema).optional(), +}); + +export const UserMessageSchema = RawUserMessageSchema.superRefine((value, ctx) => { + const hasContent = typeof value.content === "string" && value.content.trim().length > 0; + const attachments = value.attachments ?? []; + + if (!hasContent && attachments.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "User messages must include content or at least one attachment.", + path: ["attachments"], + }); + } }); export const ToolMessageSchema = z.object({ @@ -47,13 +69,24 @@ export const ToolMessageSchema = z.object({ error: z.string().optional(), }); -export const MessageSchema = z.discriminatedUnion("role", [ - DeveloperMessageSchema, - SystemMessageSchema, - AssistantMessageSchema, - UserMessageSchema, - ToolMessageSchema, -]); +export const MessageSchema = z + .discriminatedUnion("role", [ + DeveloperMessageSchema, + SystemMessageSchema, + AssistantMessageSchema, + RawUserMessageSchema, + ToolMessageSchema, + ]) + .superRefine((value, ctx) => { + if (value.role === "user") { + const parsed = UserMessageSchema.safeParse(value); + if (!parsed.success) { + for (const issue of parsed.error.issues) { + ctx.addIssue(issue); + } + } + } + }); export const RoleSchema = z.union([ z.literal("developer"), @@ -99,9 +132,23 @@ export type Tool = z.infer; export type RunAgentInput = z.infer; export type State = z.infer; export type Role = z.infer; +export type FileAttachment = z.infer; export class AGUIError extends Error { constructor(message: string) { super(message); } } + +export const userMessageHasBody = (message: UserMessage): boolean => { + const content = message.content ?? ""; + const hasContent = content.trim().length > 0; + const hasAttachments = (message.attachments?.length ?? 0) > 0; + return hasContent || hasAttachments; +}; + +export const assertUserMessageHasBody = (message: UserMessage): void => { + if (!userMessageHasBody(message)) { + throw new AGUIError("User messages must include content or at least one attachment."); + } +}; diff --git a/sdks/typescript/packages/proto/__tests__/proto.test.ts b/sdks/typescript/packages/proto/__tests__/proto.test.ts index ac3e44bd1..7556baee4 100644 --- a/sdks/typescript/packages/proto/__tests__/proto.test.ts +++ b/sdks/typescript/packages/proto/__tests__/proto.test.ts @@ -90,6 +90,11 @@ describe("Proto", () => { id: "msg-1", role: "user", content: "Hello, can you help me with something?", + attachments: [ + { + url: "data:image/png;base64,somepngbytes", + }, + ], }, { id: "msg-2", @@ -137,6 +142,10 @@ describe("Proto", () => { expect(decoded.messages[0].id).toBe(originalEvent.messages[0].id); expect(decoded.messages[0].role).toBe(originalEvent.messages[0].role); expect(decoded.messages[0].content).toBe(originalEvent.messages[0].content); + expect((decoded.messages[0] as any).attachments?.length).toBe(1); + expect((decoded.messages[0] as any).attachments?.[0].url).toBe( + (originalEvent.messages[0] as any).attachments?.[0].url, + ); // Verify second message (assistant with tool calls) expect(decoded.messages[1].id).toBe(originalEvent.messages[1].id); diff --git a/sdks/typescript/packages/proto/src/proto.ts b/sdks/typescript/packages/proto/src/proto.ts index fe3ed288c..9a640b4fd 100644 --- a/sdks/typescript/packages/proto/src/proto.ts +++ b/sdks/typescript/packages/proto/src/proto.ts @@ -17,10 +17,18 @@ export function encode(event: BaseEvent): Uint8Array { if (type === EventType.MESSAGES_SNAPSHOT) { rest.messages = rest.messages.map((message: Message) => { const untypedMessage = message as any; - if (untypedMessage.toolCalls === undefined) { - return { ...message, toolCalls: [] }; + const toolCalls = untypedMessage.toolCalls ?? []; + const attachments = untypedMessage.attachments ?? []; + + if (toolCalls === untypedMessage.toolCalls && attachments === untypedMessage.attachments) { + return message; } - return message; + + return { + ...message, + toolCalls, + attachments, + }; }); } @@ -50,8 +58,8 @@ export function encode(event: BaseEvent): Uint8Array { * The format includes a 4-byte length prefix followed by the message. */ export function decode(data: Uint8Array): BaseEvent { - const event = protoEvents.Event.decode(data); - const decoded = Object.values(event).find((value) => value !== undefined); + const event = protoEvents.Event.decode(data, data.length); + const decoded = Object.values(event).find((value) => value !== undefined) as any; if (!decoded) { throw new Error("Invalid event"); } @@ -66,6 +74,9 @@ export function decode(data: Uint8Array): BaseEvent { if (untypedMessage.toolCalls?.length === 0) { untypedMessage.toolCalls = undefined; } + if (untypedMessage.attachments?.length === 0) { + untypedMessage.attachments = undefined; + } } } diff --git a/sdks/typescript/packages/proto/src/proto/types.proto b/sdks/typescript/packages/proto/src/proto/types.proto index 880117d97..a50ae5a62 100644 --- a/sdks/typescript/packages/proto/src/proto/types.proto +++ b/sdks/typescript/packages/proto/src/proto/types.proto @@ -12,6 +12,10 @@ message ToolCall { Function function = 3; } +message Attachment { + string url = 1; +} + message Message { string id = 1; string role = 2; @@ -20,4 +24,5 @@ message Message { repeated ToolCall tool_calls = 5; optional string tool_call_id = 6; optional string error = 7; + repeated Attachment attachments = 8; }