diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 820e887b..f474541c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -32,5 +32,8 @@ jobs: - name: Run unit tests run: npm run test:unit -w @base44/sdk + - name: Check the Platform SDK event types match asyncapi.json + run: npm run check:events -w @base44/platform + - name: Test the Platform SDK run: npm test -w @base44/platform diff --git a/package-lock.json b/package-lock.json index f4fed864..b10aa8f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,24 @@ "vitest": "^4.1.9" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -597,6 +615,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", @@ -1026,6 +1051,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", @@ -3676,6 +3708,52 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-typescript": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-16.0.0.tgz", + "integrity": "sha512-Ah6kK4q6SjlMzWBH1eNOQkdRh5Qhr5T/RCFwfjQqWZA7UDW+N47A8wfyoiN0L884s4L4bAEI54IjIuN3/u0B/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.25", + "js-yaml": "^5.2.3", + "lodash": "^4.18.1", + "minimist": "^1.2.8", + "prettier": "^3.9.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/js-yaml": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4031,6 +4109,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4552,6 +4637,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -5881,6 +5982,9 @@ "license": "MIT", "dependencies": { "socket.io-client": "^4.8.3" + }, + "devDependencies": { + "json-schema-to-typescript": "16.0.0" } }, "packages/sdk": { diff --git a/packages/platform/README.md b/packages/platform/README.md index 4450e697..451ac8a5 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -10,10 +10,10 @@ shared socket. It is independent of the app SDK (`@base44/sdk`). The workspace must have white-label sockets enabled; this SDK does not open sessions. ```ts -import { Base44PlatformClient } from "@base44/platform"; +import { createPlatformClient } from "@base44/platform"; -const client = new Base44PlatformClient({ - serverUrl: "https://base44.app", +const client = createPlatformClient({ + socketUrl, // the session's socket_url, from POST /api/service/socket-sessions async getSessionToken() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); if (!response.ok) throw new Error("Session request failed"); @@ -39,7 +39,7 @@ 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` to the browser. Never pass the workspace key or any other server +`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 backend adds or removes apps with `PUT`/`DELETE /api/service/socket-sessions/{session_id}/rooms/{app_id}`. @@ -55,7 +55,7 @@ older one, which stops with `session_replaced`. Open one session per page. ## Lifecycle -`new Base44PlatformClient({ serverUrl, getSessionToken })` creates a lightweight module +`createPlatformClient({ socketUrl, getSessionToken })` creates a lightweight module container: no Socket.IO instance, timers, token request or network activity. Its `builder` module follows the server SDK's module-factory pattern. @@ -94,60 +94,53 @@ their exceptions are isolated so they cannot interrupt another app's delivery. ## Public shapes -- `PlatformClientOptions` (`client.types.ts`): `serverUrl`, `getSessionToken`. +- `PlatformClientOptions` (`client.types.ts`): `socketUrl`, `getSessionToken`. - `BuilderModule` (`modules/builder.types.ts`): lazy `init(options)` factory. - `BuilderInitOptions`: connection-level `onError` observer. - `BuilderSession`: `connect()`, `subscribe(appId, options)`, `close()`. - `SubscriptionOptions`: required `onSnapshot`, `onEvent` and `onError`. - `PlatformSubscription`: read-only `appId`, `active`, and `unsubscribe()`. -- Event/message models live in `modules/builder.events.types.ts`; error codes in `errors.types.ts`. - -Every exported shape and field has JSDoc. Run `npm run docs` for -validated reference pages in `docs`. `PlatformEvent` is a -**discriminated union**: switch on `event.type` to narrow `event.data`. Each delivery -contains `type`, `appId` and `data`. On the wire every event is `{room, data}`; the SDK -routes by `room` and validates the envelope, while payload schemas and private-field -filtering are the server's. Omitted keys mean unchanged and explicit null clears. -Events carrying `branch_id` belong to that branch; null or absent is main. Unknown -event names are not forwarded. - -| Event | Data contract | -| --- | --- | -| `message.updated` | `MessageUpdated`: the whole `message`, optional `conversation_id`. Replace the message with the same `id`. | -| `message.removed` | `MessageRemoved`: `message_id`, optional `conversation_id`. Sent when a message is deleted or hidden; an unknown ID is a no-op. | -| `app.status_changed` | `AppStatusChanged`: `status` (`AppStatus`), null clears it. | -| `preview.reload_requested` | Reload the app preview. | -| `preview.navigation_requested` | `PreviewNavigationRequested`: `path`, and `force` to navigate even when the user moved away. | -| `queue.updated` | `QueueUpdated`: `items`, `is_paused`, optional `processed_item_id`. Replaces the entire queue. | -| `task.progressed` | `TaskProgressed`: optional `tool_call_id`, `message_id`, `event_type`, numeric `progress`. | -| `image.resolved` | `ImageResolved`: `placeholder_url`, `status` (`pending`, `completed`, `failed`), nullable `image_url`. Replace the placeholder wherever it appears. | -| `conversation.changed` | The conversation was rewritten. On main, a fresh snapshot follows. | -| `files.changed`, `branch.deleted`, `repository.changed`, `pull_request.changed` | Re-read signals with no content. | - -`ChatMessage` has optional `id`, `role` (`user`/`assistant`), text/null `content`, -`tool_calls`, timestamp-only `metadata.created_date`, `checkpoint_id`, and scalar -`additional_message_params` (`client_creation_id`, `plan_mode`, `plan_approved`, -`skip_ai_response`, `system_message_type`). `ToolCall` has optional `id`, `name`, -`status`, `requires_user_input`, `auto_approved`, `mutation_applied` and -`waiting_on.kind` (`approval`/`choice`/`input`). Each builder tool and guard declares -which of its parts are public; a tool with no declaration shows only these fields. - -| Field | Public contract | -| --- | --- | -| `arguments` | One of `ToolQuestionArguments`, `ToolSecretArguments`, `ToolPackageArguments`, `ToolPlanArguments`, `ToolPrdArguments` (`generate_prd`, the plan in plan mode) or `ToolMediaArguments`, narrowed by the tool's `name`. Absent for other tools and partial streaming arguments. | -| `display` | File activity has `file_paths` and optional `content_empty`; execution activity has `summary` and `writes_entities`; entity activity has `entity_name` and `record_count`. | -| `user_input` | `ToolQuestionInput` only, for clarifying-question answers (`selected_label` for single-select, `selected_labels` for multi-select). Secret-form values are never exposed. | -| `results` | Once the call is not waiting on a guard: `ToolMediaResult` for image generation, or a URL string for game images and backgrounds (the placeholder `image.resolved` replaces) and videos (the finished video). | -| `approval` | While a call waits on a safety guard: `ToolGuardApproval` with `guard`, and `reason` and `details` where the guard declares them (shell-command approvals carry only `guard`). | - -In plan mode the message `content` beside a `generate_prd` call can be empty; render the -plan from its arguments. Approving a plan, answering a question and approving a -guarded call are HTTP actions of your backend. `Snapshot` contains `room`, `status` -(`AppStatus`: optional `state` of `ready`/`processing`/`error`, nullable -`last_updated_date`, and nullable `turn_id`, the user message that started the turn), -`messages`, and `queue` (`SnapshotQueue`: the main branch's `items` and `is_paused`; -absent from older servers). Dates remain wire strings. Structural filtering -does not promise redaction of generated prose or user content. +- `PlatformEvent`, `PlatformEventMap`, `PlatformSnapshot`, `ToolCall`, `GuardApproval` + (`modules/builder.events.types.ts`), derived from the generated event types. +- Error codes in `errors.types.ts`. + +`PlatformEvent` is a **discriminated union**: switch on `event.type` to narrow +`event.data`. Each delivery contains `type`, `appId` and `data`. On the wire every +event is `{room, data}`; the SDK routes by `room` and validates the envelope, while +payload schemas and private-field filtering are the server's. Omitted keys mean +unchanged and explicit null clears. Events carrying `branch_id` belong to that branch; +null or absent is main. Unknown event names are not forwarded. `PlatformSnapshot` is +the service's `Snapshot` plus the app's `room`. Structural filtering does not promise +redaction of generated prose or user content. + +## Event types + +The socket protocol is defined by the service's AsyncAPI document, committed as +`asyncapi.json`. `src/modules/builder.events.generated.ts` is generated from it with +`json-schema-to-typescript`: one type per schema, named after the backend model +(`Message`, `StatusObject`, `QueueState`, one `*Call` per tool, one `*Approval` per +guard), plus `ServerEventMap` and `ClientMessageMap`. Never edit it by hand. + +To pick up a protocol change: + +```sh +# Production serves only beta and GA events, so point at a dev backend while events are alpha. +npm run sync:asyncapi -w @base44/platform -- http://localhost:8000/api/asyncapi.json +npm run gen:events -w @base44/platform +``` + +`sync:asyncapi` also reads `ASYNCAPI_URL`, defaults to production, and refuses a +document with no messages. CI runs `npm run check:events` and fails when the +generated file differs from what `asyncapi.json` produces. + +## Reference docs + +Every exported shape and field has JSDoc. `npm run docs` validates it into `docs`. +`npm run create-docs` runs the app SDK's TypeDoc and Mintlify pipeline for this package, +and `npm run copy-docs-local -- --target ` copies the result into +`developers/references/platform-sdk/docs`. The pipeline's config for this package lives +in `scripts/mintlify-post-processing/`. The Socket messages page is built in mintlify-docs from +the service's AsyncAPI document, not here. `PlatformSocketError` extends `Error` with `code` and optional `appId`. Original exceptions, payloads and credentials are never attached. diff --git a/packages/platform/asyncapi.json b/packages/platform/asyncapi.json new file mode 100644 index 00000000..71721d8b --- /dev/null +++ b/packages/platform/asyncapi.json @@ -0,0 +1,5879 @@ +{ + "asyncapi": "3.0.0", + "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." + }, + "servers": { + "platform": { + "host": "ws.base44.com", + "protocol": "wss", + "pathname": "/ws/socket.io/", + "description": "Send the session token in the Socket.IO CONNECT `auth` as `session_token`. A refused connect is a `connect_error` with `message: \"connection_denied\"` and `data: { retryable }`: retry with backoff when `retryable`, otherwise ask your server for a new session." + } + }, + "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.", + "messages": { + "MessageUpdatedEvent": { + "$ref": "#/components/messages/MessageUpdatedEvent" + }, + "MessageRemovedEvent": { + "$ref": "#/components/messages/MessageRemovedEvent" + }, + "AppStatusChangedEvent": { + "$ref": "#/components/messages/AppStatusChangedEvent" + }, + "PreviewReloadRequestedEvent": { + "$ref": "#/components/messages/PreviewReloadRequestedEvent" + }, + "PreviewNavigationRequestedEvent": { + "$ref": "#/components/messages/PreviewNavigationRequestedEvent" + }, + "QueueUpdatedEvent": { + "$ref": "#/components/messages/QueueUpdatedEvent" + }, + "TaskProgressedEvent": { + "$ref": "#/components/messages/TaskProgressedEvent" + }, + "ImageResolvedEvent": { + "$ref": "#/components/messages/ImageResolvedEvent" + }, + "ConversationChangedEvent": { + "$ref": "#/components/messages/ConversationChangedEvent" + }, + "FilesChangedEvent": { + "$ref": "#/components/messages/FilesChangedEvent" + }, + "BranchDeletedEvent": { + "$ref": "#/components/messages/BranchDeletedEvent" + }, + "RepositoryChangedEvent": { + "$ref": "#/components/messages/RepositoryChangedEvent" + }, + "PullRequestChangedEvent": { + "$ref": "#/components/messages/PullRequestChangedEvent" + }, + "SessionEndedEvent": { + "$ref": "#/components/messages/SessionEndedEvent" + }, + "RoomAccessDeniedEvent": { + "$ref": "#/components/messages/RoomAccessDeniedEvent" + }, + "RoomAccessRevokedEvent": { + "$ref": "#/components/messages/RoomAccessRevokedEvent" + }, + "RoomSnapshotUnavailableEvent": { + "$ref": "#/components/messages/RoomSnapshotUnavailableEvent" + }, + "AppSnapshotEvent": { + "$ref": "#/components/messages/AppSnapshotEvent" + }, + "JoinMessage": { + "$ref": "#/components/messages/JoinMessage" + }, + "LeaveMessage": { + "$ref": "#/components/messages/LeaveMessage" + } + } + } + }, + "operations": { + "sendMessageUpdatedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/MessageUpdatedEvent" + } + ], + "summary": "A chat message was added or replaced. Replace the message with the same `id`." + }, + "sendMessageRemovedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/MessageRemovedEvent" + } + ], + "summary": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op." + }, + "sendAppStatusChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/AppStatusChangedEvent" + } + ], + "summary": "The builder started, finished or failed a turn. Null status clears it." + }, + "sendPreviewReloadRequestedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/PreviewReloadRequestedEvent" + } + ], + "summary": "Reload the app preview." + }, + "sendPreviewNavigationRequestedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/PreviewNavigationRequestedEvent" + } + ], + "summary": "Show this page of the app in the preview." + }, + "sendQueueUpdatedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/QueueUpdatedEvent" + } + ], + "summary": "The whole prompt queue, after any change." + }, + "sendTaskProgressedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/TaskProgressedEvent" + } + ], + "summary": "Progress of a long-running tool, by `tool_call_id`." + }, + "sendImageResolvedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/ImageResolvedEvent" + } + ], + "summary": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears." + }, + "sendConversationChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/ConversationChangedEvent" + } + ], + "summary": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot." + }, + "sendFilesChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/FilesChangedEvent" + } + ], + "summary": "The app's files changed outside a chat turn." + }, + "sendBranchDeletedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/BranchDeletedEvent" + } + ], + "summary": "A branch was deleted." + }, + "sendRepositoryChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/RepositoryChangedEvent" + } + ], + "summary": "The imported app's repository changed." + }, + "sendPullRequestChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/PullRequestChangedEvent" + } + ], + "summary": "The imported app's pull request changed." + }, + "sendSessionEndedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/SessionEndedEvent" + } + ], + "summary": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next." + }, + "sendRoomAccessDeniedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$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." + }, + "sendRoomAccessRevokedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/RoomAccessRevokedEvent" + } + ], + "summary": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin." + }, + "sendRoomSnapshotUnavailableEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/RoomSnapshotUnavailableEvent" + } + ], + "summary": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot." + }, + "sendAppSnapshotEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/AppSnapshotEvent" + } + ], + "summary": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it." + }, + "receiveJoinMessage": { + "action": "receive", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/JoinMessage" + } + ], + "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." + }, + "receiveLeaveMessage": { + "action": "receive", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/LeaveMessage" + } + ], + "summary": "Leave an app's room. Its events stop." + } + }, + "components": { + "messages": { + "MessageUpdatedEvent": { + "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`.", + "payload": { + "$ref": "#/components/schemas/MessageUpdatedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/MessageRemovedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/AppStatusChangedEvent" + } + }, + "PreviewReloadRequestedEvent": { + "name": "preview.reload_requested", + "title": "preview.reload_requested", + "summary": "Reload the app preview.", + "description": "Reload the app preview.", + "payload": { + "$ref": "#/components/schemas/PreviewReloadRequestedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/PreviewNavigationRequestedEvent" + } + }, + "QueueUpdatedEvent": { + "name": "queue.updated", + "title": "queue.updated", + "summary": "The whole prompt queue, after any change.", + "description": "The whole prompt queue, after any change.", + "payload": { + "$ref": "#/components/schemas/QueueUpdatedEvent" + } + }, + "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`.", + "payload": { + "$ref": "#/components/schemas/TaskProgressedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/ImageResolvedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/ConversationChangedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/FilesChangedEvent" + } + }, + "BranchDeletedEvent": { + "name": "branch.deleted", + "title": "branch.deleted", + "summary": "A branch was deleted.", + "description": "A branch was deleted.", + "payload": { + "$ref": "#/components/schemas/BranchDeletedEvent" + } + }, + "RepositoryChangedEvent": { + "name": "repository.changed", + "title": "repository.changed", + "summary": "The imported app's repository changed.", + "description": "The imported app's repository changed.", + "payload": { + "$ref": "#/components/schemas/RepositoryChangedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/PullRequestChangedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/SessionEndedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/RoomAccessDeniedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/RoomAccessRevokedEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/RoomSnapshotUnavailableEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/AppSnapshotEvent" + } + }, + "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.", + "payload": { + "$ref": "#/components/schemas/JoinMessage" + } + }, + "LeaveMessage": { + "name": "leave", + "title": "leave", + "summary": "Leave an app's room. Its events stop.", + "description": "Leave an app's room. Its events stop.", + "payload": { + "$ref": "#/components/schemas/LeaveMessage" + } + } + }, + "schemas": { + "AccessRuleChangeDetails": { + "description": "An entity access-rule change waiting for approval.", + "properties": { + "entity_name": { + "description": "Entity whose access rules change.", + "title": "Entity Name", + "type": "string" + }, + "change_type": { + "description": "How the access rules change.", + "title": "Change Type", + "type": "string" + }, + "changed_ops": { + "description": "Access operations whose rules change.", + "items": { + "type": "string" + }, + "title": "Changed Ops", + "type": "array" + } + }, + "title": "AccessRuleChangeDetails", + "type": "object" + }, + "AppSnapshotEvent": { + "description": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/Snapshot" + } + }, + "required": [ + "room", + "data" + ], + "title": "AppSnapshotEvent", + "type": "object" + }, + "AppStatusChanged": { + "properties": { + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/StatusObject" + }, + { + "type": "null" + } + ] + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "status" + ], + "title": "AppStatusChanged", + "type": "object" + }, + "AppStatusChangedEvent": { + "description": "The builder started, finished or failed a turn. Null status clears it.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/AppStatusChanged" + } + }, + "required": [ + "room", + "data" + ], + "title": "AppStatusChangedEvent", + "type": "object" + }, + "AskClarifyingQuestionsCall": { + "description": "A `ask_clarifying_questions` call.", + "properties": { + "name": { + "const": "ask_clarifying_questions", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_input": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionInput" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "AskClarifyingQuestionsCall", + "type": "object" + }, + "AskPlanQuestionsCall": { + "description": "A `ask_plan_questions` call.", + "properties": { + "name": { + "const": "ask_plan_questions", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_input": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionInput" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "AskPlanQuestionsCall", + "type": "object" + }, + "BackendFunctionEntityDeleteApproval": { + "description": "Parked by the `backend_function_entity_delete` guard.", + "properties": { + "guard": { + "const": "backend_function_entity_delete", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/FunctionEntityDeleteDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "BackendFunctionEntityDeleteApproval", + "type": "object" + }, + "BashApproval": { + "description": "Parked by the `bash_approval` guard.", + "properties": { + "guard": { + "const": "bash_approval", + "title": "Guard", + "type": "string" + } + }, + "required": [ + "guard" + ], + "title": "BashApproval", + "type": "object" + }, + "BashCall": { + "description": "A `bash` call.", + "properties": { + "name": { + "const": "bash", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "BashCall", + "type": "object" + }, + "BranchChange": { + "description": "Re-read what changed; the event carries no content.", + "properties": { + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "BranchChange", + "type": "object" + }, + "BranchDeletedEvent": { + "description": "A branch was deleted.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "BranchDeletedEvent", + "type": "object" + }, + "ConversationChangedEvent": { + "description": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "ConversationChangedEvent", + "type": "object" + }, + "CreateEntityRecordsCall": { + "description": "A `create_entity_records` call.", + "properties": { + "name": { + "const": "create_entity_records", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityRecordsDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "CreateEntityRecordsCall", + "type": "object" + }, + "DeleteEntitiesCall": { + "description": "A `delete_entities` call.", + "properties": { + "name": { + "const": "delete_entities", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityChangeArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "DeleteEntitiesCall", + "type": "object" + }, + "DeleteFileCall": { + "description": "A `delete_file` call.", + "properties": { + "name": { + "const": "delete_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "DeleteFileCall", + "type": "object" + }, + "EditRepoFileCall": { + "description": "A `edit_repo_file` call.", + "properties": { + "name": { + "const": "edit_repo_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "EditRepoFileCall", + "type": "object" + }, + "EntityChangeArguments": { + "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.", + "title": "Summary", + "type": "string" + } + }, + "title": "EntityChangeArguments", + "type": "object" + }, + "EntityDisplay": { + "description": "The entity an entity operation touched.", + "properties": { + "entity_name": { + "description": "Entity type. Records and query values are never included.", + "title": "Entity Name", + "type": "string" + } + }, + "title": "EntityDisplay", + "type": "object" + }, + "EntityMutationDetails": { + "description": "What a parked entity mutation would do. Record data is never included.", + "properties": { + "method": { + "description": "Entity method the code calls, such as `delete`.", + "title": "Method", + "type": "string" + }, + "summary": { + "description": "The agent's summary of what the code does.", + "title": "Summary", + "type": "string" + } + }, + "title": "EntityMutationDetails", + "type": "object" + }, + "EntityRecordsDisplay": { + "properties": { + "entity_name": { + "description": "Entity type. Records and query values are never included.", + "title": "Entity Name", + "type": "string" + }, + "record_count": { + "description": "Number of records created.", + "title": "Record Count", + "type": "integer" + } + }, + "title": "EntityRecordsDisplay", + "type": "object" + }, + "EntityRlsGuardApproval": { + "description": "Parked by the `entity_rls_guard` guard.", + "properties": { + "guard": { + "const": "entity_rls_guard", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/AccessRuleChangeDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "EntityRlsGuardApproval", + "type": "object" + }, + "ExecDisplay": { + "description": "What a code execution did.", + "properties": { + "summary": { + "description": "Reviewed activity summary. Code and output are never included.", + "title": "Summary", + "type": "string" + }, + "writes_entities": { + "description": "The code changed entity data: refresh the preview.", + "title": "Writes Entities", + "type": "boolean" + } + }, + "title": "ExecDisplay", + "type": "object" + }, + "ExecToolCall": { + "description": "A `exec_tool` call.", + "properties": { + "name": { + "const": "exec_tool", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "ExecToolCall", + "type": "object" + }, + "ExecToolEntityMutationApproval": { + "description": "Parked by the `exec_tool_entity_mutation` guard.", + "properties": { + "guard": { + "const": "exec_tool_entity_mutation", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityMutationDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolEntityMutationApproval", + "type": "object" + }, + "ExecToolGithubRepoWriteApproval": { + "description": "Parked by the `exec_tool_github_repo_write` guard.", + "properties": { + "guard": { + "const": "exec_tool_github_repo_write", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolGithubRepoWriteApproval", + "type": "object" + }, + "ExecToolInviteUserApproval": { + "description": "Parked by the `exec_tool_invite_user` guard.", + "properties": { + "guard": { + "const": "exec_tool_invite_user", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolInviteUserApproval", + "type": "object" + }, + "ExecToolSendEmailApproval": { + "description": "Parked by the `exec_tool_send_email` guard.", + "properties": { + "guard": { + "const": "exec_tool_send_email", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolSendEmailApproval", + "type": "object" + }, + "ExecToolSendSmsApproval": { + "description": "Parked by the `exec_tool_send_sms` guard.", + "properties": { + "guard": { + "const": "exec_tool_send_sms", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolSendSmsApproval", + "type": "object" + }, + "FileDisplay": { + "description": "The files a file operation touched.", + "properties": { + "file_paths": { + "description": "Changed file paths. Source bodies and diffs are never included.", + "items": { + "type": "string" + }, + "title": "File Paths", + "type": "array" + }, + "content_empty": { + "description": "A write intentionally used empty content.", + "title": "Content Empty", + "type": "boolean" + } + }, + "title": "FileDisplay", + "type": "object" + }, + "FilesChangedEvent": { + "description": "The app's files changed outside a chat turn.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "FilesChangedEvent", + "type": "object" + }, + "FindReplaceCall": { + "description": "A `find_replace` call.", + "properties": { + "name": { + "const": "find_replace", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "FindReplaceCall", + "type": "object" + }, + "FunctionEntityDeleteDetails": { + "description": "A backend function test that would delete entity records.", + "properties": { + "function_name": { + "description": "Backend function under test.", + "title": "Function Name", + "type": "string" + }, + "method": { + "description": "Entity method the function calls.", + "title": "Method", + "type": "string" + }, + "inspection_failed": { + "description": "The guard could not verify the function's source.", + "title": "Inspection Failed", + "type": "boolean" + } + }, + "title": "FunctionEntityDeleteDetails", + "type": "object" + }, + "GameImageArguments": { + "description": "What to generate.", + "properties": { + "aspect_ratio": { + "description": "Requested aspect ratio, such as `1:1`.", + "title": "Aspect Ratio", + "type": "string" + } + }, + "title": "GameImageArguments", + "type": "object" + }, + "GenerateGameBackgroundCall": { + "description": "A `generate_game_background` call.", + "properties": { + "name": { + "const": "generate_game_background", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "results": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Results" + } + }, + "required": [ + "name" + ], + "title": "GenerateGameBackgroundCall", + "type": "object" + }, + "GenerateGameImageCall": { + "description": "A `generate_game_image` call.", + "properties": { + "name": { + "const": "generate_game_image", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/GameImageArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "results": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Results" + } + }, + "required": [ + "name" + ], + "title": "GenerateGameImageCall", + "type": "object" + }, + "GenerateImageCall": { + "description": "A `generate_image` call.", + "properties": { + "name": { + "const": "generate_image", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/MediaArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "results": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageResult" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "GenerateImageCall", + "type": "object" + }, + "GeneratePrdCall": { + "description": "A `generate_prd` call.", + "properties": { + "name": { + "const": "generate_prd", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/PrdArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "GeneratePrdCall", + "type": "object" + }, + "GenerateVideoCall": { + "description": "A `generate_video` call.", + "properties": { + "name": { + "const": "generate_video", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/MediaArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "results": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Results" + } + }, + "required": [ + "name" + ], + "title": "GenerateVideoCall", + "type": "object" + }, + "ImageResolved": { + "properties": { + "status": { + "anyOf": [ + { + "enum": [ + "pending", + "completed", + "failed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "placeholder_url": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Placeholder Url" + }, + "image_url": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Image Url" + } + }, + "title": "ImageResolved", + "type": "object" + }, + "ImageResolvedEvent": { + "description": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/ImageResolved" + } + }, + "required": [ + "room", + "data" + ], + "title": "ImageResolvedEvent", + "type": "object" + }, + "ImageResult": { + "description": "The generated image's state.", + "properties": { + "placeholder_url": { + "description": "Placeholder URL, used to associate an `image.resolved` event with this tool.", + "title": "Placeholder Url", + "type": "string" + }, + "status": { + "description": "Current generation status.", + "enum": [ + "pending", + "completed", + "failed" + ], + "title": "Status", + "type": "string" + }, + "image_url": { + "type": [ + "string", + "null" + ], + "description": "Generated image URL, once available.", + "title": "Image Url" + } + }, + "title": "ImageResult", + "type": "object" + }, + "InstallNpmPackageCall": { + "description": "A `install_npm_package` call.", + "properties": { + "name": { + "const": "install_npm_package", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/InstallPackageArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "InstallNpmPackageCall", + "type": "object" + }, + "InstallPackageArguments": { + "description": "The package operations the builder runs.", + "properties": { + "packages": { + "description": "Requested package operations.", + "items": { + "$ref": "#/components/schemas/PackageOperation" + }, + "title": "Packages", + "type": "array" + } + }, + "title": "InstallPackageArguments", + "type": "object" + }, + "JoinMessage": { + "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.", + "pattern": "^/apps/[0-9a-f]{24}$", + "title": "JoinMessage", + "type": "string" + }, + "LeaveMessage": { + "description": "Leave an app's room. Its events stop.", + "pattern": "^/apps/[0-9a-f]{24}$", + "title": "LeaveMessage", + "type": "string" + }, + "MediaArguments": { + "description": "What to generate.", + "properties": { + "label": { + "description": "Visible media label.", + "title": "Label", + "type": "string" + }, + "aspect_ratio": { + "description": "Requested aspect ratio, such as `16:9`.", + "title": "Aspect Ratio", + "type": "string" + } + }, + "title": "MediaArguments", + "type": "object" + }, + "Message": { + "description": "A public chat message. Replace the message with the same `id` as a whole.", + "properties": { + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "role": { + "anyOf": [ + { + "enum": [ + "user", + "assistant", + "system" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Role" + }, + "content": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Content" + }, + "tool_calls": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AskClarifyingQuestionsCall" + }, + { + "$ref": "#/components/schemas/AskPlanQuestionsCall" + }, + { + "$ref": "#/components/schemas/BashCall" + }, + { + "$ref": "#/components/schemas/CreateEntityRecordsCall" + }, + { + "$ref": "#/components/schemas/DeleteEntitiesCall" + }, + { + "$ref": "#/components/schemas/DeleteFileCall" + }, + { + "$ref": "#/components/schemas/EditRepoFileCall" + }, + { + "$ref": "#/components/schemas/ExecToolCall" + }, + { + "$ref": "#/components/schemas/FindReplaceCall" + }, + { + "$ref": "#/components/schemas/GenerateGameBackgroundCall" + }, + { + "$ref": "#/components/schemas/GenerateGameImageCall" + }, + { + "$ref": "#/components/schemas/GenerateImageCall" + }, + { + "$ref": "#/components/schemas/GeneratePrdCall" + }, + { + "$ref": "#/components/schemas/GenerateVideoCall" + }, + { + "$ref": "#/components/schemas/InstallNpmPackageCall" + }, + { + "$ref": "#/components/schemas/PreviewExecuteCodeCall" + }, + { + "$ref": "#/components/schemas/PreviewScreenshotCall" + }, + { + "$ref": "#/components/schemas/ReadOnlyExecToolCall" + }, + { + "$ref": "#/components/schemas/ReadRepoFileCall" + }, + { + "$ref": "#/components/schemas/RunShellCommandCall" + }, + { + "$ref": "#/components/schemas/SetSecretsCall" + }, + { + "$ref": "#/components/schemas/UpdateEntitiesCall" + }, + { + "$ref": "#/components/schemas/UpdatePlanCall" + }, + { + "$ref": "#/components/schemas/WriteFileCall" + }, + { + "$ref": "#/components/schemas/WriteRepoFileCall" + }, + { + "$ref": "#/components/schemas/OtherToolCall" + } + ] + }, + "type": [ + "array", + "null" + ], + "default": null, + "title": "Tool Calls" + }, + "checkpoint_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Checkpoint Id" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageMetadata" + }, + { + "type": "null" + } + ], + "default": null + }, + "additional_message_params": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageParams" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Additional Message Params" + } + }, + "title": "Message", + "type": "object" + }, + "MessageMetadata": { + "description": "Message timestamp, without author identity.", + "properties": { + "created_date": { + "format": "date-time", + "type": [ + "string", + "null" + ], + "default": null, + "title": "Created Date" + } + }, + "title": "MessageMetadata", + "type": "object" + }, + "MessageParams": { + "description": "Scalar parameters a chat UI needs while a turn runs.", + "properties": { + "client_creation_id": { + "description": "Identifier the sending client attached, to match its optimistic message.", + "title": "Client Creation Id", + "type": "string" + }, + "plan_mode": { + "description": "The message was sent in plan mode.", + "title": "Plan Mode", + "type": "boolean" + }, + "plan_approved": { + "description": "The message approves a plan.", + "title": "Plan Approved", + "type": "boolean" + }, + "skip_ai_response": { + "description": "The message expects no AI reply.", + "title": "Skip Ai Response", + "type": "boolean" + }, + "system_message_type": { + "description": "System message category.", + "title": "System Message Type", + "type": "string" + } + }, + "title": "MessageParams", + "type": "object" + }, + "MessageRemoved": { + "properties": { + "message_id": { + "title": "Message Id", + "type": "string" + }, + "conversation_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Conversation Id" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "message_id" + ], + "title": "MessageRemoved", + "type": "object" + }, + "MessageRemovedEvent": { + "description": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/MessageRemoved" + } + }, + "required": [ + "room", + "data" + ], + "title": "MessageRemovedEvent", + "type": "object" + }, + "MessageUpdated": { + "properties": { + "message": { + "$ref": "#/components/schemas/Message" + }, + "conversation_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Conversation Id" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "message" + ], + "title": "MessageUpdated", + "type": "object" + }, + "MessageUpdatedEvent": { + "description": "A chat message was added or replaced. Replace the message with the same `id`.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/MessageUpdated" + } + }, + "required": [ + "room", + "data" + ], + "title": "MessageUpdatedEvent", + "type": "object" + }, + "OtherApproval": { + "description": "Parked by a guard that publishes only its name.", + "properties": { + "guard": { + "not": { + "enum": [ + "backend_function_entity_delete", + "bash_approval", + "entity_rls_guard", + "exec_tool_entity_mutation", + "exec_tool_github_repo_write", + "exec_tool_invite_user", + "exec_tool_send_email", + "exec_tool_send_sms" + ] + }, + "title": "Guard", + "type": "string" + } + }, + "required": [ + "guard" + ], + "title": "OtherApproval", + "type": "object" + }, + "OtherToolCall": { + "description": "A call to a tool that publishes only its name and progress.", + "properties": { + "name": { + "not": { + "enum": [ + "ask_clarifying_questions", + "ask_plan_questions", + "bash", + "create_entity_records", + "delete_entities", + "delete_file", + "edit_repo_file", + "exec_tool", + "find_replace", + "generate_game_background", + "generate_game_image", + "generate_image", + "generate_prd", + "generate_video", + "install_npm_package", + "preview_execute_code", + "preview_screenshot", + "read_only_exec_tool", + "read_repo_file", + "run_shell_command", + "set_secrets", + "update_entities", + "update_plan", + "write_file", + "write_repo_file" + ] + }, + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + } + }, + "required": [ + "name" + ], + "title": "OtherToolCall", + "type": "object" + }, + "PackageOperation": { + "description": "A package operation. Versions and package-manager output are never included.", + "properties": { + "name": { + "description": "Package name.", + "title": "Name", + "type": "string" + }, + "action": { + "description": "Requested operation, such as `install`.", + "title": "Action", + "type": "string" + } + }, + "title": "PackageOperation", + "type": "object" + }, + "PlanUpdate": { + "description": "An add-only plan update.", + "properties": { + "action": { + "description": "Update action.", + "title": "Action", + "type": "string" + }, + "section": { + "description": "Plan section key.", + "title": "Section", + "type": "string" + }, + "section_label": { + "description": "User-facing section label.", + "title": "Section Label", + "type": "string" + }, + "text": { + "description": "Plan point.", + "title": "Text", + "type": "string" + } + }, + "title": "PlanUpdate", + "type": "object" + }, + "PrdArguments": { + "description": "The user's own plan. In plan mode the message `content` beside it can be empty.", + "properties": { + "app_name": { + "description": "App or feature name.", + "title": "App Name", + "type": "string" + }, + "intent_and_goal": { + "description": "What is being built and why.", + "title": "Intent And Goal", + "type": "string" + }, + "audience_and_roles": { + "description": "Who uses it and their roles.", + "title": "Audience And Roles", + "type": "string" + }, + "core_flows": { + "description": "End-to-end user flows.", + "items": { + "type": "string" + }, + "title": "Core Flows", + "type": "array" + }, + "technical_requirements": { + "description": "Specific technical requirements.", + "title": "Technical Requirements", + "type": "string" + }, + "design_preferences": { + "description": "Design preferences.", + "title": "Design Preferences", + "type": "string" + }, + "prd_type": { + "description": "Build or plan declaration, when the builder asks for one.", + "title": "Prd Type", + "type": "string" + }, + "store_platform": { + "description": "Store platform settled in planning, for e-commerce plans.", + "title": "Store Platform", + "type": "string" + }, + "game_engine": { + "description": "Engine settled in planning, for game plans.", + "title": "Game Engine", + "type": "string" + } + }, + "title": "PrdArguments", + "type": "object" + }, + "PreviewExecuteCodeCall": { + "description": "A `preview_execute_code` call.", + "properties": { + "name": { + "const": "preview_execute_code", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "PreviewExecuteCodeCall", + "type": "object" + }, + "PreviewNavigationRequested": { + "properties": { + "path": { + "title": "Path", + "type": "string" + }, + "force": { + "description": "Navigate even when the user has moved away from the page.", + "title": "Force", + "type": "boolean" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "path", + "force" + ], + "title": "PreviewNavigationRequested", + "type": "object" + }, + "PreviewNavigationRequestedEvent": { + "description": "Show this page of the app in the preview.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/PreviewNavigationRequested" + } + }, + "required": [ + "room", + "data" + ], + "title": "PreviewNavigationRequestedEvent", + "type": "object" + }, + "PreviewReloadRequested": { + "properties": { + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "PreviewReloadRequested", + "type": "object" + }, + "PreviewReloadRequestedEvent": { + "description": "Reload the app preview.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/PreviewReloadRequested" + } + }, + "required": [ + "room", + "data" + ], + "title": "PreviewReloadRequestedEvent", + "type": "object" + }, + "PreviewScreenshotCall": { + "description": "A `preview_screenshot` call.", + "properties": { + "name": { + "const": "preview_screenshot", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "PreviewScreenshotCall", + "type": "object" + }, + "PullRequestChangedEvent": { + "description": "The imported app's pull request changed.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "PullRequestChangedEvent", + "type": "object" + }, + "Question": { + "description": "A question for the user. Image and HTML sources and private design context are never included.", + "properties": { + "question": { + "description": "Visible question text.", + "title": "Question", + "type": "string" + }, + "type": { + "description": "Question category, such as `text`.", + "title": "Type", + "type": "string" + }, + "description": { + "description": "Supporting text.", + "title": "Description", + "type": "string" + }, + "multi_select": { + "description": "More than one answer may be selected.", + "title": "Multi Select", + "type": "boolean" + }, + "covers": { + "description": "Plan section the question covers.", + "title": "Covers", + "type": "string" + }, + "options": { + "description": "Selectable options; a bare string is its label.", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/QuestionOption" + } + ] + }, + "title": "Options", + "type": "array" + } + }, + "title": "Question", + "type": "object" + }, + "QuestionAnswer": { + "description": "An answer to a question. Secret-form input is never included.", + "properties": { + "question_index": { + "description": "Zero-based question index.", + "title": "Question Index", + "type": "integer" + }, + "selected_label": { + "description": "Label selected for a single-select question.", + "title": "Selected Label", + "type": "string" + }, + "selected_labels": { + "description": "Labels selected for a multi-select question.", + "items": { + "type": "string" + }, + "title": "Selected Labels", + "type": "array" + }, + "custom_text": { + "description": "Free-text answer. Structural filtering does not redact prose.", + "title": "Custom Text", + "type": "string" + } + }, + "title": "QuestionAnswer", + "type": "object" + }, + "QuestionArguments": { + "description": "The questions the builder asks.", + "properties": { + "questions": { + "description": "Questions presented to the user.", + "items": { + "$ref": "#/components/schemas/Question" + }, + "title": "Questions", + "type": "array" + } + }, + "title": "QuestionArguments", + "type": "object" + }, + "QuestionInput": { + "description": "The user's answers.", + "properties": { + "answers": { + "description": "Answers supplied to the question card.", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + }, + "title": "Answers", + "type": "array" + } + }, + "title": "QuestionInput", + "type": "object" + }, + "QuestionOption": { + "description": "A selectable answer.", + "properties": { + "label": { + "description": "Visible option label.", + "title": "Label", + "type": "string" + } + }, + "title": "QuestionOption", + "type": "object" + }, + "QueueItem": { + "description": "A prompt waiting in the queue.", + "properties": { + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "content": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Content" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + }, + "created_at": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Created At" + } + }, + "title": "QueueItem", + "type": "object" + }, + "QueueState": { + "description": "The prompt queue: the prompts the builder runs next, in order.", + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "type": [ + "array", + "null" + ], + "default": null, + "title": "Items" + }, + "is_paused": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Is Paused" + } + }, + "title": "QueueState", + "type": "object" + }, + "QueueUpdated": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "type": [ + "array", + "null" + ], + "default": null, + "title": "Items" + }, + "is_paused": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Is Paused" + }, + "processed_item_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Processed Item Id" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "QueueUpdated", + "type": "object" + }, + "QueueUpdatedEvent": { + "description": "The whole prompt queue, after any change.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/QueueUpdated" + } + }, + "required": [ + "room", + "data" + ], + "title": "QueueUpdatedEvent", + "type": "object" + }, + "ReadOnlyExecToolCall": { + "description": "A `read_only_exec_tool` call.", + "properties": { + "name": { + "const": "read_only_exec_tool", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "ReadOnlyExecToolCall", + "type": "object" + }, + "ReadRepoFileCall": { + "description": "A `read_repo_file` call.", + "properties": { + "name": { + "const": "read_repo_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "ReadRepoFileCall", + "type": "object" + }, + "RepositoryChangedEvent": { + "description": "The imported app's repository changed.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "RepositoryChangedEvent", + "type": "object" + }, + "RoomAccessDeniedEvent": { + "description": "A join was refused: the app is not on the session's allowlist, or the socket joins too often.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/RoomNotice" + } + }, + "required": [ + "room", + "data" + ], + "title": "RoomAccessDeniedEvent", + "type": "object" + }, + "RoomAccessRevokedEvent": { + "description": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/RoomNotice" + } + }, + "required": [ + "room", + "data" + ], + "title": "RoomAccessRevokedEvent", + "type": "object" + }, + "RoomNotice": { + "description": "The app is named by 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.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/RoomNotice" + } + }, + "required": [ + "room", + "data" + ], + "title": "RoomSnapshotUnavailableEvent", + "type": "object" + }, + "RunShellCommandCall": { + "description": "A `run_shell_command` call.", + "properties": { + "name": { + "const": "run_shell_command", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "RunShellCommandCall", + "type": "object" + }, + "SecretField": { + "description": "A requested secret. Its value is never sent over the socket.", + "properties": { + "secretName": { + "description": "Requested secret name.", + "title": "Secretname", + "type": "string" + }, + "description": { + "description": "Where to obtain it.", + "title": "Description", + "type": "string" + } + }, + "title": "SecretField", + "type": "object" + }, + "SessionEnded": { + "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.", + "enum": [ + "expired", + "revoked", + "replaced" + ], + "title": "Reason", + "type": "string" + } + }, + "required": [ + "reason" + ], + "title": "SessionEnded", + "type": "object" + }, + "SessionEndedEvent": { + "description": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/SessionEnded" + } + }, + "required": [ + "room", + "data" + ], + "title": "SessionEndedEvent", + "type": "object" + }, + "SetSecretsArguments": { + "description": "The secrets the builder asks for.", + "properties": { + "secrets_schema": { + "description": "Requested secrets.", + "items": { + "$ref": "#/components/schemas/SecretField" + }, + "title": "Secrets Schema", + "type": "array" + } + }, + "title": "SetSecretsArguments", + "type": "object" + }, + "SetSecretsCall": { + "description": "A `set_secrets` call.", + "properties": { + "name": { + "const": "set_secrets", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/SetSecretsArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "SetSecretsCall", + "type": "object" + }, + "Snapshot": { + "properties": { + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/StatusObject" + }, + { + "type": "null" + } + ] + }, + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "title": "Messages", + "type": "array" + }, + "queue": { + "$ref": "#/components/schemas/QueueState", + "description": "The main branch's prompt queue." + } + }, + "required": [ + "status", + "messages", + "queue" + ], + "title": "Snapshot", + "type": "object" + }, + "StatusObject": { + "description": "Public builder state, without error diagnostics or billing context.", + "properties": { + "state": { + "anyOf": [ + { + "enum": [ + "ready", + "processing", + "error" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "State" + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Turn Id" + }, + "last_updated_date": { + "format": "date-time", + "type": [ + "string", + "null" + ], + "default": null, + "title": "Last Updated Date" + } + }, + "title": "StatusObject", + "type": "object" + }, + "SummaryDetails": { + "description": "What a parked code execution would do.", + "properties": { + "summary": { + "description": "The agent's summary of what the code does.", + "title": "Summary", + "type": "string" + } + }, + "title": "SummaryDetails", + "type": "object" + }, + "SummaryDisplay": { + "description": "What a command or screenshot did.", + "properties": { + "summary": { + "description": "Reviewed activity summary. Commands and output are never included.", + "title": "Summary", + "type": "string" + } + }, + "title": "SummaryDisplay", + "type": "object" + }, + "TaskProgress": { + "properties": { + "current": { + "type": [ + "number", + "null" + ], + "default": null, + "title": "Current" + }, + "total": { + "type": [ + "number", + "null" + ], + "default": null, + "title": "Total" + }, + "percentage": { + "type": [ + "number", + "null" + ], + "default": null, + "title": "Percentage" + } + }, + "title": "TaskProgress", + "type": "object" + }, + "TaskProgressed": { + "properties": { + "tool_call_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Tool Call Id" + }, + "message_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Message Id" + }, + "event_type": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Event Type" + }, + "progress": { + "anyOf": [ + { + "$ref": "#/components/schemas/TaskProgress" + }, + { + "type": "null" + } + ], + "default": null + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "TaskProgressed", + "type": "object" + }, + "TaskProgressedEvent": { + "description": "Progress of a long-running tool, by `tool_call_id`.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/TaskProgressed" + } + }, + "required": [ + "room", + "data" + ], + "title": "TaskProgressedEvent", + "type": "object" + }, + "UpdateEntitiesCall": { + "description": "A `update_entities` call.", + "properties": { + "name": { + "const": "update_entities", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityChangeArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "UpdateEntitiesCall", + "type": "object" + }, + "UpdatePlanArguments": { + "description": "Settled plan points.", + "properties": { + "updates": { + "description": "Plan updates in their emitted order.", + "items": { + "$ref": "#/components/schemas/PlanUpdate" + }, + "title": "Updates", + "type": "array" + }, + "sections_with_enough": { + "description": "Base plan sections the builder considers sufficiently specified.", + "items": { + "type": "string" + }, + "title": "Sections With Enough", + "type": "array" + } + }, + "title": "UpdatePlanArguments", + "type": "object" + }, + "UpdatePlanCall": { + "description": "A `update_plan` call.", + "properties": { + "name": { + "const": "update_plan", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/UpdatePlanArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "UpdatePlanCall", + "type": "object" + }, + "WaitingOn": { + "description": "What a parked 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.", + "enum": [ + "choice", + "input", + "approval" + ], + "title": "Kind", + "type": "string" + } + }, + "title": "WaitingOn", + "type": "object" + }, + "WriteFileCall": { + "description": "A `write_file` call.", + "properties": { + "name": { + "const": "write_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "WriteFileCall", + "type": "object" + }, + "WriteRepoFileCall": { + "description": "A `write_repo_file` call.", + "properties": { + "name": { + "const": "write_repo_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "WriteRepoFileCall", + "type": "object" + } + } + } +} diff --git a/packages/platform/examples/client.ts b/packages/platform/examples/client.ts index 2ebd9674..a3f9d3f9 100644 --- a/packages/platform/examples/client.ts +++ b/packages/platform/examples/client.ts @@ -1,7 +1,9 @@ -import { Base44PlatformClient, type PlatformEvent } from "@base44/platform"; +import { createPlatformClient, type PlatformEvent } from "@base44/platform"; -const client = new Base44PlatformClient({ - serverUrl: "https://base44.app", +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 response = await fetch("/api/builder-socket-session", { method: "POST" }); diff --git a/packages/platform/package.json b/packages/platform/package.json index 09fd8654..4b42e101 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -20,12 +20,20 @@ }, "scripts": { "build": "tsc", + "gen:events": "node scripts/gen-events.mjs", + "check:events": "node scripts/gen-events.mjs --check", + "sync:asyncapi": "node scripts/sync-asyncapi.mjs", "docs": "typedoc", "test": "npm run test:types && npm run test:unit && npm run docs && npm run test:package", "test:types": "tsc --noEmit -p tsconfig.type-tests.json", "test:unit": "vitest run", "test:package": "npm run build && node --test tests/package.test.mjs", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "create-docs": "npm run create-docs:generate && npm run create-docs:process", + "create-docs-local": "npm run create-docs && npm run copy-docs-local", + "copy-docs-local": "node ../sdk/scripts/mintlify-post-processing/copy-to-local-docs.js --package-dir .", + "create-docs:generate": "node -e \"fs.rmSync('docs',{recursive:true,force:true})\" && typedoc --options typedoc.mintlify.json", + "create-docs:process": "node ../sdk/scripts/mintlify-post-processing/file-processing/file-processing.js --package-dir ." }, "dependencies": { "socket.io-client": "^4.8.3" @@ -45,5 +53,8 @@ "bugs": { "url": "https://github.com/base44/javascript-sdk/issues" }, - "homepage": "https://github.com/base44/javascript-sdk/tree/main/packages/platform#readme" + "homepage": "https://github.com/base44/javascript-sdk/tree/main/packages/platform#readme", + "devDependencies": { + "json-schema-to-typescript": "16.0.0" + } } diff --git a/packages/platform/scripts/gen-events.mjs b/packages/platform/scripts/gen-events.mjs new file mode 100644 index 00000000..9fc4fa31 --- /dev/null +++ b/packages/platform/scripts/gen-events.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// Generates src/modules/builder.events.generated.ts from asyncapi.json. `--check` fails on drift instead. +import { readFile, writeFile } from "node:fs/promises"; +import { compile } from "json-schema-to-typescript"; + +const source = new URL("../asyncapi.json", import.meta.url); +const target = new URL("../src/modules/builder.events.generated.ts", import.meta.url); +const doc = JSON.parse(await readFile(source, "utf8")); +const schemas = doc.components.schemas; +const refName = (ref) => ref.split("/").pop(); + +// Pydantic titles every property; only component names should become TypeScript names. +function prepare(node, top) { + if (Array.isArray(node)) return node.map((item) => prepare(item, false)); + if (!node || typeof node !== "object") return node; + const out = {}; + for (const [key, value] of Object.entries(node)) { + if (key === "$ref") out.$ref = `#/definitions/${refName(value)}`; + else if (key === "properties") { + out.properties = Object.fromEntries(Object.entries(value).map(([name, property]) => { + const prepared = prepare(property, false); + // TypeDoc requires every public property documented; fall back to the field title. + prepared.description ??= property.title ? `${sentence(property.title)}.` : `The \`${name}\` field.`; + // A description beside a bare $ref would document the referenced type instead of this property. + if (prepared.$ref) return [name, { anyOf: [{ $ref: prepared.$ref }], description: prepared.description }]; + return [name, prepared]; + })); + } else if (key === "title" && !top) continue; + else out[key] = prepare(value, false); + } + return out; +} +const sentence = (title) => title.charAt(0) + title.slice(1).toLowerCase().replace(/\b(id|url)\b/g, (w) => w.toUpperCase()); + +const definitions = Object.fromEntries(Object.entries(schemas).map(([name, schema]) => { + const prepared = prepare(schema, true); + prepared.title = name; + prepared.description ??= `The \`${name}\` schema.`; + return [name, prepared]; +})); + +const body = await compile( + { title: "AsyncApiDocument", type: "object", properties: {}, definitions }, + "AsyncApiDocument", + { additionalProperties: false, unreachableDefinitions: true, bannerComment: "", format: true, strictIndexSignatures: true }, +); +// The root exists only to carry the definitions. +const types = body + .replace("export interface AsyncApiDocument {}\n", "") + .replace(/^ \*\n \* This (?:interface|type) was referenced by `AsyncApiDocument`'s JSON-Schema\n \* via the `definition` "\w+"\.\n/gm, "") + .replace(/^\/\*\*\n \* This (?:interface|type) was referenced by `AsyncApiDocument`'s JSON-Schema\n \* via the `definition` "\w+"\.\n \*\/\n/gm, ""); +if (types.includes("AsyncApiDocument")) throw new Error("Could not strip the root type"); + +const messageEntries = (action) => Object.values(doc.operations) + .filter((operation) => operation.action === action) + .flatMap((operation) => operation.messages.map((ref) => doc.components.messages[refName(ref.$ref)])); +// The service sends `send` operations; every server event is a `{room, data}` frame. +const serverEvents = messageEntries("send").map((message) => { + const frame = schemas[refName(message.payload.$ref)]; + return { name: message.name, summary: message.summary, type: refName(frame.properties.data.$ref) }; +}); +const clientMessages = messageEntries("receive").map((message) => ({ + name: message.name, summary: message.summary, type: refName(message.payload.$ref), +})); +const mapEntries = (entries) => entries.map((entry) => ` /** ${entry.summary} */\n ${JSON.stringify(entry.name)}: ${entry.type};`).join("\n"); + +const output = `// Generated from asyncapi.json by scripts/gen-events.mjs. Do not edit; run \`npm run gen:events\`. + +${types.trim()} + +/** Every event the service sends, by name, mapped to its \`data\` payload. */ +export interface ServerEventMap { +${mapEntries(serverEvents)} +} + +/** Every message a client sends, by name, mapped to its payload. */ +export interface ClientMessageMap { +${mapEntries(clientMessages)} +} + +/** @internal */ +export const serverEventNames = ${JSON.stringify(serverEvents.map((event) => event.name))} as const; +`; + +if (process.argv.includes("--check")) { + const committed = await readFile(target, "utf8").catch(() => ""); + if (committed !== output) { + console.error("builder.events.generated.ts is out of date with asyncapi.json. Run `npm run gen:events -w @base44/platform` and commit the result."); + process.exit(1); + } + console.log("builder.events.generated.ts is up to date."); +} else { + await writeFile(target, output); + console.log(`Wrote ${Object.keys(schemas).length} schemas and ${serverEvents.length} server events.`); +} diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json new file mode 100644 index 00000000..dcce4030 --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -0,0 +1,11 @@ +{ + "interfaces/builder": [ + "interfaces/BuilderSession", + "type-aliases/PlatformEvent", + "interfaces/PlatformSnapshot" + ], + "classes/PlatformSocketError": [ + "type-aliases/SessionErrorCode", + "type-aliases/AppErrorCode" + ] +} diff --git a/packages/platform/scripts/mintlify-post-processing/category-map.json b/packages/platform/scripts/mintlify-post-processing/category-map.json new file mode 100644 index 00000000..06493959 --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/category-map.json @@ -0,0 +1,6 @@ +{ + "functions": "Client", + "interfaces": "Modules", + "type-aliases": "Modules", + "classes": "Errors" +} diff --git a/packages/platform/scripts/mintlify-post-processing/mintlify-target.json b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json new file mode 100644 index 00000000..1ce7a05c --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json @@ -0,0 +1,6 @@ +{ + "path": "developers/references/platform-sdk/docs", + "dropdown": "Platform SDK", + "group": "Platform SDK Reference", + "icon": "brackets-curly" +} diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json new file mode 100644 index 00000000..eb835b5a --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -0,0 +1,9 @@ +{ + "typesWithOwnMethods": { + "BuilderSession": "Session methods" + }, + "typeDefinitionGroups": [ + ["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 new file mode 100644 index 00000000..0c677790 --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json @@ -0,0 +1,9 @@ +[ + "AppErrorCode", + "BuilderModule", + "BuilderSession", + "PlatformEvent", + "PlatformSnapshot", + "PlatformSocketError", + "SessionErrorCode" +] diff --git a/packages/platform/scripts/sync-asyncapi.mjs b/packages/platform/scripts/sync-asyncapi.mjs new file mode 100644 index 00000000..a4535abd --- /dev/null +++ b/packages/platform/scripts/sync-asyncapi.mjs @@ -0,0 +1,21 @@ +#!/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: +// 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"; + +const url = process.argv[2] ?? process.env.ASYNCAPI_URL ?? "https://app.base44.com/api/asyncapi.json"; +const target = new URL("../asyncapi.json", import.meta.url); + +const response = await fetch(url); +if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`); +const document = await response.json(); +const messages = Object.keys(document.components?.messages ?? {}).length; +if (!messages) { + console.error(`${url} has no messages (production omits alpha events). Kept the committed copy.`); + console.error("Pass a dev backend URL instead, e.g. http://localhost:8000/api/asyncapi.json"); + process.exit(1); +} +await writeFile(target, JSON.stringify(document, null, 2) + "\n"); +console.log(`Wrote ${messages} messages from ${url}. Run npm run gen:events next.`); diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index ee9888d7..d3912c2e 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -1,18 +1,39 @@ -import type { PlatformClientOptions } from "./client.types.js"; +import type { PlatformClient, PlatformClientOptions } from "./client.types.js"; import { createBuilder } from "./modules/builder.js"; -import type { BuilderModule } from "./modules/builder.types.js"; -/** Browser platform client. Construction creates no sockets, timers or network requests. */ -export class Base44PlatformClient { - /** Lazy builder subscriptions with independent session lifecycles. */ - readonly builder: BuilderModule; - - /** Configure shared service/auth settings; each module initializes its own resources. */ - constructor(options: PlatformClientOptions) { - const url = new URL(options.serverUrl); - if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") { - throw new TypeError("serverUrl must be an HTTP(S) origin without credentials, path, query or fragment"); - } - this.builder = Object.freeze(createBuilder({ ...options, serverUrl: url.origin })); +/** + * Creates a platform client. + * + * 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). + * + * @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. + * + * @example + * ```typescript + * // Create a client for your app editor + * import { createPlatformClient } from '@base44/platform'; + * + * const client = createPlatformClient({ + * socketUrl, + * async getSessionToken() { + * const response = await fetch('/api/builder-socket-session', { method: 'POST' }); + * const { session_token } = await response.json(); + * return session_token; + * }, + * }); + * + * // 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 })) }); } diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index 09c6b939..c9dc5ff3 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -1,12 +1,38 @@ -/** Shared configuration for browser platform modules. Never supply an API key. */ +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 service, e.g. https://base44.app. No path/query/credentials. */ - serverUrl: string; /** - * Return a socket-session token from your backend, which opens the session with its - * workspace key (`POST /api/service/socket-sessions`) for the apps this page watches. - * Called when a builder session first connects, and again only after the server - * rejects or expires the current session. Sessions last one hour. + * 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. + * + * Provides access to the platform's modules. + */ +export interface PlatformClient { + /** [Builder module](/developers/references/platform-sdk/docs/interfaces/builder) for following the AI chat in your users' apps live. */ + readonly builder: BuilderModule; +} diff --git a/packages/platform/src/errors.ts b/packages/platform/src/errors.ts index 6d589c26..56d0fdee 100644 --- a/packages/platform/src/errors.ts +++ b/packages/platform/src/errors.ts @@ -1,13 +1,50 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; -/** Sanitized failure. Original session-token provider, handler and server exceptions are not retained. */ +/** + * The error the SDK reports when something goes wrong in a builder session. + * + * You don't create it. The SDK passes it to your `onError` callbacks, `connect()` rejects with it, + * and `subscribe()` throws it. Check `code` to see what happened, then look the code up below: + * - [Session error codes](#sessionerrorcode) go to the `onError` you pass to `init()`. The whole + * session is affected, and `appId` isn't set. + * - [App error codes](#apperrorcode) go to the subscription's `onError`. Only that app is affected, + * and `appId` says which one. + * + * The error never carries the session token, a callback's exception, or the server's exception text. + * + * @example + * ```typescript + * // Handle session and app errors + * const builder = client.builder.init({ + * // Session error codes: the whole session is affected + * onError(error) { + * if (error.code === 'session_revoked') showSignedOut(); + * }, + * }); + * + * builder.subscribe(appId, { + * onSnapshot: renderChat, + * onEvent: applyEvent, + * // App error codes: only this app is affected, and error.appId says which + * onError(error) { + * if (error.code === 'access_revoked') hideLiveUpdates(error.appId); + * }, + * }); + * ``` + */ export class PlatformSocketError extends Error { - /** Stable machine-readable category. */ + /** What happened. One of the [session error codes](#sessionerrorcode) or [app error codes](#apperrorcode). */ readonly code: PlatformSocketErrorCode; - /** Associated app, when the server identifies a valid room. */ + /** ID of the app the error is about. Set for app error codes, and not set for session error codes. */ readonly appId?: string; - /** Create a sanitized error with no credential-bearing cause or payload. */ + /** + * Creates an error. The SDK creates these; you don't need to. + * + * @param code - Error code. + * @param appId - ID of the app the error is about. + * @internal + */ constructor(code: PlatformSocketErrorCode, appId?: string) { super(`Platform socket: ${code}`); this.name = "PlatformSocketError"; diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index 6173bf07..2ac3fa15 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -1,5 +1,98 @@ -/** Server refusals and client transport/processing failures. */ -export type PlatformSocketErrorCode = - | "connection_denied" | "connection_failed" | "session_unavailable" | "session_revoked" | "session_replaced" - | "access_denied" | "access_revoked" | "snapshot_unavailable" | "subscription_limit" - | "delivery_overflow" | "protocol_error" | "handler_failed" | "client_closed"; +/** + * Codes of errors that affect the whole session. They go to the `onError` you pass to `init()`, and + * `connect()` rejects with them. Retryable means the same call can succeed again without changing anything. + */ +export type SessionErrorCode = + /** + * `getSessionToken` threw, took longer than 20 seconds or returned no token. + * + * Check the endpoint that opens sessions, then call `connect()` again. Retryable. + */ + | "session_unavailable" + /** + * Base44 refused the session even with a fresh token: the token is invalid, or live updates aren't enabled for the workspace. + * + * Check that your server opens a new session on every call. Not retryable. + */ + | "connection_denied" + /** + * The socket couldn't connect after five retries, or the server closed it. + * + * Call `connect()` again, for example when the tab next gets focus. Retryable. + */ + | "connection_failed" + /** + * Your server or an admin ended the session, or its key lost access. + * + * Open a new session from your server if the page should still follow apps. Not retryable. + */ + | "session_revoked" + /** + * Another socket connected with the same session token. + * + * Use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable. + */ + | "session_replaced" + /** + * A session frame didn't match the protocol. + * + * Update the SDK, then call `connect()` again. Not retryable. + */ + | "protocol_error" + /** + * You called `connect()` or `subscribe()` after `close()`. + * + * Create a new session with `init()`. Not retryable. + */ + | "client_closed"; + +/** + * Codes of errors for one app. They go to the subscription's `onError`, and `subscribe()` throws + * `subscription_limit`. Every one ends the subscription except `snapshot_unavailable`. + */ +export type AppErrorCode = + /** + * The app isn't on the session's allowlist, or the socket joins too often. + * + * Add the app to the session from your server, then subscribe again. Not retryable. + */ + | "access_denied" + /** + * The app left the session's allowlist, or its workspace. + * + * Stop showing the app's live updates. Not retryable. + */ + | "access_revoked" + /** + * The subscription is active but its snapshot failed. Events keep arriving. + * + * Load the app's messages over the Apps API instead. + */ + | "snapshot_unavailable" + /** + * The session already has eight subscriptions. + * + * Unsubscribe from an app, or open another session. Not retryable. + */ + | "subscription_limit" + /** + * More than 1,000 deliveries waited for your callbacks. + * + * Make the callbacks faster, then subscribe again for a fresh snapshot. Retryable. + */ + | "delivery_overflow" + /** + * Your `onSnapshot` or `onEvent` callback threw or rejected. + * + * Fix the callback, then subscribe again. Not retryable. + */ + | "handler_failed" + /** + * An event for this app didn't match the protocol. + * + * Update the SDK, then subscribe again. Not retryable. + */ + | "protocol_error"; + +/** Every code a {@link PlatformSocketError} reports. */ +export type PlatformSocketErrorCode = SessionErrorCode | AppErrorCode; diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index ce52f374..f5703a91 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -1,13 +1,8 @@ /** Browser platform modules, separate from the runtime and server SDKs. */ -export { Base44PlatformClient } from "./client.js"; +export { createPlatformClient } from "./client.js"; export { PlatformSocketError } from "./errors.js"; -export type { PlatformSocketErrorCode } from "./errors.types.js"; -export type { PlatformClientOptions } from "./client.types.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 { - AppStatus, AppStatusChanged, BranchScoped, ChatMessage, ImageResolved, MessageParams, MessageRemoved, MessageUpdated, - PlatformEvent, PlatformEventMap, PreviewNavigationRequested, QueueItem, QueueUpdated, Snapshot, SnapshotQueue, TaskProgressed, - ToolArguments, ToolCall, ToolDisplay, ToolEntityMutationArguments, ToolGuardApproval, ToolMediaArguments, ToolMediaResult, ToolPackageArguments, - ToolPackageOperation, ToolPlanArguments, ToolPlanUpdate, ToolPrdArguments, ToolQuestion, ToolQuestionAnswer, - ToolQuestionArguments, ToolQuestionInput, ToolQuestionOption, ToolSecretArguments, ToolSecretField, -} from "./modules/builder.events.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 bee66141..b9c9cb19 100644 --- a/packages/platform/src/modules/builder-protocol.ts +++ b/packages/platform/src/modules/builder-protocol.ts @@ -1,16 +1,24 @@ -import type { PlatformEvent, PlatformEventMap, Snapshot } from "./builder.events.types.js"; +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"; -export const eventNames = [ - "message.updated", "message.removed", "app.status_changed", "preview.reload_requested", - "preview.navigation_requested", "queue.updated", "task.progressed", "image.resolved", - "conversation.changed", "files.changed", "branch.deleted", "repository.changed", "pull_request.changed", -] as const satisfies readonly (keyof PlatformEventMap)[]; -/** Notices that name an app room, by the error code each one reports. */ +const sessionEvents: readonly string[] = ["app.snapshot", "session.ended"]; +/** App events delivered to `onEvent`: every server event except the session events the socket handles. */ +export const eventNames = serverEventNames.filter( + (name): name is keyof PlatformEventMap & typeof name => !sessionEvents.includes(name) && !name.startsWith("room."), +); +/** Notices that name an app room, by the error code each one reports. A new notice fails to compile until it has one. */ export const roomNotices = { "room.access_denied": "access_denied", "room.access_revoked": "access_revoked", "room.snapshot_unavailable": "snapshot_unavailable", -} as const; +} as const satisfies Record, PlatformSocketErrorCode>; +// `expired` never reaches the partner: the session renews its token. +export const sessionEndings = { + expired: "session_expired", + revoked: "session_revoked", + replaced: "session_replaced", +} as const satisfies Record; export const appPattern = /^[a-f0-9]{24}$/; export const roomFor = (appId: string) => `/apps/${appId}`; @@ -25,12 +33,12 @@ export function appFromRoom(value: unknown): string | undefined { export function decode(type: keyof PlatformEventMap, appId: string, raw: unknown): PlatformEvent { return { type, appId, data: object(object(raw).data) } as PlatformEvent; } -export function decodeSnapshot(raw: unknown): Snapshot { +export function decodeSnapshot(raw: unknown): PlatformSnapshot { const frame = object(raw); const data = object(frame.data); if (!appFromRoom(frame.room) || !Array.isArray(data.messages) || (data.status !== null && typeof data.status !== "object")) { throw new Error("Invalid snapshot"); } const queue = data.queue !== null && typeof data.queue === "object" ? { queue: data.queue } : {}; - return { room: frame.room, status: data.status, messages: data.messages, ...queue } as Snapshot; + return { room: frame.room, status: data.status, messages: data.messages, ...queue } as PlatformSnapshot; } diff --git a/packages/platform/src/modules/builder-socket.ts b/packages/platform/src/modules/builder-socket.ts index af128aa3..e8c0755a 100644 --- a/packages/platform/src/modules/builder-socket.ts +++ b/packages/platform/src/modules/builder-socket.ts @@ -1,6 +1,6 @@ import { io, type Socket } from "socket.io-client"; import { PlatformSocketError } from "../errors.js"; -import { appFromRoom, appPattern, decode, decodeSnapshot, eventNames, object, roomFor, roomNotices } from "./builder-protocol.js"; +import { appFromRoom, appPattern, decode, 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"; @@ -16,7 +16,7 @@ export class BuilderSocket implements BuilderSession { // The session token outlives reconnects; it is replaced only when the server rejects or expires it. private token?: string; private tokenFresh = false; - private ending?: "session_expired" | "session_revoked" | "session_replaced"; + private ending?: (typeof sessionEndings)[keyof typeof sessionEndings]; private denialRetries = 0; private retryTimer?: ReturnType; private authAttempt = 0; @@ -27,7 +27,7 @@ export class BuilderSocket implements BuilderSession { constructor(config: PlatformClientOptions, options: BuilderInitOptions) { this.options = { ...config, onError: options.onError }; - this.socket = io(config.serverUrl, { + this.socket = io(config.socketUrl, { path: "/ws/socket.io/", transports: ["websocket"], autoConnect: false, forceNew: true, reconnectionAttempts: 5, reconnectionDelay: 1000, reconnectionDelayMax: 10000, timeout: 20000, @@ -188,9 +188,8 @@ export class BuilderSocket implements BuilderSession { private sessionEnded(raw: unknown): void { try { const reason = object(object(raw).data).reason; - const ending = reason === "expired" ? "session_expired" - : reason === "revoked" ? "session_revoked" - : reason === "replaced" ? "session_replaced" : undefined; + const ending = typeof reason === "string" && Object.prototype.hasOwnProperty.call(sessionEndings, reason) + ? 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; diff --git a/packages/platform/src/modules/builder-subscription.ts b/packages/platform/src/modules/builder-subscription.ts index b5627320..186ab251 100644 --- a/packages/platform/src/modules/builder-subscription.ts +++ b/packages/platform/src/modules/builder-subscription.ts @@ -1,4 +1,4 @@ -import type { PlatformEvent, Snapshot } from "./builder.events.types.js"; +import type { PlatformEvent, PlatformSnapshot } from "./builder.events.types.js"; import { PlatformSocketError } from "../errors.js"; import type { PlatformSocketErrorCode } from "../errors.types.js"; import type { PlatformSubscription, SubscriptionOptions } from "./builder.types.js"; @@ -29,7 +29,7 @@ export class Subscription implements PlatformSubscription { this.enqueue(() => this.options.onEvent(event)); } - snapshot(snapshot: Snapshot): void { + snapshot(snapshot: PlatformSnapshot): void { this.enqueue(() => this.options.onSnapshot(snapshot)); } diff --git a/packages/platform/src/modules/builder.events.generated.ts b/packages/platform/src/modules/builder.events.generated.ts new file mode 100644 index 00000000..2aba1180 --- /dev/null +++ b/packages/platform/src/modules/builder.events.generated.ts @@ -0,0 +1,2540 @@ +// Generated from asyncapi.json by scripts/gen-events.mjs. Do not edit; run `npm run gen:events`. + +/** + * 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. + */ +export type JoinMessage = string; +/** + * Leave an app's room. Its events stop. + */ +export type LeaveMessage = string; + +/** + * An entity access-rule change waiting for approval. + */ +export interface AccessRuleChangeDetails { + /** + * Entity whose access rules change. + */ + entity_name?: string; + /** + * How the access rules change. + */ + change_type?: string; + /** + * Access operations whose rules change. + */ + changed_ops?: string[]; +} +/** + * The app's current status and its last 50 public messages, after every join. Replace the app's state with it. + */ +export interface AppSnapshotEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: Snapshot; +} +/** + * The `Snapshot` schema. + */ +export interface Snapshot { + /** + * The `status` field. + */ + status: StatusObject | null; + /** + * Messages. + */ + messages: Message[]; + /** + * The main branch's prompt queue. + */ + queue: QueueState; +} +/** + * Public builder state, without error diagnostics or billing context. + */ +export interface StatusObject { + /** + * State. + */ + state?: ("ready" | "processing" | "error") | null; + /** + * Turn ID. + */ + turn_id?: string | null; + /** + * Last updated date. + */ + last_updated_date?: string | null; +} +/** + * A public chat message. Replace the message with the same `id` as a whole. + */ +export interface Message { + /** + * Id. + */ + id?: string | null; + /** + * Role. + */ + role?: ("user" | "assistant" | "system") | null; + /** + * Content. + */ + content?: string | null; + /** + * Tool calls. + */ + tool_calls?: + | ( + | AskClarifyingQuestionsCall + | AskPlanQuestionsCall + | BashCall + | CreateEntityRecordsCall + | DeleteEntitiesCall + | DeleteFileCall + | EditRepoFileCall + | ExecToolCall + | FindReplaceCall + | GenerateGameBackgroundCall + | GenerateGameImageCall + | GenerateImageCall + | GeneratePrdCall + | GenerateVideoCall + | InstallNpmPackageCall + | PreviewExecuteCodeCall + | PreviewScreenshotCall + | ReadOnlyExecToolCall + | ReadRepoFileCall + | RunShellCommandCall + | SetSecretsCall + | UpdateEntitiesCall + | UpdatePlanCall + | WriteFileCall + | WriteRepoFileCall + | OtherToolCall + )[] + | null; + /** + * Checkpoint ID. + */ + checkpoint_id?: string | null; + /** + * The `metadata` field. + */ + metadata?: MessageMetadata | null; + /** + * Additional message params. + */ + additional_message_params?: MessageParams | null; +} +/** + * A `ask_clarifying_questions` call. + */ +export interface AskClarifyingQuestionsCall { + /** + * Name. + */ + name: "ask_clarifying_questions"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: QuestionArguments | null; + /** + * The `user_input` field. + */ + user_input?: QuestionInput | null; +} +/** + * What a parked call waits for. Render the widget by kind, not by tool name. + */ +export interface WaitingOn { + /** + * `choice`: pick among options. `input`: supply values. `approval`: approve or reject. + */ + kind?: "choice" | "input" | "approval"; +} +/** + * Parked by the `backend_function_entity_delete` guard. + */ +export interface BackendFunctionEntityDeleteApproval { + /** + * Guard. + */ + guard: "backend_function_entity_delete"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: FunctionEntityDeleteDetails | null; +} +/** + * A backend function test that would delete entity records. + */ +export interface FunctionEntityDeleteDetails { + /** + * Backend function under test. + */ + function_name?: string; + /** + * Entity method the function calls. + */ + method?: string; + /** + * The guard could not verify the function's source. + */ + inspection_failed?: boolean; +} +/** + * Parked by the `bash_approval` guard. + */ +export interface BashApproval { + /** + * Guard. + */ + guard: "bash_approval"; +} +/** + * Parked by the `entity_rls_guard` guard. + */ +export interface EntityRlsGuardApproval { + /** + * Guard. + */ + guard: "entity_rls_guard"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: AccessRuleChangeDetails | null; +} +/** + * Parked by the `exec_tool_entity_mutation` guard. + */ +export interface ExecToolEntityMutationApproval { + /** + * Guard. + */ + guard: "exec_tool_entity_mutation"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: EntityMutationDetails | null; +} +/** + * What a parked entity mutation would do. Record data is never included. + */ +export interface EntityMutationDetails { + /** + * Entity method the code calls, such as `delete`. + */ + method?: string; + /** + * The agent's summary of what the code does. + */ + summary?: string; +} +/** + * Parked by the `exec_tool_github_repo_write` guard. + */ +export interface ExecToolGithubRepoWriteApproval { + /** + * Guard. + */ + guard: "exec_tool_github_repo_write"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * What a parked code execution would do. + */ +export interface SummaryDetails { + /** + * The agent's summary of what the code does. + */ + summary?: string; +} +/** + * Parked by the `exec_tool_invite_user` guard. + */ +export interface ExecToolInviteUserApproval { + /** + * Guard. + */ + guard: "exec_tool_invite_user"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * Parked by the `exec_tool_send_email` guard. + */ +export interface ExecToolSendEmailApproval { + /** + * Guard. + */ + guard: "exec_tool_send_email"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * Parked by the `exec_tool_send_sms` guard. + */ +export interface ExecToolSendSmsApproval { + /** + * Guard. + */ + guard: "exec_tool_send_sms"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * Parked by a guard that publishes only its name. + */ +export interface OtherApproval { + /** + * Guard. + */ + guard: string; +} +/** + * The questions the builder asks. + */ +export interface QuestionArguments { + /** + * Questions presented to the user. + */ + questions?: Question[]; +} +/** + * A question for the user. Image and HTML sources and private design context are never included. + */ +export interface Question { + /** + * Visible question text. + */ + question?: string; + /** + * Question category, such as `text`. + */ + type?: string; + /** + * Supporting text. + */ + description?: string; + /** + * More than one answer may be selected. + */ + multi_select?: boolean; + /** + * Plan section the question covers. + */ + covers?: string; + /** + * Selectable options; a bare string is its label. + */ + options?: (string | QuestionOption)[]; +} +/** + * A selectable answer. + */ +export interface QuestionOption { + /** + * Visible option label. + */ + label?: string; +} +/** + * The user's answers. + */ +export interface QuestionInput { + /** + * Answers supplied to the question card. + */ + answers?: QuestionAnswer[]; +} +/** + * An answer to a question. Secret-form input is never included. + */ +export interface QuestionAnswer { + /** + * Zero-based question index. + */ + question_index?: number; + /** + * Label selected for a single-select question. + */ + selected_label?: string; + /** + * Labels selected for a multi-select question. + */ + selected_labels?: string[]; + /** + * Free-text answer. Structural filtering does not redact prose. + */ + custom_text?: string; +} +/** + * A `ask_plan_questions` call. + */ +export interface AskPlanQuestionsCall { + /** + * Name. + */ + name: "ask_plan_questions"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: QuestionArguments | null; + /** + * The `user_input` field. + */ + user_input?: QuestionInput | null; +} +/** + * A `bash` call. + */ +export interface BashCall { + /** + * Name. + */ + name: "bash"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: SummaryDisplay | null; +} +/** + * What a command or screenshot did. + */ +export interface SummaryDisplay { + /** + * Reviewed activity summary. Commands and output are never included. + */ + summary?: string; +} +/** + * A `create_entity_records` call. + */ +export interface CreateEntityRecordsCall { + /** + * Name. + */ + name: "create_entity_records"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: EntityRecordsDisplay | null; +} +/** + * The `EntityRecordsDisplay` schema. + */ +export interface EntityRecordsDisplay { + /** + * Entity type. Records and query values are never included. + */ + entity_name?: string; + /** + * Number of records created. + */ + record_count?: number; +} +/** + * A `delete_entities` call. + */ +export interface DeleteEntitiesCall { + /** + * Name. + */ + name: "delete_entities"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: EntityChangeArguments | null; + /** + * The `display` field. + */ + display?: EntityDisplay | null; +} +/** + * An entity update or delete; the entity is in `display.entity_name`. + */ +export interface EntityChangeArguments { + /** + * The agent's summary of the change. Queries and record values are never included. + */ + summary?: string; +} +/** + * The entity an entity operation touched. + */ +export interface EntityDisplay { + /** + * Entity type. Records and query values are never included. + */ + entity_name?: string; +} +/** + * A `delete_file` call. + */ +export interface DeleteFileCall { + /** + * Name. + */ + name: "delete_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * The files a file operation touched. + */ +export interface FileDisplay { + /** + * Changed file paths. Source bodies and diffs are never included. + */ + file_paths?: string[]; + /** + * A write intentionally used empty content. + */ + content_empty?: boolean; +} +/** + * A `edit_repo_file` call. + */ +export interface EditRepoFileCall { + /** + * Name. + */ + name: "edit_repo_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `exec_tool` call. + */ +export interface ExecToolCall { + /** + * Name. + */ + name: "exec_tool"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: ExecDisplay | null; +} +/** + * What a code execution did. + */ +export interface ExecDisplay { + /** + * Reviewed activity summary. Code and output are never included. + */ + summary?: string; + /** + * The code changed entity data: refresh the preview. + */ + writes_entities?: boolean; +} +/** + * A `find_replace` call. + */ +export interface FindReplaceCall { + /** + * Name. + */ + name: "find_replace"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `generate_game_background` call. + */ +export interface GenerateGameBackgroundCall { + /** + * Name. + */ + name: "generate_game_background"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * Results. + */ + results?: string | null; +} +/** + * A `generate_game_image` call. + */ +export interface GenerateGameImageCall { + /** + * Name. + */ + name: "generate_game_image"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: GameImageArguments | null; + /** + * Results. + */ + results?: string | null; +} +/** + * What to generate. + */ +export interface GameImageArguments { + /** + * Requested aspect ratio, such as `1:1`. + */ + aspect_ratio?: string; +} +/** + * A `generate_image` call. + */ +export interface GenerateImageCall { + /** + * Name. + */ + name: "generate_image"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: MediaArguments | null; + /** + * The `results` field. + */ + results?: ImageResult | null; +} +/** + * What to generate. + */ +export interface MediaArguments { + /** + * Visible media label. + */ + label?: string; + /** + * Requested aspect ratio, such as `16:9`. + */ + aspect_ratio?: string; +} +/** + * The generated image's state. + */ +export interface ImageResult { + /** + * Placeholder URL, used to associate an `image.resolved` event with this tool. + */ + placeholder_url?: string; + /** + * Current generation status. + */ + status?: "pending" | "completed" | "failed"; + /** + * Generated image URL, once available. + */ + image_url?: string | null; +} +/** + * A `generate_prd` call. + */ +export interface GeneratePrdCall { + /** + * Name. + */ + name: "generate_prd"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: PrdArguments | null; +} +/** + * The user's own plan. In plan mode the message `content` beside it can be empty. + */ +export interface PrdArguments { + /** + * App or feature name. + */ + app_name?: string; + /** + * What is being built and why. + */ + intent_and_goal?: string; + /** + * Who uses it and their roles. + */ + audience_and_roles?: string; + /** + * End-to-end user flows. + */ + core_flows?: string[]; + /** + * Specific technical requirements. + */ + technical_requirements?: string; + /** + * Design preferences. + */ + design_preferences?: string; + /** + * Build or plan declaration, when the builder asks for one. + */ + prd_type?: string; + /** + * Store platform settled in planning, for e-commerce plans. + */ + store_platform?: string; + /** + * Engine settled in planning, for game plans. + */ + game_engine?: string; +} +/** + * A `generate_video` call. + */ +export interface GenerateVideoCall { + /** + * Name. + */ + name: "generate_video"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: MediaArguments | null; + /** + * Results. + */ + results?: string | null; +} +/** + * A `install_npm_package` call. + */ +export interface InstallNpmPackageCall { + /** + * Name. + */ + name: "install_npm_package"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: InstallPackageArguments | null; +} +/** + * The package operations the builder runs. + */ +export interface InstallPackageArguments { + /** + * Requested package operations. + */ + packages?: PackageOperation[]; +} +/** + * A package operation. Versions and package-manager output are never included. + */ +export interface PackageOperation { + /** + * Package name. + */ + name?: string; + /** + * Requested operation, such as `install`. + */ + action?: string; +} +/** + * A `preview_execute_code` call. + */ +export interface PreviewExecuteCodeCall { + /** + * Name. + */ + name: "preview_execute_code"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: ExecDisplay | null; +} +/** + * A `preview_screenshot` call. + */ +export interface PreviewScreenshotCall { + /** + * Name. + */ + name: "preview_screenshot"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: SummaryDisplay | null; +} +/** + * A `read_only_exec_tool` call. + */ +export interface ReadOnlyExecToolCall { + /** + * Name. + */ + name: "read_only_exec_tool"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: ExecDisplay | null; +} +/** + * A `read_repo_file` call. + */ +export interface ReadRepoFileCall { + /** + * Name. + */ + name: "read_repo_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `run_shell_command` call. + */ +export interface RunShellCommandCall { + /** + * Name. + */ + name: "run_shell_command"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: SummaryDisplay | null; +} +/** + * A `set_secrets` call. + */ +export interface SetSecretsCall { + /** + * Name. + */ + name: "set_secrets"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: SetSecretsArguments | null; +} +/** + * The secrets the builder asks for. + */ +export interface SetSecretsArguments { + /** + * Requested secrets. + */ + secrets_schema?: SecretField[]; +} +/** + * A requested secret. Its value is never sent over the socket. + */ +export interface SecretField { + /** + * Requested secret name. + */ + secretName?: string; + /** + * Where to obtain it. + */ + description?: string; +} +/** + * A `update_entities` call. + */ +export interface UpdateEntitiesCall { + /** + * Name. + */ + name: "update_entities"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: EntityChangeArguments | null; + /** + * The `display` field. + */ + display?: EntityDisplay | null; +} +/** + * A `update_plan` call. + */ +export interface UpdatePlanCall { + /** + * Name. + */ + name: "update_plan"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: UpdatePlanArguments | null; +} +/** + * Settled plan points. + */ +export interface UpdatePlanArguments { + /** + * Plan updates in their emitted order. + */ + updates?: PlanUpdate[]; + /** + * Base plan sections the builder considers sufficiently specified. + */ + sections_with_enough?: string[]; +} +/** + * An add-only plan update. + */ +export interface PlanUpdate { + /** + * Update action. + */ + action?: string; + /** + * Plan section key. + */ + section?: string; + /** + * User-facing section label. + */ + section_label?: string; + /** + * Plan point. + */ + text?: string; +} +/** + * A `write_file` call. + */ +export interface WriteFileCall { + /** + * Name. + */ + name: "write_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `write_repo_file` call. + */ +export interface WriteRepoFileCall { + /** + * Name. + */ + name: "write_repo_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A call to a tool that publishes only its name and progress. + */ +export interface OtherToolCall { + /** + * Name. + */ + name: string; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; +} +/** + * Message timestamp, without author identity. + */ +export interface MessageMetadata { + /** + * Created date. + */ + created_date?: string | null; +} +/** + * Scalar parameters a chat UI needs while a turn runs. + */ +export interface MessageParams { + /** + * Identifier the sending client attached, to match its optimistic message. + */ + client_creation_id?: string; + /** + * The message was sent in plan mode. + */ + plan_mode?: boolean; + /** + * The message approves a plan. + */ + plan_approved?: boolean; + /** + * The message expects no AI reply. + */ + skip_ai_response?: boolean; + /** + * System message category. + */ + system_message_type?: string; +} +/** + * The prompt queue: the prompts the builder runs next, in order. + */ +export interface QueueState { + /** + * Items. + */ + items?: QueueItem[] | null; + /** + * Is paused. + */ + is_paused?: boolean | null; +} +/** + * A prompt waiting in the queue. + */ +export interface QueueItem { + /** + * Id. + */ + id?: string | null; + /** + * Content. + */ + content?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; + /** + * Created at. + */ + created_at?: string | null; +} +/** + * The `AppStatusChanged` schema. + */ +export interface AppStatusChanged { + /** + * The `status` field. + */ + status: StatusObject | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * The builder started, finished or failed a turn. Null status clears it. + */ +export interface AppStatusChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: AppStatusChanged; +} +/** + * Re-read what changed; the event carries no content. + */ +export interface BranchChange { + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * A branch was deleted. + */ +export interface BranchDeletedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot. + */ +export interface ConversationChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The app's files changed outside a chat turn. + */ +export interface FilesChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The `ImageResolved` schema. + */ +export interface ImageResolved { + /** + * Status. + */ + status?: ("pending" | "completed" | "failed") | null; + /** + * Placeholder URL. + */ + placeholder_url?: string | null; + /** + * Image URL. + */ + image_url?: string | null; +} +/** + * A generated image finished or failed. Replace its `placeholder_url` wherever it appears. + */ +export interface ImageResolvedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: ImageResolved; +} +/** + * The `MessageRemoved` schema. + */ +export interface MessageRemoved { + /** + * Message ID. + */ + message_id: string; + /** + * Conversation ID. + */ + conversation_id?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * A chat message was removed or hidden. An `id` the viewer never saw is a no-op. + */ +export interface MessageRemovedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: MessageRemoved; +} +/** + * The `MessageUpdated` schema. + */ +export interface MessageUpdated { + /** + * The `message` field. + */ + message: Message; + /** + * Conversation ID. + */ + conversation_id?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * A chat message was added or replaced. Replace the message with the same `id`. + */ +export interface MessageUpdatedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: MessageUpdated; +} +/** + * The `PreviewNavigationRequested` schema. + */ +export interface PreviewNavigationRequested { + /** + * Path. + */ + path: string; + /** + * Navigate even when the user has moved away from the page. + */ + force: boolean; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * Show this page of the app in the preview. + */ +export interface PreviewNavigationRequestedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: PreviewNavigationRequested; +} +/** + * The `PreviewReloadRequested` schema. + */ +export interface PreviewReloadRequested { + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * Reload the app preview. + */ +export interface PreviewReloadRequestedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: PreviewReloadRequested; +} +/** + * The imported app's pull request changed. + */ +export interface PullRequestChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The `QueueUpdated` schema. + */ +export interface QueueUpdated { + /** + * Items. + */ + items?: QueueItem[] | null; + /** + * Is paused. + */ + is_paused?: boolean | null; + /** + * Processed item ID. + */ + processed_item_id?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * The whole prompt queue, after any change. + */ +export interface QueueUpdatedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: QueueUpdated; +} +/** + * The imported app's repository changed. + */ +export interface RepositoryChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * A join was refused: the app is not on the session's allowlist, or the socket joins too often. + */ +export interface RoomAccessDeniedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: RoomNotice; +} +/** + * The app is named by the event's room. + */ +export interface RoomNotice {} +/** + * The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin. + */ +export interface RoomAccessRevokedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: RoomNotice; +} +/** + * The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot. + */ +export interface RoomSnapshotUnavailableEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: RoomNotice; +} +/** + * The `SessionEnded` schema. + */ +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. + */ + reason: "expired" | "revoked" | "replaced"; +} +/** + * The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next. + */ +export interface SessionEndedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: SessionEnded; +} +/** + * The `TaskProgress` schema. + */ +export interface TaskProgress { + /** + * Current. + */ + current?: number | null; + /** + * Total. + */ + total?: number | null; + /** + * Percentage. + */ + percentage?: number | null; +} +/** + * The `TaskProgressed` schema. + */ +export interface TaskProgressed { + /** + * Tool call ID. + */ + tool_call_id?: string | null; + /** + * Message ID. + */ + message_id?: string | null; + /** + * Event type. + */ + event_type?: string | null; + /** + * The `progress` field. + */ + progress?: TaskProgress | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * Progress of a long-running tool, by `tool_call_id`. + */ +export interface TaskProgressedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: TaskProgressed; +} + +/** Every event the service sends, by name, mapped to its `data` payload. */ +export interface ServerEventMap { + /** A chat message was added or replaced. Replace the message with the same `id`. */ + "message.updated": MessageUpdated; + /** A chat message was removed or hidden. An `id` the viewer never saw is a no-op. */ + "message.removed": MessageRemoved; + /** The builder started, finished or failed a turn. Null status clears it. */ + "app.status_changed": AppStatusChanged; + /** Reload the app preview. */ + "preview.reload_requested": PreviewReloadRequested; + /** Show this page of the app in the preview. */ + "preview.navigation_requested": PreviewNavigationRequested; + /** The whole prompt queue, after any change. */ + "queue.updated": QueueUpdated; + /** Progress of a long-running tool, by `tool_call_id`. */ + "task.progressed": TaskProgressed; + /** A generated image finished or failed. Replace its `placeholder_url` wherever it appears. */ + "image.resolved": ImageResolved; + /** The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot. */ + "conversation.changed": BranchChange; + /** The app's files changed outside a chat turn. */ + "files.changed": BranchChange; + /** A branch was deleted. */ + "branch.deleted": BranchChange; + /** The imported app's repository changed. */ + "repository.changed": BranchChange; + /** The imported app's pull request changed. */ + "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. */ + "room.access_denied": RoomNotice; + /** 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. */ + "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; +} + +/** Every message a client sends, by name, mapped to its payload. */ +export interface ClientMessageMap { + /** 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. */ + "join": JoinMessage; + /** Leave an app's room. Its events stop. */ + "leave": LeaveMessage; +} + +/** @internal */ +export const serverEventNames = ["message.updated","message.removed","app.status_changed","preview.reload_requested","preview.navigation_requested","queue.updated","task.progressed","image.resolved","conversation.changed","files.changed","branch.deleted","repository.changed","pull_request.changed","session.ended","room.access_denied","room.access_revoked","room.snapshot_unavailable","app.snapshot"] as const; diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/modules/builder.events.types.ts index e395203d..b3d46e7c 100644 --- a/packages/platform/src/modules/builder.events.types.ts +++ b/packages/platform/src/modules/builder.events.types.ts @@ -1,404 +1,63 @@ -/** Activity a file, execution or entity tool declared public. */ -export interface ToolDisplay { - /** Changed file paths for a file operation. Source bodies and diffs are never included. */ - file_paths?: string[]; - /** Whether a file write intentionally used empty content. */ - content_empty?: boolean; - /** Reviewed execution activity summary. Commands and execution output are never included. */ - summary?: string; - /** Whether a reviewed execution action changed entity data. */ - writes_entities?: boolean; - /** Entity type affected by an entity operation. Records and query values are never included. */ - entity_name?: string; - /** Number of records affected when supplied by the producer. */ - record_count?: number; -} - -/** A selectable answer to a builder question. */ -export interface ToolQuestionOption { - /** Visible option label. */ - label?: string; -} - -/** A builder question. Image/HTML source and private design context are excluded. */ -export interface ToolQuestion { - /** Visible question text. */ - question?: string; - /** Question category. */ - type?: string; - /** Optional visible supporting text. */ - description?: string; - /** Whether more than one answer may be selected. */ - multi_select?: boolean; - /** Plan section the question covers. */ - covers?: string; - /** Selectable options; a bare string option is its label. */ - options?: (ToolQuestionOption | string)[]; -} - -/** `ask_clarifying_questions` and `ask_plan_questions` arguments. */ -export interface ToolQuestionArguments { - /** Questions presented to the user. */ - questions?: ToolQuestion[]; -} - -/** A requested secret field. The secret value is never sent over the socket. */ -export interface ToolSecretField { - /** Requested secret name. */ - secretName?: string; - /** Optional explanation of where to obtain it. */ - description?: string; -} - -/** `set_secrets` arguments. */ -export interface ToolSecretArguments { - /** Requested secret fields. */ - secrets_schema?: ToolSecretField[]; -} - -/** A package operation. Versions and package-manager output are excluded. */ -export interface ToolPackageOperation { - /** Package name. */ - name?: string; - /** Requested package operation. */ - action?: string; -} - -/** `install_npm_package` arguments. */ -export interface ToolPackageArguments { - /** Requested package operations. */ - packages?: ToolPackageOperation[]; -} - -/** An add-only builder plan update. */ -export interface ToolPlanUpdate { - /** Update action. */ - action?: string; - /** Plan section key. */ - section?: string; - /** Optional user-facing section label. */ - section_label?: string; - /** Plan point. */ - text?: string; -} - -/** `update_plan` arguments. */ -export interface ToolPlanArguments { - /** Plan updates in their emitted order. */ - updates?: ToolPlanUpdate[]; - /** Base plan sections the builder considers sufficiently specified. */ - sections_with_enough?: string[]; -} - -/** `generate_prd` arguments: the user's own plan. In plan mode the message `content` beside it can be empty. */ -export interface ToolPrdArguments { - /** App or feature name. */ - app_name?: string; - /** What is being built and why. */ - intent_and_goal?: string; - /** Who uses it and their roles. */ - audience_and_roles?: string; - /** End-to-end user flows. */ - core_flows?: string[]; - /** Specific technical requirements. */ - technical_requirements?: string; - /** Design preferences. */ - design_preferences?: string; - /** Build or plan declaration, when the builder asks for one. */ - prd_type?: string; - /** Store platform settled in planning, for e-commerce plans. */ - store_platform?: string; - /** Engine settled in planning, for game plans. */ - game_engine?: string; -} - -/** Image, game-image and video generation arguments. */ -export interface ToolMediaArguments { - /** Visible media label. */ - label?: string; - /** Requested image or video aspect ratio. */ - aspect_ratio?: string; -} - -/** `update_entities` and `delete_entities` arguments; the entity is in `display.entity_name`. */ -export interface ToolEntityMutationArguments { - /** The agent's summary of the change. Queries and record values are never included. */ - summary?: string; -} - -/** The public arguments of a tool that declares them; narrow by the tool call's `name`. */ -export type ToolArguments = - | ToolQuestionArguments | ToolSecretArguments | ToolPackageArguments - | ToolPlanArguments | ToolPrdArguments | ToolMediaArguments | ToolEntityMutationArguments; - -/** Generated-media state. Raw generation prompts and IDs are never included. */ -export interface ToolMediaResult { - /** Placeholder URL, used to associate an `image.resolved` event with this tool. */ - placeholder_url?: string; - /** Current media-generation status. */ - status?: "pending" | "completed" | "failed"; - /** Generated asset URL, once available. */ - image_url?: string | null; -} - -/** An answer to a clarifying question. Secret-form input is never included. */ -export interface ToolQuestionAnswer { - /** Zero-based question index. */ - question_index?: number; - /** Label selected for a single-select question. */ - selected_label?: string; - /** Labels selected for a multi-select question. */ - selected_labels?: string[]; - /** User-provided free-text answer. Structural filtering does not redact prose. */ - custom_text?: string; -} - -/** Clarifying-question answers. */ -export interface ToolQuestionInput { - /** Answers supplied to the question card. */ - answers?: ToolQuestionAnswer[]; -} - -/** Why a safety guard parked a call for approval. Only present while the call waits. */ -export interface ToolGuardApproval { - /** Guard identifier, e.g. `exec_tool_send_email` or `bash_approval`. */ - guard: string; - /** Human-readable reason, for guards whose reason never quotes input (not shell commands). */ - reason?: string; - /** The details the guard declares public; commands and record data are never included. */ - details?: { - /** Entity method the code calls, e.g. `delete`. */ - method?: string; - /** The agent's summary of what the code does. */ - summary?: string; - /** Entity whose access rules change. */ - entity_name?: string; - /** How the access rules change. */ - change_type?: string; - /** Access operations whose rules change. */ - changed_ops?: string[]; - /** Backend function under test. */ - function_name?: string; - /** The guard could not verify the function's source. */ - inspection_failed?: boolean; - }; -} - -/** Public progress of a builder tool call. Only what the tool and its guard declare public is present. */ -export interface ToolCall { - /** Stable tool call identifier. */ - id?: string; - /** Tool name. */ - name?: string; - /** Current execution state. */ - status?: "running" | "success" | "error" | "stopped" | "waiting_for_user_input"; - /** Whether the tool needs a user response through the partner backend. */ - requires_user_input?: boolean; - /** Whether the builder auto-approved the operation. */ - auto_approved?: boolean; - /** Whether the mutation was applied. */ - mutation_applied?: boolean | null; - /** Kind of response a parked call waits for. */ - waiting_on?: { - /** Kind of response expected. */ - kind?: "approval" | "choice" | "input"; - }; - /** The tool's public arguments; absent for tools that declare none and for partial streaming arguments. */ - arguments?: ToolArguments; - /** What a file, execution or entity tool touched. */ - display?: ToolDisplay; - /** Clarifying-question answers. */ - user_input?: ToolQuestionInput; - /** Generated media, once the call is not waiting on a guard: the image tool's state, or a URL alone - * (a game asset's placeholder, which `image.resolved` replaces, or a finished video). */ - results?: ToolMediaResult | string; - /** A parked call's guard, and its reason and details where the guard declares them. */ - approval?: ToolGuardApproval; -} - -/** Scalar parameters a chat UI needs while a turn runs. */ -export interface MessageParams { - /** Identifier the sending client attached, to match its optimistic message. */ - client_creation_id?: string; - /** The message was sent in plan mode. */ - plan_mode?: boolean; - /** The message approves a plan. */ - plan_approved?: boolean; - /** The message expects no AI reply. */ - skip_ai_response?: boolean; - /** System message category. */ - system_message_type?: string; -} - -/** A public chat message. Replace the message with the same `id` as a whole. */ -export interface ChatMessage { - /** Message identifier. */ - id?: string; - /** Author category. System and hidden messages are never delivered. */ - role?: "user" | "assistant"; - /** Message text; structured content is withheld. Structural filtering is not prose redaction. */ - content?: string | null; - /** Public tool progress. */ - tool_calls?: ToolCall[] | null; - /** Message timestamp, without author identity. */ - metadata?: { - /** ISO timestamp string. */ - created_date?: string; - }; - /** Checkpoint reference; mutations remain on the partner backend. */ - checkpoint_id?: string | null; - /** Scalar message parameters. */ - additional_message_params?: MessageParams; -} - -/** Public builder state, without error diagnostics or billing context. */ -export interface AppStatus { - /** Current builder state. */ - state?: "ready" | "processing" | "error"; - /** State timestamp. */ - last_updated_date?: string | null; - /** The user message that started the turn, when a turn set this state. Quote it to Base44 support. */ - turn_id?: string | null; -} - -/** Branch scope carried by app events. Null or absent is the main branch. */ -export interface BranchScoped { - /** Branch the change belongs to; null is main. */ - branch_id?: string | null; -} - -/** `message.updated`: a chat message was added or replaced. */ -export interface MessageUpdated extends BranchScoped { - /** The whole message; replace the one with the same `id`. */ - message: ChatMessage; - /** Conversation containing the message. */ - conversation_id?: string | null; -} - -/** `message.removed`: a message was removed or hidden. An `id` the viewer never saw is a no-op. */ -export interface MessageRemoved extends BranchScoped { - /** Identifier of the message to remove. */ - message_id: string; - /** Conversation that contained the message. */ - conversation_id?: string | null; -} - -/** `app.status_changed`: a turn started, finished or failed. */ -export interface AppStatusChanged extends BranchScoped { - /** The new state; null clears it. */ - status: AppStatus | null; -} - -/** `preview.navigation_requested`: show this page of the app in the preview. */ -export interface PreviewNavigationRequested extends BranchScoped { - /** App path to show. */ - path: string; - /** Navigate even when the user has moved away from the page. */ - force: boolean; -} - -/** A queued builder request. */ -export interface QueueItem { - /** Queue item identifier. */ - id?: string; - /** User-authored request text. */ - content?: string; - /** Creation timestamp. */ - created_at?: string; - /** Branch the request belongs to. */ - branch_id?: string | null; -} - -/** `queue.updated`: the whole prompt queue, replacing the previous one. */ -export interface QueueUpdated extends BranchScoped { - /** Pending items. */ - items?: QueueItem[]; - /** Whether queue processing is paused. */ - is_paused?: boolean; - /** Identifier of the item just processed, when supplied. */ - processed_item_id?: string | null; -} - -/** `task.progressed`: progress of a long-running tool. */ -export interface TaskProgressed extends BranchScoped { - /** Associated tool call. */ - tool_call_id?: string | null; - /** Associated chat message. */ - message_id?: string | null; - /** Task lifecycle event, e.g. `task_progress`. */ - event_type?: string | null; - /** Numeric progress only; diagnostic text is withheld. */ - progress?: { - /** Completed work units. */ - current?: number | null; - /** Total work units, when known. */ - total?: number | null; - /** Producer-supplied percentage. */ - percentage?: number | null; - } | null; -} - -/** `image.resolved`: a generated image finished or failed. Replace its `placeholder_url` wherever it appears. */ -export type ImageResolved = ToolMediaResult; - -/** Mapping of public event names to their payloads. */ -export interface PlatformEventMap { - /** A chat message was added or replaced. */ - "message.updated": MessageUpdated; - /** A chat message was removed or hidden. */ - "message.removed": MessageRemoved; - /** The builder started, finished or failed a turn. */ - "app.status_changed": AppStatusChanged; - /** Reload the app preview. */ - "preview.reload_requested": BranchScoped; - /** Show a page of the app in the preview. */ - "preview.navigation_requested": PreviewNavigationRequested; - /** The whole prompt queue. */ - "queue.updated": QueueUpdated; - /** Progress of a long-running tool. */ - "task.progressed": TaskProgressed; - /** A generated image finished or failed. */ - "image.resolved": ImageResolved; - /** The conversation was rewritten (undo, restore, sync). On main, the SDK rejoins for a fresh snapshot. */ - "conversation.changed": BranchScoped; - /** The app's files changed outside a chat turn. Re-read them. */ - "files.changed": BranchScoped; - /** A branch was deleted. */ - "branch.deleted": BranchScoped; - /** The imported app's repository changed. */ - "repository.changed": BranchScoped; - /** The imported app's pull request changed. */ - "pull_request.changed": BranchScoped; -} - -/** Ordered delivery of one public event. */ +import type { Message, QueueState, ServerEventMap, Snapshot } from "./builder.events.generated.js"; + +/** + * Maps each live event name to the type of its `data` payload. + * + * Generated from the service's AsyncAPI document, so it covers every app event the service sends. + * The client handles the session events `app.snapshot`, `session.ended` and `room.*` itself, and + * reports them through `onSnapshot` and `onError` instead. + */ +export type PlatformEventMap = { + [K in keyof ServerEventMap as K extends "app.snapshot" | "session.ended" | `room.${string}` ? never : K]: ServerEventMap[K]; +}; + +/** + * One live event for a subscribed app. + * + * Check `type` to narrow `data` to that event's payload. A key missing from `data` means the value + * is unchanged, and an explicit `null` clears it. For every event and its payload, see + * [Socket messages](/developers/references/apps-api/sections/socket-messages). + * + * @example + * ```typescript + * // Narrow an event by type + * function onEvent(event: PlatformEvent) { + * if (event.type === "message.updated") { + * const { message } = event.data; + * console.log(`Message ${message.id} changed`); + * } + * } + * ``` + */ export type PlatformEvent = { [K in keyof PlatformEventMap]: { - /** Public event name; narrows the payload type. */ + /** Event name. Narrows the type of `data`. */ type: K; - /** Authorized app receiving this event. */ + /** ID of the app the event belongs to. */ appId: string; - /** Event payload. Omitted keys mean unchanged; explicit null clears. */ + /** Event payload. */ data: PlatformEventMap[K]; - } + }; }[keyof PlatformEventMap]; -/** An app's current state: sent after every join and rejoin, and after a main-conversation rewrite. */ -export interface Snapshot { - /** Canonical app room. */ +/** + * An app's current state. + * + * 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. + */ +export interface PlatformSnapshot extends Omit { + /** The app's room, `/apps/{app_id}`. */ room: string; - /** Current builder state. */ - status: AppStatus | null; - /** The last 50 public messages, oldest first. */ - messages: ChatMessage[]; - /** The main branch's prompt queue. Replace it, as with `queue.updated`. Absent from older servers. */ - queue?: SnapshotQueue; + /** The main branch's prompt queue. Missing when the server sends none. */ + queue?: QueueState; } -/** The prompt queue in a snapshot. */ -export interface SnapshotQueue { - /** Pending items. */ - items: QueueItem[]; - /** Whether queue processing is paused. */ - is_paused: boolean; -} +/** + * One tool call in a message's `tool_calls`. Checking `name` narrows it to that tool's call and + * `OtherToolCall`, which covers tools the SDK doesn't know, so check a field with `in` before using it. + */ +export type ToolCall = NonNullable[number]; + +/** Why a safety guard parked a tool call for approval. Check `guard` to narrow it. */ +export type GuardApproval = NonNullable; diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 4d6c9324..81128234 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -1,52 +1,167 @@ -import type { PlatformEvent, Snapshot } from "./builder.events.types.js"; +import type { PlatformEvent, PlatformSnapshot } from "./builder.events.types.js"; import type { PlatformSocketError } from "../errors.js"; -/** Options for an independent builder socket session. */ +/** Options for {@linkcode BuilderModule.init | init()}. */ export interface BuilderInitOptions { - /** Connection-level errors, sanitized to exclude tokens and server exception text. */ + /** + * 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 + * [error codes](/developers/references/platform-sdk/docs/classes/PlatformSocketError#platformsocketerrorcode). + */ onError: (error: PlatformSocketError) => void; } -/** Lazy builder module; no socket exists until init is called. */ +/** + * Builder module for following the AI chat in your users' apps live. + * + * Each session holds one socket and up to eight app subscriptions. For each app you get a + * snapshot of its chat, then every change as it happens: + * - New and updated chat messages, with the AI's tool calls. + * - The AI's status, from processing to ready. + * - Preview reloads and navigation, the prompt queue, and generated images. + * + * The module does nothing until you call {@linkcode init | init()}. + */ export interface BuilderModule { - /** Create an independent session without connecting. Call connect on the returned session. */ + /** + * 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. + * + * @param options - Session options. + * @returns The new session. + * + * @example + * ```typescript + * // Create and connect a session + * const builder = client.builder.init({ + * onError(error) { + * console.error('Builder session error', error.code); + * }, + * }); + * await builder.connect(); + * ``` + */ init(options: BuilderInitOptions): BuilderSession; } -/** One builder socket session. Owns its subscriptions and connection lifecycle. */ +/** + * A `BuilderSession`: one socket with its app subscriptions and its reconnection. Use its methods + * to connect, follow apps and close. + */ export interface BuilderSession { - /** Connect with the session token; resolves on CONNECT, before snapshots arrive. - * Transport loss retries five times and rejoins active subscriptions, each with a fresh - * snapshot. An expired or rejected session is replaced once through `getSessionToken`. - * Call again after a failure or `session_replaced`. Concurrent calls share one attempt. + /** + * Connects the session's socket. + * + * 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. + * + * @returns Promise that resolves when the socket connects. + * @throws {PlatformSocketError} With `connection_denied`, `connection_failed` or `session_unavailable` + * when the session can't connect, or `client_closed` when the session is closed first. + * + * @example + * ```typescript + * // Connect and handle failure + * try { + * await builder.connect(); + * } catch (error) { + * console.error('Could not connect', error.code); + * } + * ``` */ connect(): Promise; - /** Subscribe before or after connecting. One subscription per app, up to eight per session, - * and each app must be on the session's allowlist. Callbacks run serially per app. + /** + * Subscribes to one app's chat. + * + * You can subscribe before or after connecting. Each app can have one subscription per session, + * and a session can have up to eight. The app must be on the session's allowlist. The + * subscription first receives a snapshot of the app, then each live event. + * + * Callbacks for an app run one at a time, in order, and the session waits for a returned promise + * before the next one. A slow app doesn't hold up the others. If more than 1,000 deliveries wait + * for one app, its subscription ends with `delivery_overflow`. + * + * @param appId - ID of the app to follow, 24 lowercase hexadecimal characters. + * @param options - Callbacks for the app's snapshots, events and errors. + * @returns The subscription. + * @throws {TypeError} When `appId` isn't a valid app ID or the app already has a subscription. + * @throws {PlatformSocketError} With `subscription_limit` when the session has eight subscriptions. + * + * @example + * ```typescript + * // Follow an app + * const subscription = builder.subscribe(appId, { + * onSnapshot(snapshot) { + * renderChat(snapshot.messages); + * }, + * onEvent(event) { + * if (event.type === 'message.updated') upsertMessage(event.data.message); + * }, + * onError(error) { + * console.error(`App ${error.appId}`, error.code); + * }, + * }); + * ``` */ subscribe(appId: string, options: SubscriptionOptions): PlatformSubscription; - /** Stop this session, its subscriptions and reconnection. Idempotent and terminal. */ + /** + * Closes the session. + * + * Ends every subscription, stops reconnecting and disconnects the socket. Calling it again does + * nothing. A closed session can't be reused, so call [`init()`](/developers/references/platform-sdk/docs/interfaces/builder#init) + * for a new one. + * + * @example + * ```typescript + * // Close a session + * builder.close(); + * ``` + */ close(): void; } -/** One app subscription; at most eight may be active per builder session. */ +/** Callbacks for one app subscription. */ export interface SubscriptionOptions { - /** Replace this app's state. Arrives after every join and rejoin, and after a main-conversation - * rewrite (undo, restore, sync). Live events follow it; it holds only the last 50 messages. + /** + * Called with the app's current state, a [`PlatformSnapshot`](#platformsnapshot). Replace everything you hold for the app with it. + * + * Arrives after every join and rejoin, and after the main conversation is rewritten by an undo, + * restore or sync. Live events follow it. It holds only the last 50 messages, so merge by message + * ID to keep older history. + */ + onSnapshot: (snapshot: PlatformSnapshot) => void | Promise; + /** + * Called with each live event, a [`PlatformEvent`](#platformevent). Events for an app arrive one at a time, in order. If the callback + * throws or rejects, the subscription ends with `handler_failed`. New event types can be added at + * any time, so ignore types you don't handle. For every event and its payload, + * see [Socket messages](/developers/references/apps-api/sections/socket-messages). */ - onSnapshot: (snapshot: Snapshot) => void | Promise; - /** Apply each live event. Delivery is serial per app; a rejection ends this subscription. */ onEvent: (event: PlatformEvent) => void | Promise; - /** Subscription errors. Only `snapshot_unavailable` leaves the subscription active. */ + /** + * Called on errors for this app. Every error ends the subscription except `snapshot_unavailable`. + * See the [error codes](/developers/references/platform-sdk/docs/classes/PlatformSocketError#platformsocketerrorcode). + */ onError: (error: PlatformSocketError) => void; } -/** One app subscription's lifetime. */ +/** One app subscription. */ export interface PlatformSubscription { - /** App identifier. */ + /** ID of the app. */ readonly appId: string; - /** True while subscribed; false after an error or explicit unsubscribe. */ + /** Whether the subscription still receives updates. It turns false after an error or {@linkcode unsubscribe | unsubscribe()}. */ readonly active: boolean; - /** Stop this app's delivery and release its subscription slot. Idempotent. */ + /** + * Stops updates for the app and frees its subscription slot. Calling it again does nothing. + * + * @example + * ```typescript + * // Stop following an app + * subscription.unsubscribe(); + * ``` + */ unsubscribe(): void; } diff --git a/packages/platform/tests/package.test.mjs b/packages/platform/tests/package.test.mjs index f0fdb71a..389fd9bf 100644 --- a/packages/platform/tests/package.test.mjs +++ b/packages/platform/tests/package.test.mjs @@ -34,16 +34,16 @@ test("the client resolves from the installed package without the runtime SDK", ( assert.equal("@base44/sdk" in { ...manifest.dependencies, ...manifest.peerDependencies }, false); run(process.execPath, ["--input-type=module", "--eval", ` import assert from "node:assert/strict"; - import { Base44PlatformClient } from "@base44/platform"; - const client = new Base44PlatformClient({ serverUrl: "https://example.test", getSessionToken: () => "session-token" }); + import { createPlatformClient } from "@base44/platform"; + const client = createPlatformClient({ socketUrl: "https://example.test", getSessionToken: () => "session-token" }); assert.equal(typeof client.builder.init, "function"); client.builder.init({ onError() {} }).close(); `]); }); const consumer = ` -import { Base44PlatformClient, type PlatformEvent } from "@base44/platform"; -const platform = new Base44PlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "token" }); +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), { onSnapshot(snapshot) { void snapshot.messages; }, onEvent(event: PlatformEvent) { @@ -55,7 +55,7 @@ platform.builder.init({ onError() {} }).subscribe("a".repeat(24), { onError() {}, }); // @ts-expect-error No browser API key. -new Base44PlatformClient({ apiKey: "private" }); +createPlatformClient({ apiKey: "private" }); // @ts-expect-error No write channel. platform.send("write_file", {}); `; diff --git a/packages/platform/tests/types/client.types.ts b/packages/platform/tests/types/client.types.ts index 1fd226d2..695fcfab 100644 --- a/packages/platform/tests/types/client.types.ts +++ b/packages/platform/tests/types/client.types.ts @@ -1,9 +1,14 @@ -import { Base44PlatformClient, type PlatformEvent, type Snapshot, type ToolCall } from "@base44/platform"; -const client = new Base44PlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "session-token" }); +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; } }); const subscription = builder.subscribe("a".repeat(24), { - onSnapshot(snapshot: Snapshot) { - const state: "ready" | "processing" | "error" | undefined = snapshot.status?.state; + onSnapshot(snapshot: PlatformSnapshot) { + const state: "ready" | "processing" | "error" | null | undefined = snapshot.status?.state; + const room: string = snapshot.room; + void room; + void snapshot.queue?.items; + // @ts-expect-error A snapshot may arrive without a queue. + void snapshot.queue.items; void state; void snapshot.messages[0]?.additional_message_params?.plan_mode; }, @@ -19,7 +24,7 @@ const subscription = builder.subscribe("a".repeat(24), { void id; } if (event.type === "image.resolved") { - const status: "pending" | "completed" | "failed" | undefined = event.data.status; + const status: "pending" | "completed" | "failed" | null | undefined = event.data.status; void status; } // @ts-expect-error Payload must be narrowed by event.type. @@ -37,33 +42,45 @@ builder.subscribe("a".repeat(24), { afterSeq: "saved", onSnapshot() {}, onEvent( // @ts-expect-error No browser mutation channel. client.send("write_file", {}); // @ts-expect-error API keys are not browser credentials. -new Base44PlatformClient({ apiKey: "private" }); +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. -new Base44PlatformClient({ serverUrl: "https://example.test", refreshToken: async () => "token" }); +createPlatformClient({ socketUrl: "https://example.test", refreshToken: async () => "token" }); const tool: ToolCall = { - display: { file_paths: ["src/App.tsx"] }, + name: "ask_clarifying_questions", arguments: { questions: [{ question: "Which layout?", options: [{ label: "Cards" }] }] }, user_input: { answers: [{ question_index: 0, selected_labels: ["Cards"] }] }, }; void tool; +const write: ToolCall = { name: "write_file", display: { file_paths: ["src/App.tsx"] } }; +void write; +function describe(call: ToolCall) { + if (call.name === "generate_image" && "results" in call) void call.results; + // @ts-expect-error Narrowing by name leaves only that tool's fields (and the catch-all call's). + if (call.name === "bash") void call.display; +} +void describe; const guarded: ToolCall = { + name: "update_entities", status: "waiting_for_user_input", waiting_on: { kind: "approval" }, - approval: { guard: "entity_rls_guard", reason: "RLS rules on entity 'Order' will be modified", details: { entity_name: "Order", changed_ops: ["read"] } }, + approval: { guard: "entity_rls_guard", details: { entity_name: "Order", changed_ops: ["read"] } }, }; void guarded; -const generatedMedia: ToolCall = { - results: { - placeholder_url: "/__generating__/hero.png", - status: "completed", - image_url: "https://images.example/hero.png", - }, -}; -void generatedMedia; -// @ts-expect-error Raw command text is not part of the public display. -void tool.display?.command; +const approval: GuardApproval = { guard: "some_new_guard" }; +void approval; +// @ts-expect-error Every tool call names its tool. +const unnamed: ToolCall = { status: "running" }; +void unnamed; // @ts-expect-error Raw arguments are never delivered. void tool.arguments_string; +const message: Message = { id: "m1", role: "assistant", content: "Done", tool_calls: [tool] }; +void message; +const status: StatusObject = { state: "processing", turn_id: "m1" }; +void status; +// @ts-expect-error Session events are handled by the client, not delivered to onEvent. +type NoSnapshot = PlatformEventMap["app.snapshot"]; +// @ts-expect-error Room notices arrive through onError. +type NoNotice = PlatformEventMap["room.access_denied"]; diff --git a/packages/platform/tests/unit/client.test.ts b/packages/platform/tests/unit/client.test.ts index 16c11934..c4e2915c 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 { Base44PlatformClient, type BuilderSession } from "../../src/index.js"; +import { createPlatformClient, type BuilderSession } from "../../src/index.js"; const fake = vi.hoisted(() => { const handlers: Record void> = {}; @@ -26,7 +26,7 @@ const auth = async () => { }; function setup(getSessionToken = vi.fn(async () => "session-token")) { const onError = vi.fn(); - const platform = new Base44PlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); + const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken }); const client = platform.builder.init({ onError }); clients.push(client); return { client, getSessionToken, onError }; @@ -41,7 +41,7 @@ afterEach(() => { clients.splice(0).forEach(client => client.close()); vi.useRea describe("platform client", () => { test("constructing the root client does not initialize sockets or fetch session tokens", () => { const getSessionToken = vi.fn(async () => "token"); - const platform = new Base44PlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); + 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() }); @@ -53,7 +53,7 @@ describe("platform client", () => { }); test("closing one initialized builder does not close another or the root module", () => { - const platform = new Base44PlatformClient({ serverUrl: "https://api.example.test", getSessionToken: async () => "token" }); + const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken: async () => "token" }); const firstSocket = { ...fake.socket, disconnect: vi.fn() }; const secondSocket = { ...fake.socket, disconnect: vi.fn() }; fake.io.mockReturnValueOnce(firstSocket).mockReturnValueOnce(secondSocket); @@ -290,8 +290,8 @@ describe("platform client", () => { }); test("validates origins, app IDs, duplicate subscriptions and limits", () => { - for (const serverUrl of ["https://secret@example.test", "https://example.test?token=secret", "https://example.test/path", "ws://example.test"]) { - expect(() => new Base44PlatformClient({ serverUrl, getSessionToken: () => "token" })).toThrow(TypeError); + 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); } const { client } = setup(); const options = { onSnapshot: vi.fn(), onEvent: vi.fn(), onError: vi.fn() }; expect(() => client.subscribe("bad", options)).toThrow(TypeError); diff --git a/packages/platform/tests/unit/socket.test.js b/packages/platform/tests/unit/socket.test.js index 76a6220a..bfc0e19f 100644 --- a/packages/platform/tests/unit/socket.test.js +++ b/packages/platform/tests/unit/socket.test.js @@ -1,7 +1,7 @@ import { createServer } from "node:http"; import { WebSocketServer } from "ws"; import { expect, test, vi } from "vitest"; -import { Base44PlatformClient } from "../../src/index.js"; +import { createPlatformClient } from "../../src/index.js"; // Minimal Engine.IO/Socket.IO peer exercises the real client without a new server dependency. test("real Socket.IO handshake, join, snapshot, reconnect and session renewal", async () => { @@ -30,8 +30,8 @@ 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 = new Base44PlatformClient({ - serverUrl: `http://127.0.0.1:${http.address().port}`, + const client = createPlatformClient({ + socketUrl: `http://127.0.0.1:${http.address().port}`, getSessionToken: async () => `session-${++tokenCalls}`, }); const builder = client.builder.init({ onError: error => errors.push(error) }); diff --git a/packages/platform/typedoc.mintlify.json b/packages/platform/typedoc.mintlify.json new file mode 100644 index 00000000..f317419a --- /dev/null +++ b/packages/platform/typedoc.mintlify.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "extends": ["./typedoc.json"], + "out": "docs/content", + "plugin": [ + "typedoc-plugin-markdown", + "../sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js" + ], + "fileExtension": ".mdx", + "gitRevision": "main", + "sort": ["source-order"], + "entryFileName": "README.mdx", + "maxTypeConversionDepth": 3, + "hideBreadcrumbs": true, + "disableSources": true, + "hidePageTitle": true +} diff --git a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js index 4b857875..af0523d3 100644 --- a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js +++ b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js @@ -19,11 +19,13 @@ import fs from "fs"; import path from "path"; +import { packagePaths } from "./package-paths.js"; console.debug = () => {}; // Disable debug logging. Comment this out to enable debug logging. -const DOCS_SOURCE_PATH = path.join(import.meta.dirname, "../../docs/content"); -const CATEGORY_MAP_PATH = path.join(import.meta.dirname, "./category-map.json"); +const { docsDir, configDir, target: TARGET } = packagePaths(); +const DOCS_SOURCE_PATH = path.join(docsDir, "content"); +const CATEGORY_MAP_PATH = path.join(configDir, "category-map.json"); // Default: assume mintlify-docs is a sibling directory to javascript-sdk const SDK_ROOT = path.join(import.meta.dirname, "../.."); @@ -56,6 +58,7 @@ Usage: Options: --target, -t Path to the mintlify-docs repo. Defaults to ../mintlify-docs (sibling directory) + --package-dir Package to copy the docs of. Defaults to packages/sdk. --help, -h Show this help message Examples: @@ -71,8 +74,8 @@ Examples: return { target }; } -// Target location within mintlify-docs for SDK reference docs -const SDK_DOCS_TARGET_PATH = "developers/references/sdk/docs"; +// Target location within mintlify-docs for the reference docs +const SDK_DOCS_TARGET_PATH = TARGET.path; function scanSdkDocs(sdkDocsDir) { const result = {}; @@ -127,14 +130,10 @@ function updateDocsJson(repoDir, sdkFiles) { }; const p = (kind, file) => `${prefix}${basePath}/${kind}/${file}`; - if (sdkFiles.functions?.length > 0 && categoryMap.functions) - addToGroup(categoryMap.functions, sdkFiles.functions.map((f) => p("functions", f))); - if (sdkFiles.interfaces?.length > 0 && categoryMap.interfaces) - addToGroup(categoryMap.interfaces, sdkFiles.interfaces.map((f) => p("interfaces", f))); - if (sdkFiles.classes?.length > 0 && categoryMap.classes) - addToGroup(categoryMap.classes, sdkFiles.classes.map((f) => p("classes", f))); - if (sdkFiles["type-aliases"]?.length > 0 && categoryMap["type-aliases"]) - addToGroup(categoryMap["type-aliases"], sdkFiles["type-aliases"].map((f) => p("type-aliases", f))); + // Groups follow the category map's key order. + for (const [kind, group] of Object.entries(categoryMap)) { + if (sdkFiles[kind]?.length > 0) addToGroup(group, sdkFiles[kind].map((f) => p(kind, f))); + } return Array.from(groupMap.entries()).map(([group, pages]) => ({ group, @@ -177,11 +176,14 @@ function updateDocsJson(repoDir, sdkFiles) { for (const tab of tabs) { const sdkAnchor = - tab.dropdowns?.find((d) => d.dropdown === "SDK") ?? - tab.anchors?.find((a) => a.anchor === "SDK"); + tab.dropdowns?.find((d) => d.dropdown === TARGET.dropdown) ?? + tab.anchors?.find((a) => a.anchor === TARGET.dropdown); if (!sdkAnchor?.groups) continue; - const sdkRefIndex = sdkAnchor.groups.findIndex(groupReferencesSdkDocs); + let sdkRefIndex = sdkAnchor.groups.findIndex(groupReferencesSdkDocs); + if (sdkRefIndex === -1 && TARGET.group) { + sdkRefIndex = sdkAnchor.groups.push({ group: TARGET.group, icon: TARGET.icon, pages: [] }) - 1; + } if (sdkRefIndex === -1) continue; const existing = sdkAnchor.groups[sdkRefIndex]; diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 43a7dcb9..6cea33b3 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -14,28 +14,23 @@ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; +import { packagePaths } from "../package-paths.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const DOCS_DIR = path.join(__dirname, "..", "..", "..", "docs"); +const { docsDir: DOCS_DIR, configDir: CONFIG_DIR } = packagePaths(); const CONTENT_DIR = path.join(DOCS_DIR, "content"); const LINKED_TYPES_FILE = path.join(CONTENT_DIR, ".linked-types.json"); const TEMPLATE_PATH = path.join(__dirname, "docs-json-template.json"); const STYLING_CSS_PATH = path.join(__dirname, "styling.css"); -const CATEGORY_MAP_PATH = path.join(__dirname, "../category-map.json"); -const TYPES_TO_EXPOSE_PATH = path.join(__dirname, "..", "types-to-expose.json"); -const TYPES_TO_DELETE_PATH = path.join(__dirname, "..", "types-to-delete-after-processing.json"); -const APPENDED_ARTICLES_PATH = path.join( - __dirname, - "../appended-articles.json" -); -const METHOD_ORDER_PATH = path.join(__dirname, "..", "method-order.json"); -const OVERLOAD_PRESENTATION_PATH = path.join( - __dirname, - "..", - "overload-presentation.json" -); +const CATEGORY_MAP_PATH = path.join(CONFIG_DIR, "category-map.json"); +const TYPES_TO_EXPOSE_PATH = path.join(CONFIG_DIR, "types-to-expose.json"); +const TYPES_TO_DELETE_PATH = path.join(CONFIG_DIR, "types-to-delete-after-processing.json"); +const APPENDED_ARTICLES_PATH = path.join(CONFIG_DIR, "appended-articles.json"); +const METHOD_ORDER_PATH = path.join(CONFIG_DIR, "method-order.json"); +const OVERLOAD_PRESENTATION_PATH = path.join(CONFIG_DIR, "overload-presentation.json"); +const PAGE_SECTIONS_PATH = path.join(CONFIG_DIR, "page-sections.json"); // Controlled via env var so we can re-enable Panel injection when needed. const PANELS_ENABLED = process.env.MINTLIFY_INCLUDE_PANELS === "true"; @@ -1437,12 +1432,11 @@ function applyTypeDeclarationLinking(dir) { * `## Properties` section. The value renames the heading, or is null to keep the * interface name. */ -const TYPES_WITH_OWN_METHODS = { - EntityHandler: "Entity Handler Methods", - ActorRef: "Actor methods", - Connection: "Connection methods", - ActorSubscription: "Subscription methods", -}; +// Appended types whose methods get their own section, by section title, and groups of appended +// types nested under "## Type Definitions" (the first type of a group starts the section). A group +// is a list of type names, or `{ "title": "...", "types": [...] }` for another section heading. +const { typesWithOwnMethods: TYPES_WITH_OWN_METHODS = {}, typeDefinitionGroups: TYPE_DEFINITION_GROUPS = [] } = + fs.existsSync(PAGE_SECTIONS_PATH) ? JSON.parse(fs.readFileSync(PAGE_SECTIONS_PATH, "utf-8")) : {}; /** * Group intro sections (like "Built-in User Entity", "Generated Types") under an "Overview" heading @@ -1556,7 +1550,7 @@ function applyIntroSectionGrouping(dir) { /** * Group type definition sections under a parent heading. * - * Each group below must list every entry from appended-articles.json for that + * Each group in page-sections.json must list every entry from appended-articles.json for that * module except the one that gets absorbed into the "## ... Methods" section * (typically named `*Module` or `*Handler`, e.g. EntityHandler) — otherwise * the appended type keeps its original `##` heading and renders as a sibling @@ -1565,39 +1559,10 @@ function applyIntroSectionGrouping(dir) { function groupTypeDefinitions(content) { let modified = false; - // Define type definition patterns for different modules - const typeGroups = [ - // Connectors module - { - types: ["ConnectorIntegrationType", "ConnectorIntegrationTypeRegistry"], - indicator: "ConnectorIntegrationType" - }, - // Entities module - { - types: [ - "EntityRecord", - "EntityTypeRegistry", - "SortField", - "EntityFilterQuery" - ], - indicator: "EntityRecord" - }, - // Functions module - { - types: ["FunctionName", "FunctionNameRegistry"], - indicator: "FunctionName" - }, - // Agents module - { - types: ["AgentName", "AgentNameRegistry"], - indicator: "AgentName" - }, - // Actors module - { - types: ["ActorClient", "ActorRegistry", "ActorNameRegistry"], - indicator: "ActorClient" - } - ]; + const typeGroups = TYPE_DEFINITION_GROUPS.map((group) => { + const { title = "Type Definitions", types } = Array.isArray(group) ? { types: group } : group; + return { types, indicator: types[0], title }; + }); // Find which type group exists in this file let matchedGroup = null; @@ -1638,7 +1603,7 @@ function groupTypeDefinitions(content) { const updatedContent = beforeTypeDefinitions + - "## Type Definitions\n\n" + + `## ${matchedGroup.title}\n\n` + demotedSection; return { content: updatedContent, modified: true }; @@ -2564,6 +2529,97 @@ function restructureActorsPage() { fs.writeFileSync(file, tidied, "utf-8"); } +/** Class pages: no "Extends Error", which tells a reader nothing. */ +function cleanupClassPage(content) { + return content.replace(/^### Extends\n\n(- .*\n)+\n?/m, ""); +} + +/** + * A class page's ## Properties, and any #### Properties (a type under Type Definitions), as + * ResponseFields, the component Returns and Parameters use, without TypeDoc's inheritance notes. + */ +function propertiesAsFields(content, classPage) { + const fieldsOf = (body) => body.split(/^\*{3}$/m).map((block) => { + const signature = block.match(/^> (.*)$/m)?.[1] ?? ""; + const name = block.match(/^#### (.+?)\??$/m)?.[1]?.replace(/\\/g, ""); + if (!name) return ""; + const type = signature.split(/\*\*: /)[1]?.replace(/`/g, "").trim() ?? ""; + const description = block.replace(/^#### .*$/m, "").replace(/^> .*$/m, "").trim(); + const required = signature.includes("`optional`") ? "" : " required"; + return `\n\n${description}\n\n`; + }).filter(Boolean).join("\n\n"); + return content + .replace(/^#### Inherited from\n\n.*\n\n?/gm, "") + .replace(/^#### Extends\n\n(- .*\n)+\n?/gm, "") + .replace(classPage ? /^## Properties\n([\s\S]*?)(?=^## |(?![\s\S]))/m : /(?!)/, (section, body) => `## Properties\n\n${fieldsOf(body)}\n\n`) + .replace(/^#### Properties\n([\s\S]*?)(?=^#{1,3} |(?![\s\S]))/gm, (section, body) => `${fieldsOf(body)}\n\n`); +} + +function applyPropertiesAsFields(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyPropertiesAsFields(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + const converted = propertiesAsFields(content, path.basename(dir) === "classes"); + if (converted !== content) fs.writeFileSync(entryPath, converted, "utf-8"); + } + } +} + +function applyClassPageCleanup(dir) { + const classesDir = path.join(dir, "content", "classes"); + if (!fs.existsSync(classesDir)) return; + for (const file of fs.readdirSync(classesDir).filter((name) => name.endsWith(".mdx"))) { + const filePath = path.join(classesDir, file); + fs.writeFileSync(filePath, cleanupClassPage(fs.readFileSync(filePath, "utf-8")), "utf-8"); + } +} + +/** + * A Returns type line such as `BuilderSession` becomes a link when the same page documents that type: + * its methods section (page-sections.json `typesWithOwnMethods`) or its own ### heading. + */ +function linkReturnTypes(content) { + const slug = (heading) => heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); + const anchors = new Map(); + for (const [type, title] of Object.entries(TYPES_WITH_OWN_METHODS)) { + if (content.includes(`\n## ${title}\n`)) anchors.set(type, slug(title)); + } + for (const [, type] of content.matchAll(/^### (\w+)$/gm)) anchors.set(type, slug(type)); + // The section a type heads, with no separator under its heading, and its description. + const descriptions = new Map(); + const headings = [...Object.entries(TYPES_WITH_OWN_METHODS).map(([type, title]) => [type, `## ${title}`]), + ...[...anchors.keys()].map((type) => [type, `### ${type}`])]; + for (const [type, heading] of headings) { + const start = content.indexOf(`\n${heading}\n\n***\n\n`); + if (start === -1) continue; + content = content.replace(`\n${heading}\n\n***\n\n`, `\n${heading}\n\n`); + const body = content.slice(start + heading.length + 3); + descriptions.set(type, body.slice(0, body.search(/\n(#|> |<|```)/)).trim()); + } + return content.replace(/(^#{4,5} Returns\n\n)`(\w+)`$/gm, (match, heading, type) => + anchors.has(type) ? `${heading}[\`${type}\`](#${anchors.get(type)})` : match) + // A linked Returns points at the type's own description; don't repeat it there. + .replace(/(^#{4,5} Returns\n\n\[`(\w+)`\]\(#[^)]+\)\n\n)([\s\S]*?)(?=\n#|\n<)/gm, (match, head, type, rest) => { + const description = descriptions.get(type); + const text = rest.replace(/``/g, "`"); + return description && text.includes(description) ? head + text.replace(description, "").trimStart() : match; + }); +} + +function applyReturnTypeLinks(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyReturnTypeLinks(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + const linked = linkReturnTypes(content); + if (linked !== content) fs.writeFileSync(entryPath, linked, "utf-8"); + } + } +} + function main() { console.log("Processing TypeDoc MDX files for Mintlify...\n"); @@ -2613,6 +2669,9 @@ function main() { // Group type definitions under a parent heading applyTypeDefinitionGrouping(DOCS_DIR); + applyClassPageCleanup(DOCS_DIR); + applyPropertiesAsFields(DOCS_DIR); + // Reorder methods according to method-order.json applyMethodOrdering(DOCS_DIR); @@ -2632,6 +2691,10 @@ function main() { // Delete types that should not appear in navigation but were needed for inline rendering deleteTypesAfterProcessing(DOCS_DIR); + // Link a Returns type to where the same page documents it. Last: it drops the *** separators + // that method ordering splits on. + applyReturnTypeLinks(DOCS_DIR); + // Clean up the linked types file try { if (fs.existsSync(LINKED_TYPES_FILE)) { diff --git a/packages/sdk/scripts/mintlify-post-processing/package-paths.js b/packages/sdk/scripts/mintlify-post-processing/package-paths.js new file mode 100644 index 00000000..af5d7c39 --- /dev/null +++ b/packages/sdk/scripts/mintlify-post-processing/package-paths.js @@ -0,0 +1,23 @@ +import fs from "fs"; +import path from "path"; + +const SDK_DIR = path.join(import.meta.dirname, "..", ".."); + +/** + * Where the pipeline reads and writes for one package. Defaults to @base44/sdk; pass + * `--package-dir ` to document another package, whose config files live under + * `/scripts/mintlify-post-processing/` like this package's. + */ +export function packagePaths(argv = process.argv.slice(2)) { + const index = argv.indexOf("--package-dir"); + const packageDir = index === -1 ? SDK_DIR : path.resolve(argv[index + 1]); + const configDir = path.join(packageDir, "scripts", "mintlify-post-processing"); + const targetFile = path.join(configDir, "mintlify-target.json"); + // Where the reference lands in mintlify-docs. `group` creates the nav group when it is missing. + const target = { + path: "developers/references/sdk/docs", + dropdown: "SDK", + ...(fs.existsSync(targetFile) ? JSON.parse(fs.readFileSync(targetFile, "utf8")) : {}), + }; + return { packageDir, docsDir: path.join(packageDir, "docs"), configDir, target }; +} diff --git a/packages/sdk/scripts/mintlify-post-processing/page-sections.json b/packages/sdk/scripts/mintlify-post-processing/page-sections.json new file mode 100644 index 00000000..72407e5c --- /dev/null +++ b/packages/sdk/scripts/mintlify-post-processing/page-sections.json @@ -0,0 +1,33 @@ +{ + "typesWithOwnMethods": { + "EntityHandler": "Entity Handler Methods", + "ActorRef": "Actor methods", + "Connection": "Connection methods", + "ActorSubscription": "Subscription methods" + }, + "typeDefinitionGroups": [ + [ + "ConnectorIntegrationType", + "ConnectorIntegrationTypeRegistry" + ], + [ + "EntityRecord", + "EntityTypeRegistry", + "SortField", + "EntityFilterQuery" + ], + [ + "FunctionName", + "FunctionNameRegistry" + ], + [ + "AgentName", + "AgentNameRegistry" + ], + [ + "ActorClient", + "ActorRegistry", + "ActorNameRegistry" + ] + ] +} diff --git a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js index 966c4a39..8098a313 100644 --- a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js +++ b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js @@ -92,6 +92,11 @@ export function load(app) { ); } + // 2a. Document each value of a literal union; typedoc-plugin-markdown drops their comments + if (page.model?.kind === ReflectionKind.TypeAlias) { + content = appendUnionValueFields(content, page.model.type); + } + // 3. Convert code examples to CodeGroup content = convertExamplesToCodeGroup(content); @@ -258,3 +263,15 @@ function fixEscapedTypeCharacters(content) { // Simply replace all instances of \| with | in the entire content return content.replace(/\\\|/g, "|"); } + +function appendUnionValueFields(content, type) { + const summaries = type?.type === "union" ? type.elementSummaries : undefined; + if (!summaries || !type.types.every((t) => t.type === "literal")) return content; + const fields = type.types.map((t, i) => { + const text = (summaries[i] ?? []).map((part) => part.text).join("").trim(); + return `\n\n${text}\n\n`; + }); + // Each value is listed below with its comment, so the one-line union signature only repeats them. + const withoutSignature = content.replace(/^> \*\*\w+\*\* = .*\n+/m, ""); + return `${withoutSignature.trimEnd()}\n\n${fields.join("\n\n")}\n`; +} diff --git a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js index 82d50e3c..4236dff8 100644 --- a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js +++ b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js @@ -579,10 +579,11 @@ function rewriteReturnSections(content, options) { if (fields.length === 0 && !indexSignature) { result.push(...sectionLines); } else { + // The section's own leading type link is the fallback when the signature gave no type. const typeNameForDisplay = enrichTypeNameWithGenerics( extractedTypeName || returnTypeName, returnTypeFromSignature - ); + ) || sectionContent.match(/^\[`([^`]+)`\]\(/)?.[1]; if (typeNameForDisplay) { result.push(""); result.push(`\`${typeNameForDisplay}\``); diff --git a/packages/sdk/writing-docs.md b/packages/sdk/writing-docs.md index ed194756..60f6a461 100644 --- a/packages/sdk/writing-docs.md +++ b/packages/sdk/writing-docs.md @@ -57,7 +57,13 @@ After generating and reviewing the docs, you can push them to the `base44/mintli 1. Open the [docs repo](https://github.com/base44-dev/mintlify-docs) and created a PR for your branch. 1. Preview your docs using the [Mintlify dashboard](https://dashboard.mintlify.com/base44/base44?section=previews). -The Platform SDK (`packages/platform`) is documented separately. Add field-level -JSDoc to all exports in `packages/platform/src`, update `packages/platform/README.md`, -and run `npm run docs -w @base44/platform`. TypeDoc treats missing public -documentation as an error and emits its reference under `packages/platform/docs`. +## Platform SDK docs + +The same pipeline documents the Platform SDK (`packages/platform`). Pass `--package-dir ` to `file-processing.js` and `copy-to-local-docs.js` to run them for another package. They then read that package's `docs` folder and its config files under `/scripts/mintlify-post-processing/`. A `mintlify-target.json` there sets where the reference lands in mintlify-docs and which nav dropdown it belongs to. Without one, the scripts keep the SDK's defaults. + +`page-sections.json` sets the page shape, for both packages: appended types whose methods get their own section (`typesWithOwnMethods`, by section title), and groups of appended types nested under **Type Definitions** or a `title` you give the group (`typeDefinitionGroups`). + +From `packages/platform`: +* `npm run docs` validates that every export has JSDoc. Its output in `docs` is not published. +* `npm run create-docs` generates the Mintlify reference into `docs/content`. +* `npm run copy-docs-local -- --target ` copies it into `developers/references/platform-sdk/docs` and updates the **Platform SDK** dropdown in `docs.json`.