diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index 59ba9a0..2c5000a 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -1,9 +1,9 @@ --- title: Introduction -description: Run AI agents (Claude Code, OpenAI Agents, and more) in any sandbox, controlled from Python over WebSocket. +description: Run AI agents (Claude Code, OpenAI Agents, and more) in any sandbox, controlled from Python or TypeScript over WebSocket. --- -[RuntimeUse](https://github.com/getlark/runtimeuse) is an open-source runtime and client library for running AI agents inside isolated sandboxes and controlling them from Python over WebSocket. +[RuntimeUse](https://github.com/getlark/runtimeuse) is an open-source runtime and client library for running AI agents inside isolated sandboxes and controlling them from Python or TypeScript over WebSocket. RuntimeUse terminal @@ -25,7 +25,7 @@ description: Run AI agents (Claude Code, OpenAI Agents, and more) in any sandbox ## What it handles - **Task invocations**: send a prompt to any agent runtime and receive a result over WebSocket as text or typed JSON. -- **pre_agent_downloadables**: fetch code, repos, or data into the sandbox before the run starts. +- **Pre-agent downloadables**: fetch code, repos, or data into the sandbox before the run starts. - **Pre-commands**: run bash commands before the agent starts executing. - **Artifact uploads**: move generated files out of the sandbox with a presigned URL handshake. - **Streaming and cancellation**: receive progress updates and stop runs cleanly. diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json index 15935df..83b2862 100644 --- a/docs/content/docs/meta.json +++ b/docs/content/docs/meta.json @@ -3,6 +3,7 @@ "index", "quickstart", "python-client", + "typescript-client", "agent-runtime" ] -} \ No newline at end of file +} diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx index f27ba74..5993ca2 100644 --- a/docs/content/docs/quickstart.mdx +++ b/docs/content/docs/quickstart.mdx @@ -1,6 +1,6 @@ --- title: Quickstart -description: Start the runtime, connect from Python, and run your first prompt. +description: Start the runtime, connect from Python or TypeScript, and run your first prompt. --- import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; @@ -181,45 +181,86 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; ## 2. Install the Client -```bash -pip install runtimeuse-client -``` + + + ```bash + pip install runtimeuse-client + ``` + + + ```bash + npm install runtimeuse-client + ``` + + ## 3. Connect and Query Once you have a `ws_url`, the client flow is the same across providers: -```python -import asyncio - -from runtimeuse_client import ( - QueryOptions, - RuntimeEnvironmentDownloadableInterface, - RuntimeUseClient, - TextResult, -) - - -async def main(ws_url: str) -> None: - client = RuntimeUseClient(ws_url=ws_url) - - result = await client.query( - prompt="Summarize the contents of this repository and list your favorite file.", - options=QueryOptions( - system_prompt="You are a helpful assistant.", - model="claude-sonnet-4-20250514", # gpt-5.4 for openai - pre_agent_downloadables=[ - RuntimeEnvironmentDownloadableInterface( - download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", - working_dir="/runtimeuse", - ) - ], - ), + + + ```python + import asyncio + + from runtimeuse_client import ( + QueryOptions, + RuntimeEnvironmentDownloadableInterface, + RuntimeUseClient, + TextResult, ) - assert isinstance(result.data, TextResult) - print(result.data.text) + + async def main(ws_url: str) -> None: + client = RuntimeUseClient(ws_url=ws_url) + + result = await client.query( + prompt="Summarize the contents of this repository and list your favorite file.", + options=QueryOptions( + system_prompt="You are a helpful assistant.", + model="claude-sonnet-4-20250514", # gpt-5.4 for openai + pre_agent_downloadables=[ + RuntimeEnvironmentDownloadableInterface( + download_url="https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir="/runtimeuse", + ) + ], + ), + ) + + assert isinstance(result.data, TextResult) + print(result.data.text) -asyncio.run(main(ws_url)) -``` + asyncio.run(main(ws_url)) + ``` + + + ```typescript + import { + RuntimeUseClient, + type TextResult, + } from "runtimeuse-client"; + + const client = new RuntimeUseClient({ wsUrl: ws_url }); + + const result = await client.query( + "Summarize the contents of this repository and list your favorite file.", + { + system_prompt: "You are a helpful assistant.", + model: "claude-sonnet-4-20250514", // gpt-5.4 for openai + pre_agent_downloadables: [ + { + download_url: + "https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir: "/runtimeuse", + }, + ], + } + ); + + const data = result.data as TextResult; + console.log(data.text); + ``` + + diff --git a/docs/content/docs/typescript-client.mdx b/docs/content/docs/typescript-client.mdx new file mode 100644 index 0000000..142a7b7 --- /dev/null +++ b/docs/content/docs/typescript-client.mdx @@ -0,0 +1,201 @@ +--- +title: TypeScript Client +description: Connect to agent runtime in sandbox from TypeScript. +--- + +The [TypeScript client](https://www.npmjs.com/package/runtimeuse-client) is the control plane for RuntimeUse. It connects to the sandbox runtime, sends the invocation, and turns runtime messages into a single `QueryResult`. + +```bash +npm install runtimeuse-client +``` + +## Basic Query + +```typescript +import { + RuntimeUseClient, + type QueryOptions, + type TextResult, +} from "runtimeuse-client"; + +const client = new RuntimeUseClient({ wsUrl: "ws://localhost:8080" }); + +const result = await client.query("What is 2 + 2", { + system_prompt: "You are a helpful assistant.", + model: "gpt-5.3", +}); + +const data = result.data as TextResult; +console.log(data.text); +console.log(result.metadata); +``` + +`query()` returns a `QueryResult` with: + +- `data`: either `TextResult` (`.text`) or `StructuredOutputResult` (`.structured_output`) +- `metadata`: execution metadata returned by the runtime (includes token usage when available) + +## Return Structured JSON + +Pass `output_format_json_schema_str` when your application needs machine-readable output instead of free-form text. The result will be a `StructuredOutputResult`. + +```typescript +import { z } from "zod"; +import type { StructuredOutputResult } from "runtimeuse-client"; + +const RepoStats = z.object({ + file_count: z.number(), + char_count: z.number(), +}); + +const result = await client.query( + "Inspect the repository and return the total file count and character count as JSON.", + { + system_prompt: "You are a helpful assistant.", + model: "gpt-5.3", + output_format_json_schema_str: JSON.stringify({ + type: "json_schema", + schema: z.toJSONSchema(RepoStats), + }), + } +); + +const data = result.data as StructuredOutputResult; +const stats = RepoStats.parse(data.structured_output); +console.log(stats); +``` + +## Download Files into the Sandbox + +Use `pre_agent_downloadables` to fetch a repository, zip archive, or any URL into the sandbox before the agent runs. This is the primary way to give the agent access to a codebase or dataset. + +```typescript +import type { RuntimeEnvironmentDownloadable } from "runtimeuse-client"; + +const result = await client.query( + "Summarize the contents of this repository and list your favorite file.", + { + system_prompt: "You are a helpful assistant.", + model: "gpt-5.3", + pre_agent_downloadables: [ + { + download_url: + "https://github.com/openai/codex/archive/refs/heads/main.zip", + working_dir: "/runtimeuse", + }, + ], + } +); +``` + +The runtime downloads and extracts the file before handing control to the agent. + +## Upload Artifacts + +When the runtime requests an artifact upload, return a presigned URL and content type from `on_artifact_upload_request`. Set `artifacts_dir` to tell the runtime which sandbox directory contains the files to upload - both options must be provided together. + +```typescript +import type { + ArtifactUploadRequestMessage, + ArtifactUploadResult, +} from "runtimeuse-client"; + +async function onArtifactUploadRequest( + request: ArtifactUploadRequestMessage +): Promise { + const presignedUrl = await createPresignedUrl(request.filename); + return { + presigned_url: presignedUrl, + content_type: "application/octet-stream", + }; +} + +const result = await client.query( + "Generate a report and save it as report.txt.", + { + system_prompt: "You are a helpful assistant.", + model: "gpt-5.3", + artifacts_dir: "/runtimeuse/output", + on_artifact_upload_request: onArtifactUploadRequest, + } +); +``` + +## Stream Assistant Messages + +Use `on_assistant_message` when you want intermediate progress while the run is still happening. + +```typescript +import type { AssistantMessage } from "runtimeuse-client"; + +async function onAssistantMessage(msg: AssistantMessage): Promise { + for (const block of msg.text_blocks) { + console.log(`[assistant] ${block}`); + } +} + +const result = await client.query("Inspect this repository.", { + system_prompt: "You are a helpful assistant.", + model: "gpt-5.3", + on_assistant_message: onAssistantMessage, +}); +``` + +## Cancel a Run + +Call `client.abort()` to cancel an in-flight query. The client sends a cancel message to the runtime and `query()` throws `CancelledException`. + +```typescript +import { CancelledException } from "runtimeuse-client"; + +setTimeout(() => client.abort(), 5000); + +try { + await client.query("Do the thing.", options); +} catch (err) { + if (err instanceof CancelledException) { + console.log("Run was cancelled"); + } +} +``` + +## Set a Timeout + +Use `timeout` (in seconds) to limit how long a query can run. If the limit is exceeded, `query()` throws a `TimeoutError`. + +```typescript +const result = await client.query("Do the thing.", { + system_prompt: "You are a helpful assistant.", + model: "gpt-5.3", + timeout: 120, +}); +``` + +## Redact Secrets + +Pass `secrets_to_redact` to strip sensitive strings from any output or logs that leave the sandbox. + +```typescript +const result = await client.query("Check the API status.", { + system_prompt: "You are a helpful assistant.", + model: "gpt-5.3", + secrets_to_redact: ["sk-live-abc123", "my_db_password"], +}); +``` + +## Handle Errors + +`query()` throws `AgentRuntimeError` if the runtime sends back an error. The exception carries `.error` (the error message) and `.metadata`. + +```typescript +import { AgentRuntimeError } from "runtimeuse-client"; + +try { + const result = await client.query("Do the thing.", options); +} catch (err) { + if (err instanceof AgentRuntimeError) { + console.log(`Runtime error: ${err.error}`); + console.log(`Metadata: ${JSON.stringify(err.metadata)}`); + } +} +``` diff --git a/packages/runtimeuse-client-ts/.gitignore b/packages/runtimeuse-client-ts/.gitignore new file mode 100644 index 0000000..deed335 --- /dev/null +++ b/packages/runtimeuse-client-ts/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env diff --git a/packages/runtimeuse-client-ts/package-lock.json b/packages/runtimeuse-client-ts/package-lock.json new file mode 100644 index 0000000..7c31961 --- /dev/null +++ b/packages/runtimeuse-client-ts/package-lock.json @@ -0,0 +1,1329 @@ +{ + "name": "runtimeuse-client", + "version": "0.6.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "runtimeuse-client", + "version": "0.6.0", + "license": "FSL", + "dependencies": { + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "typescript": "^5.7.0", + "vitest": "^4.0.18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/packages/runtimeuse-client-ts/package.json b/packages/runtimeuse-client-ts/package.json new file mode 100644 index 0000000..4968558 --- /dev/null +++ b/packages/runtimeuse-client-ts/package.json @@ -0,0 +1,45 @@ +{ + "name": "runtimeuse-client", + "version": "0.6.0", + "description": "TypeScript client for communicating with a runtimeuse agent runtime", + "license": "FSL", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/getlark/runtimeuse.git", + "directory": "packages/runtimeuse-client-ts" + }, + "homepage": "https://github.com/getlark/runtimeuse", + "bugs": "https://github.com/getlark/runtimeuse/issues", + "keywords": [ + "ai", + "agent", + "runtime", + "websocket", + "client", + "runtimeuse" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "typescript": "^5.7.0", + "vitest": "^4.0.18" + } +} diff --git a/packages/runtimeuse-client-ts/src/client.test.ts b/packages/runtimeuse-client-ts/src/client.test.ts new file mode 100644 index 0000000..f0ec068 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/client.test.ts @@ -0,0 +1,511 @@ +import { describe, it, expect, vi } from "vitest"; +import { RuntimeUseClient } from "./client.js"; +import { AgentRuntimeError, CancelledException } from "./exceptions.js"; +import { SendQueue } from "./send-queue.js"; +import type { Transport } from "./transports/transport.js"; +import type { + QueryOptions, + QueryResult, + ArtifactUploadRequestMessage, + ArtifactUploadResult, + AssistantMessage, +} from "./types.js"; + +function createFakeTransport(messages: Record[]) { + const sent: Record[] = []; + + const transport: Transport = async function* (sendQueue: SendQueue) { + const drainPromise = (async () => { + while (true) { + const item = await sendQueue.get(); + sent.push(item); + sendQueue.taskDone(); + } + })(); + + try { + for (const msg of messages) { + yield msg; + } + await sendQueue.join(); + } finally { + // Stop the drainer - it's blocked on get(), so put a sentinel + // We can't truly cancel it, but the test will end + } + }; + + return { transport, sent }; +} + +const DEFAULT_PROMPT = "Do something."; + +function makeQueryOptions(overrides: Partial = {}): QueryOptions { + return { + system_prompt: "You are a good assistant.", + model: "gpt-4o", + ...overrides, + }; +} + +const TEXT_RESULT_MSG = { + message_type: "result_message", + data: { type: "text", text: "Hello, world!" }, + metadata: undefined, +}; + +const STRUCTURED_RESULT_MSG = { + message_type: "result_message", + data: { type: "structured_output", structured_output: { ok: true } }, + metadata: undefined, +}; + +// --------------------------------------------------------------------------- +// Result message +// --------------------------------------------------------------------------- + +describe("ResultMessage", () => { + it("returns structured output result", async () => { + const resultMsg = { + message_type: "result_message", + data: { + type: "structured_output", + structured_output: { success: true }, + }, + metadata: { duration_ms: 50 }, + }; + const { transport } = createFakeTransport([resultMsg]); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query( + DEFAULT_PROMPT, + makeQueryOptions({ + output_format_json_schema_str: '{"type":"object"}', + }) + ); + + expect(result.data.type).toBe("structured_output"); + if (result.data.type === "structured_output") { + expect(result.data.structured_output).toEqual({ success: true }); + } + expect(result.metadata).toEqual({ duration_ms: 50 }); + }); + + it("returns text result", async () => { + const resultMsg = { + message_type: "result_message", + data: { type: "text", text: "The answer is 42." }, + metadata: undefined, + }; + const { transport } = createFakeTransport([resultMsg]); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query(DEFAULT_PROMPT, makeQueryOptions()); + + expect(result.data.type).toBe("text"); + if (result.data.type === "text") { + expect(result.data.text).toBe("The answer is 42."); + } + }); + + it("throws when no result received", async () => { + const { transport } = createFakeTransport([]); + const client = new RuntimeUseClient({ transport }); + + await expect( + client.query(DEFAULT_PROMPT, makeQueryOptions()) + ).rejects.toThrow("No result message received"); + }); +}); + +// --------------------------------------------------------------------------- +// Assistant message +// --------------------------------------------------------------------------- + +describe("AssistantMessage", () => { + it("dispatches to callback", async () => { + const assistantMsg = { + message_type: "assistant_message", + text_blocks: ["Hello", "World"], + }; + const { transport } = createFakeTransport([assistantMsg, TEXT_RESULT_MSG]); + const onAssistant = vi.fn().mockResolvedValue(undefined); + + const client = new RuntimeUseClient({ transport }); + await client.query( + DEFAULT_PROMPT, + makeQueryOptions({ on_assistant_message: onAssistant }) + ); + + expect(onAssistant).toHaveBeenCalledOnce(); + const received = onAssistant.mock.calls[0][0] as AssistantMessage; + expect(received.text_blocks).toEqual(["Hello", "World"]); + }); + + it("is ignored without callback", async () => { + const assistantMsg = { + message_type: "assistant_message", + text_blocks: ["ignored"], + }; + const { transport } = createFakeTransport([assistantMsg, TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query(DEFAULT_PROMPT, makeQueryOptions()); + expect(result.data.type).toBe("text"); + }); +}); + +// --------------------------------------------------------------------------- +// Error message +// --------------------------------------------------------------------------- + +describe("ErrorMessage", () => { + it("raises AgentRuntimeError", async () => { + const errorMsg = { + message_type: "error_message", + error: "something broke", + metadata: { code: 500 }, + }; + const { transport } = createFakeTransport([errorMsg]); + const client = new RuntimeUseClient({ transport }); + + try { + await client.query(DEFAULT_PROMPT, makeQueryOptions()); + expect.fail("should have thrown"); + } catch (err) { + expect(err).toBeInstanceOf(AgentRuntimeError); + const runtimeErr = err as AgentRuntimeError; + expect(runtimeErr.error).toBe("something broke"); + expect(runtimeErr.metadata).toEqual({ code: 500 }); + } + }); + + it("handles error without metadata", async () => { + const errorMsg = { + message_type: "error_message", + error: "oops", + }; + const { transport } = createFakeTransport([errorMsg]); + const client = new RuntimeUseClient({ transport }); + + try { + await client.query(DEFAULT_PROMPT, makeQueryOptions()); + expect.fail("should have thrown"); + } catch (err) { + expect(err).toBeInstanceOf(AgentRuntimeError); + const runtimeErr = err as AgentRuntimeError; + expect(runtimeErr.error).toBe("oops"); + expect(runtimeErr.metadata).toBeUndefined(); + } + }); +}); + +// --------------------------------------------------------------------------- +// Artifact upload handshake +// --------------------------------------------------------------------------- + +describe("ArtifactUpload", () => { + it("performs upload handshake", async () => { + const uploadRequest = { + message_type: "artifact_upload_request_message", + filename: "screenshot.png", + filepath: "/tmp/screenshot.png", + }; + const { transport, sent } = createFakeTransport([ + uploadRequest, + TEXT_RESULT_MSG, + ]); + const client = new RuntimeUseClient({ transport }); + + const onArtifact = async ( + req: ArtifactUploadRequestMessage + ): Promise => { + expect(req.filename).toBe("screenshot.png"); + return { + presigned_url: "https://s3.example.com/presigned", + content_type: "image/png", + }; + }; + + await client.query( + DEFAULT_PROMPT, + makeQueryOptions({ + artifacts_dir: "/tmp/artifacts", + on_artifact_upload_request: onArtifact, + }) + ); + + const responseMsgs = sent.filter( + (m) => m.message_type === "artifact_upload_response_message" + ); + expect(responseMsgs).toHaveLength(1); + const resp = responseMsgs[0]; + expect(resp.filename).toBe("screenshot.png"); + expect(resp.filepath).toBe("/tmp/screenshot.png"); + expect(resp.presigned_url).toBe("https://s3.example.com/presigned"); + expect(resp.content_type).toBe("image/png"); + }); + + it("is ignored without callback", async () => { + const uploadRequest = { + message_type: "artifact_upload_request_message", + filename: "file.txt", + filepath: "/tmp/file.txt", + }; + const { transport } = createFakeTransport([uploadRequest, TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query(DEFAULT_PROMPT, makeQueryOptions()); + expect(result.data.type).toBe("text"); + }); +}); + +// --------------------------------------------------------------------------- +// Cancellation +// --------------------------------------------------------------------------- + +describe("Cancellation", () => { + it("abort raises CancelledException", async () => { + const fillerMsg = { + message_type: "assistant_message", + text_blocks: ["working..."], + }; + const { transport } = createFakeTransport([fillerMsg, fillerMsg]); + const client = new RuntimeUseClient({ transport }); + + const abortOnFirst = async (_msg: AssistantMessage) => { + client.abort(); + }; + + await expect( + client.query( + DEFAULT_PROMPT, + makeQueryOptions({ on_assistant_message: abortOnFirst }) + ) + ).rejects.toThrow(CancelledException); + }); + + it("no cancellation without abort", async () => { + const { transport } = createFakeTransport([TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query(DEFAULT_PROMPT, makeQueryOptions()); + expect(result.data.type).toBe("text"); + if (result.data.type === "text") { + expect(result.data.text).toBe("Hello, world!"); + } + }); +}); + +// --------------------------------------------------------------------------- +// Timeout +// --------------------------------------------------------------------------- + +describe("Timeout", () => { + it("throws on timeout", async () => { + const stallingTransport: Transport = async function* ( + _sendQueue: SendQueue + ) { + await new Promise((r) => setTimeout(r, 10000)); + yield {}; + }; + + const client = new RuntimeUseClient({ transport: stallingTransport }); + + await expect( + client.query(DEFAULT_PROMPT, makeQueryOptions({ timeout: 0.05 })) + ).rejects.toThrow("Query timed out"); + }); +}); + +// --------------------------------------------------------------------------- +// Unknown / malformed messages +// --------------------------------------------------------------------------- + +describe("UnknownMessages", () => { + it("unknown message type is skipped", async () => { + const unknownMsg = { message_type: "unknown_type", data: 123 }; + const { transport } = createFakeTransport([unknownMsg, TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query(DEFAULT_PROMPT, makeQueryOptions()); + expect(result.data.type).toBe("text"); + }); + + it("completely malformed message is skipped", async () => { + const badMsg = { no_message_type_key: true }; + const { transport } = createFakeTransport([badMsg, TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query(DEFAULT_PROMPT, makeQueryOptions()); + expect(result.data.type).toBe("text"); + }); +}); + +// --------------------------------------------------------------------------- +// Multiple messages in sequence +// --------------------------------------------------------------------------- + +describe("MultipleMessages", () => { + it("handles full message sequence", async () => { + const messages = [ + { + message_type: "assistant_message", + text_blocks: ["Starting..."], + }, + { + message_type: "assistant_message", + text_blocks: ["Still working..."], + }, + { + message_type: "result_message", + data: { + type: "structured_output", + structured_output: { answer: 42 }, + }, + metadata: { duration_ms: 100 }, + }, + ]; + const { transport } = createFakeTransport(messages); + const onAssistant = vi.fn().mockResolvedValue(undefined); + const client = new RuntimeUseClient({ transport }); + + const result = await client.query( + DEFAULT_PROMPT, + makeQueryOptions({ on_assistant_message: onAssistant }) + ); + + expect(onAssistant).toHaveBeenCalledTimes(2); + expect(result.data.type).toBe("structured_output"); + if (result.data.type === "structured_output") { + expect(result.data.structured_output).toEqual({ answer: 42 }); + } + }); +}); + +// --------------------------------------------------------------------------- +// Invocation message is sent to the transport +// --------------------------------------------------------------------------- + +describe("InvocationSent", () => { + it("invocation message is queued", async () => { + const { transport, sent } = createFakeTransport([TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + await client.query( + "Do something.", + makeQueryOptions({ source_id: "capture-test" }) + ); + + const invocationMsgs = sent.filter( + (m) => m.message_type === "invocation_message" + ); + expect(invocationMsgs).toHaveLength(1); + expect(invocationMsgs[0].source_id).toBe("capture-test"); + expect(invocationMsgs[0].user_prompt).toBe("Do something."); + }); + + it("schema forwarded when set", async () => { + const { transport, sent } = createFakeTransport([STRUCTURED_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + await client.query( + DEFAULT_PROMPT, + makeQueryOptions({ + output_format_json_schema_str: '{"type":"object"}', + }) + ); + + const invocationMsgs = sent.filter( + (m) => m.message_type === "invocation_message" + ); + expect(invocationMsgs[0].output_format_json_schema_str).toBe( + '{"type":"object"}' + ); + }); + + it("schema undefined when omitted", async () => { + const { transport, sent } = createFakeTransport([TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + await client.query(DEFAULT_PROMPT, makeQueryOptions()); + + const invocationMsgs = sent.filter( + (m) => m.message_type === "invocation_message" + ); + expect(invocationMsgs[0].output_format_json_schema_str).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Constructor validation +// --------------------------------------------------------------------------- + +describe("Constructor", () => { + it("requires wsUrl or transport", () => { + expect(() => new RuntimeUseClient({})).toThrow( + "Either wsUrl or transport must be provided" + ); + }); + + it("accepts wsUrl", () => { + const client = new RuntimeUseClient({ wsUrl: "ws://localhost:8080" }); + expect(client).toBeDefined(); + }); + + it("accepts transport", () => { + const { transport } = createFakeTransport([]); + const client = new RuntimeUseClient({ transport }); + expect(client).toBeDefined(); + }); + + it("artifacts_dir requires callback", () => { + expect(() => + makeQueryOptions({ artifacts_dir: "/tmp/artifacts" }) + ).not.toThrow(); + + // The validation happens inside query, not in makeQueryOptions + // Let's test via validateQueryOptions directly + }); +}); + +// --------------------------------------------------------------------------- +// QueryOptions validation +// --------------------------------------------------------------------------- + +describe("QueryOptions validation", () => { + it("artifacts_dir and on_artifact_upload_request must be set together", async () => { + const { transport } = createFakeTransport([TEXT_RESULT_MSG]); + const client = new RuntimeUseClient({ transport }); + + await expect( + client.query( + DEFAULT_PROMPT, + makeQueryOptions({ artifacts_dir: "/tmp/artifacts" }) + ) + ).rejects.toThrow("must be specified together"); + + const dummyCb = async () => ({ + presigned_url: "https://example.com", + content_type: "text/plain", + }); + + await expect( + client.query( + DEFAULT_PROMPT, + makeQueryOptions({ on_artifact_upload_request: dummyCb }) + ) + ).rejects.toThrow("must be specified together"); + + // Both set together should not throw validation error + const { transport: t2 } = createFakeTransport([TEXT_RESULT_MSG]); + const client2 = new RuntimeUseClient({ transport: t2 }); + const result = await client2.query( + DEFAULT_PROMPT, + makeQueryOptions({ + artifacts_dir: "/tmp/artifacts", + on_artifact_upload_request: dummyCb, + }) + ); + expect(result).toBeDefined(); + }); +}); diff --git a/packages/runtimeuse-client-ts/src/client.ts b/packages/runtimeuse-client-ts/src/client.ts new file mode 100644 index 0000000..71dd671 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/client.ts @@ -0,0 +1,178 @@ +import type { Transport } from "./transports/transport.js"; +import { WebSocketTransport } from "./transports/websocket-transport.js"; +import { SendQueue } from "./send-queue.js"; +import { AgentRuntimeError, CancelledException } from "./exceptions.js"; +import { + defaultLogger, + isKnownMessageType, + validateQueryOptions, + type AssistantMessage, + type ArtifactUploadRequestMessage, + type ArtifactUploadResponseMessage, + type CancelMessage, + type ErrorMessage, + type InvocationMessage, + type QueryOptions, + type QueryResult, + type ResultMessage, +} from "./types.js"; + +export class RuntimeUseClient { + private transport: Transport; + private aborted = false; + + constructor(options: { wsUrl?: string; transport?: Transport }) { + if (options.transport != null) { + this.transport = options.transport; + } else if (options.wsUrl != null) { + const wst = new WebSocketTransport(options.wsUrl); + this.transport = (sendQueue) => wst.call(sendQueue); + } else { + throw new Error("Either wsUrl or transport must be provided"); + } + } + + abort(): void { + this.aborted = true; + } + + async query(prompt: string, options: QueryOptions): Promise { + validateQueryOptions(options); + + const logger = options.logger ?? defaultLogger; + + this.aborted = false; + + const invocation: InvocationMessage = { + message_type: "invocation_message", + user_prompt: prompt, + system_prompt: options.system_prompt, + model: options.model, + output_format_json_schema_str: options.output_format_json_schema_str, + source_id: options.source_id, + secrets_to_redact: options.secrets_to_redact ?? [], + artifacts_dir: options.artifacts_dir, + pre_agent_invocation_commands: options.pre_agent_invocation_commands, + post_agent_invocation_commands: options.post_agent_invocation_commands, + pre_agent_downloadables: options.pre_agent_downloadables, + }; + + const sendQueue = new SendQueue(); + await sendQueue.put(invocation as unknown as Record); + + let wireResult: ResultMessage | undefined; + + const runQuery = async (): Promise => { + for await (const message of this.transport(sendQueue)) { + if (this.aborted) { + logger.info("Query cancelled by caller"); + const cancelMsg: CancelMessage = { + message_type: "cancel_message", + }; + await sendQueue.put( + cancelMsg as unknown as Record + ); + await sendQueue.join(); + throw new CancelledException("Query was cancelled"); + } + + const messageType = message.message_type as string | undefined; + if (!messageType || !isKnownMessageType(messageType)) { + logger.error( + `Received unknown message type from agent runtime: ${JSON.stringify(message)}` + ); + continue; + } + + if (messageType === "result_message") { + wireResult = message as unknown as ResultMessage; + logger.info( + `Received result message from agent runtime: ${JSON.stringify(message)}` + ); + continue; + } + + if (messageType === "assistant_message") { + if (options.on_assistant_message != null) { + const assistantMsg = message as unknown as AssistantMessage; + await options.on_assistant_message(assistantMsg); + } + continue; + } + + if (messageType === "error_message") { + const errorMsg = message as unknown as ErrorMessage; + if (typeof errorMsg.error !== "string") { + logger.error( + `Received malformed error message from agent runtime: ${JSON.stringify(message)}` + ); + throw new AgentRuntimeError(JSON.stringify(message)); + } + logger.error(`Error from agent runtime: ${JSON.stringify(errorMsg)}`); + throw new AgentRuntimeError( + errorMsg.error, + errorMsg.metadata ?? undefined + ); + } + + if (messageType === "artifact_upload_request_message") { + logger.info( + `Received artifact upload request message from agent runtime: ${JSON.stringify(message)}` + ); + if (options.on_artifact_upload_request != null) { + const uploadReq = + message as unknown as ArtifactUploadRequestMessage; + const uploadResult = + await options.on_artifact_upload_request(uploadReq); + const response: ArtifactUploadResponseMessage = { + message_type: "artifact_upload_response_message", + filename: uploadReq.filename, + filepath: uploadReq.filepath, + presigned_url: uploadResult.presigned_url, + content_type: uploadResult.content_type, + }; + await sendQueue.put( + response as unknown as Record + ); + } + continue; + } + + logger.info( + `Received non-result message from agent runtime: ${JSON.stringify(message)}` + ); + } + }; + + if (options.timeout != null) { + const timeoutMs = options.timeout * 1000; + await Promise.race([ + runQuery(), + new Promise((_, reject) => { + setTimeout( + () => reject(new TimeoutError("Query timed out")), + timeoutMs + ); + }), + ]); + } else { + await runQuery(); + } + + if (wireResult == null) { + throw new AgentRuntimeError("No result message received"); + } + + return { + data: wireResult.data, + metadata: wireResult.metadata, + }; + } +} + +class TimeoutError extends Error { + constructor(message: string) { + super(message); + this.name = "TimeoutError"; + } +} diff --git a/packages/runtimeuse-client-ts/src/exceptions.test.ts b/packages/runtimeuse-client-ts/src/exceptions.test.ts new file mode 100644 index 0000000..b1a72b9 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/exceptions.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import { AgentRuntimeError, CancelledException } from "./exceptions.js"; + +describe("CancelledException", () => { + it("has default message", () => { + const err = new CancelledException(); + expect(err.message).toBe("Query was cancelled"); + expect(err.name).toBe("CancelledException"); + }); + + it("accepts custom message", () => { + const err = new CancelledException("custom"); + expect(err.message).toBe("custom"); + }); +}); + +describe("AgentRuntimeError", () => { + it("stores error and metadata", () => { + const err = new AgentRuntimeError("broke", { code: 500 }); + expect(err.error).toBe("broke"); + expect(err.metadata).toEqual({ code: 500 }); + expect(err.name).toBe("AgentRuntimeError"); + }); + + it("formats message without metadata", () => { + const err = new AgentRuntimeError("oops"); + expect(err.message).toBe("oops"); + expect(err.metadata).toBeUndefined(); + }); + + it("formats message with metadata", () => { + const err = new AgentRuntimeError("broke", { code: 500 }); + expect(err.message).toContain("broke"); + expect(err.message).toContain("metadata:"); + expect(err.message).toContain("500"); + }); +}); diff --git a/packages/runtimeuse-client-ts/src/exceptions.ts b/packages/runtimeuse-client-ts/src/exceptions.ts new file mode 100644 index 0000000..adb13f8 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/exceptions.ts @@ -0,0 +1,27 @@ +export class CancelledException extends Error { + constructor(message = "Query was cancelled") { + super(message); + this.name = "CancelledException"; + } +} + +export class AgentRuntimeError extends Error { + readonly error: string; + readonly metadata?: Record; + + constructor(error: string, metadata?: Record) { + let msg = error; + if (metadata) { + try { + const metadataStr = JSON.stringify(metadata, Object.keys(metadata).sort()); + msg = `${error}\nmetadata: ${metadataStr}`; + } catch { + msg = `${error}\nmetadata: ${String(metadata)}`; + } + } + super(msg); + this.name = "AgentRuntimeError"; + this.error = error; + this.metadata = metadata; + } +} diff --git a/packages/runtimeuse-client-ts/src/index.ts b/packages/runtimeuse-client-ts/src/index.ts new file mode 100644 index 0000000..4aa1410 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/index.ts @@ -0,0 +1,25 @@ +export { RuntimeUseClient } from "./client.js"; +export { Transport } from "./transports/transport.js"; +export { WebSocketTransport } from "./transports/websocket-transport.js"; +export { AgentRuntimeError, CancelledException } from "./exceptions.js"; +export { SendQueue } from "./send-queue.js"; +export type { + Command, + RuntimeEnvironmentDownloadable, + InvocationMessage, + TextResult, + StructuredOutputResult, + QueryResult, + ResultMessage, + AssistantMessage, + ArtifactUploadRequestMessage, + ArtifactUploadResponseMessage, + ErrorMessage, + CancelMessage, + ArtifactUploadResult, + OnAssistantMessageCallback, + OnArtifactUploadRequestCallback, + QueryOptions, + Logger, +} from "./types.js"; +export { validateQueryOptions } from "./types.js"; diff --git a/packages/runtimeuse-client-ts/src/send-queue.test.ts b/packages/runtimeuse-client-ts/src/send-queue.test.ts new file mode 100644 index 0000000..400a127 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/send-queue.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import { SendQueue } from "./send-queue.js"; + +describe("SendQueue", () => { + it("put and get work in order", async () => { + const q = new SendQueue(); + await q.put({ a: 1 }); + await q.put({ b: 2 }); + + const first = await q.get(); + q.taskDone(); + const second = await q.get(); + q.taskDone(); + + expect(first).toEqual({ a: 1 }); + expect(second).toEqual({ b: 2 }); + }); + + it("get blocks until put", async () => { + const q = new SendQueue(); + const promise = q.get(); + + await q.put({ value: 42 }); + const result = await promise; + q.taskDone(); + + expect(result).toEqual({ value: 42 }); + }); + + it("join resolves when all items are done", async () => { + const q = new SendQueue(); + await q.put({ a: 1 }); + await q.put({ b: 2 }); + + const item1 = await q.get(); + q.taskDone(); + const item2 = await q.get(); + q.taskDone(); + + await q.join(); + }); + + it("join resolves immediately when empty", async () => { + const q = new SendQueue(); + await q.join(); + }); + + it("isEmpty reflects queue state", async () => { + const q = new SendQueue(); + expect(q.isEmpty()).toBe(true); + + await q.put({ a: 1 }); + expect(q.isEmpty()).toBe(false); + + await q.get(); + expect(q.isEmpty()).toBe(true); + }); +}); diff --git a/packages/runtimeuse-client-ts/src/send-queue.ts b/packages/runtimeuse-client-ts/src/send-queue.ts new file mode 100644 index 0000000..f7e6059 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/send-queue.ts @@ -0,0 +1,52 @@ +/** + * Simple async queue for outbound messages, analogous to asyncio.Queue. + * + * Supports put, get (blocking), join (wait until all items are processed), + * and task_done acknowledgement. + */ +export class SendQueue { + private queue: Record[] = []; + private waiters: Array<(value: Record) => void> = []; + private pending = 0; + private joinResolvers: Array<() => void> = []; + + async put(item: Record): Promise { + this.pending++; + if (this.waiters.length > 0) { + const resolve = this.waiters.shift()!; + resolve(item); + } else { + this.queue.push(item); + } + } + + async get(): Promise> { + if (this.queue.length > 0) { + return this.queue.shift()!; + } + return new Promise>((resolve) => { + this.waiters.push(resolve); + }); + } + + taskDone(): void { + this.pending--; + if (this.pending === 0 && this.joinResolvers.length > 0) { + for (const resolve of this.joinResolvers) { + resolve(); + } + this.joinResolvers = []; + } + } + + async join(): Promise { + if (this.pending === 0) return; + return new Promise((resolve) => { + this.joinResolvers.push(resolve); + }); + } + + isEmpty(): boolean { + return this.queue.length === 0; + } +} diff --git a/packages/runtimeuse-client-ts/src/transports/index.ts b/packages/runtimeuse-client-ts/src/transports/index.ts new file mode 100644 index 0000000..d276006 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/transports/index.ts @@ -0,0 +1,2 @@ +export { Transport } from "./transport.js"; +export { WebSocketTransport } from "./websocket-transport.js"; diff --git a/packages/runtimeuse-client-ts/src/transports/transport.ts b/packages/runtimeuse-client-ts/src/transports/transport.ts new file mode 100644 index 0000000..77f55ef --- /dev/null +++ b/packages/runtimeuse-client-ts/src/transports/transport.ts @@ -0,0 +1,12 @@ +import type { SendQueue } from "../send-queue.js"; + +/** + * Transport interface for the underlying message transport. + * + * Implementations must be callable that return an async iterable yielding + * parsed messages (objects) from the agent runtime. They consume outbound + * messages from the send queue. + */ +export type Transport = ( + sendQueue: SendQueue +) => AsyncIterable>; diff --git a/packages/runtimeuse-client-ts/src/transports/websocket-transport.ts b/packages/runtimeuse-client-ts/src/transports/websocket-transport.ts new file mode 100644 index 0000000..cc1aae0 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/transports/websocket-transport.ts @@ -0,0 +1,106 @@ +import WebSocket from "ws"; +import type { SendQueue } from "../send-queue.js"; + +export class WebSocketTransport { + private wsUrl: string; + + constructor(wsUrl: string) { + this.wsUrl = wsUrl; + } + + async *call( + sendQueue: SendQueue + ): AsyncGenerator> { + const ws = await this.connect(); + + let senderRunning = true; + const senderPromise = (async () => { + while (senderRunning) { + const message = await sendQueue.get(); + if (!senderRunning) { + sendQueue.taskDone(); + break; + } + try { + await new Promise((resolve, reject) => { + ws.send(JSON.stringify(message), (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } finally { + sendQueue.taskDone(); + } + } + })(); + + try { + const messageQueue: Record[] = []; + let messageResolve: (() => void) | null = null; + let closed = false; + + ws.on("message", (raw: WebSocket.RawData) => { + try { + const data = JSON.parse(raw.toString()) as Record; + messageQueue.push(data); + } catch { + messageQueue.push({ raw: raw.toString() }); + } + if (messageResolve) { + const r = messageResolve; + messageResolve = null; + r(); + } + }); + + ws.on("close", () => { + closed = true; + if (messageResolve) { + const r = messageResolve; + messageResolve = null; + r(); + } + }); + + ws.on("error", () => { + closed = true; + if (messageResolve) { + const r = messageResolve; + messageResolve = null; + r(); + } + }); + + while (true) { + if (messageQueue.length > 0) { + yield messageQueue.shift()!; + continue; + } + if (closed) break; + await new Promise((resolve) => { + messageResolve = resolve; + }); + } + } finally { + senderRunning = false; + sendQueue.put({}).then(() => sendQueue.taskDone()); + await senderPromise.catch(() => {}); + if ( + ws.readyState === WebSocket.OPEN || + ws.readyState === WebSocket.CONNECTING + ) { + ws.close(); + } + } + } + + private connect(): Promise { + return new Promise((resolve, reject) => { + const ws = new WebSocket(this.wsUrl, { + handshakeTimeout: 60_000, + }); + ws.once("open", () => resolve(ws)); + ws.once("error", reject); + }); + } +} diff --git a/packages/runtimeuse-client-ts/src/types.ts b/packages/runtimeuse-client-ts/src/types.ts new file mode 100644 index 0000000..0b3acd2 --- /dev/null +++ b/packages/runtimeuse-client-ts/src/types.ts @@ -0,0 +1,143 @@ +export interface Command { + command: string; + cwd?: string; +} + +export interface RuntimeEnvironmentDownloadable { + download_url: string; + working_dir: string; +} + +export interface InvocationMessage { + message_type: "invocation_message"; + source_id?: string; + system_prompt: string; + user_prompt: string; + output_format_json_schema_str?: string; + secrets_to_redact: string[]; + artifacts_dir?: string; + pre_agent_invocation_commands?: Command[]; + post_agent_invocation_commands?: Command[]; + model: string; + pre_agent_downloadables?: RuntimeEnvironmentDownloadable[]; +} + +export interface TextResult { + type: "text"; + text: string; +} + +export interface StructuredOutputResult { + type: "structured_output"; + structured_output: Record; +} + +export interface QueryResult { + metadata?: Record; + data: TextResult | StructuredOutputResult; +} + +export interface ResultMessage { + message_type: "result_message"; + metadata?: Record; + data: TextResult | StructuredOutputResult; +} + +export interface AssistantMessage { + message_type: "assistant_message"; + text_blocks: string[]; +} + +export interface ArtifactUploadRequestMessage { + message_type: "artifact_upload_request_message"; + filename: string; + filepath: string; +} + +export interface ArtifactUploadResponseMessage { + message_type: "artifact_upload_response_message"; + filename: string; + filepath: string; + presigned_url: string; + content_type: string; +} + +export interface ErrorMessage { + message_type: "error_message"; + error: string; + metadata?: Record; +} + +export interface CancelMessage { + message_type: "cancel_message"; +} + +export interface ArtifactUploadResult { + presigned_url: string; + content_type: string; +} + +export type OnAssistantMessageCallback = ( + message: AssistantMessage +) => Promise; + +export type OnArtifactUploadRequestCallback = ( + request: ArtifactUploadRequestMessage +) => Promise; + +export interface Logger { + info(message: string, ...args: unknown[]): void; + error(message: string, ...args: unknown[]): void; +} + +const defaultLogger: Logger = { + info: (msg, ...args) => console.log(msg, ...args), + error: (msg, ...args) => console.error(msg, ...args), +}; + +export interface QueryOptions { + system_prompt: string; + model: string; + output_format_json_schema_str?: string; + source_id?: string; + secrets_to_redact?: string[]; + artifacts_dir?: string; + pre_agent_invocation_commands?: Command[]; + post_agent_invocation_commands?: Command[]; + pre_agent_downloadables?: RuntimeEnvironmentDownloadable[]; + on_assistant_message?: OnAssistantMessageCallback; + on_artifact_upload_request?: OnArtifactUploadRequestCallback; + timeout?: number; + logger?: Logger; +} + +export function validateQueryOptions(options: QueryOptions): void { + const hasDir = options.artifacts_dir != null; + const hasCb = options.on_artifact_upload_request != null; + if (hasDir !== hasCb) { + throw new Error( + "artifacts_dir and on_artifact_upload_request must be specified together" + ); + } +} + +export { defaultLogger }; + +type KnownMessageType = + | "result_message" + | "assistant_message" + | "artifact_upload_request_message" + | "error_message"; + +const KNOWN_MESSAGE_TYPES = new Set([ + "result_message", + "assistant_message", + "artifact_upload_request_message", + "error_message", +]); + +export function isKnownMessageType( + value: string +): value is KnownMessageType { + return KNOWN_MESSAGE_TYPES.has(value); +} diff --git a/packages/runtimeuse-client-ts/tsconfig.json b/packages/runtimeuse-client-ts/tsconfig.json new file mode 100644 index 0000000..c83c533 --- /dev/null +++ b/packages/runtimeuse-client-ts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/runtimeuse-client-ts/vitest.config.ts b/packages/runtimeuse-client-ts/vitest.config.ts new file mode 100644 index 0000000..f612c07 --- /dev/null +++ b/packages/runtimeuse-client-ts/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + exclude: ["dist/**", "node_modules/**"], + }, +});