From ae36f1d143ade3fc2492015b0ef4d6864c12ac18 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Mon, 12 Oct 2026 17:15:54 +0300 Subject: [PATCH 1/3] chore(platform): sync asyncapi.json from production asyncapi.json is the document production serves now that base44-dev/apper#29607 has deployed, and gen:events regenerated the types: TaskProgressed.event_type is the five task values, and current/total are integers. sync-asyncapi.mjs documents production as its default source, and PlatformSnapshot's JSDoc links to app.snapshot on Socket messages. Co-Authored-By: Claude Opus 5.5 --- packages/platform/asyncapi.json | 1049 +++++++++++++---- packages/platform/scripts/sync-asyncapi.mjs | 4 +- .../src/modules/builder.events.generated.ts | 822 ++++++------- .../src/modules/builder.events.types.ts | 3 + 4 files changed, 1244 insertions(+), 634 deletions(-) diff --git a/packages/platform/asyncapi.json b/packages/platform/asyncapi.json index 71721d8..c8b85f7 100644 --- a/packages/platform/asyncapi.json +++ b/packages/platform/asyncapi.json @@ -3,7 +3,7 @@ "info": { "title": "Base44 Live Builder Updates", "version": "1.0.0", - "description": "Live updates of the builder chat for the apps a white-label session may watch. The connection is Socket.IO, not a raw WebSocket: connect with `io(socket_url, { path: socket_path, transports: [\"websocket\"], auth: { session_token } })`, using the values your server received from `POST /api/service/socket-sessions`. Then send `join` for each app, and receive its snapshot followed by live events." + "description": "Live updates of the builder chat for the apps a white-label session may watch. The connection is Socket.IO, not a raw WebSocket: connect with `io(socket_url, { path: socket_path, transports: [\"websocket\"], auth: { session_token } })`, using the values your server received from `POST /api/service/socket-sessions`. Then send `join` for each app, and receive its snapshot followed by live events.\n\nAvailable to white-label workspaces with live builder updates turned on. Your onboarding contact turns them on for your workspace." }, "servers": { "platform": { @@ -16,7 +16,7 @@ "channels": { "platform": { "title": "Platform socket", - "description": "One connection per session. Every event carries the `room` it belongs to, `/apps/{app_id}`; `room` is null for `session.ended`. Omitted keys in `data` are unchanged; explicit null clears.", + "description": "One connection per session carries the events of every app it joins. In an event's `data`, a missing key is unchanged and an explicit `null` clears it. `app.snapshot` is the exception, since it replaces the app's state.", "messages": { "MessageUpdatedEvent": { "$ref": "#/components/messages/MessageUpdatedEvent" @@ -260,7 +260,7 @@ "$ref": "#/channels/platform/messages/RoomAccessDeniedEvent" } ], - "summary": "A join was refused: the app is not on the session's allowlist, or the socket joins too often." + "summary": "A join was refused because the app is not on the session's allowlist, or the socket joins too often." }, "sendRoomAccessRevokedEvent": { "action": "send", @@ -272,7 +272,7 @@ "$ref": "#/channels/platform/messages/RoomAccessRevokedEvent" } ], - "summary": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin." + "summary": "The app left the session's allowlist, or its workspace. Events for it stop. Do not rejoin." }, "sendRoomSnapshotUnavailableEvent": { "action": "send", @@ -284,7 +284,7 @@ "$ref": "#/channels/platform/messages/RoomSnapshotUnavailableEvent" } ], - "summary": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot." + "summary": "The join held but its snapshot failed. Live events still arrive. Rejoin later for a snapshot." }, "sendAppSnapshotEvent": { "action": "send", @@ -329,181 +329,479 @@ "name": "message.updated", "title": "message.updated", "summary": "A chat message was added or replaced. Replace the message with the same `id`.", - "description": "A chat message was added or replaced. Replace the message with the same `id`.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nA chat message was added or replaced. Replace the message with the same `id`.", "payload": { "$ref": "#/components/schemas/MessageUpdatedEvent" - } + }, + "examples": [ + { + "name": "message.updated", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "message": { + "id": "c4f2a8e1-6b3d-4f9a-8e2c-1d7b5a9f3e60", + "role": "assistant", + "content": "I added a sign-in page with email and Google.", + "tool_calls": [ + { + "id": "toolu_01HXq8", + "name": "write_file", + "requires_user_input": false, + "auto_approved": false, + "status": "success", + "mutation_applied": null, + "display": { + "file_paths": [ + "src/pages/SignIn.jsx" + ] + } + } + ], + "checkpoint_id": "68f1c2d4e7b91d003c45a1f9", + "metadata": { + "created_date": "2026-10-10T09:23:41" + } + }, + "conversation_id": "68f1c2d4e7b91d003c45a1f2", + "branch_id": null + } + } + } + ] }, "MessageRemovedEvent": { "name": "message.removed", "title": "message.removed", "summary": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op.", - "description": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nA chat message was removed or hidden. An `id` the viewer never saw is a no-op.", "payload": { "$ref": "#/components/schemas/MessageRemovedEvent" - } + }, + "examples": [ + { + "name": "message.removed", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "message_id": "c4f2a8e1-6b3d-4f9a-8e2c-1d7b5a9f3e60", + "conversation_id": "68f1c2d4e7b91d003c45a1f2", + "branch_id": null + } + } + } + ] }, "AppStatusChangedEvent": { "name": "app.status_changed", "title": "app.status_changed", "summary": "The builder started, finished or failed a turn. Null status clears it.", - "description": "The builder started, finished or failed a turn. Null status clears it.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe builder started, finished or failed a turn. Null status clears it.", "payload": { "$ref": "#/components/schemas/AppStatusChangedEvent" - } + }, + "examples": [ + { + "name": "app.status_changed", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "status": { + "state": "processing", + "turn_id": "c4f2a8e1-6b3d-4f9a-8e2c-1d7b5a9f3e60", + "last_updated_date": "2026-10-10T09:23:41Z" + }, + "branch_id": null + } + } + } + ] }, "PreviewReloadRequestedEvent": { "name": "preview.reload_requested", "title": "preview.reload_requested", "summary": "Reload the app preview.", - "description": "Reload the app preview.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nReload the app preview.", "payload": { "$ref": "#/components/schemas/PreviewReloadRequestedEvent" - } + }, + "examples": [ + { + "name": "preview.reload_requested", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "branch_id": null + } + } + } + ] }, "PreviewNavigationRequestedEvent": { "name": "preview.navigation_requested", "title": "preview.navigation_requested", "summary": "Show this page of the app in the preview.", - "description": "Show this page of the app in the preview.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nShow this page of the app in the preview.", "payload": { "$ref": "#/components/schemas/PreviewNavigationRequestedEvent" - } + }, + "examples": [ + { + "name": "preview.navigation_requested", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "path": "/sign-in", + "force": false, + "branch_id": null + } + } + } + ] }, "QueueUpdatedEvent": { "name": "queue.updated", "title": "queue.updated", "summary": "The whole prompt queue, after any change.", - "description": "The whole prompt queue, after any change.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe whole prompt queue, after any change.", "payload": { "$ref": "#/components/schemas/QueueUpdatedEvent" - } + }, + "examples": [ + { + "name": "queue.updated", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "items": [ + { + "id": "q_7d1e9c", + "content": "Add a dark mode toggle to the header.", + "branch_id": null, + "created_at": "2026-10-10T09:24:05+00:00" + } + ], + "is_paused": false, + "branch_id": null + } + } + } + ] }, "TaskProgressedEvent": { "name": "task.progressed", "title": "task.progressed", "summary": "Progress of a long-running tool, by `tool_call_id`.", - "description": "Progress of a long-running tool, by `tool_call_id`.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nProgress of a long-running tool, by `tool_call_id`.", "payload": { "$ref": "#/components/schemas/TaskProgressedEvent" - } + }, + "examples": [ + { + "name": "task.progressed", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "tool_call_id": "toolu_01HXq8", + "message_id": "c4f2a8e1-6b3d-4f9a-8e2c-1d7b5a9f3e60", + "event_type": "task_progress", + "progress": { + "current": 250, + "total": 1000, + "percentage": 25 + }, + "branch_id": null + } + } + } + ] }, "ImageResolvedEvent": { "name": "image.resolved", "title": "image.resolved", "summary": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears.", - "description": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nA generated image finished or failed. Replace its `placeholder_url` wherever it appears.", "payload": { "$ref": "#/components/schemas/ImageResolvedEvent" - } + }, + "examples": [ + { + "name": "image.resolved", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "status": "completed", + "placeholder_url": "/__generating__/5e8b2f4a-1c3d-4e6f-9a7b-2d4c6e8f0a1b.png", + "image_url": "https://media.base44.com/images/public/6a2024af837c7a06c83021c1/hero.png" + } + } + } + ] }, "ConversationChangedEvent": { "name": "conversation.changed", "title": "conversation.changed", "summary": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot.", - "description": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot.", "payload": { "$ref": "#/components/schemas/ConversationChangedEvent" - } + }, + "examples": [ + { + "name": "conversation.changed", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "branch_id": null + } + } + } + ] }, "FilesChangedEvent": { "name": "files.changed", "title": "files.changed", "summary": "The app's files changed outside a chat turn.", - "description": "The app's files changed outside a chat turn.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe app's files changed outside a chat turn.", "payload": { "$ref": "#/components/schemas/FilesChangedEvent" - } + }, + "examples": [ + { + "name": "files.changed", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "branch_id": null + } + } + } + ] }, "BranchDeletedEvent": { "name": "branch.deleted", "title": "branch.deleted", "summary": "A branch was deleted.", - "description": "A branch was deleted.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nA branch was deleted.", "payload": { "$ref": "#/components/schemas/BranchDeletedEvent" - } + }, + "examples": [ + { + "name": "branch.deleted", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "branch_id": "68f1c2d4e7b91d003c45a1fa" + } + } + } + ] }, "RepositoryChangedEvent": { "name": "repository.changed", "title": "repository.changed", "summary": "The imported app's repository changed.", - "description": "The imported app's repository changed.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe imported app's repository changed.", "payload": { "$ref": "#/components/schemas/RepositoryChangedEvent" - } + }, + "examples": [ + { + "name": "repository.changed", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "branch_id": null + } + } + } + ] }, "PullRequestChangedEvent": { "name": "pull_request.changed", "title": "pull_request.changed", "summary": "The imported app's pull request changed.", - "description": "The imported app's pull request changed.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe imported app's pull request changed.", "payload": { "$ref": "#/components/schemas/PullRequestChangedEvent" - } + }, + "examples": [ + { + "name": "pull_request.changed", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "branch_id": "68f1c2d4e7b91d003c45a1fa" + } + } + } + ] }, "SessionEndedEvent": { "name": "session.ended", "title": "session.ended", "summary": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next.", - "description": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe session ended (expired, revoked, or taken by a newer socket). The socket disconnects next.", "payload": { "$ref": "#/components/schemas/SessionEndedEvent" - } + }, + "examples": [ + { + "name": "session.ended", + "payload": { + "room": null, + "data": { + "reason": "revoked" + } + } + } + ] }, "RoomAccessDeniedEvent": { "name": "room.access_denied", "title": "room.access_denied", - "summary": "A join was refused: the app is not on the session's allowlist, or the socket joins too often.", - "description": "A join was refused: the app is not on the session's allowlist, or the socket joins too often.", + "summary": "A join was refused because the app is not on the session's allowlist, or the socket joins too often.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nA join was refused because the app is not on the session's allowlist, or the socket joins too often.", "payload": { "$ref": "#/components/schemas/RoomAccessDeniedEvent" - } + }, + "examples": [ + { + "name": "room.access_denied", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": {} + } + } + ] }, "RoomAccessRevokedEvent": { "name": "room.access_revoked", "title": "room.access_revoked", - "summary": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin.", - "description": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin.", + "summary": "The app left the session's allowlist, or its workspace. Events for it stop. Do not rejoin.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe app left the session's allowlist, or its workspace. Events for it stop. Do not rejoin.", "payload": { "$ref": "#/components/schemas/RoomAccessRevokedEvent" - } + }, + "examples": [ + { + "name": "room.access_revoked", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": {} + } + } + ] }, "RoomSnapshotUnavailableEvent": { "name": "room.snapshot_unavailable", "title": "room.snapshot_unavailable", - "summary": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot.", - "description": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot.", + "summary": "The join held but its snapshot failed. Live events still arrive. Rejoin later for a snapshot.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe join held but its snapshot failed. Live events still arrive. Rejoin later for a snapshot.", "payload": { "$ref": "#/components/schemas/RoomSnapshotUnavailableEvent" - } + }, + "examples": [ + { + "name": "room.snapshot_unavailable", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": {} + } + } + ] }, "AppSnapshotEvent": { "name": "app.snapshot", "title": "app.snapshot", "summary": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it.", - "description": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nThe app's current status and its last 50 public messages, after every join. Replace the app's state with it.", "payload": { "$ref": "#/components/schemas/AppSnapshotEvent" - } + }, + "examples": [ + { + "name": "app.snapshot", + "payload": { + "room": "/apps/6a2024af837c7a06c83021c1", + "data": { + "status": { + "state": "ready", + "turn_id": "c4f2a8e1-6b3d-4f9a-8e2c-1d7b5a9f3e60", + "last_updated_date": "2026-10-10T09:23:41Z" + }, + "messages": [ + { + "id": "c4f2a8e1-6b3d-4f9a-8e2c-1d7b5a9f3e60", + "role": "assistant", + "content": "I added a sign-in page with email and Google.", + "tool_calls": [ + { + "id": "toolu_01HXq8", + "name": "write_file", + "requires_user_input": false, + "auto_approved": false, + "status": "success", + "mutation_applied": null, + "display": { + "file_paths": [ + "src/pages/SignIn.jsx" + ] + } + } + ], + "checkpoint_id": "68f1c2d4e7b91d003c45a1f9", + "metadata": { + "created_date": "2026-10-10T09:23:41" + } + } + ], + "queue": { + "items": [ + { + "id": "q_7d1e9c", + "content": "Add a dark mode toggle to the header.", + "branch_id": null, + "created_at": "2026-10-10T09:24:05+00:00" + } + ], + "is_paused": false + } + } + } + } + ] }, "JoinMessage": { "name": "join", "title": "join", "summary": "Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute.", - "description": "Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nJoin an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute.", "payload": { "$ref": "#/components/schemas/JoinMessage" - } + }, + "examples": [ + { + "name": "join", + "payload": "/apps/6a2024af837c7a06c83021c1" + } + ] }, "LeaveMessage": { "name": "leave", "title": "leave", "summary": "Leave an app's room. Its events stop.", - "description": "Leave an app's room. Its events stop.", + "description": "This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.\n\nLeave an app's room. Its events stop.", "payload": { "$ref": "#/components/schemas/LeaveMessage" - } + }, + "examples": [ + { + "name": "leave", + "payload": "/apps/6a2024af837c7a06c83021c1" + } + ] } }, "schemas": { @@ -540,10 +838,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/Snapshot" + "$ref": "#/components/schemas/Snapshot", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -554,6 +854,7 @@ "type": "object" }, "AppStatusChanged": { + "description": "The app's build status changed, because a turn started, finished or failed.", "properties": { "status": { "anyOf": [ @@ -563,7 +864,8 @@ { "type": "null" } - ] + ], + "description": "The new status, or `null` when the status was cleared." }, "branch_id": { "type": [ @@ -571,6 +873,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -588,10 +891,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/AppStatusChanged" + "$ref": "#/components/schemas/AppStatusChanged", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -602,10 +907,11 @@ "type": "object" }, "AskClarifyingQuestionsCall": { - "description": "A `ask_clarifying_questions` call.", + "description": "The AI asks the user questions before it goes on. The call waits until the user answers.", "properties": { "name": { "const": "ask_clarifying_questions", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -615,6 +921,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -623,6 +930,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -631,6 +939,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -650,6 +959,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -658,6 +968,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -670,7 +981,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -711,7 +1022,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -723,7 +1034,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." }, "user_input": { "anyOf": [ @@ -734,7 +1046,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "The user's answer to the call, once given, for tools that take one." } }, "required": [ @@ -744,10 +1057,11 @@ "type": "object" }, "AskPlanQuestionsCall": { - "description": "A `ask_plan_questions` call.", + "description": "The AI asks the user questions that shape the plan, in plan mode. The call waits until the user answers.", "properties": { "name": { "const": "ask_plan_questions", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -757,6 +1071,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -765,6 +1080,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -773,6 +1089,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -792,6 +1109,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -800,6 +1118,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -812,7 +1131,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -853,7 +1172,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -865,7 +1184,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." }, "user_input": { "anyOf": [ @@ -876,7 +1196,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "The user's answer to the call, once given, for tools that take one." } }, "required": [ @@ -886,10 +1207,11 @@ "type": "object" }, "BackendFunctionEntityDeleteApproval": { - "description": "Parked by the `backend_function_entity_delete` guard.", + "description": "Waiting for approval: testing a backend function would delete records from an entity.", "properties": { "guard": { "const": "backend_function_entity_delete", + "description": "The check that paused the call.", "title": "Guard", "type": "string" }, @@ -899,7 +1221,7 @@ "null" ], "default": null, - "description": "Why the guard parked the call.", + "description": "Why the call needs approval, as the check explained it.", "title": "Reason" }, "details": { @@ -911,7 +1233,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the check found." } }, "required": [ @@ -921,10 +1244,11 @@ "type": "object" }, "BashApproval": { - "description": "Parked by the `bash_approval` guard.", + "description": "Waiting for approval: the AI wants to run a shell command that needs a review first. The command itself is not published.", "properties": { "guard": { "const": "bash_approval", + "description": "The check that paused the call.", "title": "Guard", "type": "string" } @@ -936,10 +1260,11 @@ "type": "object" }, "BashCall": { - "description": "A `bash` call.", + "description": "The AI runs a shell command in the app's sandbox.", "properties": { "name": { "const": "bash", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -949,6 +1274,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -957,6 +1283,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -965,6 +1292,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -984,6 +1312,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -992,6 +1321,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -1004,7 +1334,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -1045,7 +1375,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -1057,7 +1387,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -1067,7 +1398,7 @@ "type": "object" }, "BranchChange": { - "description": "Re-read what changed; the event carries no content.", + "description": "Something changed outside the chat. Re-read it, because the event carries no content.", "properties": { "branch_id": { "type": [ @@ -1075,6 +1406,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -1089,10 +1421,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/BranchChange" + "$ref": "#/components/schemas/BranchChange", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -1110,10 +1444,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/BranchChange" + "$ref": "#/components/schemas/BranchChange", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -1124,10 +1460,11 @@ "type": "object" }, "CreateEntityRecordsCall": { - "description": "A `create_entity_records` call.", + "description": "The AI adds records to one of the app's entities.", "properties": { "name": { "const": "create_entity_records", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -1137,6 +1474,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -1145,6 +1483,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -1153,6 +1492,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -1172,6 +1512,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -1180,6 +1521,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -1192,7 +1534,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -1233,7 +1575,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -1245,7 +1587,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -1255,10 +1598,11 @@ "type": "object" }, "DeleteEntitiesCall": { - "description": "A `delete_entities` call.", + "description": "The AI deletes records from one of the app's entities.", "properties": { "name": { "const": "delete_entities", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -1268,6 +1612,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -1276,6 +1621,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -1284,6 +1630,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -1303,6 +1650,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -1311,6 +1659,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -1323,7 +1672,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -1364,7 +1713,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -1376,7 +1725,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." }, "display": { "anyOf": [ @@ -1387,7 +1737,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -1397,10 +1748,11 @@ "type": "object" }, "DeleteFileCall": { - "description": "A `delete_file` call.", + "description": "The AI deletes a file from the app.", "properties": { "name": { "const": "delete_file", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -1410,6 +1762,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -1418,6 +1771,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -1426,6 +1780,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -1445,6 +1800,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -1453,6 +1809,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -1465,7 +1822,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -1506,7 +1863,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -1518,7 +1875,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -1528,10 +1886,11 @@ "type": "object" }, "EditRepoFileCall": { - "description": "A `edit_repo_file` call.", + "description": "The AI edits a file in the imported repository.", "properties": { "name": { "const": "edit_repo_file", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -1541,6 +1900,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -1549,6 +1909,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -1557,6 +1918,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -1576,6 +1938,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -1584,6 +1947,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -1596,7 +1960,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -1637,7 +2001,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -1649,7 +2013,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -1659,7 +2024,7 @@ "type": "object" }, "EntityChangeArguments": { - "description": "An entity update or delete; the entity is in `display.entity_name`.", + "description": "An entity update or delete. The entity is in `display.entity_name`.", "properties": { "summary": { "description": "The agent's summary of the change. Queries and record values are never included.", @@ -1683,7 +2048,7 @@ "type": "object" }, "EntityMutationDetails": { - "description": "What a parked entity mutation would do. Record data is never included.", + "description": "What the entity change waiting for approval would do. Record data is never included.", "properties": { "method": { "description": "Entity method the code calls, such as `delete`.", @@ -1700,6 +2065,7 @@ "type": "object" }, "EntityRecordsDisplay": { + "description": "The entity an operation added records to.", "properties": { "entity_name": { "description": "Entity type. Records and query values are never included.", @@ -1716,10 +2082,11 @@ "type": "object" }, "EntityRlsGuardApproval": { - "description": "Parked by the `entity_rls_guard` guard.", + "description": "Waiting for approval: the AI wants to change who can read or write an entity's records.", "properties": { "guard": { "const": "entity_rls_guard", + "description": "The check that paused the call.", "title": "Guard", "type": "string" }, @@ -1729,7 +2096,7 @@ "null" ], "default": null, - "description": "Why the guard parked the call.", + "description": "Why the call needs approval, as the check explained it.", "title": "Reason" }, "details": { @@ -1741,7 +2108,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the check found." } }, "required": [ @@ -1759,7 +2127,7 @@ "type": "string" }, "writes_entities": { - "description": "The code changed entity data: refresh the preview.", + "description": "The code changed entity data, so refresh the preview.", "title": "Writes Entities", "type": "boolean" } @@ -1768,10 +2136,11 @@ "type": "object" }, "ExecToolCall": { - "description": "A `exec_tool` call.", + "description": "The AI runs code against the app's data and integrations.", "properties": { "name": { "const": "exec_tool", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -1781,6 +2150,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -1789,6 +2159,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -1797,6 +2168,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -1816,6 +2188,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -1824,6 +2197,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -1836,7 +2210,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -1877,7 +2251,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -1889,7 +2263,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -1899,10 +2274,11 @@ "type": "object" }, "ExecToolEntityMutationApproval": { - "description": "Parked by the `exec_tool_entity_mutation` guard.", + "description": "Waiting for approval: the AI's code would update or delete records in an entity.", "properties": { "guard": { "const": "exec_tool_entity_mutation", + "description": "The check that paused the call.", "title": "Guard", "type": "string" }, @@ -1912,7 +2288,7 @@ "null" ], "default": null, - "description": "Why the guard parked the call.", + "description": "Why the call needs approval, as the check explained it.", "title": "Reason" }, "details": { @@ -1924,7 +2300,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the check found." } }, "required": [ @@ -1934,10 +2311,11 @@ "type": "object" }, "ExecToolGithubRepoWriteApproval": { - "description": "Parked by the `exec_tool_github_repo_write` guard.", + "description": "Waiting for approval: the AI's code would write to a GitHub repository.", "properties": { "guard": { "const": "exec_tool_github_repo_write", + "description": "The check that paused the call.", "title": "Guard", "type": "string" }, @@ -1947,7 +2325,7 @@ "null" ], "default": null, - "description": "Why the guard parked the call.", + "description": "Why the call needs approval, as the check explained it.", "title": "Reason" }, "details": { @@ -1959,7 +2337,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the check found." } }, "required": [ @@ -1969,10 +2348,11 @@ "type": "object" }, "ExecToolInviteUserApproval": { - "description": "Parked by the `exec_tool_invite_user` guard.", + "description": "Waiting for approval: the AI's code would invite someone to the app by email.", "properties": { "guard": { "const": "exec_tool_invite_user", + "description": "The check that paused the call.", "title": "Guard", "type": "string" }, @@ -1982,7 +2362,7 @@ "null" ], "default": null, - "description": "Why the guard parked the call.", + "description": "Why the call needs approval, as the check explained it.", "title": "Reason" }, "details": { @@ -1994,7 +2374,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the check found." } }, "required": [ @@ -2004,10 +2385,11 @@ "type": "object" }, "ExecToolSendEmailApproval": { - "description": "Parked by the `exec_tool_send_email` guard.", + "description": "Waiting for approval: the AI's code would send an email.", "properties": { "guard": { "const": "exec_tool_send_email", + "description": "The check that paused the call.", "title": "Guard", "type": "string" }, @@ -2017,7 +2399,7 @@ "null" ], "default": null, - "description": "Why the guard parked the call.", + "description": "Why the call needs approval, as the check explained it.", "title": "Reason" }, "details": { @@ -2029,7 +2411,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the check found." } }, "required": [ @@ -2039,10 +2422,11 @@ "type": "object" }, "ExecToolSendSmsApproval": { - "description": "Parked by the `exec_tool_send_sms` guard.", + "description": "Waiting for approval: the AI's code would send a text message.", "properties": { "guard": { "const": "exec_tool_send_sms", + "description": "The check that paused the call.", "title": "Guard", "type": "string" }, @@ -2052,7 +2436,7 @@ "null" ], "default": null, - "description": "Why the guard parked the call.", + "description": "Why the call needs approval, as the check explained it.", "title": "Reason" }, "details": { @@ -2064,7 +2448,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the check found." } }, "required": [ @@ -2101,10 +2486,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/BranchChange" + "$ref": "#/components/schemas/BranchChange", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -2115,10 +2502,11 @@ "type": "object" }, "FindReplaceCall": { - "description": "A `find_replace` call.", + "description": "The AI edits files by replacing text in them.", "properties": { "name": { "const": "find_replace", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -2128,6 +2516,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -2136,6 +2525,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -2144,6 +2534,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -2163,6 +2554,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -2171,6 +2563,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -2183,7 +2576,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -2224,7 +2617,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -2236,7 +2629,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -2259,7 +2653,7 @@ "type": "string" }, "inspection_failed": { - "description": "The guard could not verify the function's source.", + "description": "The check could not verify the function's source.", "title": "Inspection Failed", "type": "boolean" } @@ -2280,10 +2674,11 @@ "type": "object" }, "GenerateGameBackgroundCall": { - "description": "A `generate_game_background` call.", + "description": "The AI generates a background image for a game. An `image.resolved` event follows when it is ready.", "properties": { "name": { "const": "generate_game_background", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -2293,6 +2688,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -2301,6 +2697,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -2309,6 +2706,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -2328,6 +2726,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -2336,6 +2735,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -2348,7 +2748,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -2389,7 +2789,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "results": { @@ -2398,6 +2798,7 @@ "null" ], "default": null, + "description": "The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval.", "title": "Results" } }, @@ -2408,10 +2809,11 @@ "type": "object" }, "GenerateGameImageCall": { - "description": "A `generate_game_image` call.", + "description": "The AI generates an image asset for a game. An `image.resolved` event follows when it is ready.", "properties": { "name": { "const": "generate_game_image", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -2421,6 +2823,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -2429,6 +2832,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -2437,6 +2841,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -2456,6 +2861,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -2464,6 +2870,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -2476,7 +2883,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -2517,7 +2924,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -2529,7 +2936,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." }, "results": { "type": [ @@ -2537,6 +2945,7 @@ "null" ], "default": null, + "description": "The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval.", "title": "Results" } }, @@ -2547,10 +2956,11 @@ "type": "object" }, "GenerateImageCall": { - "description": "A `generate_image` call.", + "description": "The AI generates an image for the app. An `image.resolved` event follows when it is ready.", "properties": { "name": { "const": "generate_image", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -2560,6 +2970,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -2568,6 +2979,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -2576,6 +2988,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -2595,6 +3008,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -2603,6 +3017,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -2615,7 +3030,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -2656,7 +3071,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -2668,7 +3083,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." }, "results": { "anyOf": [ @@ -2679,7 +3095,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval." } }, "required": [ @@ -2689,10 +3106,11 @@ "type": "object" }, "GeneratePrdCall": { - "description": "A `generate_prd` call.", + "description": "The AI writes down the plan it settled with the user. In plan mode this call carries the plan itself.", "properties": { "name": { "const": "generate_prd", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -2702,6 +3120,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -2710,6 +3129,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -2718,6 +3138,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -2737,6 +3158,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -2745,6 +3167,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -2757,7 +3180,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -2798,7 +3221,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -2810,7 +3233,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." } }, "required": [ @@ -2820,10 +3244,11 @@ "type": "object" }, "GenerateVideoCall": { - "description": "A `generate_video` call.", + "description": "The AI generates a video for the app.", "properties": { "name": { "const": "generate_video", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -2833,6 +3258,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -2841,6 +3267,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -2849,6 +3276,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -2868,6 +3296,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -2876,6 +3305,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -2888,7 +3318,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -2929,7 +3359,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -2941,7 +3371,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." }, "results": { "type": [ @@ -2949,6 +3380,7 @@ "null" ], "default": null, + "description": "The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval.", "title": "Results" } }, @@ -2959,6 +3391,7 @@ "type": "object" }, "ImageResolved": { + "description": "A generated image finished or failed. Replace `placeholder_url` with `image_url` wherever the app shows it.", "properties": { "status": { "anyOf": [ @@ -2975,6 +3408,7 @@ } ], "default": null, + "description": "`pending` while the image generates, `completed` once `image_url` is ready, `failed` when generation failed.", "title": "Status" }, "placeholder_url": { @@ -2983,6 +3417,7 @@ "null" ], "default": null, + "description": "Placeholder URL the app shows while the image generates. Match it to find where the image appears.", "title": "Placeholder Url" }, "image_url": { @@ -2991,6 +3426,7 @@ "null" ], "default": null, + "description": "URL of the finished image, or `null` while it is pending or after it failed.", "title": "Image Url" } }, @@ -3005,10 +3441,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/ImageResolved" + "$ref": "#/components/schemas/ImageResolved", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -3049,10 +3487,11 @@ "type": "object" }, "InstallNpmPackageCall": { - "description": "A `install_npm_package` call.", + "description": "The AI installs or removes npm packages in the app.", "properties": { "name": { "const": "install_npm_package", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -3062,6 +3501,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -3070,6 +3510,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -3078,6 +3519,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -3097,6 +3539,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -3105,6 +3548,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -3117,7 +3561,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -3158,7 +3602,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -3170,7 +3614,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." } }, "required": [ @@ -3232,6 +3677,7 @@ "null" ], "default": null, + "description": "ID of the message. An update carries the whole message again under the same ID.", "title": "Id" }, "role": { @@ -3249,6 +3695,7 @@ } ], "default": null, + "description": "`user` for a message a person sent, `assistant` for the AI's reply. `system` messages are never sent.", "title": "Role" }, "content": { @@ -3257,6 +3704,7 @@ "null" ], "default": null, + "description": "Text of the message. Attachments and structured content are left out.", "title": "Content" }, "tool_calls": { @@ -3347,6 +3795,7 @@ "null" ], "default": null, + "description": "The tool calls the AI made in this message, in order. Each is updated in place as it progresses.", "title": "Tool Calls" }, "checkpoint_id": { @@ -3355,6 +3804,7 @@ "null" ], "default": null, + "description": "ID of the checkpoint tied to the message. On a message a person sent it holds the app before the turn, on the AI's final reply the app after it. Absent when the turn made no checkpoint.", "title": "Checkpoint Id" }, "metadata": { @@ -3366,7 +3816,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "When the message was created." }, "additional_message_params": { "anyOf": [ @@ -3378,6 +3829,7 @@ } ], "default": null, + "description": "Flags on the message, such as plan mode. Only the flags listed here are published.", "title": "Additional Message Params" } }, @@ -3394,6 +3846,7 @@ "null" ], "default": null, + "description": "Time the message was created, as a UTC timestamp in ISO 8601 format.", "title": "Created Date" } }, @@ -3433,8 +3886,10 @@ "type": "object" }, "MessageRemoved": { + "description": "A chat message was removed or hidden. Drop the message with `message_id`.", "properties": { "message_id": { + "description": "ID of the removed message.", "title": "Message Id", "type": "string" }, @@ -3444,6 +3899,7 @@ "null" ], "default": null, + "description": "ID of the conversation the message belongs to. Each branch has its own conversation, so a viewer showing one branch can drop another branch's messages.", "title": "Conversation Id" }, "branch_id": { @@ -3452,6 +3908,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -3469,10 +3926,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/MessageRemoved" + "$ref": "#/components/schemas/MessageRemoved", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -3483,9 +3942,11 @@ "type": "object" }, "MessageUpdated": { + "description": "A chat message was added or changed. Replace the message with the same `id`.", "properties": { "message": { - "$ref": "#/components/schemas/Message" + "$ref": "#/components/schemas/Message", + "description": "The message in full, as it now reads." }, "conversation_id": { "type": [ @@ -3493,6 +3954,7 @@ "null" ], "default": null, + "description": "ID of the conversation the message belongs to. Each branch has its own conversation, so a viewer showing one branch can drop another branch's messages.", "title": "Conversation Id" }, "branch_id": { @@ -3501,6 +3963,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -3518,10 +3981,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/MessageUpdated" + "$ref": "#/components/schemas/MessageUpdated", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -3532,9 +3997,10 @@ "type": "object" }, "OtherApproval": { - "description": "Parked by a guard that publishes only its name.", + "description": "Waiting for approval from a check that publishes only its name.", "properties": { "guard": { + "description": "The check that paused the call.", "not": { "enum": [ "backend_function_entity_delete", @@ -3558,9 +4024,10 @@ "type": "object" }, "OtherToolCall": { - "description": "A call to a tool that publishes only its name and progress.", + "description": "A call to any other tool. Only its name and progress are published.", "properties": { "name": { + "description": "The tool that was called.", "not": { "enum": [ "ask_clarifying_questions", @@ -3599,6 +4066,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -3607,6 +4075,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -3615,6 +4084,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -3634,6 +4104,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -3642,6 +4113,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -3654,7 +4126,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -3695,7 +4167,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" } }, @@ -3805,10 +4277,11 @@ "type": "object" }, "PreviewExecuteCodeCall": { - "description": "A `preview_execute_code` call.", + "description": "The AI runs code inside the app preview to inspect it.", "properties": { "name": { "const": "preview_execute_code", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -3818,6 +4291,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -3826,6 +4300,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -3834,6 +4309,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -3853,6 +4329,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -3861,6 +4338,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -3873,7 +4351,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -3914,7 +4392,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -3926,7 +4404,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -3936,8 +4415,10 @@ "type": "object" }, "PreviewNavigationRequested": { + "description": "The app preview should show a page of the app, usually one the AI just built.", "properties": { "path": { + "description": "Path of the page to show, relative to the app's root, such as `/settings`.", "title": "Path", "type": "string" }, @@ -3952,6 +4433,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -3970,10 +4452,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/PreviewNavigationRequested" + "$ref": "#/components/schemas/PreviewNavigationRequested", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -3984,6 +4468,7 @@ "type": "object" }, "PreviewReloadRequested": { + "description": "The app preview should reload, because its files changed under it.", "properties": { "branch_id": { "type": [ @@ -3991,6 +4476,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -4005,10 +4491,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/PreviewReloadRequested" + "$ref": "#/components/schemas/PreviewReloadRequested", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4019,10 +4507,11 @@ "type": "object" }, "PreviewScreenshotCall": { - "description": "A `preview_screenshot` call.", + "description": "The AI takes a screenshot of the app preview to check its work.", "properties": { "name": { "const": "preview_screenshot", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -4032,6 +4521,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -4040,6 +4530,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -4048,6 +4539,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -4067,6 +4559,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -4075,6 +4568,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -4087,7 +4581,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -4128,7 +4622,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -4140,7 +4634,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -4157,10 +4652,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/BranchChange" + "$ref": "#/components/schemas/BranchChange", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4199,7 +4696,7 @@ "type": "string" }, "options": { - "description": "Selectable options; a bare string is its label.", + "description": "Selectable options. A bare string is its label.", "items": { "anyOf": [ { @@ -4298,6 +4795,7 @@ "null" ], "default": null, + "description": "ID of the queued prompt.", "title": "Id" }, "content": { @@ -4306,6 +4804,7 @@ "null" ], "default": null, + "description": "Text of the prompt.", "title": "Content" }, "branch_id": { @@ -4314,6 +4813,7 @@ "null" ], "default": null, + "description": "ID of the branch the prompt runs on, or `null` for the main branch.", "title": "Branch Id" }, "created_at": { @@ -4322,6 +4822,7 @@ "null" ], "default": null, + "description": "Time the prompt was queued, as a UTC timestamp in ISO 8601 format.", "title": "Created At" } }, @@ -4329,7 +4830,7 @@ "type": "object" }, "QueueState": { - "description": "The prompt queue: the prompts the builder runs next, in order.", + "description": "The prompts the builder runs next, in order.", "properties": { "items": { "items": { @@ -4340,6 +4841,7 @@ "null" ], "default": null, + "description": "Queued prompts, in the order they will run.", "title": "Items" }, "is_paused": { @@ -4348,6 +4850,7 @@ "null" ], "default": null, + "description": "Whether the queue is paused, so queued prompts wait. A person can pause it, and the builder pauses it while the AI waits for an answer.", "title": "Is Paused" } }, @@ -4355,6 +4858,7 @@ "type": "object" }, "QueueUpdated": { + "description": "The whole prompt queue after a change. Replace the queue with it.", "properties": { "items": { "items": { @@ -4365,6 +4869,7 @@ "null" ], "default": null, + "description": "Queued prompts, in the order they will run.", "title": "Items" }, "is_paused": { @@ -4373,6 +4878,7 @@ "null" ], "default": null, + "description": "Whether the queue is paused, so queued prompts wait. A person can pause it, and the builder pauses it while the AI waits for an answer.", "title": "Is Paused" }, "processed_item_id": { @@ -4381,6 +4887,7 @@ "null" ], "default": null, + "description": "ID of the queued prompt the builder just picked up to run. Absent when a prompt was added, removed or cleared.", "title": "Processed Item Id" }, "branch_id": { @@ -4389,6 +4896,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -4403,10 +4911,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/QueueUpdated" + "$ref": "#/components/schemas/QueueUpdated", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4417,10 +4927,11 @@ "type": "object" }, "ReadOnlyExecToolCall": { - "description": "A `read_only_exec_tool` call.", + "description": "The AI runs code that only reads the app's data.", "properties": { "name": { "const": "read_only_exec_tool", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -4430,6 +4941,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -4438,6 +4950,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -4446,6 +4959,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -4465,6 +4979,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -4473,6 +4988,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -4485,7 +5001,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -4526,7 +5042,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -4538,7 +5054,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -4548,10 +5065,11 @@ "type": "object" }, "ReadRepoFileCall": { - "description": "A `read_repo_file` call.", + "description": "The AI reads a file in the imported repository.", "properties": { "name": { "const": "read_repo_file", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -4561,6 +5079,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -4569,6 +5088,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -4577,6 +5097,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -4596,6 +5117,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -4604,6 +5126,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -4616,7 +5139,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -4657,7 +5180,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -4669,7 +5192,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -4686,10 +5210,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/BranchChange" + "$ref": "#/components/schemas/BranchChange", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4700,17 +5226,19 @@ "type": "object" }, "RoomAccessDeniedEvent": { - "description": "A join was refused: the app is not on the session's allowlist, or the socket joins too often.", + "description": "A join was refused because the app is not on the session's allowlist, or the socket joins too often.", "properties": { "room": { "type": [ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/RoomNotice" + "$ref": "#/components/schemas/RoomNotice", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4721,17 +5249,19 @@ "type": "object" }, "RoomAccessRevokedEvent": { - "description": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin.", + "description": "The app left the session's allowlist, or its workspace. Events for it stop. Do not rejoin.", "properties": { "room": { "type": [ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/RoomNotice" + "$ref": "#/components/schemas/RoomNotice", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4742,23 +5272,25 @@ "type": "object" }, "RoomNotice": { - "description": "The app is named by the event's room.", + "description": "An empty payload. The app concerned is the event's `room`.", "properties": {}, "title": "RoomNotice", "type": "object" }, "RoomSnapshotUnavailableEvent": { - "description": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot.", + "description": "The join held but its snapshot failed. Live events still arrive. Rejoin later for a snapshot.", "properties": { "room": { "type": [ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/RoomNotice" + "$ref": "#/components/schemas/RoomNotice", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4769,10 +5301,11 @@ "type": "object" }, "RunShellCommandCall": { - "description": "A `run_shell_command` call.", + "description": "The AI runs a shell command in the imported repository's sandbox.", "properties": { "name": { "const": "run_shell_command", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -4782,6 +5315,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -4790,6 +5324,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -4798,6 +5333,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -4817,6 +5353,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -4825,6 +5362,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -4837,7 +5375,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -4878,7 +5416,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -4890,7 +5428,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -4917,9 +5456,10 @@ "type": "object" }, "SessionEnded": { + "description": "The session is over. The socket disconnects right after this event.", "properties": { "reason": { - "description": "`expired`: ask the partner backend for a new session. `revoked`: the partner or an admin ended it; don't reconnect. `replaced`: another tab took it; stop.", + "description": "`expired`: ask the partner backend for a new session. `revoked`: the partner or an admin ended it, so do not reconnect. `replaced`: a newer socket took the session, so stop.", "enum": [ "expired", "revoked", @@ -4943,10 +5483,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/SessionEnded" + "$ref": "#/components/schemas/SessionEnded", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -4972,10 +5514,11 @@ "type": "object" }, "SetSecretsCall": { - "description": "A `set_secrets` call.", + "description": "The AI asks the user for secret values, such as API keys. The call waits until the user supplies them.", "properties": { "name": { "const": "set_secrets", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -4985,6 +5528,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -4993,6 +5537,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -5001,6 +5546,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -5020,6 +5566,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -5028,6 +5575,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -5040,7 +5588,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -5081,7 +5629,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -5093,7 +5641,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." } }, "required": [ @@ -5103,6 +5652,7 @@ "type": "object" }, "Snapshot": { + "description": "The app's current state, sent after every join. Replace what the viewer holds for the app with it.", "properties": { "status": { "anyOf": [ @@ -5112,9 +5662,11 @@ { "type": "null" } - ] + ], + "description": "The app's build status, or `null` when none is recorded." }, "messages": { + "description": "The last 50 messages visible to partners, oldest first.", "items": { "$ref": "#/components/schemas/Message" }, @@ -5135,7 +5687,7 @@ "type": "object" }, "StatusObject": { - "description": "Public builder state, without error diagnostics or billing context.", + "description": "The app's current build status.", "properties": { "state": { "anyOf": [ @@ -5152,6 +5704,7 @@ } ], "default": null, + "description": "Where the app is in its build lifecycle. Ready means idle with no build in progress, processing means the app is being generated or modified, and error means the last build failed. This tracks building, not publishing.", "title": "State" }, "turn_id": { @@ -5160,6 +5713,7 @@ "null" ], "default": null, + "description": "ID of the user message whose turn the status belongs to.", "title": "Turn Id" }, "last_updated_date": { @@ -5169,6 +5723,7 @@ "null" ], "default": null, + "description": "Time the status was last updated, as a UTC timestamp in ISO 8601 format.", "title": "Last Updated Date" } }, @@ -5176,7 +5731,7 @@ "type": "object" }, "SummaryDetails": { - "description": "What a parked code execution would do.", + "description": "What the code waiting for approval would do.", "properties": { "summary": { "description": "The agent's summary of what the code does.", @@ -5200,21 +5755,24 @@ "type": "object" }, "TaskProgress": { + "description": "How far a long-running task has got.", "properties": { "current": { "type": [ - "number", + "integer", "null" ], "default": null, + "description": "Items processed so far.", "title": "Current" }, "total": { "type": [ - "number", + "integer", "null" ], "default": null, + "description": "Items to process in all.", "title": "Total" }, "percentage": { @@ -5223,6 +5781,7 @@ "null" ], "default": null, + "description": "Share of the work done, from 0 to 100.", "title": "Percentage" } }, @@ -5230,6 +5789,7 @@ "type": "object" }, "TaskProgressed": { + "description": "A long-running task, such as a data import, started, made progress, finished, failed or was cancelled.", "properties": { "tool_call_id": { "type": [ @@ -5237,6 +5797,7 @@ "null" ], "default": null, + "description": "ID of the tool call that started the task.", "title": "Tool Call Id" }, "message_id": { @@ -5245,14 +5806,27 @@ "null" ], "default": null, + "description": "ID of the message that holds that tool call.", "title": "Message Id" }, "event_type": { - "type": [ - "string", - "null" + "anyOf": [ + { + "enum": [ + "task_started", + "task_progress", + "task_completed", + "task_failed", + "task_cancelled" + ], + "type": "string" + }, + { + "type": "null" + } ], "default": null, + "description": "`task_started`, `task_progress`, `task_completed`, `task_failed` or `task_cancelled`.", "title": "Event Type" }, "progress": { @@ -5264,7 +5838,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "Progress so far." }, "branch_id": { "type": [ @@ -5272,6 +5847,7 @@ "null" ], "default": null, + "description": "ID of the branch the change belongs to. Omitted or `null` means the main branch.", "title": "Branch Id" } }, @@ -5286,10 +5862,12 @@ "string", "null" ], + "description": "The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session.", "title": "Room" }, "data": { - "$ref": "#/components/schemas/TaskProgressed" + "$ref": "#/components/schemas/TaskProgressed", + "description": "The event's payload. Its fields depend on the event." } }, "required": [ @@ -5300,10 +5878,11 @@ "type": "object" }, "UpdateEntitiesCall": { - "description": "A `update_entities` call.", + "description": "The AI updates records in one of the app's entities.", "properties": { "name": { "const": "update_entities", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -5313,6 +5892,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -5321,6 +5901,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -5329,6 +5910,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -5348,6 +5930,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -5356,6 +5939,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -5368,7 +5952,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -5409,7 +5993,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -5421,7 +6005,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." }, "display": { "anyOf": [ @@ -5432,7 +6017,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -5465,10 +6051,11 @@ "type": "object" }, "UpdatePlanCall": { - "description": "A `update_plan` call.", + "description": "The AI adds settled points to the plan.", "properties": { "name": { "const": "update_plan", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -5478,6 +6065,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -5486,6 +6074,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -5494,6 +6083,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -5513,6 +6103,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -5521,6 +6112,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -5533,7 +6125,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -5574,7 +6166,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "arguments": { @@ -5586,7 +6178,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the AI asked the tool to do, narrowed to what this tool publishes." } }, "required": [ @@ -5596,7 +6189,7 @@ "type": "object" }, "WaitingOn": { - "description": "What a parked call waits for. Render the widget by kind, not by tool name.", + "description": "What the call waits for. Render the widget by kind, not by tool name.", "properties": { "kind": { "description": "`choice`: pick among options. `input`: supply values. `approval`: approve or reject.", @@ -5613,10 +6206,11 @@ "type": "object" }, "WriteFileCall": { - "description": "A `write_file` call.", + "description": "The AI writes a file in the app.", "properties": { "name": { "const": "write_file", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -5626,6 +6220,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -5634,6 +6229,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -5642,6 +6238,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -5661,6 +6258,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -5669,6 +6267,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -5681,7 +6280,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -5722,7 +6321,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -5734,7 +6333,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ @@ -5744,10 +6344,11 @@ "type": "object" }, "WriteRepoFileCall": { - "description": "A `write_repo_file` call.", + "description": "The AI writes a file in the imported repository.", "properties": { "name": { "const": "write_repo_file", + "description": "The tool that was called.", "title": "Name", "type": "string" }, @@ -5757,6 +6358,7 @@ "null" ], "default": null, + "description": "ID of the call. Every update to the call carries the same ID.", "title": "Id" }, "requires_user_input": { @@ -5765,6 +6367,7 @@ "null" ], "default": null, + "description": "`true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting.", "title": "Requires User Input" }, "auto_approved": { @@ -5773,6 +6376,7 @@ "null" ], "default": null, + "description": "`true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real.", "title": "Auto Approved" }, "status": { @@ -5792,6 +6396,7 @@ } ], "default": null, + "description": "`running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval.", "title": "Status" }, "mutation_applied": { @@ -5800,6 +6405,7 @@ "null" ], "default": null, + "description": "Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown.", "title": "Mutation Applied" }, "waiting_on": { @@ -5812,7 +6418,7 @@ } ], "default": null, - "description": "What kind of answer a parked call waits for.", + "description": "What kind of answer the call waits for, while `status` is `waiting_for_user_input`.", "title": "Waiting On" }, "approval": { @@ -5853,7 +6459,7 @@ } ], "default": null, - "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "description": "Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`.", "title": "Approval" }, "display": { @@ -5865,7 +6471,8 @@ "type": "null" } ], - "default": null + "default": null, + "description": "What the tool touched, for showing its progress. Never file contents or commands." } }, "required": [ diff --git a/packages/platform/scripts/sync-asyncapi.mjs b/packages/platform/scripts/sync-asyncapi.mjs index a4535ab..326010e 100644 --- a/packages/platform/scripts/sync-asyncapi.mjs +++ b/packages/platform/scripts/sync-asyncapi.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -// Replaces asyncapi.json with the document a backend serves. Production publishes only beta and GA -// messages, so while the events are alpha point this at a dev backend, e.g. a local apper: +// Replaces asyncapi.json with the document a backend serves, production by default. Production +// publishes only beta and GA messages; for alpha ones point this at a dev backend, e.g. a local apper: // npm run sync:asyncapi -- http://localhost:8000/api/asyncapi.json // or set ASYNCAPI_URL. Then run `npm run gen:events`. import { writeFile } from "node:fs/promises"; diff --git a/packages/platform/src/modules/builder.events.generated.ts b/packages/platform/src/modules/builder.events.generated.ts index 2aba118..7fe45d5 100644 --- a/packages/platform/src/modules/builder.events.generated.ts +++ b/packages/platform/src/modules/builder.events.generated.ts @@ -31,24 +31,24 @@ export interface AccessRuleChangeDetails { */ export interface AppSnapshotEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: Snapshot; } /** - * The `Snapshot` schema. + * The app's current state, sent after every join. Replace what the viewer holds for the app with it. */ export interface Snapshot { /** - * The `status` field. + * The app's build status, or `null` when none is recorded. */ status: StatusObject | null; /** - * Messages. + * The last 50 messages visible to partners, oldest first. */ messages: Message[]; /** @@ -57,19 +57,19 @@ export interface Snapshot { queue: QueueState; } /** - * Public builder state, without error diagnostics or billing context. + * The app's current build status. */ export interface StatusObject { /** - * State. + * Where the app is in its build lifecycle. Ready means idle with no build in progress, processing means the app is being generated or modified, and error means the last build failed. This tracks building, not publishing. */ state?: ("ready" | "processing" | "error") | null; /** - * Turn ID. + * ID of the user message whose turn the status belongs to. */ turn_id?: string | null; /** - * Last updated date. + * Time the status was last updated, as a UTC timestamp in ISO 8601 format. */ last_updated_date?: string | null; } @@ -78,19 +78,19 @@ export interface StatusObject { */ export interface Message { /** - * Id. + * ID of the message. An update carries the whole message again under the same ID. */ id?: string | null; /** - * Role. + * `user` for a message a person sent, `assistant` for the AI's reply. `system` messages are never sent. */ role?: ("user" | "assistant" | "system") | null; /** - * Content. + * Text of the message. Attachments and structured content are left out. */ content?: string | null; /** - * Tool calls. + * The tool calls the AI made in this message, in order. Each is updated in place as it progresses. */ tool_calls?: | ( @@ -123,52 +123,52 @@ export interface Message { )[] | null; /** - * Checkpoint ID. + * ID of the checkpoint tied to the message. On a message a person sent it holds the app before the turn, on the AI's final reply the app after it. Absent when the turn made no checkpoint. */ checkpoint_id?: string | null; /** - * The `metadata` field. + * When the message was created. */ metadata?: MessageMetadata | null; /** - * Additional message params. + * Flags on the message, such as plan mode. Only the flags listed here are published. */ additional_message_params?: MessageParams | null; } /** - * A `ask_clarifying_questions` call. + * The AI asks the user questions before it goes on. The call waits until the user answers. */ export interface AskClarifyingQuestionsCall { /** - * Name. + * The tool that was called. */ name: "ask_clarifying_questions"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -184,16 +184,16 @@ export interface AskClarifyingQuestionsCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: QuestionArguments | null; /** - * The `user_input` field. + * The user's answer to the call, once given, for tools that take one. */ user_input?: QuestionInput | null; } /** - * What a parked call waits for. Render the widget by kind, not by tool name. + * What the call waits for. Render the widget by kind, not by tool name. */ export interface WaitingOn { /** @@ -202,19 +202,19 @@ export interface WaitingOn { kind?: "choice" | "input" | "approval"; } /** - * Parked by the `backend_function_entity_delete` guard. + * Waiting for approval: testing a backend function would delete records from an entity. */ export interface BackendFunctionEntityDeleteApproval { /** - * Guard. + * The check that paused the call. */ guard: "backend_function_entity_delete"; /** - * Why the guard parked the call. + * Why the call needs approval, as the check explained it. */ reason?: string | null; /** - * The `details` field. + * What the check found. */ details?: FunctionEntityDeleteDetails | null; } @@ -231,55 +231,55 @@ export interface FunctionEntityDeleteDetails { */ method?: string; /** - * The guard could not verify the function's source. + * The check could not verify the function's source. */ inspection_failed?: boolean; } /** - * Parked by the `bash_approval` guard. + * Waiting for approval: the AI wants to run a shell command that needs a review first. The command itself is not published. */ export interface BashApproval { /** - * Guard. + * The check that paused the call. */ guard: "bash_approval"; } /** - * Parked by the `entity_rls_guard` guard. + * Waiting for approval: the AI wants to change who can read or write an entity's records. */ export interface EntityRlsGuardApproval { /** - * Guard. + * The check that paused the call. */ guard: "entity_rls_guard"; /** - * Why the guard parked the call. + * Why the call needs approval, as the check explained it. */ reason?: string | null; /** - * The `details` field. + * What the check found. */ details?: AccessRuleChangeDetails | null; } /** - * Parked by the `exec_tool_entity_mutation` guard. + * Waiting for approval: the AI's code would update or delete records in an entity. */ export interface ExecToolEntityMutationApproval { /** - * Guard. + * The check that paused the call. */ guard: "exec_tool_entity_mutation"; /** - * Why the guard parked the call. + * Why the call needs approval, as the check explained it. */ reason?: string | null; /** - * The `details` field. + * What the check found. */ details?: EntityMutationDetails | null; } /** - * What a parked entity mutation would do. Record data is never included. + * What the entity change waiting for approval would do. Record data is never included. */ export interface EntityMutationDetails { /** @@ -292,24 +292,24 @@ export interface EntityMutationDetails { summary?: string; } /** - * Parked by the `exec_tool_github_repo_write` guard. + * Waiting for approval: the AI's code would write to a GitHub repository. */ export interface ExecToolGithubRepoWriteApproval { /** - * Guard. + * The check that paused the call. */ guard: "exec_tool_github_repo_write"; /** - * Why the guard parked the call. + * Why the call needs approval, as the check explained it. */ reason?: string | null; /** - * The `details` field. + * What the check found. */ details?: SummaryDetails | null; } /** - * What a parked code execution would do. + * What the code waiting for approval would do. */ export interface SummaryDetails { /** @@ -318,62 +318,62 @@ export interface SummaryDetails { summary?: string; } /** - * Parked by the `exec_tool_invite_user` guard. + * Waiting for approval: the AI's code would invite someone to the app by email. */ export interface ExecToolInviteUserApproval { /** - * Guard. + * The check that paused the call. */ guard: "exec_tool_invite_user"; /** - * Why the guard parked the call. + * Why the call needs approval, as the check explained it. */ reason?: string | null; /** - * The `details` field. + * What the check found. */ details?: SummaryDetails | null; } /** - * Parked by the `exec_tool_send_email` guard. + * Waiting for approval: the AI's code would send an email. */ export interface ExecToolSendEmailApproval { /** - * Guard. + * The check that paused the call. */ guard: "exec_tool_send_email"; /** - * Why the guard parked the call. + * Why the call needs approval, as the check explained it. */ reason?: string | null; /** - * The `details` field. + * What the check found. */ details?: SummaryDetails | null; } /** - * Parked by the `exec_tool_send_sms` guard. + * Waiting for approval: the AI's code would send a text message. */ export interface ExecToolSendSmsApproval { /** - * Guard. + * The check that paused the call. */ guard: "exec_tool_send_sms"; /** - * Why the guard parked the call. + * Why the call needs approval, as the check explained it. */ reason?: string | null; /** - * The `details` field. + * What the check found. */ details?: SummaryDetails | null; } /** - * Parked by a guard that publishes only its name. + * Waiting for approval from a check that publishes only its name. */ export interface OtherApproval { /** - * Guard. + * The check that paused the call. */ guard: string; } @@ -411,7 +411,7 @@ export interface Question { */ covers?: string; /** - * Selectable options; a bare string is its label. + * Selectable options. A bare string is its label. */ options?: (string | QuestionOption)[]; } @@ -455,39 +455,39 @@ export interface QuestionAnswer { custom_text?: string; } /** - * A `ask_plan_questions` call. + * The AI asks the user questions that shape the plan, in plan mode. The call waits until the user answers. */ export interface AskPlanQuestionsCall { /** - * Name. + * The tool that was called. */ name: "ask_plan_questions"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -503,48 +503,48 @@ export interface AskPlanQuestionsCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: QuestionArguments | null; /** - * The `user_input` field. + * The user's answer to the call, once given, for tools that take one. */ user_input?: QuestionInput | null; } /** - * A `bash` call. + * The AI runs a shell command in the app's sandbox. */ export interface BashCall { /** - * Name. + * The tool that was called. */ name: "bash"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -560,7 +560,7 @@ export interface BashCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: SummaryDisplay | null; } @@ -574,39 +574,39 @@ export interface SummaryDisplay { summary?: string; } /** - * A `create_entity_records` call. + * The AI adds records to one of the app's entities. */ export interface CreateEntityRecordsCall { /** - * Name. + * The tool that was called. */ name: "create_entity_records"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -622,12 +622,12 @@ export interface CreateEntityRecordsCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: EntityRecordsDisplay | null; } /** - * The `EntityRecordsDisplay` schema. + * The entity an operation added records to. */ export interface EntityRecordsDisplay { /** @@ -640,39 +640,39 @@ export interface EntityRecordsDisplay { record_count?: number; } /** - * A `delete_entities` call. + * The AI deletes records from one of the app's entities. */ export interface DeleteEntitiesCall { /** - * Name. + * The tool that was called. */ name: "delete_entities"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -688,16 +688,16 @@ export interface DeleteEntitiesCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: EntityChangeArguments | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: EntityDisplay | null; } /** - * An entity update or delete; the entity is in `display.entity_name`. + * An entity update or delete. The entity is in `display.entity_name`. */ export interface EntityChangeArguments { /** @@ -715,39 +715,39 @@ export interface EntityDisplay { entity_name?: string; } /** - * A `delete_file` call. + * The AI deletes a file from the app. */ export interface DeleteFileCall { /** - * Name. + * The tool that was called. */ name: "delete_file"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -763,7 +763,7 @@ export interface DeleteFileCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: FileDisplay | null; } @@ -781,39 +781,39 @@ export interface FileDisplay { content_empty?: boolean; } /** - * A `edit_repo_file` call. + * The AI edits a file in the imported repository. */ export interface EditRepoFileCall { /** - * Name. + * The tool that was called. */ name: "edit_repo_file"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -829,44 +829,44 @@ export interface EditRepoFileCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: FileDisplay | null; } /** - * A `exec_tool` call. + * The AI runs code against the app's data and integrations. */ export interface ExecToolCall { /** - * Name. + * The tool that was called. */ name: "exec_tool"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -882,7 +882,7 @@ export interface ExecToolCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: ExecDisplay | null; } @@ -895,44 +895,44 @@ export interface ExecDisplay { */ summary?: string; /** - * The code changed entity data: refresh the preview. + * The code changed entity data, so refresh the preview. */ writes_entities?: boolean; } /** - * A `find_replace` call. + * The AI edits files by replacing text in them. */ export interface FindReplaceCall { /** - * Name. + * The tool that was called. */ name: "find_replace"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -948,44 +948,44 @@ export interface FindReplaceCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: FileDisplay | null; } /** - * A `generate_game_background` call. + * The AI generates a background image for a game. An `image.resolved` event follows when it is ready. */ export interface GenerateGameBackgroundCall { /** - * Name. + * The tool that was called. */ name: "generate_game_background"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1001,44 +1001,44 @@ export interface GenerateGameBackgroundCall { ) | null; /** - * Results. + * The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval. */ results?: string | null; } /** - * A `generate_game_image` call. + * The AI generates an image asset for a game. An `image.resolved` event follows when it is ready. */ export interface GenerateGameImageCall { /** - * Name. + * The tool that was called. */ name: "generate_game_image"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1054,11 +1054,11 @@ export interface GenerateGameImageCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: GameImageArguments | null; /** - * Results. + * The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval. */ results?: string | null; } @@ -1072,39 +1072,39 @@ export interface GameImageArguments { aspect_ratio?: string; } /** - * A `generate_image` call. + * The AI generates an image for the app. An `image.resolved` event follows when it is ready. */ export interface GenerateImageCall { /** - * Name. + * The tool that was called. */ name: "generate_image"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1120,11 +1120,11 @@ export interface GenerateImageCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: MediaArguments | null; /** - * The `results` field. + * The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval. */ results?: ImageResult | null; } @@ -1159,39 +1159,39 @@ export interface ImageResult { image_url?: string | null; } /** - * A `generate_prd` call. + * The AI writes down the plan it settled with the user. In plan mode this call carries the plan itself. */ export interface GeneratePrdCall { /** - * Name. + * The tool that was called. */ name: "generate_prd"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1207,7 +1207,7 @@ export interface GeneratePrdCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: PrdArguments | null; } @@ -1253,39 +1253,39 @@ export interface PrdArguments { game_engine?: string; } /** - * A `generate_video` call. + * The AI generates a video for the app. */ export interface GenerateVideoCall { /** - * Name. + * The tool that was called. */ name: "generate_video"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1301,48 +1301,48 @@ export interface GenerateVideoCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: MediaArguments | null; /** - * Results. + * The tool's result, narrowed to what this tool publishes. Absent while the call waits for approval. */ results?: string | null; } /** - * A `install_npm_package` call. + * The AI installs or removes npm packages in the app. */ export interface InstallNpmPackageCall { /** - * Name. + * The tool that was called. */ name: "install_npm_package"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1358,7 +1358,7 @@ export interface InstallNpmPackageCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: InstallPackageArguments | null; } @@ -1385,39 +1385,39 @@ export interface PackageOperation { action?: string; } /** - * A `preview_execute_code` call. + * The AI runs code inside the app preview to inspect it. */ export interface PreviewExecuteCodeCall { /** - * Name. + * The tool that was called. */ name: "preview_execute_code"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1433,44 +1433,44 @@ export interface PreviewExecuteCodeCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: ExecDisplay | null; } /** - * A `preview_screenshot` call. + * The AI takes a screenshot of the app preview to check its work. */ export interface PreviewScreenshotCall { /** - * Name. + * The tool that was called. */ name: "preview_screenshot"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1486,44 +1486,44 @@ export interface PreviewScreenshotCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: SummaryDisplay | null; } /** - * A `read_only_exec_tool` call. + * The AI runs code that only reads the app's data. */ export interface ReadOnlyExecToolCall { /** - * Name. + * The tool that was called. */ name: "read_only_exec_tool"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1539,44 +1539,44 @@ export interface ReadOnlyExecToolCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: ExecDisplay | null; } /** - * A `read_repo_file` call. + * The AI reads a file in the imported repository. */ export interface ReadRepoFileCall { /** - * Name. + * The tool that was called. */ name: "read_repo_file"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1592,44 +1592,44 @@ export interface ReadRepoFileCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: FileDisplay | null; } /** - * A `run_shell_command` call. + * The AI runs a shell command in the imported repository's sandbox. */ export interface RunShellCommandCall { /** - * Name. + * The tool that was called. */ name: "run_shell_command"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1645,44 +1645,44 @@ export interface RunShellCommandCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: SummaryDisplay | null; } /** - * A `set_secrets` call. + * The AI asks the user for secret values, such as API keys. The call waits until the user supplies them. */ export interface SetSecretsCall { /** - * Name. + * The tool that was called. */ name: "set_secrets"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1698,7 +1698,7 @@ export interface SetSecretsCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: SetSecretsArguments | null; } @@ -1725,39 +1725,39 @@ export interface SecretField { description?: string; } /** - * A `update_entities` call. + * The AI updates records in one of the app's entities. */ export interface UpdateEntitiesCall { /** - * Name. + * The tool that was called. */ name: "update_entities"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1773,48 +1773,48 @@ export interface UpdateEntitiesCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: EntityChangeArguments | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: EntityDisplay | null; } /** - * A `update_plan` call. + * The AI adds settled points to the plan. */ export interface UpdatePlanCall { /** - * Name. + * The tool that was called. */ name: "update_plan"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1830,7 +1830,7 @@ export interface UpdatePlanCall { ) | null; /** - * The `arguments` field. + * What the AI asked the tool to do, narrowed to what this tool publishes. */ arguments?: UpdatePlanArguments | null; } @@ -1869,39 +1869,39 @@ export interface PlanUpdate { text?: string; } /** - * A `write_file` call. + * The AI writes a file in the app. */ export interface WriteFileCall { /** - * Name. + * The tool that was called. */ name: "write_file"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1917,44 +1917,44 @@ export interface WriteFileCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: FileDisplay | null; } /** - * A `write_repo_file` call. + * The AI writes a file in the imported repository. */ export interface WriteRepoFileCall { /** - * Name. + * The tool that was called. */ name: "write_repo_file"; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -1970,44 +1970,44 @@ export interface WriteRepoFileCall { ) | null; /** - * The `display` field. + * What the tool touched, for showing its progress. Never file contents or commands. */ display?: FileDisplay | null; } /** - * A call to a tool that publishes only its name and progress. + * A call to any other tool. Only its name and progress are published. */ export interface OtherToolCall { /** - * Name. + * The tool that was called. */ name: string; /** - * Id. + * ID of the call. Every update to the call carries the same ID. */ id?: string | null; /** - * Requires user input. + * `true` once the call has paused for the user's answer or approval. It stays `true` after the user answers, so read `status` to know whether the call is still waiting. */ requires_user_input?: boolean | null; /** - * Auto approved. + * `true` when the call needed approval but ran at once because the app has Auto approve on. It never paused, and its result is real. */ auto_approved?: boolean | null; /** - * Status. + * `running` while the tool works, `success` or `error` once it finished, `stopped` when the user stopped the turn, and `waiting_for_user_input` while it waits for the user's answer or approval. */ status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; /** - * Mutation applied. + * Whether the call changed the app or its data. `false` means it finished without changing anything. `null` means the tool did not report it, so whether it changed anything is unknown. */ mutation_applied?: boolean | null; /** - * What kind of answer a parked call waits for. + * What kind of answer the call waits for, while `status` is `waiting_for_user_input`. */ waiting_on?: WaitingOn | null; /** - * Why a safety guard parked the call: the guard, and its declared reason and details. + * Why the call waits for approval, from the check that paused it. Present only while `status` is `waiting_for_user_input`. */ approval?: | ( @@ -2028,7 +2028,7 @@ export interface OtherToolCall { */ export interface MessageMetadata { /** - * Created date. + * Time the message was created, as a UTC timestamp in ISO 8601 format. */ created_date?: string | null; } @@ -2058,15 +2058,15 @@ export interface MessageParams { system_message_type?: string; } /** - * The prompt queue: the prompts the builder runs next, in order. + * The prompts the builder runs next, in order. */ export interface QueueState { /** - * Items. + * Queued prompts, in the order they will run. */ items?: QueueItem[] | null; /** - * Is paused. + * Whether the queue is paused, so queued prompts wait. A person can pause it, and the builder pauses it while the AI waits for an answer. */ is_paused?: boolean | null; } @@ -2075,32 +2075,32 @@ export interface QueueState { */ export interface QueueItem { /** - * Id. + * ID of the queued prompt. */ id?: string | null; /** - * Content. + * Text of the prompt. */ content?: string | null; /** - * Branch ID. + * ID of the branch the prompt runs on, or `null` for the main branch. */ branch_id?: string | null; /** - * Created at. + * Time the prompt was queued, as a UTC timestamp in ISO 8601 format. */ created_at?: string | null; } /** - * The `AppStatusChanged` schema. + * The app's build status changed, because a turn started, finished or failed. */ export interface AppStatusChanged { /** - * The `status` field. + * The new status, or `null` when the status was cleared. */ status: StatusObject | null; /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2109,20 +2109,20 @@ export interface AppStatusChanged { */ export interface AppStatusChangedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: AppStatusChanged; } /** - * Re-read what changed; the event carries no content. + * Something changed outside the chat. Re-read it, because the event carries no content. */ export interface BranchChange { /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2131,11 +2131,11 @@ export interface BranchChange { */ export interface BranchDeletedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: BranchChange; } @@ -2144,11 +2144,11 @@ export interface BranchDeletedEvent { */ export interface ConversationChangedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: BranchChange; } @@ -2157,28 +2157,28 @@ export interface ConversationChangedEvent { */ export interface FilesChangedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: BranchChange; } /** - * The `ImageResolved` schema. + * A generated image finished or failed. Replace `placeholder_url` with `image_url` wherever the app shows it. */ export interface ImageResolved { /** - * Status. + * `pending` while the image generates, `completed` once `image_url` is ready, `failed` when generation failed. */ status?: ("pending" | "completed" | "failed") | null; /** - * Placeholder URL. + * Placeholder URL the app shows while the image generates. Match it to find where the image appears. */ placeholder_url?: string | null; /** - * Image URL. + * URL of the finished image, or `null` while it is pending or after it failed. */ image_url?: string | null; } @@ -2187,28 +2187,28 @@ export interface ImageResolved { */ export interface ImageResolvedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: ImageResolved; } /** - * The `MessageRemoved` schema. + * A chat message was removed or hidden. Drop the message with `message_id`. */ export interface MessageRemoved { /** - * Message ID. + * ID of the removed message. */ message_id: string; /** - * Conversation ID. + * ID of the conversation the message belongs to. Each branch has its own conversation, so a viewer showing one branch can drop another branch's messages. */ conversation_id?: string | null; /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2217,28 +2217,28 @@ export interface MessageRemoved { */ export interface MessageRemovedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: MessageRemoved; } /** - * The `MessageUpdated` schema. + * A chat message was added or changed. Replace the message with the same `id`. */ export interface MessageUpdated { /** - * The `message` field. + * The message in full, as it now reads. */ message: Message; /** - * Conversation ID. + * ID of the conversation the message belongs to. Each branch has its own conversation, so a viewer showing one branch can drop another branch's messages. */ conversation_id?: string | null; /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2247,20 +2247,20 @@ export interface MessageUpdated { */ export interface MessageUpdatedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: MessageUpdated; } /** - * The `PreviewNavigationRequested` schema. + * The app preview should show a page of the app, usually one the AI just built. */ export interface PreviewNavigationRequested { /** - * Path. + * Path of the page to show, relative to the app's root, such as `/settings`. */ path: string; /** @@ -2268,7 +2268,7 @@ export interface PreviewNavigationRequested { */ force: boolean; /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2277,20 +2277,20 @@ export interface PreviewNavigationRequested { */ export interface PreviewNavigationRequestedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: PreviewNavigationRequested; } /** - * The `PreviewReloadRequested` schema. + * The app preview should reload, because its files changed under it. */ export interface PreviewReloadRequested { /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2299,11 +2299,11 @@ export interface PreviewReloadRequested { */ export interface PreviewReloadRequestedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: PreviewReloadRequested; } @@ -2312,32 +2312,32 @@ export interface PreviewReloadRequestedEvent { */ export interface PullRequestChangedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: BranchChange; } /** - * The `QueueUpdated` schema. + * The whole prompt queue after a change. Replace the queue with it. */ export interface QueueUpdated { /** - * Items. + * Queued prompts, in the order they will run. */ items?: QueueItem[] | null; /** - * Is paused. + * Whether the queue is paused, so queued prompts wait. A person can pause it, and the builder pauses it while the AI waits for an answer. */ is_paused?: boolean | null; /** - * Processed item ID. + * ID of the queued prompt the builder just picked up to run. Absent when a prompt was added, removed or cleared. */ processed_item_id?: string | null; /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2346,11 +2346,11 @@ export interface QueueUpdated { */ export interface QueueUpdatedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: QueueUpdated; } @@ -2359,63 +2359,63 @@ export interface QueueUpdatedEvent { */ export interface RepositoryChangedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: BranchChange; } /** - * A join was refused: the app is not on the session's allowlist, or the socket joins too often. + * A join was refused because the app is not on the session's allowlist, or the socket joins too often. */ export interface RoomAccessDeniedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: RoomNotice; } /** - * The app is named by the event's room. + * An empty payload. The app concerned is the event's `room`. */ export interface RoomNotice {} /** - * The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin. + * The app left the session's allowlist, or its workspace. Events for it stop. Do not rejoin. */ export interface RoomAccessRevokedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: RoomNotice; } /** - * The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot. + * The join held but its snapshot failed. Live events still arrive. Rejoin later for a snapshot. */ export interface RoomSnapshotUnavailableEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: RoomNotice; } /** - * The `SessionEnded` schema. + * The session is over. The socket disconnects right after this event. */ export interface SessionEnded { /** - * `expired`: ask the partner backend for a new session. `revoked`: the partner or an admin ended it; don't reconnect. `replaced`: another tab took it; stop. + * `expired`: ask the partner backend for a new session. `revoked`: the partner or an admin ended it, so do not reconnect. `replaced`: a newer socket took the session, so stop. */ reason: "expired" | "revoked" | "replaced"; } @@ -2424,53 +2424,53 @@ export interface SessionEnded { */ export interface SessionEndedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: SessionEnded; } /** - * The `TaskProgress` schema. + * How far a long-running task has got. */ export interface TaskProgress { /** - * Current. + * Items processed so far. */ current?: number | null; /** - * Total. + * Items to process in all. */ total?: number | null; /** - * Percentage. + * Share of the work done, from 0 to 100. */ percentage?: number | null; } /** - * The `TaskProgressed` schema. + * A long-running task, such as a data import, started, made progress, finished, failed or was cancelled. */ export interface TaskProgressed { /** - * Tool call ID. + * ID of the tool call that started the task. */ tool_call_id?: string | null; /** - * Message ID. + * ID of the message that holds that tool call. */ message_id?: string | null; /** - * Event type. + * `task_started`, `task_progress`, `task_completed`, `task_failed` or `task_cancelled`. */ - event_type?: string | null; + event_type?: ("task_started" | "task_progress" | "task_completed" | "task_failed" | "task_cancelled") | null; /** - * The `progress` field. + * Progress so far. */ progress?: TaskProgress | null; /** - * Branch ID. + * ID of the branch the change belongs to. Omitted or `null` means the main branch. */ branch_id?: string | null; } @@ -2479,11 +2479,11 @@ export interface TaskProgressed { */ export interface TaskProgressedEvent { /** - * Room. + * The app the event belongs to, as `/apps/{app_id}`. It is `null` for `session.ended`, which concerns the whole session. */ room: string | null; /** - * The `data` field. + * The event's payload. Its fields depend on the event. */ data: TaskProgressed; } @@ -2518,11 +2518,11 @@ export interface ServerEventMap { "pull_request.changed": BranchChange; /** The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next. */ "session.ended": SessionEnded; - /** A join was refused: the app is not on the session's allowlist, or the socket joins too often. */ + /** A join was refused because the app is not on the session's allowlist, or the socket joins too often. */ "room.access_denied": RoomNotice; - /** The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin. */ + /** The app left the session's allowlist, or its workspace. Events for it stop. Do not rejoin. */ "room.access_revoked": RoomNotice; - /** The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot. */ + /** The join held but its snapshot failed. Live events still arrive. Rejoin later for a snapshot. */ "room.snapshot_unavailable": RoomNotice; /** The app's current status and its last 50 public messages, after every join. Replace the app's state with it. */ "app.snapshot": Snapshot; diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/modules/builder.events.types.ts index b3d46e7..dfc9dc3 100644 --- a/packages/platform/src/modules/builder.events.types.ts +++ b/packages/platform/src/modules/builder.events.types.ts @@ -45,6 +45,9 @@ export type PlatformEvent = { * * Arrives after every join and rejoin, and after a rewrite of the main conversation. Replace * everything you hold for the app with it. It holds only the last 50 messages. + * + * Its fields are the `data` of [`app.snapshot`](/developers/references/apps-api/sections/socket-messages#app-snapshot), + * plus `room`. */ export interface PlatformSnapshot extends Omit { /** The app's room, `/apps/{app_id}`. */ From c6d7478cab576192969cee980faf309a78a985ce Mon Sep 17 00:00:00 2001 From: amitbardos Date: Mon, 12 Oct 2026 17:19:22 +0300 Subject: [PATCH 2/3] feat(platform)!: take the socket session from one getSession pass-through builder.init() takes getSession, which returns the Create socket session body unchanged. The socket opens at the session's socket_url and socket_path on the first connect(), and moves when a renewed session has a different address. createPlatformClient() takes no options; socketUrl, getSessionToken and PlatformClientOptions are gone. An invalid session rejects connect() with session_unavailable. SocketSession is listed with PlatformSnapshot under the builder page's Type Definitions. Version 0.2.0, with a CHANGELOG.md written against what 0.1.0 shipped. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 2 +- packages/platform/CHANGELOG.md | 69 +++++++ packages/platform/README.md | 48 +++-- packages/platform/examples/client.ts | 15 +- packages/platform/package.json | 2 +- .../appended-articles.json | 1 + .../page-sections.json | 2 +- .../types-to-expose.json | 3 +- packages/platform/src/client.ts | 31 ++- packages/platform/src/client.types.ts | 27 --- packages/platform/src/errors.types.ts | 6 +- packages/platform/src/index.ts | 4 +- .../platform/src/modules/builder-protocol.ts | 12 ++ .../platform/src/modules/builder-socket.ts | 153 +++++++------- packages/platform/src/modules/builder.ts | 5 +- .../platform/src/modules/builder.types.ts | 37 +++- packages/platform/tests/package.test.mjs | 8 +- packages/platform/tests/types/client.types.ts | 21 +- packages/platform/tests/unit/client.test.ts | 195 ++++++++++++------ packages/platform/tests/unit/socket.test.js | 12 +- 20 files changed, 422 insertions(+), 231 deletions(-) create mode 100644 packages/platform/CHANGELOG.md diff --git a/package-lock.json b/package-lock.json index b10aa8f..6113bf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5978,7 +5978,7 @@ }, "packages/platform": { "name": "@base44/platform", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "socket.io-client": "^4.8.3" diff --git a/packages/platform/CHANGELOG.md b/packages/platform/CHANGELOG.md new file mode 100644 index 0000000..67590b3 --- /dev/null +++ b/packages/platform/CHANGELOG.md @@ -0,0 +1,69 @@ +# Changelog + +## 0.2.0 + +**Breaking.** The client, its options and the event type names change. Runtime delivery of events +is unchanged. + +### Client and session + +- `new Base44PlatformClient(options)` is replaced by `createPlatformClient()`, which takes no + options. `serverUrl`, `getSessionToken` and the `PlatformClientOptions` type are removed. +- `builder.init()` takes `getSession`, which returns the body your server received from + `POST /api/service/socket-sessions`, unchanged. The SDK reads `session_token`, `socket_url` and + `socket_path`, and ignores other keys. +- The socket connects on the session's `socket_path` instead of a hard-coded `/ws/socket.io/`, and + is created on the first `connect()`. When a renewed session has a different address, the SDK + moves the socket and rejoins every active subscription. +- An invalid `session_token`, `socket_url` or `socket_path` rejects `connect()` with + `session_unavailable` instead of throwing a `TypeError`. + +Before: + +```ts +const client = new Base44PlatformClient({ + serverUrl, + getSessionToken: async () => (await fetchSession()).session_token, +}); +const builder = client.builder.init({ onError }); +``` + +After: + +```ts +const client = createPlatformClient(); +const builder = client.builder.init({ getSession: fetchSession, onError }); +``` + +### Event types + +The event types are generated from the service's AsyncAPI document and take the backend's model +names. + +| 0.1.0 | 0.2.0 | +| --- | --- | +| `ChatMessage` | `Message` | +| `AppStatus` | `StatusObject` | +| `Snapshot` | `PlatformSnapshot` | +| `SnapshotQueue` | `QueueState` | +| `ToolGuardApproval` | `GuardApproval`, a union of one `*Approval` per guard | +| `ToolMediaResult` | `ImageResult` | +| `ToolQuestionArguments`, `ToolQuestion`, `ToolQuestionOption` | `QuestionArguments`, `Question`, `QuestionOption` | +| `ToolQuestionInput`, `ToolQuestionAnswer` | `QuestionInput`, `QuestionAnswer` | +| `ToolSecretArguments`, `ToolSecretField` | `SetSecretsArguments`, `SecretField` | +| `ToolPackageArguments`, `ToolPackageOperation` | `InstallPackageArguments`, `PackageOperation` | +| `ToolPlanArguments`, `ToolPlanUpdate` | `UpdatePlanArguments`, `PlanUpdate` | +| `ToolPrdArguments` | `PrdArguments` | +| `ToolMediaArguments` | `MediaArguments` | +| `ToolEntityMutationArguments` | `EntityChangeArguments` | + +- `ToolCall` is a union of one `*Call` per tool, such as `AskClarifyingQuestionsCall` and + `WriteFileCall`. Check `name` to narrow it to that tool's `arguments` and `display`. + `ToolArguments` and `ToolDisplay` are removed. +- `BranchScoped` is removed. Each payload that belongs to a branch has its own `branch_id`. +- New: `SessionErrorCode` and `AppErrorCode` split `PlatformSocketErrorCode`, and every schema in + the protocol is exported, with `ServerEventMap` and `ClientMessageMap`. + +## 0.1.0 + +First release. diff --git a/packages/platform/README.md b/packages/platform/README.md index 451ac8a..a39273a 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -12,15 +12,14 @@ The workspace must have white-label sockets enabled; this SDK does not open sess ```ts import { createPlatformClient } from "@base44/platform"; -const client = createPlatformClient({ - socketUrl, // the session's socket_url, from POST /api/service/socket-sessions - async getSessionToken() { +const client = createPlatformClient(); +const builder = client.builder.init({ + // Returns the body of POST /api/service/socket-sessions, unchanged. + async getSession() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); if (!response.ok) throw new Error("Session request failed"); - return (await response.json()).session_token; + return response.json(); }, -}); -const builder = client.builder.init({ onError(error) { showConnectionError(error.code); }, }); const subscription = builder.subscribe(appId, { @@ -38,34 +37,40 @@ builder.close(); `/api/builder-socket-session` is your backend's route, not an SDK endpoint. It opens a socket session with its workspace key (`apps:watch` scope) through -`POST /api/service/socket-sessions` with `{ app_ids }`, and returns only the -`session_token` and `socket_url` to the browser. Pass `socket_url` as `socketUrl`. Never pass the workspace key or any other server -credential to this client. Subscribe only to apps on the session's allowlist; your +`POST /api/service/socket-sessions` with `{ app_ids }`, and returns the response body +to the browser. `getSession` returns that body unchanged: the SDK reads `session_token`, +`socket_url` and `socket_path` and ignores other keys such as `session_id` and +`expires_in`. Never pass the workspace key or any other server credential to this client. Subscribe only to apps on the session's allowlist; your backend adds or removes apps with `PUT`/`DELETE /api/service/socket-sessions/{session_id}/rooms/{app_id}`. A session lasts one hour and is never extended. The token goes exclusively in CONNECT `auth.session_token`, never a URL, and the SDK keeps it in memory only. It -is reused across automatic reconnects. `getSessionToken` is called when a builder +is reused across automatic reconnects. `getSession` is called when a builder session first connects, and again only when the server rejects the cached token or -expires the session; the SDK then reconnects once with the new token. Retrieval has -a twenty-second timeout and reports `session_unavailable` on failure. +expires the session; the SDK then reconnects once with the new session. If the new +session has a different `socket_url` or `socket_path`, the SDK closes the old socket, +opens one at the new address and rejoins every active subscription. Retrieval has a +twenty-second timeout. A failure, a timeout, or a session without a token, an +HTTP(S) origin as `socket_url` (no credentials, path, query or fragment) or an +absolute `socket_path` reports `session_unavailable`. One live socket per session: a newer connection with the same token replaces the older one, which stops with `session_replaced`. Open one session per page. ## Lifecycle -`createPlatformClient({ socketUrl, getSessionToken })` creates a lightweight module -container: no Socket.IO instance, timers, token request or network activity. Its +`createPlatformClient()` creates a lightweight module container: no Socket.IO +instance, timers, session request or network activity. It takes no options. Its `builder` module follows the server SDK's module-factory pattern. -`client.builder.init({ onError })` synchronously creates an independent `BuilderSession` -without connecting. Repeated calls create separate sessions, each owning its own +`client.builder.init({ getSession, onError })` synchronously creates an independent +`BuilderSession` without connecting or creating a socket. Repeated calls create separate sessions, each owning its own socket, listeners, subscriptions and cleanup. Close the returned session when its view is disposed; the root client and other sessions remain usable. -`builder.connect()` resolves on CONNECT, before any snapshot. The socket uses the -default namespace on `/ws/socket.io/`, WebSocket only, with a dedicated manager. +`builder.connect()` fetches the first session, creates the socket at its address and +resolves on CONNECT, before any snapshot. The socket uses the default namespace on the +session's `socket_path`, WebSocket only, with a dedicated manager. Transport reconnection uses five attempts, starting at one second and capped at ten seconds with jitter; each attempt has a twenty-second timeout. A temporary server refusal is retried the same way. After exhaustion, a refusal or `session_replaced`, @@ -94,9 +99,10 @@ their exceptions are isolated so they cannot interrupt another app's delivery. ## Public shapes -- `PlatformClientOptions` (`client.types.ts`): `socketUrl`, `getSessionToken`. +- `PlatformClient` (`client.types.ts`): the `builder` module. - `BuilderModule` (`modules/builder.types.ts`): lazy `init(options)` factory. -- `BuilderInitOptions`: connection-level `onError` observer. +- `BuilderInitOptions`: `getSession` and the connection-level `onError` observer. +- `SocketSession`: the `session_token`, `socket_url` and `socket_path` that `getSession` returns. - `BuilderSession`: `connect()`, `subscribe(appId, options)`, `close()`. - `SubscriptionOptions`: required `onSnapshot`, `onEvent` and `onError`. - `PlatformSubscription`: read-only `appId`, `active`, and `unsubscribe()`. @@ -148,7 +154,7 @@ exceptions, payloads and credentials are never attached. | Error code | Action | | --- | --- | | `connection_denied` | The session was rejected even with a fresh token: the workspace, key or session is not usable. | -| `session_unavailable` | `getSessionToken` failed or timed out; no provider details are forwarded. | +| `session_unavailable` | `getSession` failed, timed out or returned an invalid session; no provider details are forwarded. | | `session_replaced` | Another connection with the same session took over. Stop, or open a new session. | | `connection_failed` | Transport failure or exhausted retries; call `connect()` after fixing connectivity. | | `access_denied` | The join was refused: the app is not on the session's allowlist, or joins were rate limited. | diff --git a/packages/platform/examples/client.ts b/packages/platform/examples/client.ts index a3f9d3f..3eae618 100644 --- a/packages/platform/examples/client.ts +++ b/packages/platform/examples/client.ts @@ -1,18 +1,13 @@ import { createPlatformClient, type PlatformEvent } from "@base44/platform"; -declare const socketUrl: string; - -const client = createPlatformClient({ - socketUrl, // the session's socket_url, from POST /api/service/socket-sessions - // Your backend opens the session with its workspace key and returns only the session token. - async getSessionToken() { +const client = createPlatformClient(); +const builder = client.builder.init({ + // Your backend opens the session with its workspace key and returns the Create socket session body. + async getSession() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); if (!response.ok) throw new Error("Unable to open a socket session"); - const { session_token } = await response.json(); - return session_token; + return response.json(); }, -}); -const builder = client.builder.init({ onError(error) { console.error(error.code); }, }); diff --git a/packages/platform/package.json b/packages/platform/package.json index 4b42e10..541238e 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -1,6 +1,6 @@ { "name": "@base44/platform", - "version": "0.1.0", + "version": "0.2.0", "description": "Browser client for building on the Base44 platform", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json index dcce403..43da4b7 100644 --- a/packages/platform/scripts/mintlify-post-processing/appended-articles.json +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -1,6 +1,7 @@ { "interfaces/builder": [ "interfaces/BuilderSession", + "interfaces/SocketSession", "type-aliases/PlatformEvent", "interfaces/PlatformSnapshot" ], diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index eb835b5..a0bc015 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -3,7 +3,7 @@ "BuilderSession": "Session methods" }, "typeDefinitionGroups": [ - ["PlatformEvent", "PlatformSnapshot"], + ["SocketSession", "PlatformEvent", "PlatformSnapshot"], { "title": "Error codes", "types": ["SessionErrorCode", "AppErrorCode"] } ] } diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json index 0c67779..93a2d85 100644 --- a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json +++ b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json @@ -5,5 +5,6 @@ "PlatformEvent", "PlatformSnapshot", "PlatformSocketError", - "SessionErrorCode" + "SessionErrorCode", + "SocketSession" ] diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index d3912c2..a3cf659 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -1,4 +1,4 @@ -import type { PlatformClient, PlatformClientOptions } from "./client.types.js"; +import type { PlatformClient } from "./client.types.js"; import { createBuilder } from "./modules/builder.js"; /** @@ -6,34 +6,29 @@ import { createBuilder } from "./modules/builder.js"; * * This is the entry point of the Platform SDK. The client gives your white-label app editor * access to the platform's modules, such as [`builder`](/developers/references/platform-sdk/docs/interfaces/builder). + * It takes no options and opens nothing: each builder session gets its socket session from the + * `getSession` you pass to `init()`. * - * @param options - Configuration object for the client. - * @returns A configured platform client with access to the platform's modules. - * @throws {TypeError} When `socketUrl` isn't an HTTP or HTTPS origin. + * @returns A platform client with access to the platform's modules. * * @example * ```typescript * // Create a client for your app editor * import { createPlatformClient } from '@base44/platform'; * - * const client = createPlatformClient({ - * socketUrl, - * async getSessionToken() { + * const client = createPlatformClient(); + * + * // Use the client to follow the AI chat in an app + * const builder = client.builder.init({ + * async getSession() { * const response = await fetch('/api/builder-socket-session', { method: 'POST' }); - * const { session_token } = await response.json(); - * return session_token; + * return response.json(); * }, + * onError: (error) => console.error(error.code), * }); - * - * // Use the client to follow the AI chat in an app - * const builder = client.builder.init({ onError: (error) => console.error(error.code) }); * await builder.connect(); * ``` */ -export function createPlatformClient(options: PlatformClientOptions): PlatformClient { - const url = new URL(options.socketUrl); - if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") { - throw new TypeError("socketUrl must be an HTTP(S) origin without credentials, path, query or fragment"); - } - return Object.freeze({ builder: Object.freeze(createBuilder({ ...options, socketUrl: url.origin })) }); +export function createPlatformClient(): PlatformClient { + return Object.freeze({ builder: Object.freeze(createBuilder()) }); } diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index c9dc5ff..6ed309d 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -1,32 +1,5 @@ import type { BuilderModule } from "./modules/builder.types.js"; -/** - * Options for {@linkcode createPlatformClient | createPlatformClient()}. - * - * The client runs in the browser and never sees your workspace API key. Your server opens a socket - * session with the key and hands the browser only the session's token and socket URL. - */ -export interface PlatformClientOptions { - /** - * Origin of the platform socket. - * - * Use the `socket_url` your server receives from `POST /api/service/socket-sessions`. It's the same - * for every session in an environment, so your server can pass it to the page once. It must be an - * origin with no path, query, fragment, or credentials. - */ - socketUrl: string; - /** - * Returns a socket session token from your server. - * - * Your server opens a session with `POST /api/service/socket-sessions` and returns its - * `session_token`. The client calls this when it connects, and again when the token expires. - * Return a new token on every call. - * - * @returns The session token, or a promise resolving to it. - */ - getSessionToken: () => string | Promise; -} - /** * The platform client. * diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index 2ac3fa1..6dc1d77 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -4,9 +4,11 @@ */ export type SessionErrorCode = /** - * `getSessionToken` threw, took longer than 20 seconds or returned no token. + * `getSession` threw, took longer than 20 seconds, or returned a session without a valid + * `session_token`, `socket_url` (an HTTP or HTTPS origin) or `socket_path` (an absolute path). * - * Check the endpoint that opens sessions, then call `connect()` again. Retryable. + * Check the endpoint that opens sessions, then call `connect()` again. Retryable, except that an + * invalid field needs a fix on your server first. */ | "session_unavailable" /** diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index f5703a9..b8f93fb 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -2,7 +2,7 @@ export { createPlatformClient } from "./client.js"; export { PlatformSocketError } from "./errors.js"; export type { AppErrorCode, PlatformSocketErrorCode, SessionErrorCode } from "./errors.types.js"; -export type { PlatformClient, PlatformClientOptions } from "./client.types.js"; -export type { BuilderModule, BuilderInitOptions, BuilderSession, PlatformSubscription, SubscriptionOptions } from "./modules/builder.types.js"; +export type { PlatformClient } from "./client.types.js"; +export type { BuilderModule, BuilderInitOptions, BuilderSession, PlatformSubscription, SocketSession, SubscriptionOptions } from "./modules/builder.types.js"; export type { GuardApproval, PlatformEvent, PlatformEventMap, PlatformSnapshot, ToolCall } from "./modules/builder.events.types.js"; export type * from "./modules/builder.events.generated.js"; diff --git a/packages/platform/src/modules/builder-protocol.ts b/packages/platform/src/modules/builder-protocol.ts index b9c9cb1..91f8f21 100644 --- a/packages/platform/src/modules/builder-protocol.ts +++ b/packages/platform/src/modules/builder-protocol.ts @@ -1,6 +1,7 @@ import { serverEventNames, type ServerEventMap, type SessionEnded } from "./builder.events.generated.js"; import type { PlatformSocketErrorCode } from "../errors.types.js"; import type { PlatformEvent, PlatformEventMap, PlatformSnapshot } from "./builder.events.types.js"; +import type { SocketSession } from "./builder.types.js"; const sessionEvents: readonly string[] = ["app.snapshot", "session.ended"]; /** App events delivered to `onEvent`: every server event except the session events the socket handles. */ @@ -42,3 +43,14 @@ export function decodeSnapshot(raw: unknown): PlatformSnapshot { const queue = data.queue !== null && typeof data.queue === "object" ? { queue: data.queue } : {}; return { room: frame.room, status: data.status, messages: data.messages, ...queue } as PlatformSnapshot; } +/** Keeps only the fields the socket needs; a URL with credentials, a path, query or fragment is refused. */ +export function decodeSession(raw: unknown): SocketSession { + const { session_token, socket_url, socket_path } = object(raw); + if (typeof session_token !== "string" || !session_token.trim()) throw new Error("Invalid session token"); + if (typeof socket_url !== "string" || typeof socket_path !== "string" || !/^\/(?!\/)[^?#\s]*$/.test(socket_path)) throw new Error("Invalid socket address"); + const url = new URL(socket_url); + if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") { + throw new Error("Invalid socket URL"); + } + return { session_token, socket_url: url.origin, socket_path }; +} diff --git a/packages/platform/src/modules/builder-socket.ts b/packages/platform/src/modules/builder-socket.ts index e8c0755..542fb87 100644 --- a/packages/platform/src/modules/builder-socket.ts +++ b/packages/platform/src/modules/builder-socket.ts @@ -1,20 +1,20 @@ import { io, type Socket } from "socket.io-client"; import { PlatformSocketError } from "../errors.js"; -import { appFromRoom, appPattern, decode, decodeSnapshot, eventNames, object, roomFor, roomNotices, sessionEndings } from "./builder-protocol.js"; +import { appFromRoom, appPattern, decode, decodeSession, decodeSnapshot, eventNames, object, roomFor, roomNotices, sessionEndings } from "./builder-protocol.js"; import { notify, Subscription } from "./builder-subscription.js"; -import type { PlatformClientOptions } from "../client.types.js"; -import type { BuilderInitOptions, BuilderSession, PlatformSubscription, SubscriptionOptions } from "./builder.types.js"; +import type { BuilderInitOptions, BuilderSession, PlatformSubscription, SocketSession, SubscriptionOptions } from "./builder.types.js"; const MAX_DENIAL_RETRIES = 5; /** @internal */ export class BuilderSocket implements BuilderSession { - private readonly socket: Socket; + private socket?: Socket; + private address?: string; private readonly subscriptions = new Map(); - private readonly options: PlatformClientOptions & BuilderInitOptions; + private readonly options: BuilderInitOptions; private closed = false; - // The session token outlives reconnects; it is replaced only when the server rejects or expires it. - private token?: string; + // The session outlives reconnects; it is replaced only when the server rejects or expires its token. + private session?: SocketSession; private tokenFresh = false; private ending?: (typeof sessionEndings)[keyof typeof sessionEndings]; private denialRetries = 0; @@ -25,66 +25,26 @@ export class BuilderSocket implements BuilderSession { private resolveConnect?: () => void; private rejectConnect?: (error: PlatformSocketError) => void; - constructor(config: PlatformClientOptions, options: BuilderInitOptions) { - this.options = { ...config, onError: options.onError }; - this.socket = io(config.socketUrl, { - path: "/ws/socket.io/", transports: ["websocket"], autoConnect: false, - forceNew: true, reconnectionAttempts: 5, reconnectionDelay: 1000, reconnectionDelayMax: 10000, - timeout: 20000, - auth: (callback) => { void this.authenticate(callback); }, - }); - this.socket.on("connect", () => { - this.tokenFresh = false; - this.denialRetries = 0; - for (const subscription of this.subscriptions.values()) this.join(subscription); - this.resolveConnect?.(); - this.clearConnecting(); - }); - this.socket.on("disconnect", (reason) => { - ++this.authAttempt; - if (this.closed || reason !== "io server disconnect") return; // Transport loss reconnects by itself. - const ending = this.ending; - this.ending = undefined; - if (ending === "session_expired") this.socket.connect(); // Renew through getSessionToken. - else this.connectionError(new PlatformSocketError(ending ?? "connection_failed")); - }); - this.socket.on("connect_error", (error) => this.refused(error as Error & { data?: { retryable?: boolean } })); - this.socket.io.on("reconnect_failed", () => this.connectionError(new PlatformSocketError("connection_failed"))); - this.socket.on("app.snapshot", (raw: unknown) => { - try { - const snapshot = decodeSnapshot(raw); - this.subscriptions.get(appFromRoom(snapshot.room)!)?.snapshot(snapshot); - } catch { this.protocolError(raw); } - }); - this.socket.on("session.ended", (raw: unknown) => this.sessionEnded(raw)); - for (const [name, code] of Object.entries(roomNotices)) this.socket.on(name, (raw: unknown) => this.roomNotice(code, raw)); - for (const type of eventNames) this.socket.on(type, (raw: unknown) => { - try { - const appId = appFromRoom(object(raw).room); - if (!appId) throw new Error("Invalid app"); - const event = decode(type, appId, raw); - const subscription = this.subscriptions.get(appId); - subscription?.event(event); - // The server does not resend a snapshot after a rewrite of main: rejoining asks for one. - if (subscription && event.type === "conversation.changed" && event.data.branch_id == null) this.join(subscription); - } catch { this.protocolError(raw); } - }); + constructor(options: BuilderInitOptions) { + this.options = { getSession: options.getSession, onError: options.onError }; } - /** Connect with the session token. Resolves on CONNECT, before snapshots arrive. + /** Connect with the session's token. Resolves on CONNECT, before snapshots arrive. + * The first call fetches the session, whose address the socket opens on. * Transport loss retries up to five times and rejoins active subscriptions. * Call again after a failure or `session_replaced`; concurrent calls share one attempt. */ connect(): Promise { if (this.closed) return Promise.reject(new PlatformSocketError("client_closed")); - if (this.socket.connected) return Promise.resolve(); + if (this.socket?.connected) return Promise.resolve(); if (this.connecting) return this.connecting; const promise = new Promise((resolve, reject) => { this.resolveConnect = resolve; this.rejectConnect = reject; }); this.connecting = promise; - this.socket.connect(); + if (this.socket) this.socket.connect(); + else void this.authenticate(); return promise; } @@ -99,10 +59,10 @@ export class BuilderSocket implements BuilderSession { if (this.subscriptions.size >= 8) throw new PlatformSocketError("subscription_limit", appId); const subscription = new Subscription(appId, { ...options }, () => { this.subscriptions.delete(appId); - if (this.socket.connected) this.socket.emit("leave", roomFor(appId)); + if (this.socket?.connected) this.socket.emit("leave", roomFor(appId)); }); this.subscriptions.set(appId, subscription); - if (this.socket.connected) this.join(subscription); + if (this.socket?.connected) this.join(subscription); return subscription; } @@ -116,35 +76,90 @@ export class BuilderSocket implements BuilderSession { for (const subscription of this.subscriptions.values()) subscription.unsubscribe(); this.rejectConnect?.(new PlatformSocketError("client_closed")); this.clearConnecting(); + this.discard(); + } + + private open(url: string, path: string): void { + this.discard(); + const socket = this.socket = io(url, { + path, transports: ["websocket"], autoConnect: false, + forceNew: true, reconnectionAttempts: 5, reconnectionDelay: 1000, reconnectionDelayMax: 10000, + timeout: 20000, + auth: (callback) => { void this.authenticate(callback); }, + }); + this.address = url + path; + socket.on("connect", () => { + this.tokenFresh = false; + this.denialRetries = 0; + for (const subscription of this.subscriptions.values()) this.join(subscription); + this.resolveConnect?.(); + this.clearConnecting(); + }); + socket.on("disconnect", (reason) => { + ++this.authAttempt; + if (this.closed || reason !== "io server disconnect") return; // Transport loss reconnects by itself. + const ending = this.ending; + this.ending = undefined; + if (ending === "session_expired") socket.connect(); // Renew through getSession. + else this.connectionError(new PlatformSocketError(ending ?? "connection_failed")); + }); + socket.on("connect_error", (error) => this.refused(error as Error & { data?: { retryable?: boolean } })); + socket.io.on("reconnect_failed", () => this.connectionError(new PlatformSocketError("connection_failed"))); + socket.on("app.snapshot", (raw: unknown) => { + try { + const snapshot = decodeSnapshot(raw); + this.subscriptions.get(appFromRoom(snapshot.room)!)?.snapshot(snapshot); + } catch { this.protocolError(raw); } + }); + socket.on("session.ended", (raw: unknown) => this.sessionEnded(raw)); + for (const [name, code] of Object.entries(roomNotices)) socket.on(name, (raw: unknown) => this.roomNotice(code, raw)); + for (const type of eventNames) socket.on(type, (raw: unknown) => { + try { + const appId = appFromRoom(object(raw).room); + if (!appId) throw new Error("Invalid app"); + const event = decode(type, appId, raw); + const subscription = this.subscriptions.get(appId); + subscription?.event(event); + // The server does not resend a snapshot after a rewrite of main: rejoining asks for one. + if (subscription && event.type === "conversation.changed" && event.data.branch_id == null) this.join(subscription); + } catch { this.protocolError(raw); } + }); + socket.connect(); + } + + private discard(): void { + if (!this.socket) return; this.socket.removeAllListeners(); this.socket.io.removeAllListeners(); this.socket.disconnect(); } private join(subscription: Subscription): void { - this.socket.emit("join", roomFor(subscription.appId)); + this.socket?.emit("join", roomFor(subscription.appId)); } - private async authenticate(callback: (auth: { session_token: string }) => void): Promise { + /** Without a callback, or when a new session moves the socket, opens a socket at the session's address. */ + private async authenticate(callback?: (auth: { session_token: string }) => void): Promise { const attempt = ++this.authAttempt; this.cancelAuth?.(); let timer: ReturnType | undefined; const timeout = new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error("Session token timeout")), 20000); + timer = setTimeout(() => reject(new Error("Session timeout")), 20000); this.cancelAuth = () => { clearTimeout(timer); reject(new Error("Cancelled")); }; }); try { - if (!this.token) { - const token = await Promise.race([Promise.resolve().then(() => this.options.getSessionToken()), timeout]); + if (!this.session) { + const session = await Promise.race([Promise.resolve().then(() => this.options.getSession()), timeout]); if (this.closed || attempt !== this.authAttempt) return; - if (typeof token !== "string" || !token.trim()) throw new Error("Missing session token"); - this.token = token; + this.session = decodeSession(session); this.tokenFresh = true; } - callback({ session_token: this.token }); + const { session_token, socket_url, socket_path } = this.session; + if (callback && this.address === socket_url + socket_path) callback({ session_token }); + else this.open(socket_url, socket_path); } catch { if (this.closed || attempt !== this.authAttempt) return; - this.socket.disconnect(); + this.socket?.disconnect(); this.connectionError(new PlatformSocketError("session_unavailable")); } finally { clearTimeout(timer); @@ -158,8 +173,8 @@ export class BuilderSocket implements BuilderSession { if (error.data?.retryable === true) { this.retryDenied(); return; } // A token this attempt did not just fetch may have expired: renew it once. const renew = !this.tokenFresh; - this.token = undefined; - if (renew) this.socket.connect(); + this.session = undefined; + if (renew) this.socket?.connect(); else this.connectionError(new PlatformSocketError("connection_denied")); } @@ -170,7 +185,7 @@ export class BuilderSocket implements BuilderSession { return; } const delay = Math.min(1000 * 2 ** (this.denialRetries - 1), 10000) * (0.5 + Math.random() / 2); - this.retryTimer = setTimeout(() => { this.retryTimer = undefined; if (!this.closed) this.socket.connect(); }, delay); + this.retryTimer = setTimeout(() => { this.retryTimer = undefined; if (!this.closed) this.socket?.connect(); }, delay); } private clearConnecting(): void { @@ -192,7 +207,7 @@ export class BuilderSocket implements BuilderSession { ? sessionEndings[reason as keyof typeof sessionEndings] : undefined; if (!ending) throw new Error("Unknown reason"); // The server disconnects next; the disconnect handler renews or stops. - this.token = undefined; + this.session = undefined; this.ending = ending; } catch { this.protocolError(raw); } } diff --git a/packages/platform/src/modules/builder.ts b/packages/platform/src/modules/builder.ts index c05f752..4e71199 100644 --- a/packages/platform/src/modules/builder.ts +++ b/packages/platform/src/modules/builder.ts @@ -1,10 +1,9 @@ -import type { PlatformClientOptions } from "../client.types.js"; import { BuilderSocket } from "./builder-socket.js"; import type { BuilderModule } from "./builder.types.js"; /** @internal */ -export function createBuilder(config: PlatformClientOptions): BuilderModule { +export function createBuilder(): BuilderModule { return { - init(options) { return new BuilderSocket(config, options); }, + init(options) { return new BuilderSocket(options); }, }; } diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 8112823..7f55d68 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -1,8 +1,35 @@ import type { PlatformEvent, PlatformSnapshot } from "./builder.events.types.js"; import type { PlatformSocketError } from "../errors.js"; +/** + * The socket session your server opened, as [Create socket session](/api-reference/create-socket-session) returned it. + * + * Pass the response body through unchanged. The client reads these three fields and ignores the + * rest, such as `session_id` and `expires_in`. + */ +export interface SocketSession { + /** Token that authenticates the socket. The client keeps it in memory and sends it only when the socket connects. */ + session_token: string; + /** Origin of the platform socket, with no path, query, fragment or credentials. */ + socket_url: string; + /** Absolute path the socket connects on, such as `/ws/socket.io/`. */ + socket_path: string; +} + /** Options for {@linkcode BuilderModule.init | init()}. */ export interface BuilderInitOptions { + /** + * Returns a new socket session from your server. + * + * Your server opens the session with its workspace API key through + * [Create socket session](/api-reference/create-socket-session) and returns the response body, a [`SocketSession`](#socketsession). + * The client calls this on the first `connect()`, and again only when the session expires or + * Base44 rejects its token. Return a new session on every call. If a new session has a different + * `socket_url` or `socket_path`, the client moves to it and rejoins every active subscription. + * + * @returns The session, as your server received it. + */ + getSession: () => Promise; /** * Called on errors that affect the whole session, such as a failed connection or an ended session. * Errors for one app go to that subscription's `onError` instead. See the @@ -27,7 +54,7 @@ export interface BuilderModule { * Creates a builder session without connecting it. * * Call {@linkcode BuilderSession.connect | connect()} on the returned session to open the socket. - * Each session is independent, with its own socket, token and subscriptions. + * Each session is independent, with its own socket, socket session and subscriptions. * * @param options - Session options. * @returns The new session. @@ -36,6 +63,10 @@ export interface BuilderModule { * ```typescript * // Create and connect a session * const builder = client.builder.init({ + * async getSession() { + * const response = await fetch('/api/builder-socket-session', { method: 'POST' }); + * return response.json(); + * }, * onError(error) { * console.error('Builder session error', error.code); * }, @@ -56,8 +87,8 @@ export interface BuilderSession { * * Resolves once the socket connects, before any snapshot arrives. If the connection drops, the * session retries five times and rejoins every active subscription, each with a fresh snapshot. - * An expired or rejected token is replaced once through `getSessionToken`. Concurrent calls share - * one attempt. Call it again after a failure or a `session_replaced` error. + * The first call fetches a session through `getSession`, and an expired or rejected token is + * replaced once the same way. Concurrent calls share one attempt. Call it again after a failure or a `session_replaced` error. * * @returns Promise that resolves when the socket connects. * @throws {PlatformSocketError} With `connection_denied`, `connection_failed` or `session_unavailable` diff --git a/packages/platform/tests/package.test.mjs b/packages/platform/tests/package.test.mjs index 389fd9b..9bfb5ed 100644 --- a/packages/platform/tests/package.test.mjs +++ b/packages/platform/tests/package.test.mjs @@ -35,16 +35,16 @@ test("the client resolves from the installed package without the runtime SDK", ( run(process.execPath, ["--input-type=module", "--eval", ` import assert from "node:assert/strict"; import { createPlatformClient } from "@base44/platform"; - const client = createPlatformClient({ socketUrl: "https://example.test", getSessionToken: () => "session-token" }); + const client = createPlatformClient(); assert.equal(typeof client.builder.init, "function"); - client.builder.init({ onError() {} }).close(); + client.builder.init({ getSession: async () => ({ session_token: "t", socket_url: "https://example.test", socket_path: "/ws/socket.io/" }), onError() {} }).close(); `]); }); const consumer = ` import { createPlatformClient, type PlatformEvent } from "@base44/platform"; -const platform = createPlatformClient({ socketUrl: "https://example.test", getSessionToken: async () => "token" }); -platform.builder.init({ onError() {} }).subscribe("a".repeat(24), { +const platform = createPlatformClient(); +platform.builder.init({ getSession: async () => (await fetch("/session")).json(), onError() {} }).subscribe("a".repeat(24), { onSnapshot(snapshot) { void snapshot.messages; }, onEvent(event: PlatformEvent) { if (event.type === "message.updated") { diff --git a/packages/platform/tests/types/client.types.ts b/packages/platform/tests/types/client.types.ts index 695fcfa..6750e82 100644 --- a/packages/platform/tests/types/client.types.ts +++ b/packages/platform/tests/types/client.types.ts @@ -1,6 +1,19 @@ -import { createPlatformClient, type PlatformEvent, type PlatformEventMap, type PlatformSnapshot, type ToolCall, type GuardApproval, type Message, type StatusObject } from "@base44/platform"; -const client = createPlatformClient({ socketUrl: "https://example.test", getSessionToken: async () => "session-token" }); -const builder = client.builder.init({ onError: error => { void error.code; } }); +import { createPlatformClient, type SocketSession, type PlatformEvent, type PlatformEventMap, type PlatformSnapshot, type ToolCall, type GuardApproval, type Message, type StatusObject } from "@base44/platform"; +const client = createPlatformClient(); +const builder = client.builder.init({ + // The Create socket session body passes through, extra keys included. + getSession: async () => ({ session_id: "s1", session_token: "t", socket_url: "https://example.test", socket_path: "/ws/socket.io/", expires_in: 3600 }), + onError: error => { void error.code; }, +}); +client.builder.init({ getSession: async () => (await fetch("/session")).json(), onError() {} }); +const body: SocketSession = { session_token: "t", socket_url: "https://example.test", socket_path: "/ws/socket.io/" }; +void body; +// @ts-expect-error The session needs its socket path. +client.builder.init({ getSession: async () => ({ session_token: "t", socket_url: "https://example.test" }), onError() {} }); +// @ts-expect-error The session comes from init, not the client. +createPlatformClient({ socketUrl: "https://example.test", getSessionToken: async () => "token" }); +// @ts-expect-error A session is required. +client.builder.init({ onError() {} }); const subscription = builder.subscribe("a".repeat(24), { onSnapshot(snapshot: PlatformSnapshot) { const state: "ready" | "processing" | "error" | null | undefined = snapshot.status?.state; @@ -45,8 +58,6 @@ client.send("write_file", {}); createPlatformClient({ apiKey: "private" }); // @ts-expect-error Socket methods belong to the builder session. client.connect(); -// @ts-expect-error The retired token callback is not accepted. -createPlatformClient({ socketUrl: "https://example.test", refreshToken: async () => "token" }); const tool: ToolCall = { name: "ask_clarifying_questions", diff --git a/packages/platform/tests/unit/client.test.ts b/packages/platform/tests/unit/client.test.ts index c4e2915..0f4ad83 100644 --- a/packages/platform/tests/unit/client.test.ts +++ b/packages/platform/tests/unit/client.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { createPlatformClient, type BuilderSession } from "../../src/index.js"; +import { createPlatformClient, PlatformSocketError, type BuilderSession } from "../../src/index.js"; const fake = vi.hoisted(() => { const handlers: Record void> = {}; @@ -21,75 +21,88 @@ const snapshot = (messages: unknown[] = []) => fake.handlers["app.snapshot"]({ r const update = (data: unknown = { status: null }) => fake.handlers["app.status_changed"]({ room, data }); const notice = (name: string, data: unknown = {}, at: string | null = room) => fake.handlers[name]({ room: at, data }); const denied = (retryable: boolean) => fake.handlers.connect_error(Object.assign(new Error("connection_denied"), { data: { retryable } })); +const session = (fields: Record = {}) => ({ + session_id: "s1", session_token: "session-token", socket_url: "https://api.example.test", socket_path: "/ws/socket.io/", expires_in: 3600, ...fields, +}); const auth = async () => { const callback = vi.fn(); (fake.io.mock.calls.at(-1) as any)[1].auth(callback); await settle(); return callback; }; -function setup(getSessionToken = vi.fn(async () => "session-token")) { +function setup(getSession = vi.fn(async (): Promise => session())) { const onError = vi.fn(); - const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken }); - const client = platform.builder.init({ onError }); + const client = createPlatformClient().builder.init({ getSession, onError }); clients.push(client); - return { client, getSessionToken, onError }; + return { client, getSession, onError }; +} +async function begin(client: BuilderSession) { + client.connect().catch(() => {}); await settle(); } async function connected(client: BuilderSession) { - const ready = client.connect(); fake.socket.connected = true; fake.handlers.connect(); await ready; await settle(); + const ready = client.connect(); await settle(); fake.socket.connected = true; fake.handlers.connect(); await ready; await settle(); } const joins = () => fake.socket.emit.mock.calls.filter(c => c[0] === "join"); beforeEach(() => { vi.clearAllMocks(); fake.socket.connected = false; }); afterEach(() => { clients.splice(0).forEach(client => client.close()); vi.useRealTimers(); }); describe("platform client", () => { - test("constructing the root client does not initialize sockets or fetch session tokens", () => { - const getSessionToken = vi.fn(async () => "token"); - const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken }); - expect(fake.io).not.toHaveBeenCalled(); - const first = platform.builder.init({ onError: vi.fn() }); - const second = platform.builder.init({ onError: vi.fn() }); + test("creating the client and initializing sessions opens no socket and fetches no session", () => { + const getSession = vi.fn(async () => session()); + const platform = createPlatformClient(); + const first = platform.builder.init({ getSession, onError: vi.fn() }); + const second = platform.builder.init({ getSession, onError: vi.fn() }); clients.push(first, second); expect(first).not.toBe(second); - expect(fake.io).toHaveBeenCalledTimes(2); - expect(fake.socket.connect).not.toHaveBeenCalled(); - expect(getSessionToken).not.toHaveBeenCalled(); + expect(fake.io).not.toHaveBeenCalled(); + expect(getSession).not.toHaveBeenCalled(); }); - test("closing one initialized builder does not close another or the root module", () => { - const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken: async () => "token" }); + test("closing one initialized builder does not close another or the root module", async () => { const firstSocket = { ...fake.socket, disconnect: vi.fn() }; const secondSocket = { ...fake.socket, disconnect: vi.fn() }; fake.io.mockReturnValueOnce(firstSocket).mockReturnValueOnce(secondSocket); - const first = platform.builder.init({ onError: vi.fn() }); - const second = platform.builder.init({ onError: vi.fn() }); - clients.push(first, second); + const { client: first } = setup(), { client: second } = setup(); + await begin(first); await begin(second); first.close(); expect(firstSocket.disconnect).toHaveBeenCalledOnce(); expect(secondSocket.disconnect).not.toHaveBeenCalled(); }); - test("connects to the shared platform socket with the session token in CONNECT auth only", async () => { - const { getSessionToken } = setup(); + test("opens the socket at the session's URL and path, with the token in CONNECT auth only", async () => { + const { client, getSession } = setup(vi.fn(async () => session({ socket_url: "https://live.example.test/", socket_path: "/partner/socket.io/" }))); + await begin(client); const [url, options] = fake.io.mock.calls[0] as unknown as [string, any]; - expect(url).toBe("https://api.example.test"); - expect(options).toMatchObject({ path: "/ws/socket.io/", transports: ["websocket"], autoConnect: false, forceNew: true, reconnectionAttempts: 5 }); + expect(url).toBe("https://live.example.test"); + expect(options).toMatchObject({ path: "/partner/socket.io/", transports: ["websocket"], autoConnect: false, forceNew: true, reconnectionAttempts: 5 }); expect(options.query).toBeUndefined(); + expect(fake.socket.connect).toHaveBeenCalledOnce(); expect(await auth()).toHaveBeenLastCalledWith({ session_token: "session-token" }); - expect(getSessionToken).toHaveBeenCalledOnce(); + expect(getSession).toHaveBeenCalledOnce(); + }); + + test("ignores the session body's other keys", async () => { + const { client } = setup(vi.fn(async () => session({ future_field: { nested: true } }))); + await begin(client); + const [, options] = fake.io.mock.calls[0] as unknown as [string, any]; + for (const key of ["session_id", "expires_in", "future_field"]) expect(options).not.toHaveProperty(key); + const callback = await auth(); + expect(callback.mock.calls[0][0]).toStrictEqual({ session_token: "session-token" }); }); - test("transport reconnects reuse the session token instead of opening a new session", async () => { - const { client, getSessionToken } = setup(); - await auth(); await connected(client); + test("transport reconnects reuse the session instead of opening a new one", async () => { + const { client, getSession } = setup(); + await connected(client); fake.handlers.disconnect("transport close"); expect(await auth()).toHaveBeenLastCalledWith({ session_token: "session-token" }); - expect(getSessionToken).toHaveBeenCalledOnce(); + expect(getSession).toHaveBeenCalledOnce(); + expect(fake.io).toHaveBeenCalledOnce(); }); test("a rejected cached token is renewed once, and a rejected fresh one is reported", async () => { - const { client, getSessionToken, onError } = setup(); - await auth(); await connected(client); + const { client, getSession, onError } = setup(); + await connected(client); fake.handlers.disconnect("transport close"); fake.socket.connected = false; fake.socket.connect.mockClear(); denied(false); expect(fake.socket.connect).toHaveBeenCalledOnce(); - getSessionToken.mockResolvedValue("renewed"); + getSession.mockResolvedValue(session({ session_token: "renewed" })); expect(await auth()).toHaveBeenLastCalledWith({ session_token: "renewed" }); denied(false); expect(fake.socket.connect).toHaveBeenCalledOnce(); @@ -100,27 +113,56 @@ describe("platform client", () => { vi.useFakeTimers(); const { client, onError } = setup(); const failure = expect(client.connect()).rejects.toMatchObject({ code: "connection_failed" }); - fake.socket.connect.mockClear(); + await settle(); fake.socket.connect.mockClear(); for (let i = 0; i < 5; i++) { denied(true); await vi.advanceTimersByTimeAsync(10000); } expect(fake.socket.connect).toHaveBeenCalledTimes(5); denied(true); await failure; expect(onError.mock.calls[0][0].code).toBe("connection_failed"); }); - test("an expired session reconnects with a new session token", async () => { - const { client, getSessionToken, onError } = setup(); - await auth(); await connected(client); fake.socket.connect.mockClear(); + test("an expired session reconnects with a new session", async () => { + const { client, getSession, onError } = setup(); + await connected(client); fake.socket.connect.mockClear(); notice("session.ended", { reason: "expired" }, null); fake.handlers.disconnect("io server disconnect"); expect(fake.socket.connect).toHaveBeenCalledOnce(); - getSessionToken.mockResolvedValue("next-session"); + getSession.mockResolvedValue(session({ session_token: "next-session" })); expect(await auth()).toHaveBeenLastCalledWith({ session_token: "next-session" }); + expect(fake.io).toHaveBeenCalledOnce(); + expect(onError).not.toHaveBeenCalled(); + }); + + test("a renewed session at a new address moves the socket and rejoins every subscription", async () => { + const { client, getSession, onError } = setup(); + client.subscribe(app, { onSnapshot: vi.fn(), onEvent: vi.fn(), onError: vi.fn() }); + client.subscribe(other, { onSnapshot: vi.fn(), onEvent: vi.fn(), onError: vi.fn() }); + await connected(client); + const handlers: Record void> = {}; + const moved = { + ...fake.socket, connected: false, on: vi.fn((name, handler) => { handlers[name] = handler; }), + emit: vi.fn(), connect: vi.fn(), disconnect: vi.fn(), removeAllListeners: vi.fn(), + io: { on: vi.fn(), removeAllListeners: vi.fn() }, + }; + fake.io.mockReturnValueOnce(moved); + getSession.mockResolvedValue(session({ session_token: "moved", socket_url: "https://eu.example.test", socket_path: "/eu/socket.io/" })); + notice("session.ended", { reason: "expired" }, null); + fake.handlers.disconnect("io server disconnect"); + expect(await auth()).not.toHaveBeenCalled(); + expect(fake.socket.removeAllListeners).toHaveBeenCalledOnce(); + expect(fake.socket.disconnect).toHaveBeenCalledOnce(); + const [url, options] = fake.io.mock.calls.at(-1) as unknown as [string, any]; + expect([url, options.path]).toEqual(["https://eu.example.test", "/eu/socket.io/"]); + expect(moved.connect).toHaveBeenCalledOnce(); + expect(await auth()).toHaveBeenLastCalledWith({ session_token: "moved" }); + moved.connected = true; handlers.connect(); + expect(moved.emit.mock.calls).toEqual([["join", room], ["join", `/apps/${other}`]]); + expect(getSession).toHaveBeenCalledTimes(2); expect(onError).not.toHaveBeenCalled(); }); test("a replaced session stops without reconnecting", async () => { const { client, onError } = setup(); - await auth(); await connected(client); fake.socket.connect.mockClear(); + await connected(client); fake.socket.connect.mockClear(); notice("session.ended", { reason: "replaced" }, null); fake.handlers.disconnect("io server disconnect"); expect(fake.socket.connect).not.toHaveBeenCalled(); @@ -128,39 +170,79 @@ describe("platform client", () => { }); test("a revoked session stops without asking for a new one", async () => { - const { client, getSessionToken, onError } = setup(); - await auth(); await connected(client); fake.socket.connect.mockClear(); getSessionToken.mockClear(); + const { client, getSession, onError } = setup(); + await connected(client); fake.socket.connect.mockClear(); getSession.mockClear(); notice("session.ended", { reason: "revoked" }, null); fake.handlers.disconnect("io server disconnect"); expect(fake.socket.connect).not.toHaveBeenCalled(); - expect(getSessionToken).not.toHaveBeenCalled(); + expect(getSession).not.toHaveBeenCalled(); expect(onError.mock.calls[0][0].code).toBe("session_revoked"); }); - test("shares concurrent connects; session-token failures never reach Socket.IO", async () => { + test("shares concurrent connects; session failures never reach Socket.IO", async () => { const { client, onError } = setup(vi.fn(async () => { throw new Error("secret"); })); const a = client.connect(), b = client.connect(); expect(a).toBe(b); - const rejection = expect(a).rejects.toMatchObject({ code: "session_unavailable" }); - const callback = await auth(); - await rejection; - expect(callback).not.toHaveBeenCalled(); + await expect(a).rejects.toMatchObject({ code: "session_unavailable" }); + expect(fake.io).not.toHaveBeenCalled(); expect(onError.mock.calls[0][0].message).not.toContain("secret"); }); - test("session-token retrieval has a bounded timeout", async () => { + test("a failed renewal stops the socket without authenticating it", async () => { + const { client, getSession, onError } = setup(); + await connected(client); + getSession.mockRejectedValue(new Error("secret")); + notice("session.ended", { reason: "expired" }, null); + fake.handlers.disconnect("io server disconnect"); + expect(await auth()).not.toHaveBeenCalled(); + expect(fake.socket.disconnect).toHaveBeenCalledOnce(); + expect(onError.mock.calls[0][0].code).toBe("session_unavailable"); + }); + + test("session retrieval has a bounded timeout", async () => { vi.useFakeTimers(); - const { client } = setup(vi.fn(() => new Promise(() => {}))); + const { client } = setup(vi.fn(() => new Promise(() => {}))); const failure = expect(client.connect()).rejects.toMatchObject({ code: "session_unavailable" }); - const callback = vi.fn(); (fake.io.mock.calls[0] as any)[1].auth(callback); await vi.advanceTimersByTimeAsync(20000); await failure; + expect(fake.io).not.toHaveBeenCalled(); + }); + + test("a late session opens no socket after close", async () => { + let resolve!: (value: unknown) => void; + const { client } = setup(vi.fn(() => new Promise(r => { resolve = r; }))); + await begin(client); client.close(); resolve(session()); await settle(); + expect(fake.io).not.toHaveBeenCalled(); + }); + + test("a late renewed session cannot authenticate after close", async () => { + let resolve!: (value: unknown) => void; + const { client, getSession } = setup(); + await connected(client); + getSession.mockImplementation(() => new Promise(r => { resolve = r; })); + notice("session.ended", { reason: "expired" }, null); + fake.handlers.disconnect("io server disconnect"); + const callback = await auth(); + client.close(); resolve(session({ session_token: "secret" })); await settle(); expect(callback).not.toHaveBeenCalled(); + expect(fake.io).toHaveBeenCalledOnce(); }); - test("late session tokens cannot authenticate after close", async () => { - let resolve!: (token: string) => void; - const { client } = setup(vi.fn(() => new Promise(r => { resolve = r; }))); - const callback = vi.fn(); (fake.io.mock.calls[0] as any)[1].auth(callback); - await settle(); client.close(); resolve("secret"); await settle(); expect(callback).not.toHaveBeenCalled(); + test.each([ + { session_token: "", socket_url: "https://example.test", socket_path: "/ws/socket.io/" }, + { session_token: "t", socket_url: "https://secret@example.test", socket_path: "/ws/socket.io/" }, + { session_token: "t", socket_url: "https://example.test?token=secret", socket_path: "/ws/socket.io/" }, + { session_token: "t", socket_url: "https://example.test/path", socket_path: "/ws/socket.io/" }, + { session_token: "t", socket_url: "ws://example.test", socket_path: "/ws/socket.io/" }, + { session_token: "t", socket_url: "example.test", socket_path: "/ws/socket.io/" }, + { session_token: "t", socket_url: "https://example.test", socket_path: "ws/socket.io/" }, + { session_token: "t", socket_url: "https://example.test", socket_path: "//evil.test/socket.io/" }, + { session_token: "t", socket_url: "https://example.test", socket_path: "/ws/socket.io/?token=secret" }, + { session_token: "t", socket_url: "https://example.test" }, + "not a session", + ])("an invalid session is session_unavailable: %j", async body => { + const { client, onError } = setup(vi.fn(async () => body)); + await expect(client.connect()).rejects.toBeInstanceOf(PlatformSocketError); + expect(onError.mock.calls[0][0].code).toBe("session_unavailable"); + expect(fake.io).not.toHaveBeenCalled(); }); test("joins without metadata and delivers the snapshot, then events in order", async () => { @@ -289,10 +371,7 @@ describe("platform client", () => { expect(fake.socket.removeAllListeners).toHaveBeenCalledOnce(); }); - test("validates origins, app IDs, duplicate subscriptions and limits", () => { - for (const socketUrl of ["https://secret@example.test", "https://example.test?token=secret", "https://example.test/path", "ws://example.test"]) { - expect(() => createPlatformClient({ socketUrl, getSessionToken: () => "token" })).toThrow(TypeError); - } + test("validates app IDs, duplicate subscriptions and limits", () => { const { client } = setup(); const options = { onSnapshot: vi.fn(), onEvent: vi.fn(), onError: vi.fn() }; expect(() => client.subscribe("bad", options)).toThrow(TypeError); client.subscribe(app, options); expect(() => client.subscribe(app, options)).toThrow(TypeError); diff --git a/packages/platform/tests/unit/socket.test.js b/packages/platform/tests/unit/socket.test.js index bfc0e19..e98bfe1 100644 --- a/packages/platform/tests/unit/socket.test.js +++ b/packages/platform/tests/unit/socket.test.js @@ -30,11 +30,13 @@ test("real Socket.IO handshake, join, snapshot, reconnect and session renewal", }); }); await new Promise(resolve => http.listen(0, "127.0.0.1", resolve)); - const client = createPlatformClient({ - socketUrl: `http://127.0.0.1:${http.address().port}`, - getSessionToken: async () => `session-${++tokenCalls}`, + const builder = createPlatformClient().builder.init({ + getSession: async () => ({ + session_id: `id-${tokenCalls}`, session_token: `session-${++tokenCalls}`, expires_in: 3600, + socket_url: `http://127.0.0.1:${http.address().port}`, socket_path: "/live/socket.io/", + }), + onError: error => errors.push(error), }); - const builder = client.builder.init({ onError: error => errors.push(error) }); try { builder.subscribe(appId, { onSnapshot: s => { snapshots.push(s.messages[0].id); }, onEvent: vi.fn(), onError: error => errors.push(error) }); await builder.connect(); @@ -48,7 +50,7 @@ test("real Socket.IO handshake, join, snapshot, reconnect and session renewal", expect(joins).toEqual([[room], [room], [room]]); expect(handshakes.map(item => item.token)).toEqual(["session-1", "session-1", "session-2"]); for (const { url } of handshakes) { - expect(url.pathname).toBe("/ws/socket.io/"); + expect(url.pathname).toBe("/live/socket.io/"); expect(url.searchParams.get("transport")).toBe("websocket"); expect([...url.searchParams.keys()].some(key => /token/i.test(key))).toBe(false); } From 95678d9e62e4a0713c9cadc71b1637ec3883d4fc Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sun, 11 Oct 2026 15:41:45 +0300 Subject: [PATCH 3/3] ci(platform): sync asyncapi.json from production every week A weekly workflow (and workflow_dispatch) fetches https://app.base44.com/api/asyncapi.json, regenerates the event types, builds the docs and runs the Platform SDK tests, and opens or updates one rolling PR on bot/platform-asyncapi-sync only when asyncapi.json or the generated types changed. It closes that PR when main already matches production. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/sync-asyncapi.yml | 92 +++++++++++++++++++++ packages/platform/scripts/sync-asyncapi.mjs | 2 +- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sync-asyncapi.yml diff --git a/.github/workflows/sync-asyncapi.yml b/.github/workflows/sync-asyncapi.yml new file mode 100644 index 0000000..83d57c4 --- /dev/null +++ b/.github/workflows/sync-asyncapi.yml @@ -0,0 +1,92 @@ +name: Sync Platform SDK AsyncAPI + +# Refreshes packages/platform/asyncapi.json from production and regenerates the event +# types from it. Changes go to one rolling PR; closing it without merging only skips +# that week, since the next run proposes the same changes again. + +on: + schedule: + - cron: "0 6 * * 1" # Mondays at 06:00 UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: sync-asyncapi + cancel-in-progress: false + +env: + BRANCH: bot/platform-asyncapi-sync + ASYNCAPI_URL: https://app.base44.com/api/asyncapi.json + +jobs: + sync: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: main + + - name: Wix gateway proxy (mandatory) + uses: ./.github/actions/wix-gateway-proxy + + - name: Use Node.js 20.x + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "20.x" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Sync asyncapi.json and regenerate the event types + run: | + npm run sync:asyncapi -w @base44/platform -- "$ASYNCAPI_URL" + npm run gen:events -w @base44/platform + + - name: Check for changes + id: changes + run: | + if git diff --quiet -- packages/platform/asyncapi.json packages/platform/src/modules/builder.events.generated.ts; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "✅ asyncapi.json and the event types are up to date" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Close a stale sync PR + if: steps.changes.outputs.changed == 'false' + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') + if [ -n "$pr" ]; then gh pr close "$pr" --delete-branch --comment "main already matches production."; fi + + - name: Build the docs and test the Platform SDK + if: steps.changes.outputs.changed == 'true' + run: | + npm run create-docs -w @base44/platform + npm test -w @base44/platform + + - name: Update the sync PR + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + git add packages/platform/asyncapi.json packages/platform/src/modules/builder.events.generated.ts + git commit -m "chore(platform): sync asyncapi.json and the event types from production" + # Rebuilt from main every run, so the branch never drifts or conflicts. + git push --force origin "$BRANCH" + + pr=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') + body="Refreshes \`packages/platform/asyncapi.json\` from $ASYNCAPI_URL and regenerates \`builder.events.generated.ts\`. The docs build and the Platform SDK tests passed on this change. Updated automatically every week." + if [ -z "$pr" ]; then + gh pr create --head "$BRANCH" --base main --title "Sync Platform SDK events from production" --body "$body" + fi diff --git a/packages/platform/scripts/sync-asyncapi.mjs b/packages/platform/scripts/sync-asyncapi.mjs index 326010e..8b750a3 100644 --- a/packages/platform/scripts/sync-asyncapi.mjs +++ b/packages/platform/scripts/sync-asyncapi.mjs @@ -2,7 +2,7 @@ // Replaces asyncapi.json with the document a backend serves, production by default. Production // publishes only beta and GA messages; for alpha ones point this at a dev backend, e.g. a local apper: // npm run sync:asyncapi -- http://localhost:8000/api/asyncapi.json -// or set ASYNCAPI_URL. Then run `npm run gen:events`. +// or set ASYNCAPI_URL. Then run `npm run gen:events`. The weekly sync-asyncapi workflow does both. import { writeFile } from "node:fs/promises"; const url = process.argv[2] ?? process.env.ASYNCAPI_URL ?? "https://app.base44.com/api/asyncapi.json";