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.
@@ -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/**"],
+ },
+});