nanocodex 0.6.1 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -16
- package/browser/Agent.d.mts +1 -1
- package/browser/ElevenLabsPlayback.mjs +119 -0
- package/browser/InlineAgent.mjs +24 -17
- package/browser/Transport.d.mts +5 -0
- package/browser/Transport.mjs +4 -2
- package/browser/Voice.d.mts +6 -0
- package/browser/Voice.mjs +39 -8
- package/browser/VoiceSession.mjs +90 -24
- package/browser/WorkerAgent.mjs +2 -1
- package/browser/host.d.mts +27 -4
- package/browser/host.mjs +175 -18
- package/cloud/Decorator.d.mts +9 -0
- package/cloud/Decorator.mjs +9 -0
- package/cloud/Transport.mjs +6 -0
- package/cloud/actions/connection.mjs +3 -0
- package/cloud/actions/connector.d.mts +18 -0
- package/cloud/actions/connector.mjs +22 -0
- package/cloud/actions/index.d.mts +2 -0
- package/cloud/actions/index.mjs +2 -0
- package/cloud/internal.mjs +5 -0
- package/cloud/types.d.mts +2 -0
- package/cloudflare/Agent.d.mts +10 -9
- package/cloudflare/Agent.mjs +261 -91
- package/cloudflare/agent-settings.d.mts +15 -0
- package/cloudflare/agent-settings.mjs +135 -0
- package/cloudflare/chat-reasoning.mjs +20 -0
- package/cloudflare/durable-placement.d.mts +12 -0
- package/cloudflare/durable-placement.mjs +82 -0
- package/cloudflare/egress.d.mts +34 -1
- package/cloudflare/egress.mjs +14 -0
- package/cloudflare/gateway-binding-responses.mjs +84 -0
- package/cloudflare/gateway-responses.d.mts +48 -0
- package/cloudflare/gateway-responses.mjs +191 -0
- package/cloudflare/index.d.mts +6 -0
- package/cloudflare/index.mjs +3 -0
- package/cloudflare/managed-access.d.mts +23 -0
- package/cloudflare/managed-access.mjs +120 -0
- package/cloudflare/managed-auth.d.mts +15 -0
- package/cloudflare/managed-auth.mjs +113 -0
- package/cloudflare/managed-live.d.mts +7 -0
- package/cloudflare/managed-live.mjs +56 -0
- package/cloudflare/prepared-connection.mjs +49 -0
- package/cloudflare/provider-stream.mjs +353 -0
- package/cloudflare/rpc.d.mts +9 -0
- package/cloudflare/rpc.mjs +14 -0
- package/cloudflare/tool-alias-diagnostic.mjs +21 -0
- package/cloudflare/workers-ai-responses.d.mts +28 -0
- package/cloudflare/workers-ai-responses.mjs +398 -0
- package/host/Agent.d.mts +1 -1
- package/index.d.mts +2 -0
- package/internal.mjs +75 -14
- package/managed/Access.d.mts +5 -0
- package/managed/Access.mjs +66 -0
- package/managed/Agent.d.mts +29 -67
- package/managed/Agent.mjs +74 -183
- package/managed/README.md +9 -38
- package/managed/Voice.mjs +3 -1
- package/managed/index.d.mts +1 -3
- package/managed/index.mjs +1 -0
- package/node/Agent.d.mts +1 -1
- package/node/Agent.mjs +9 -3
- package/node/host.mjs +17 -0
- package/package.json +39 -2
- package/pkg-node/nanocodex.d.ts +8 -16
- package/pkg-node/nanocodex.js +107 -89
- package/pkg-node/nanocodex_bg.wasm.d.ts +4 -5
- package/pkg-web/.nanocodex-bindgen-stamp +7 -1
- package/pkg-web/nanocodex-build.json +1 -1
- package/pkg-web/nanocodex.d.ts +12 -21
- package/pkg-web/nanocodex.js +107 -88
- package/pkg-web/nanocodex_bg.js +107 -88
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +4 -5
- package/pkg-web/nanocodex_worker.js +1 -1
- package/runtime/before-compaction.mjs +49 -0
- package/runtime/code-evaluator.worker.mjs +30 -31
- package/runtime/managed-transport.mjs +11 -2
- package/runtime/mcp-runtime.mjs +15 -1
- package/runtime/quickjs-evaluator.mjs +69 -21
- package/runtime/response-controls.mjs +94 -25
- package/runtime/responses-http.mjs +69 -0
- package/runtime/steer-receipt.mjs +13 -0
- package/runtime/subagent-routing.d.mts +38 -0
- package/runtime/subagent-routing.mjs +80 -0
- package/runtime/subagents.d.mts +1 -1
- package/runtime/tempo-provider.d.mts +1 -1
- package/runtime/tempo-provider.mjs +1 -1
- package/tools/browser/accountInfo.mjs +53 -8
- package/tools/browser/browserShell.mjs +2 -2
- package/tools/browser/index.mjs +3 -3
- package/tools/environment.d.mts +40 -0
- package/tools/environment.mjs +73 -0
- package/tools/index.d.mts +2 -0
- package/types.d.mts +28 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ import { Actions, Agent, Transport } from "nanocodex/node";
|
|
|
10
10
|
|
|
11
11
|
const agent = await Agent.create({
|
|
12
12
|
transport: Transport.openAi({ apiKey: process.env.OPENAI_API_KEY }),
|
|
13
|
-
model: "gpt-
|
|
13
|
+
model: "gpt-6-luna",
|
|
14
14
|
instructions: "You are a Rust coding agent. Preserve unrelated work and run relevant tests.",
|
|
15
15
|
reasoningMode: "pro",
|
|
16
16
|
thinking: "high",
|
|
@@ -80,6 +80,23 @@ Worker, Durable Object, or application proxy that owns rotating credentials.
|
|
|
80
80
|
Authentication modes are constructors rather than a union of mutually
|
|
81
81
|
exclusive fields on `Agent.create`.
|
|
82
82
|
|
|
83
|
+
### Personal memories and caller context
|
|
84
|
+
|
|
85
|
+
Managed agents can keep user preferences separate from team knowledge:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { Agent } from "nanocodex/managed";
|
|
89
|
+
|
|
90
|
+
await Agent.memory({ operation: "scan", query: "my preferences" }, { scope: "personal" });
|
|
91
|
+
await Agent.memory({ operation: "put", content: "I prefer concise replies." }, { scope: "personal" });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The default scope remains `team`. Personal memories belong to the authenticated
|
|
95
|
+
user within their organization, across teams; use the same scope when reading,
|
|
96
|
+
replacing, or deleting a memory. Managed clients also accept descriptive
|
|
97
|
+
`requestOrigin: { client: "desktop", hand: "user:HAND_ID", cwd: "/HAND_ID" }`.
|
|
98
|
+
Caller context is included once at startup and never grants execution authority.
|
|
99
|
+
|
|
83
100
|
### Compose and place tools
|
|
84
101
|
|
|
85
102
|
`createTools` owns one deterministic tool recipe. Custom functions, a portable
|
|
@@ -158,8 +175,11 @@ workspace or MCP configuration to an Agent that already receives them through
|
|
|
158
175
|
`Tools`.
|
|
159
176
|
|
|
160
177
|
Browser consumers can attach Codex's ChatGPT Realtime voice lifecycle to the
|
|
161
|
-
same retained Agent. The resource owns microphone, speaker, WebRTC
|
|
178
|
+
same retained Agent. The resource owns microphone, speaker, WebRTC control,
|
|
162
179
|
and delegation cleanup; stopping voice does not cancel an active coding turn.
|
|
180
|
+
Managed and Connect voice use the WebRTC data channel for live events and
|
|
181
|
+
commands. Speech and captions can flow while durable startup completes;
|
|
182
|
+
delegated work still waits for admission. Local Agents retain the sideband path.
|
|
163
183
|
Snapshots update each speaker's transcript row as speech arrives, using a stable
|
|
164
184
|
`id` and `isPartial` flag. Completion replaces that row. `transcript.delta` events
|
|
165
185
|
carry the current partial text; `transcript` events retain completed-turn semantics.
|
|
@@ -214,15 +234,22 @@ subscription adapter (which treats all roles as context), and
|
|
|
214
234
|
`appendContext(voice, text)` adds background commentary without
|
|
215
235
|
requesting speech. Context and speech are split into provider-sized messages.
|
|
216
236
|
These commands retain frames until sent and preserve them
|
|
217
|
-
across
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
across reconnects when using the sideband transport. They are also methods on
|
|
238
|
+
the resource and on
|
|
239
|
+
`useVoice` from `nanocodex-react`. Choose `outputProvider: "elevenlabs"` and `elevenLabsVoiceId` to synthesize
|
|
240
|
+
spoken output with an ElevenLabs account voice, including an instant clone.
|
|
241
|
+
The account voice settings panel connects the API key, lists voices, and uploads
|
|
242
|
+
cloning samples after explicit consent. Keys are encrypted on the server and
|
|
243
|
+
never included in saved voice settings. ChatGPT still owns live input and agent
|
|
244
|
+
handoffs; `voice` continues to select its built-in voice. The default output
|
|
245
|
+
provider is `openai`. Platform audio configuration is not accepted.
|
|
220
246
|
|
|
221
247
|
`Voice.create(...)` remains the equivalent namespaced resource constructor, and
|
|
222
248
|
`Voice.voices` is the exact ChatGPT V3 voice catalog. The constructor accepts a
|
|
223
249
|
normal browser Agent, an account-owned managed Agent, or a grant-scoped
|
|
224
|
-
`ConnectAgent`. Authentication stays in the owning host routes
|
|
225
|
-
|
|
250
|
+
`ConnectAgent`. Authentication stays in the owning host routes. An explicit
|
|
251
|
+
`sidebandUrl` override selects the sideband transport; Connect uses a fresh
|
|
252
|
+
one-use ticket for that path. The browser binding never receives ChatGPT
|
|
226
253
|
credentials or places its reusable grant bearer in a WebSocket URL.
|
|
227
254
|
|
|
228
255
|
### Durable Cloudflare Agent
|
|
@@ -280,10 +307,12 @@ Cloudflare Agents default to direct tool mode because Workers prohibit dynamic
|
|
|
280
307
|
`eval`/`new Function`. Caller-defined tools therefore work without a code
|
|
281
308
|
evaluator. Select `toolMode: "code"` only when also supplying an evaluator that
|
|
282
309
|
is explicitly compatible with the deployed Worker runtime. Runtime-owned
|
|
283
|
-
Subagents are installed by default, including on a durable root.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
310
|
+
Subagents are installed by default, including on a durable root. All children are
|
|
311
|
+
ephemeral: their identities, topology, conversations, results, and routing exist
|
|
312
|
+
only for the lifetime of the root runtime. Root shutdown or restart discards the
|
|
313
|
+
entire child tree; only the root's own durable history resumes. Existing child
|
|
314
|
+
checkpoints from older versions are discarded. Within a live runtime, completed
|
|
315
|
+
children remain available for follow-up messages until closed. Use
|
|
287
316
|
`Subagents.create({ maxConcurrency })` in `tools` to set an explicit finite
|
|
288
317
|
concurrency limit. Active subagent turns are unlimited by default.
|
|
289
318
|
|
|
@@ -327,6 +356,11 @@ native and WASM consumers run the same Rust implementation and receive the
|
|
|
327
356
|
same seven tools: `spawn_agent`, `submit_result`, `send_agent_message`,
|
|
328
357
|
`list_agents`, `wait_agent`, `interrupt_agent`, and `close_agent`.
|
|
329
358
|
|
|
359
|
+
Children call `submit_result({output})`; the runtime supplies the trusted
|
|
360
|
+
instruction revision. The response is `{accepted: true, status: "accepted"}` or
|
|
361
|
+
`{accepted: false, status: "superseded"}`. Superseded submissions are normal
|
|
362
|
+
continuations: incorporate the updated instructions and submit again.
|
|
363
|
+
|
|
330
364
|
Inside a caller-owned Worker or server isolate, host capabilities stay as
|
|
331
365
|
ordinary functions without crossing another compatibility protocol:
|
|
332
366
|
|
|
@@ -500,7 +534,7 @@ use the individual factories in server-side Cloudflare Workers. Vite integration
|
|
|
500
534
|
is provided separately by `nanocodex-vite`.
|
|
501
535
|
|
|
502
536
|
The browser composition includes native `browseX` public X browsing, advertised
|
|
503
|
-
by `
|
|
537
|
+
by `environment().apis` without an X connector. The embedding app serves
|
|
504
538
|
`/api/tools/x/browse` and `/api/tools/x/convert`; Nanocodex's account app forwards
|
|
505
539
|
these requests to the private X Worker.
|
|
506
540
|
|
|
@@ -664,7 +698,7 @@ const mpp = tempoProvider.session;
|
|
|
664
698
|
|
|
665
699
|
const agent = await Agent.create({
|
|
666
700
|
transport: Transport.mpp({ session: tempoProvider }),
|
|
667
|
-
thinking: "
|
|
701
|
+
thinking: "low",
|
|
668
702
|
fastMode: true,
|
|
669
703
|
tools,
|
|
670
704
|
});
|
|
@@ -714,8 +748,8 @@ of its wallet adapter, and constructs both payment paths from that provider's
|
|
|
714
748
|
adapter-neutral `getMppxParameters()` contract. The lower-level
|
|
715
749
|
`createTempoProvider({ session, payment })` remains available when the
|
|
716
750
|
application constructs MPPx itself. Both explicitly select Tempo provider mode.
|
|
717
|
-
In that mode Nanocodex automatically adds its built-in Mercator MCP
|
|
718
|
-
with the same wallet and payment policy. The provider also exposes an MPP-aware
|
|
751
|
+
In that mode Nanocodex automatically adds its built-in Mercator MCP at
|
|
752
|
+
`https://mercator.sh/mcp` and wraps it with the same wallet and payment policy. The provider also exposes an MPP-aware
|
|
719
753
|
`fetch`; Mercator's paid REST handoffs use that same method rather than a second
|
|
720
754
|
wallet or payment configuration. Its MCP transport remains wrapped at the MCP
|
|
721
755
|
protocol layer, so browser requests do not need an `Accept-Payment` CORS header.
|
|
@@ -1133,7 +1167,7 @@ package manager or build step:
|
|
|
1133
1167
|
|
|
1134
1168
|
```html
|
|
1135
1169
|
<script type="module">
|
|
1136
|
-
import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.
|
|
1170
|
+
import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.5/host/index.mjs";
|
|
1137
1171
|
const agent = await Agent.create({
|
|
1138
1172
|
transport: Transport.hostManaged({
|
|
1139
1173
|
websocketUrl: "/api/responses",
|
|
@@ -1183,3 +1217,133 @@ inspect `agent.configuration()`, `environment()`, `usage()`, `requests()`,
|
|
|
1183
1217
|
`artifacts`, `webhook`, and `requiredActions`. See the
|
|
1184
1218
|
[managed configuration and operations guide](../../docs/MANAGED_AGENT_CONFIGURATION.md)
|
|
1185
1219
|
for examples, authorization, delivery semantics, and runtime limits.
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
## Call connected services from your app
|
|
1223
|
+
|
|
1224
|
+
After Nanocodex Connect login, the app and its agents share the same approved
|
|
1225
|
+
service capabilities and exact account selections. Request the services during
|
|
1226
|
+
login and call them directly without creating an agent or running a turn:
|
|
1227
|
+
|
|
1228
|
+
```js
|
|
1229
|
+
const connection = await client.connection.connect({
|
|
1230
|
+
capabilities: { cloudAccounts: { spotify: true, soundcloud: true } },
|
|
1231
|
+
});
|
|
1232
|
+
const response = await client.connectors.spotify.request({
|
|
1233
|
+
path: "/v1/me/playlists?limit=20",
|
|
1234
|
+
// Select an approved account when more than one is connected:
|
|
1235
|
+
connectionId: connection.grant.connectorConnections.spotify[0],
|
|
1236
|
+
});
|
|
1237
|
+
if (!response.ok) throw new Error(`Spotify returned ${response.status}`);
|
|
1238
|
+
const playlists = await response.json();
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
`client` is a `Client.create(...)` instance from `nanocodex/connect`. For a dynamic service, use
|
|
1242
|
+
`client.connectors.request({ connector: "spotify", path: "/v1/me/playlists" })`.
|
|
1243
|
+
The standalone form is `Actions.connectors.request(client, options)`.
|
|
1244
|
+
Every API service has a scoped entry point, including `client.connectors.gmail`
|
|
1245
|
+
and `client.connectors.soundcloud`. The original singular `client.connector.request`
|
|
1246
|
+
and `Actions.connector.request` remain supported. Requests support a provider
|
|
1247
|
+
path, HTTP method, optional `connectionId`, JSON object `body`, and abort `signal`.
|
|
1248
|
+
The response preserves provider HTTP status, body, and pagination/rate-limit
|
|
1249
|
+
headers. Writes are never automatically retried.
|
|
1250
|
+
|
|
1251
|
+
The underlying API is `POST /v1/connectors/:connector/request` at the configured
|
|
1252
|
+
Connect API origin, authenticated with the app's Connect grant bearer token and
|
|
1253
|
+
`X-Nanocodex-App-Id`. Browser calls must come from the grant's approved app origin.
|
|
1254
|
+
Its JSON body uses `path`, `method`, optional `connection_id`, and optional `body`.
|
|
1255
|
+
It requires no conversation or thread ID. Both this endpoint and agent egress
|
|
1256
|
+
check current grant revocation, expiry, approved service, and exact account IDs.
|
|
1257
|
+
Connecting an account alone does not grant it to every app; the user must approve
|
|
1258
|
+
that service in the app's Connect flow. Provider tokens remain in the broker.
|
|
1259
|
+
ChatGPT is a model connector and has no generic HTTP endpoint here.
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
### Use a provider SDK with a custom base URL
|
|
1263
|
+
|
|
1264
|
+
Native SDKs can use normal HTTP through
|
|
1265
|
+
`https://nanocodex.gakonst.workers.dev/connectors/<service>/<provider-path>`.
|
|
1266
|
+
All 13 API capabilities share this route. Append the provider's existing path:
|
|
1267
|
+
|
|
1268
|
+
| Connector | Example route |
|
|
1269
|
+
| --- | --- |
|
|
1270
|
+
| Spotify | `/connectors/spotify/v1/me/playlists` |
|
|
1271
|
+
| SoundCloud | `/connectors/soundcloud/me/playlists` |
|
|
1272
|
+
| Gmail | `/connectors/gmail/gmail/v1/users/me/messages` |
|
|
1273
|
+
| Google Drive | `/connectors/gdrive/drive/v3/files` |
|
|
1274
|
+
| Google Calendar | `/connectors/gcalendar/calendar/v3/calendars/primary/events` |
|
|
1275
|
+
| Google Tasks | `/connectors/gtasks/tasks/v1/users/@me/lists` |
|
|
1276
|
+
| Google Docs | `/connectors/gdocs/v1/documents/{id}` |
|
|
1277
|
+
| Google Sheets | `/connectors/gsheets/v4/spreadsheets/{id}` |
|
|
1278
|
+
| Google Slides | `/connectors/gslides/v1/presentations/{id}` |
|
|
1279
|
+
| Google Contacts | `/connectors/gcontacts/v1/people/me/connections?personFields=names` |
|
|
1280
|
+
| GitHub | `/connectors/github/user` |
|
|
1281
|
+
| Slack | `/connectors/slack/api/auth.test` |
|
|
1282
|
+
| X | `/connectors/x/2/users/me` |
|
|
1283
|
+
|
|
1284
|
+
Provider path restrictions and upstream OAuth permissions still apply. Google
|
|
1285
|
+
services are individually opted in even though they share a Google login.
|
|
1286
|
+
ChatGPT and remote MCP connections use their existing separate protocols.
|
|
1287
|
+
The same routes are available on the configured Connect API origin.
|
|
1288
|
+
|
|
1289
|
+
Send the app's **Nanocodex Connect grant token** as the bearer token. Native
|
|
1290
|
+
requests need no extra app identity headers: the token identifies its approved
|
|
1291
|
+
app and user. SDK routes also accept `Authorization: OAuth <grant-token>`
|
|
1292
|
+
(SoundCloud) and `Authorization: token <grant-token>` (GitHub). Tokens must stay in
|
|
1293
|
+
headers, not query parameters. Browser Origins and any explicit app ID must match that grant.
|
|
1294
|
+
The grant must already include the requested service; this route cannot connect
|
|
1295
|
+
accounts or enlarge the grant. If multiple accounts were approved, set
|
|
1296
|
+
`X-Nanocodex-Connector-Connection` to the selected approved connection ID.
|
|
1297
|
+
|
|
1298
|
+
For example, with [Spotipy](https://github.com/spotipy-dev/spotipy):
|
|
1299
|
+
|
|
1300
|
+
```python
|
|
1301
|
+
import spotipy
|
|
1302
|
+
|
|
1303
|
+
spotify = spotipy.Spotify(auth=nanocodex_grant_token, retries=0, status_retries=0)
|
|
1304
|
+
spotify.prefix = "https://nanocodex.gakonst.workers.dev/connectors/spotify/v1/"
|
|
1305
|
+
page = spotify.current_user_playlists(limit=20)
|
|
1306
|
+
next_page = spotify.next(page)
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
For Gmail with Google's [Python API client](https://github.com/googleapis/google-api-python-client):
|
|
1310
|
+
|
|
1311
|
+
```python
|
|
1312
|
+
from google.oauth2.credentials import Credentials
|
|
1313
|
+
from googleapiclient.discovery import build
|
|
1314
|
+
|
|
1315
|
+
gmail = build(
|
|
1316
|
+
"gmail", "v1",
|
|
1317
|
+
credentials=Credentials(token=nanocodex_grant_token),
|
|
1318
|
+
client_options={
|
|
1319
|
+
"api_endpoint": "https://nanocodex.gakonst.workers.dev/connectors/gmail/",
|
|
1320
|
+
},
|
|
1321
|
+
cache_discovery=False,
|
|
1322
|
+
)
|
|
1323
|
+
messages = gmail.users().messages().list(userId="me").execute(num_retries=0)
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
Use `userId="me"`; Gmail routes are restricted to the connected user. These
|
|
1327
|
+
credentials hold only the Connect grant; renew an expired grant through Nanocodex
|
|
1328
|
+
Connect instead of asking the provider SDK to refresh it with Google or Spotify.
|
|
1329
|
+
SoundCloud clients use `/connectors/soundcloud/` as their base URL and the same
|
|
1330
|
+
Connect grant, using [SoundCloud's OAuth header format](https://developers.soundcloud.com/docs).
|
|
1331
|
+
|
|
1332
|
+
The proxy preserves request methods, query parameters, and body bytes. Provider
|
|
1333
|
+
status codes, JSON errors, rate-limit headers, and non-JSON bodies are preserved.
|
|
1334
|
+
API links in JSON, pagination Link headers, and same-provider redirects are
|
|
1335
|
+
rewritten through the proxy; artwork and public share URLs stay unchanged.
|
|
1336
|
+
Cross-provider redirects are rejected. JSON link rewriting is bounded to 16 MiB
|
|
1337
|
+
per response; use provider pagination for larger collections. Spotify GETs share
|
|
1338
|
+
one-second read results and the broker's registration-wide rate-limit cooldown.
|
|
1339
|
+
The broker retries a rate-limited Spotify GET at most twice within a ten-second
|
|
1340
|
+
cooldown-wait budget; longer limits return 429 with the remaining `Retry-After`.
|
|
1341
|
+
No writes are automatically retried. Disable SDK write retries to avoid repeating
|
|
1342
|
+
a successful write after an ambiguous network failure.
|
|
1343
|
+
|
|
1344
|
+
See the runnable [Python example](../../examples/python/spotify_proxy.py).
|
|
1345
|
+
The integration test uses real Spotipy 2.26.0 and google-api-python-client 2.192.0
|
|
1346
|
+
against the Connect Worker with fixture provider responses, covering reads,
|
|
1347
|
+
writes, pagination, rate limits, and revoked grants. Worker tests cover routing
|
|
1348
|
+
and service denial for all 13 API connectors. SDKs that hardcode their API host need a custom transport instead of only
|
|
1349
|
+
a base URL setting.
|
package/browser/Agent.d.mts
CHANGED
|
@@ -33,7 +33,7 @@ export declare namespace create {
|
|
|
33
33
|
transport: ManagedTransport;
|
|
34
34
|
tools?: Tools | undefined;
|
|
35
35
|
}>;
|
|
36
|
-
type Options = AgentOptions & WorkerToolExposureOptions & {
|
|
36
|
+
type Options = Omit<AgentOptions, "beforeCompaction"> & WorkerToolExposureOptions & {
|
|
37
37
|
/** Precompiled browser module; WebAssembly modules are structured-clone-safe. */
|
|
38
38
|
module?: WebAssembly.Module | undefined;
|
|
39
39
|
/** Fixed browser workspace facts, including its AGENTS.md snapshot. */
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Plays account-proxied PCM without exposing provider credentials to the browser. */
|
|
2
|
+
export class ElevenLabsPlayback {
|
|
3
|
+
#synthesize;
|
|
4
|
+
#onError;
|
|
5
|
+
#context;
|
|
6
|
+
#abort = new AbortController();
|
|
7
|
+
#queue = Promise.resolve();
|
|
8
|
+
#sources = new Set();
|
|
9
|
+
#nextTime = 0;
|
|
10
|
+
#caption;
|
|
11
|
+
#offset = 0;
|
|
12
|
+
#suppressedThrough = -1;
|
|
13
|
+
#pending = 0;
|
|
14
|
+
#pendingChars = 0;
|
|
15
|
+
#closed = false;
|
|
16
|
+
|
|
17
|
+
constructor(synthesize, onError) {
|
|
18
|
+
this.#synthesize = synthesize;
|
|
19
|
+
this.#onError = onError;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
prime() {
|
|
23
|
+
if (this.#closed) return;
|
|
24
|
+
this.#context ??= new AudioContext({ sampleRate: 24000 });
|
|
25
|
+
return this.#context.resume();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
transcript(entry) {
|
|
29
|
+
if (this.#closed || entry.speaker !== "assistant" || entry.id <= this.#suppressedThrough) return;
|
|
30
|
+
if (this.#caption !== undefined && entry.id < this.#caption) return;
|
|
31
|
+
if (entry.id !== this.#caption) { this.#caption = entry.id; this.#offset = 0; }
|
|
32
|
+
const remaining = entry.text.slice(this.#offset);
|
|
33
|
+
// Buffer partial words; emit complete sentences before the turn finishes.
|
|
34
|
+
const boundary = entry.is_partial ? [...remaining.matchAll(/[.!?\n](?:\s|$)/g)].at(-1) : undefined;
|
|
35
|
+
const length = entry.is_partial ? (boundary ? boundary.index + boundary[0].length : 0) : remaining.length;
|
|
36
|
+
const text = remaining.slice(0, length).trim();
|
|
37
|
+
this.#offset += length;
|
|
38
|
+
if (!text) return;
|
|
39
|
+
if (this.#pending >= 32 || this.#pendingChars + text.length > 32768) {
|
|
40
|
+
this.interrupt();
|
|
41
|
+
this.#onError(new Error("ElevenLabs playback queue exceeded its limit"));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.#pending++;
|
|
45
|
+
this.#pendingChars += text.length;
|
|
46
|
+
const signal = this.#abort.signal;
|
|
47
|
+
this.#queue = this.#queue.then(async () => {
|
|
48
|
+
if (signal.aborted) return;
|
|
49
|
+
this.#context ??= new AudioContext({ sampleRate: 24000 });
|
|
50
|
+
await this.#context.resume();
|
|
51
|
+
if (signal.aborted) return;
|
|
52
|
+
const response = await this.#synthesize(text, signal);
|
|
53
|
+
if (!response.ok) throw new Error(`ElevenLabs synthesis failed (${response.status})`);
|
|
54
|
+
const reader = response.body?.getReader();
|
|
55
|
+
if (!reader) throw new Error("ElevenLabs returned no audio stream");
|
|
56
|
+
const cancel = () => { void reader.cancel().catch(() => {}); };
|
|
57
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
58
|
+
let carry;
|
|
59
|
+
try {
|
|
60
|
+
while (!signal.aborted) {
|
|
61
|
+
const { value, done } = await reader.read();
|
|
62
|
+
if (done) break;
|
|
63
|
+
if (signal.aborted) break;
|
|
64
|
+
if (value.byteLength > 1024 * 1024) throw new Error("ElevenLabs audio chunk exceeded its limit");
|
|
65
|
+
const bytes = carry === undefined ? value : Uint8Array.from([carry, ...value]);
|
|
66
|
+
const size = bytes.length - bytes.length % 2;
|
|
67
|
+
carry = size < bytes.length ? bytes[size] : undefined;
|
|
68
|
+
if (!size) continue;
|
|
69
|
+
// Keep decoded/scheduled audio bounded even when synthesis outruns playback.
|
|
70
|
+
for (let offset = 0; offset < size && !signal.aborted; offset += 9600) {
|
|
71
|
+
while (!signal.aborted && (this.#nextTime - this.#context.currentTime > 2 || this.#sources.size >= 128)) {
|
|
72
|
+
await new Promise((resolve) => {
|
|
73
|
+
const done = () => { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); };
|
|
74
|
+
const timer = setTimeout(done, 25);
|
|
75
|
+
signal.addEventListener("abort", done, { once: true });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (signal.aborted) break;
|
|
79
|
+
const length = Math.min(9600, size - offset);
|
|
80
|
+
const buffer = this.#context.createBuffer(1, length / 2, 24000);
|
|
81
|
+
const output = buffer.getChannelData(0);
|
|
82
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, length);
|
|
83
|
+
for (let i = 0; i < output.length; i++) output[i] = view.getInt16(i * 2, true) / 32768;
|
|
84
|
+
const source = this.#context.createBufferSource();
|
|
85
|
+
source.buffer = buffer;
|
|
86
|
+
source.connect(this.#context.destination);
|
|
87
|
+
this.#sources.add(source);
|
|
88
|
+
source.onended = () => { this.#sources.delete(source); source.disconnect(); };
|
|
89
|
+
this.#nextTime = Math.max(this.#nextTime, this.#context.currentTime + 0.02);
|
|
90
|
+
source.start(this.#nextTime);
|
|
91
|
+
this.#nextTime += buffer.duration;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} finally { signal.removeEventListener("abort", cancel); await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
95
|
+
}).catch((error) => { if (!signal.aborted) this.#onError(error); }).finally(() => {
|
|
96
|
+
if (!signal.aborted) { this.#pending--; this.#pendingChars -= text.length; }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interrupt() {
|
|
101
|
+
this.#suppressedThrough = Math.max(this.#suppressedThrough, this.#caption ?? -1);
|
|
102
|
+
this.#pending = 0;
|
|
103
|
+
this.#pendingChars = 0;
|
|
104
|
+
this.#abort.abort();
|
|
105
|
+
this.#abort = new AbortController();
|
|
106
|
+
this.#queue = Promise.resolve();
|
|
107
|
+
for (const source of this.#sources) { try { source.stop(); } catch {} source.disconnect(); }
|
|
108
|
+
this.#sources.clear();
|
|
109
|
+
this.#nextTime = 0;
|
|
110
|
+
// Caption IDs are monotonic: every late delta/final for this epoch stays silent.
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
close() {
|
|
114
|
+
this.#closed = true;
|
|
115
|
+
this.interrupt();
|
|
116
|
+
void this.#context?.close();
|
|
117
|
+
this.#context = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
package/browser/InlineAgent.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
registerDefinitionHost,
|
|
17
17
|
releaseDefinitionHost,
|
|
18
18
|
releaseHostSession,
|
|
19
|
+
releaseHostSessions,
|
|
19
20
|
toWasmConfig,
|
|
20
21
|
} from "../internal.mjs";
|
|
21
22
|
import { createBrowserHost } from "./host.mjs";
|
|
@@ -45,8 +46,10 @@ export async function create(options = {}) {
|
|
|
45
46
|
thinking,
|
|
46
47
|
reasoningMode,
|
|
47
48
|
fastMode,
|
|
49
|
+
rawApiEvents,
|
|
48
50
|
instructions,
|
|
49
51
|
additionalInstructions,
|
|
52
|
+
beforeCompaction,
|
|
50
53
|
sessionId,
|
|
51
54
|
workspace,
|
|
52
55
|
resume,
|
|
@@ -75,8 +78,10 @@ export async function create(options = {}) {
|
|
|
75
78
|
websocketPreconnect,
|
|
76
79
|
apiBaseUrl,
|
|
77
80
|
websocketWarmup,
|
|
81
|
+
stateless,
|
|
78
82
|
WebSocketImpl,
|
|
79
83
|
createWebSocket,
|
|
84
|
+
createResponse,
|
|
80
85
|
} = resolveResponsesTransport(transport ?? defaultHostManagedTransport());
|
|
81
86
|
const subagentsEnabled = internalRuntime?.subagentsEnabled;
|
|
82
87
|
if (subagentsEnabled !== undefined && typeof subagentsEnabled !== "boolean") {
|
|
@@ -109,16 +114,20 @@ export async function create(options = {}) {
|
|
|
109
114
|
const host = createBrowserHost({
|
|
110
115
|
WebSocketImpl,
|
|
111
116
|
createWebSocket,
|
|
117
|
+
createResponse,
|
|
112
118
|
hostAuth: hostAuth === true
|
|
113
119
|
|| (apiKey === undefined && mpp === undefined && subscription === undefined),
|
|
114
120
|
hostManagedProtocol,
|
|
115
121
|
mpp,
|
|
116
122
|
onEvent: events.emit,
|
|
123
|
+
beforeCompaction,
|
|
117
124
|
filesystem,
|
|
118
125
|
filesystemTools,
|
|
119
126
|
tools: hostTools,
|
|
120
127
|
toolProviders,
|
|
121
128
|
subagentSessions,
|
|
129
|
+
subagentRouting: internalRuntime?.subagentRouting,
|
|
130
|
+
onSocketTiming: internalRuntime?.onSocketTiming,
|
|
122
131
|
toolMode,
|
|
123
132
|
mcp: mcp === false
|
|
124
133
|
? undefined
|
|
@@ -161,8 +170,11 @@ export async function create(options = {}) {
|
|
|
161
170
|
: "wss://openai.mpp.tempo.xyz/v1/responses"),
|
|
162
171
|
apiBaseUrl,
|
|
163
172
|
websocketWarmup,
|
|
173
|
+
stateless,
|
|
164
174
|
subagents: subagentConfig,
|
|
175
|
+
subagentRouting: internalRuntime?.subagentRouting !== undefined,
|
|
165
176
|
hostDefinitionId,
|
|
177
|
+
beforeCompaction: beforeCompaction !== undefined,
|
|
166
178
|
...config,
|
|
167
179
|
durabilityHostId: durabilityOwner?.id,
|
|
168
180
|
}));
|
|
@@ -174,29 +186,19 @@ export async function create(options = {}) {
|
|
|
174
186
|
);
|
|
175
187
|
if (cloudflareReservation !== undefined) {
|
|
176
188
|
activateCloudflareAgentSession(cloudflareReservation);
|
|
177
|
-
const restoredSubagents = subagentSessions?.restore?.() ?? [];
|
|
178
|
-
if (restoredSubagents.length > 0) {
|
|
179
|
-
const restoredHostContextRefs = Object.fromEntries(
|
|
180
|
-
restoredSubagents.map((descriptor) => {
|
|
181
|
-
const hostContextRef = subagentSessions?.hostContextRef?.(descriptor.sessionId);
|
|
182
|
-
return [descriptor.sessionId, hostContextRef ?? null];
|
|
183
|
-
}),
|
|
184
|
-
);
|
|
185
|
-
await raw.restoreSubagents(
|
|
186
|
-
JSON.stringify(restoredSubagents),
|
|
187
|
-
JSON.stringify(restoredHostContextRefs),
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
189
|
}
|
|
191
190
|
return raw;
|
|
192
191
|
} catch (error) {
|
|
193
192
|
const cleanupErrors = [];
|
|
194
193
|
if (raw !== undefined) {
|
|
195
|
-
try {
|
|
194
|
+
try {
|
|
195
|
+
await raw.shutdown();
|
|
196
|
+
}
|
|
196
197
|
catch (cleanupError) { cleanupErrors.push(cleanupError); }
|
|
197
198
|
try { raw.free(); }
|
|
198
199
|
catch (cleanupError) { cleanupErrors.push(cleanupError); }
|
|
199
200
|
}
|
|
201
|
+
releaseHostSessions(host);
|
|
200
202
|
try { durabilityOwner?.abandon(); }
|
|
201
203
|
catch (cleanupError) { cleanupErrors.push(cleanupError); }
|
|
202
204
|
try { await host.dispose(); }
|
|
@@ -210,16 +212,20 @@ export async function create(options = {}) {
|
|
|
210
212
|
throw error;
|
|
211
213
|
}
|
|
212
214
|
},
|
|
215
|
+
async shutdown(raw) {
|
|
216
|
+
await raw.shutdown();
|
|
217
|
+
},
|
|
213
218
|
subscribe: events.subscribe,
|
|
214
219
|
adopt(raw) {
|
|
215
220
|
host.retain();
|
|
216
221
|
try {
|
|
217
|
-
|
|
222
|
+
// Adopted child handles are ephemeral and do not own the root store.
|
|
223
|
+
if (raw.sessionId === stableSessionId) durabilityOwner?.retain();
|
|
218
224
|
bindHostSession(host, raw.sessionId, cloudflareReservation);
|
|
219
225
|
events.addSource(raw);
|
|
220
226
|
} catch (error) {
|
|
221
227
|
events.removeSource(raw);
|
|
222
|
-
durabilityOwner?.release();
|
|
228
|
+
if (raw.sessionId === stableSessionId) durabilityOwner?.release();
|
|
223
229
|
releaseHost(host);
|
|
224
230
|
throw error;
|
|
225
231
|
}
|
|
@@ -228,7 +234,7 @@ export async function create(options = {}) {
|
|
|
228
234
|
events.removeSource(raw);
|
|
229
235
|
host.releaseSession(raw.sessionId);
|
|
230
236
|
releaseHostSession(host, raw.sessionId);
|
|
231
|
-
durabilityOwner?.release();
|
|
237
|
+
if (raw.sessionId === stableSessionId) durabilityOwner?.release();
|
|
232
238
|
releaseHost(host);
|
|
233
239
|
},
|
|
234
240
|
decorate: (agent) => agent.extend(agentActions()),
|
|
@@ -240,6 +246,7 @@ export async function create(options = {}) {
|
|
|
240
246
|
thinking,
|
|
241
247
|
reasoningMode,
|
|
242
248
|
fastMode,
|
|
249
|
+
rawApiEvents,
|
|
243
250
|
instructions,
|
|
244
251
|
additionalInstructions,
|
|
245
252
|
sessionId: stableSessionId,
|
package/browser/Transport.d.mts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from "../types.mjs";
|
|
5
5
|
import type { Options as ManagedClientOptions } from "../managed/Agent.mjs";
|
|
6
6
|
import type {
|
|
7
|
+
BrowserHttpRequest,
|
|
7
8
|
BrowserWebSocketConnection,
|
|
8
9
|
BrowserWebSocketRequest,
|
|
9
10
|
} from "./host.mjs";
|
|
@@ -25,6 +26,8 @@ export type WorkerTransport = ResponsesTransport & Readonly<{
|
|
|
25
26
|
|
|
26
27
|
type SharedEndpointOptions = Readonly<{
|
|
27
28
|
apiBaseUrl?: string | undefined;
|
|
29
|
+
/** Use HTTPS with full history replay, no stored responses, and no WebSocket warmup or preconnect. */
|
|
30
|
+
stateless?: boolean | undefined;
|
|
28
31
|
websocketUrl?: string | undefined;
|
|
29
32
|
/** Open the persistent socket as soon as Agent.create returns. Defaults to true for hostManaged. */
|
|
30
33
|
websocketPreconnect?: boolean | undefined;
|
|
@@ -34,9 +37,11 @@ type SharedEndpointOptions = Readonly<{
|
|
|
34
37
|
type WorkerEndpointOptions = SharedEndpointOptions & Readonly<{
|
|
35
38
|
WebSocketImpl?: never;
|
|
36
39
|
createWebSocket?: never;
|
|
40
|
+
createResponse?: never;
|
|
37
41
|
}>;
|
|
38
42
|
|
|
39
43
|
type EndpointOptions = SharedEndpointOptions & Readonly<{
|
|
44
|
+
createResponse?(endpoint: string, sessionId: string, request: BrowserHttpRequest): Promise<Response>;
|
|
40
45
|
WebSocketImpl?: typeof WebSocket | undefined;
|
|
41
46
|
createWebSocket?(
|
|
42
47
|
endpoint: string,
|
package/browser/Transport.mjs
CHANGED
|
@@ -25,12 +25,12 @@ export function chatGpt(options) {
|
|
|
25
25
|
|
|
26
26
|
export function hostManaged(options = {}) {
|
|
27
27
|
const websocketUrl = options.websocketUrl
|
|
28
|
-
?? (options.createWebSocket ? undefined : defaultHostManagedWebSocketUrl());
|
|
28
|
+
?? (options.stateless === true || options.createWebSocket ? undefined : defaultHostManagedWebSocketUrl());
|
|
29
29
|
return createResponsesTransport({
|
|
30
30
|
hostAuth: true,
|
|
31
31
|
hostManagedProtocol: true,
|
|
32
32
|
...connection({ ...options, websocketUrl }),
|
|
33
|
-
websocketPreconnect: options.websocketPreconnect ?? true,
|
|
33
|
+
websocketPreconnect: options.stateless === true ? false : options.websocketPreconnect ?? true,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -53,7 +53,9 @@ function connection(options = {}) {
|
|
|
53
53
|
return {
|
|
54
54
|
WebSocketImpl: options.WebSocketImpl,
|
|
55
55
|
apiBaseUrl: options.apiBaseUrl,
|
|
56
|
+
stateless: options.stateless,
|
|
56
57
|
createWebSocket: options.createWebSocket,
|
|
58
|
+
createResponse: options.createResponse,
|
|
57
59
|
websocketUrl: options.websocketUrl,
|
|
58
60
|
websocketPreconnect: options.websocketPreconnect,
|
|
59
61
|
websocketWarmup: options.websocketWarmup,
|
package/browser/Voice.d.mts
CHANGED
|
@@ -34,6 +34,7 @@ export type Snapshot = Readonly<{
|
|
|
34
34
|
microphoneLevel: number;
|
|
35
35
|
speakerLevel: number;
|
|
36
36
|
error: Error | undefined;
|
|
37
|
+
/** Active means media and live control are ready; task admission may still be pending. */
|
|
37
38
|
status: "idle" | "connecting" | "active" | "error";
|
|
38
39
|
statusText: string | undefined;
|
|
39
40
|
/** Latest 200 rows, retained across stop/start. Subscribe to events for longer history. */
|
|
@@ -50,6 +51,8 @@ export type Event =
|
|
|
50
51
|
| Readonly<{ type: "stopped" }>;
|
|
51
52
|
export type Settings = Readonly<{
|
|
52
53
|
voice?: VoiceName | undefined;
|
|
54
|
+
outputProvider?: "openai" | "elevenlabs" | undefined;
|
|
55
|
+
elevenLabsVoiceId?: string | undefined;
|
|
53
56
|
/** Additional speaking preferences; base assistant instructions are retained. */
|
|
54
57
|
instructions?: string | undefined;
|
|
55
58
|
pace?: "slow" | "natural" | "fast" | undefined;
|
|
@@ -58,6 +61,8 @@ export type Settings = Readonly<{
|
|
|
58
61
|
acknowledgements?: boolean | undefined;
|
|
59
62
|
}>;
|
|
60
63
|
export type Options = Settings & Readonly<{
|
|
64
|
+
/** Authenticated synthesis transport returning mono signed 16-bit little-endian PCM at 24 kHz. */
|
|
65
|
+
synthesize?(text: string, signal: AbortSignal): Promise<Response>;
|
|
61
66
|
callUrl?: string | URL | undefined;
|
|
62
67
|
sidebandUrl?(callId: string, sessionId: string): string | URL | Promise<string | URL>;
|
|
63
68
|
captureMicrophone?(): Promise<MediaStream>;
|
|
@@ -78,6 +83,7 @@ export type Voice = Readonly<{
|
|
|
78
83
|
destroy(): Promise<void>;
|
|
79
84
|
getSnapshot(): Snapshot;
|
|
80
85
|
onEvent(listener: (event: Event) => void): () => void;
|
|
86
|
+
/** Resolves after media and task admission are ready. The active snapshot may arrive first. */
|
|
81
87
|
start(options?: Settings): Promise<void>;
|
|
82
88
|
stop(): Promise<void>;
|
|
83
89
|
subscribe(listener: () => void): () => void;
|