nexus-ai-pro 2.9.0 → 2.11.0
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/API_STABILITY.md +73 -0
- package/CHANGELOG.md +208 -1
- package/README.md +14 -5
- package/dist/agent/files.d.ts +36 -0
- package/dist/agent/files.js +112 -0
- package/dist/agent/middleware/filesystem.d.ts +3 -3
- package/dist/agent/sandbox-container.js +6 -4
- package/dist/agent/sandbox.d.ts +17 -58
- package/dist/agent/sandbox.js +19 -229
- package/dist/agent/workspace.d.ts +220 -0
- package/dist/agent/workspace.js +673 -0
- package/dist/core/lifecycle.d.ts +11 -1
- package/dist/core/lifecycle.js +9 -0
- package/dist/core/nexus.d.ts +7 -0
- package/dist/core/nexus.js +7 -0
- package/dist/deep-agent/helpers.d.ts +32 -0
- package/dist/deep-agent/helpers.js +62 -1
- package/dist/deep-agent/index.d.ts +25 -8
- package/dist/deep-agent/index.js +20 -18
- package/dist/graph/graph.d.ts +5 -0
- package/dist/graph/graph.js +6 -0
- package/dist/lifecycle/index.d.ts +1 -1
- package/dist/protocols/a2a.d.ts +84 -1
- package/dist/protocols/a2a.js +178 -14
- package/dist/protocols/acp.d.ts +16 -1
- package/dist/protocols/acp.js +78 -17
- package/dist/protocols/shared.d.ts +15 -0
- package/dist/realtime/conversation.d.ts +28 -1
- package/dist/realtime/conversation.js +62 -1
- package/dist/realtime/gemini-live.transport.d.ts +134 -0
- package/dist/realtime/gemini-live.transport.js +769 -0
- package/dist/realtime/index.d.ts +8 -4
- package/dist/realtime/index.js +5 -3
- package/dist/realtime/openai-server.d.ts +49 -0
- package/dist/realtime/openai-server.js +103 -0
- package/dist/realtime/openai-sip.d.ts +114 -0
- package/dist/realtime/openai-sip.js +111 -0
- package/dist/realtime/openai-webrtc.transport.d.ts +7 -0
- package/dist/realtime/openai-webrtc.transport.js +4 -0
- package/dist/realtime/openai-websocket.transport.d.ts +5 -0
- package/dist/realtime/openai-websocket.transport.js +3 -0
- package/dist/realtime/provider.d.ts +20 -0
- package/dist/realtime/provider.js +17 -0
- package/dist/realtime/session.d.ts +57 -2
- package/dist/realtime/session.js +283 -23
- package/dist/realtime/types.d.ts +73 -2
- package/dist/telephony/index.d.ts +4 -2
- package/dist/telephony/index.js +2 -1
- package/dist/telephony/manager.d.ts +31 -1
- package/dist/telephony/manager.js +84 -0
- package/dist/telephony/providers/telnyx.d.ts +108 -0
- package/dist/telephony/providers/telnyx.js +646 -0
- package/dist/telephony/providers/twilio.d.ts +24 -3
- package/dist/telephony/providers/twilio.js +174 -0
- package/dist/telephony/realtime-bridge.d.ts +5 -0
- package/dist/telephony/realtime-bridge.js +1 -0
- package/dist/telephony/recordings.d.ts +91 -0
- package/dist/telephony/recordings.js +86 -0
- package/dist/telephony/twiml.d.ts +2 -1
- package/dist/telephony/twiml.js +20 -0
- package/dist/types/lifecycle.d.ts +14 -1
- package/dist/types/telephony.d.ts +247 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/agent/files.d.ts +36 -0
- package/dist-cjs/agent/files.js +148 -0
- package/dist-cjs/agent/middleware/filesystem.d.ts +3 -3
- package/dist-cjs/agent/sandbox-container.js +6 -4
- package/dist-cjs/agent/sandbox.d.ts +17 -58
- package/dist-cjs/agent/sandbox.js +19 -229
- package/dist-cjs/agent/workspace.d.ts +220 -0
- package/dist-cjs/agent/workspace.js +686 -0
- package/dist-cjs/core/lifecycle.d.ts +11 -1
- package/dist-cjs/core/lifecycle.js +9 -0
- package/dist-cjs/core/nexus.d.ts +7 -0
- package/dist-cjs/core/nexus.js +7 -0
- package/dist-cjs/deep-agent/helpers.d.ts +32 -0
- package/dist-cjs/deep-agent/helpers.js +62 -1
- package/dist-cjs/deep-agent/index.d.ts +25 -8
- package/dist-cjs/deep-agent/index.js +20 -18
- package/dist-cjs/graph/graph.d.ts +5 -0
- package/dist-cjs/graph/graph.js +6 -0
- package/dist-cjs/lifecycle/index.d.ts +1 -1
- package/dist-cjs/protocols/a2a.d.ts +84 -1
- package/dist-cjs/protocols/a2a.js +178 -14
- package/dist-cjs/protocols/acp.d.ts +16 -1
- package/dist-cjs/protocols/acp.js +78 -17
- package/dist-cjs/protocols/shared.d.ts +15 -0
- package/dist-cjs/realtime/conversation.d.ts +28 -1
- package/dist-cjs/realtime/conversation.js +66 -1
- package/dist-cjs/realtime/gemini-live.transport.d.ts +134 -0
- package/dist-cjs/realtime/gemini-live.transport.js +773 -0
- package/dist-cjs/realtime/index.d.ts +8 -4
- package/dist-cjs/realtime/index.js +15 -1
- package/dist-cjs/realtime/openai-server.d.ts +49 -0
- package/dist-cjs/realtime/openai-server.js +108 -0
- package/dist-cjs/realtime/openai-sip.d.ts +114 -0
- package/dist-cjs/realtime/openai-sip.js +114 -0
- package/dist-cjs/realtime/openai-webrtc.transport.d.ts +7 -0
- package/dist-cjs/realtime/openai-webrtc.transport.js +4 -0
- package/dist-cjs/realtime/openai-websocket.transport.d.ts +5 -0
- package/dist-cjs/realtime/openai-websocket.transport.js +3 -0
- package/dist-cjs/realtime/provider.d.ts +20 -0
- package/dist-cjs/realtime/provider.js +19 -1
- package/dist-cjs/realtime/session.d.ts +57 -2
- package/dist-cjs/realtime/session.js +282 -22
- package/dist-cjs/realtime/types.d.ts +73 -2
- package/dist-cjs/telephony/index.d.ts +4 -2
- package/dist-cjs/telephony/index.js +4 -1
- package/dist-cjs/telephony/manager.d.ts +31 -1
- package/dist-cjs/telephony/manager.js +84 -0
- package/dist-cjs/telephony/providers/telnyx.d.ts +108 -0
- package/dist-cjs/telephony/providers/telnyx.js +650 -0
- package/dist-cjs/telephony/providers/twilio.d.ts +24 -3
- package/dist-cjs/telephony/providers/twilio.js +174 -0
- package/dist-cjs/telephony/realtime-bridge.d.ts +5 -0
- package/dist-cjs/telephony/realtime-bridge.js +2 -0
- package/dist-cjs/telephony/recordings.d.ts +91 -0
- package/dist-cjs/telephony/recordings.js +89 -0
- package/dist-cjs/telephony/twiml.d.ts +2 -1
- package/dist-cjs/telephony/twiml.js +20 -0
- package/dist-cjs/types/lifecycle.d.ts +14 -1
- package/dist-cjs/types/telephony.d.ts +247 -1
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +59 -5
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,79 @@ Both are covered by this policy:
|
|
|
41
41
|
Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
|
|
42
42
|
one of its explicit subpaths.
|
|
43
43
|
|
|
44
|
+
## 2.11 stage (2.11.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.11 adds is stable from the start and additive, unless this section says otherwise.
|
|
47
|
+
|
|
48
|
+
New and stable:
|
|
49
|
+
- `nexus-ai-pro/agent/workspace`: `Workspace`, `WorkspaceGlobOptions`, `WorkspaceGrepOptions`,
|
|
50
|
+
`WorkspaceMatch`, `WorkspacePathError`, `WorkspaceReadOnlyError`, `resolveWorkspacePath()`,
|
|
51
|
+
`memoryWorkspace()`, `fileWorkspace()`, `storeWorkspace()`, `readOnlyWorkspace()`,
|
|
52
|
+
`compositeWorkspace()` and their options, `WorkspaceMount`, `workspaceTools()` and
|
|
53
|
+
`WorkspaceToolsOptions`, and `runWorkspaceConformance()` with its report and check;
|
|
54
|
+
- `DeepAgentOptions.workspace`;
|
|
55
|
+
- `nexus-ai-pro/telephony/telnyx`, with `TelnyxTelephonyProvider` and its config;
|
|
56
|
+
- on `nexus-ai-pro/telephony`: `TelephonyManager.answer()`, `transferCall()`, `joinConference()`,
|
|
57
|
+
`startRecording()`, `stopRecording()`, `parseRecordingCallback()`, `downloadRecording()`,
|
|
58
|
+
`deleteRecording()`, `sendMessage()`, and `parseMessageWebhook()`, the matching optional
|
|
59
|
+
`TelephonyProvider` methods, their request and result types, `TelephonyDialConfig`,
|
|
60
|
+
`TelephonyConferenceConfig`, and `TelephonyResponseRequest.dial`; `recordingKeeper()` with
|
|
61
|
+
`RecordingKeeper`, `RecordingKeeperOptions`, `RecordingRetention`, and `KeptRecording`;
|
|
62
|
+
`telnyxRealtimeAudioOptions()`; the `TelephonyProviderInfo.supports` flags `transfer`,
|
|
63
|
+
`conference`, `recording`, and `messaging`; `TelephonyWebhookValidationRequest.publicKey`;
|
|
64
|
+
- `NexusAI.telephony`; the operation family `messaging`;
|
|
65
|
+
- `Sandbox` extends `Workspace`, so it gains `writeBytes()`, optional; `SandboxGlobOptions`,
|
|
66
|
+
`SandboxGrepOptions`, and `SandboxMatch` are the workspace's types under their old names, and
|
|
67
|
+
`SandboxPathError` is a `WorkspacePathError`.
|
|
68
|
+
|
|
69
|
+
Changed, each to keep a promise the surface already made:
|
|
70
|
+
- A sandbox's file methods take a leading `/` as the sandbox's root, as the workspace contract
|
|
71
|
+
says; a path that leaves the root is still refused. The container sandbox rejects reading a
|
|
72
|
+
missing file with `ENOENT`.
|
|
73
|
+
|
|
74
|
+
## 2.10 stage (2.10.0)
|
|
75
|
+
|
|
76
|
+
Everything 2.10 adds is stable from the start and additive, unless this section says otherwise.
|
|
77
|
+
|
|
78
|
+
New and stable:
|
|
79
|
+
- on `nexus-ai-pro/protocols/a2a`: `A2aHandler`, which `a2aHandler()` now returns, with `owner`,
|
|
80
|
+
`recover()`, and `close()`; `A2aRecoveryReport`; `A2aTaskLease` and `A2aTaskRun`, and
|
|
81
|
+
`A2aTaskRecord.lease`, `run`, and `version`; `A2aTaskStore.claim()`, optional;
|
|
82
|
+
`A2aServerOptions.lease` and `recoverEveryMs`. The handler is still the request function it was;
|
|
83
|
+
- on `nexus-ai-pro/protocols/acp`: `AcpTurnRecord` and `AcpSessionRecord.turn`;
|
|
84
|
+
- on `nexus-ai-pro/deep-agent`: `DeepSubagent.remote`, `DeepRemoteHelper`, and `DeepRemoteRunResult`;
|
|
85
|
+
- `CompiledGraph.continueWith()`, and `ProtocolGraph.continueWith()` and `state()`, both optional;
|
|
86
|
+
- on `nexus-ai-pro/lifecycle`: `ProviderHealthOutcome`, `OperationLifecycle.reportHealth()`,
|
|
87
|
+
`OperationLifecycleLike.reportHealth()`, optional, and `LifecycleRuntime.providerHealth`;
|
|
88
|
+
- on `nexus-ai-pro/realtime`: `RealtimeSessionConfig.tenantId`, `userId`, `principal`,
|
|
89
|
+
`traceContext`, `conversationStore`, and `resume`; `RealtimeConversationOptions.replayItems`;
|
|
90
|
+
`RealtimeConversationStore`, `MemoryRealtimeConversationStore`, `storeRealtimeConversations()`,
|
|
91
|
+
`replayRealtimeConversation()`, and `REALTIME_REPLAY_PREFIX`; `GeminiLiveProvider` and
|
|
92
|
+
`GeminiLiveProviderOptions`; `RealtimeSession.sendImage()` and `sendVideoFrame()`,
|
|
93
|
+
`RealtimeSessionConfig.video`, `RealtimeImageInput`, `RealtimeImageOptions`, and
|
|
94
|
+
`RealtimeVideoOptions`; `RealtimeTransport.sendVideo()`, optional;
|
|
95
|
+
- `RealtimeConversationOptions.retainRawAudio`, `RealtimeConversationReducerOptions.inPlace`, and
|
|
96
|
+
`RealtimePeerConnectionLike.addTransceiver()`, optional;
|
|
97
|
+
- `nexus-ai-pro/realtime/gemini-live`, with `GeminiLiveTransport` and `GeminiLiveTransportOptions`;
|
|
98
|
+
- `nexus-ai-pro/realtime/openai-sip`, with `answerOpenAISipCalls()` and its types, also on
|
|
99
|
+
`nexus-ai-pro/realtime`;
|
|
100
|
+
- on `nexus-ai-pro/realtime/openai-server` and `nexus-ai-pro/realtime`: `acceptOpenAIRealtimeCall()`,
|
|
101
|
+
`rejectOpenAIRealtimeCall()`, `referOpenAIRealtimeCall()`, `hangupOpenAIRealtimeCall()`,
|
|
102
|
+
`verifyOpenAIWebhook()`, `OpenAIRealtimeCallOptions`, `OpenAIWebhookEvent`, `OpenAIWebhookHeaders`,
|
|
103
|
+
and `OpenAIWebhookVerifyOptions`; `OpenAIWebSocketTransportOptions.callId`.
|
|
104
|
+
|
|
105
|
+
The realtime family stays experimental, and all of these with it.
|
|
106
|
+
|
|
107
|
+
Changed, each to keep a promise the surface already made:
|
|
108
|
+
- A realtime session's raw events keep an audio event's size, `delta_bytes`, in place of its audio,
|
|
109
|
+
unless `retainRawAudio`: a session held every frame it played, about 13 MB a 3-minute call. The
|
|
110
|
+
`openai-events` export follows.
|
|
111
|
+
- A realtime session announces `conversation.updated` only to a listener, as before, but no longer
|
|
112
|
+
copies its record for each event when none is listening.
|
|
113
|
+
- A realtime session given a lifecycle runs each turn as a `realtime.turn` operation and charges it
|
|
114
|
+
as it finishes; its `realtime.session` operation reports the total and charges nothing more. Its
|
|
115
|
+
usage and cost are the sum of its turns', where each turn's replaced the last before.
|
|
116
|
+
|
|
44
117
|
## 2.9 stage (2.9.0)
|
|
45
118
|
|
|
46
119
|
Everything 2.9 adds is stable from the start and additive, unless this section says otherwise.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,211 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.11.0] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
Workspaces, and telephony in full.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Workspaces: where an agent's files live, apart from where its code runs.** A `Workspace`, on
|
|
14
|
+
the new `nexus-ai-pro/agent/workspace`, reads, writes, and lists files, and optionally searches
|
|
15
|
+
them, removes them, and holds bytes, with paths that mean the same in every one. Five ship:
|
|
16
|
+
`memoryWorkspace()`, `fileWorkspace()`, `storeWorkspace()` on any long-term `Store` (each file its
|
|
17
|
+
contents and a small tree entry, so listing reads no contents), `readOnlyWorkspace()`, and
|
|
18
|
+
`compositeWorkspace()`, which mounts several at paths of its own, read-only or not, and searches
|
|
19
|
+
across them. `workspaceTools()` gives the file tools over any of them, and
|
|
20
|
+
`runWorkspaceConformance()` holds any workspace to the contract. A `Sandbox` is now a workspace
|
|
21
|
+
that can also run commands. A deep agent takes `workspace` apart from `sandbox`: its files, skills,
|
|
22
|
+
instruction files, and offloaded results live in the workspace, and its commands run in the sandbox.
|
|
23
|
+
- `Workspace.writeBytes()`, optional, which the process sandbox now has.
|
|
24
|
+
- **Telephony in full, on a second provider.** `TelnyxTelephonyProvider`, on the new
|
|
25
|
+
`nexus-ai-pro/telephony/telnyx`, on Telnyx's own call-control protocol: calls placed and answered
|
|
26
|
+
by command, Ed25519-signed webhooks verified with Web Crypto, media streams that read as Twilio's
|
|
27
|
+
do, phone numbers, and everything below. `TelephonyManager.answer()` answers a call webhook the same
|
|
28
|
+
way on either provider: TwiML on Twilio, commands on Telnyx.
|
|
29
|
+
- **Transfer and conferences** on both: `transferCall()` and `joinConference()`, and a `dial` in a
|
|
30
|
+
webhook answer, rendered as `<Dial>` on Twilio.
|
|
31
|
+
- **Call recordings kept on your terms.** `startRecording()`, `stopRecording()`,
|
|
32
|
+
`parseRecordingCallback()`, `downloadRecording()`, and `deleteRecording()` on both, and
|
|
33
|
+
`recordingKeeper()`, which keeps each finished recording in an `AssetStore` to expire under a
|
|
34
|
+
`RecordingRetention`, deletes the provider's copy, keeps a retried callback's recording once, and
|
|
35
|
+
sweeps what expired.
|
|
36
|
+
- **Messages, as their own operation family.** `sendMessage()` (SMS and MMS) and
|
|
37
|
+
`parseMessageWebhook()` on both, running through the client's lifecycle as `messaging` operations.
|
|
38
|
+
- `NexusAI.telephony`, the client's telephony manager; `telnyxRealtimeAudioOptions()`;
|
|
39
|
+
`TelephonyWebhookValidationRequest.publicKey`.
|
|
40
|
+
- The weekly live workflow runs a Gemini Live session that calls a tool, OpenAI's SIP call control,
|
|
41
|
+
and the phone numbers of Twilio and Telnyx, where the repository's secrets are set.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- A realtime session's `whenIdle()` resolved while a tool it ran on its own was still running, so
|
|
46
|
+
the tool's result could be missing from the record, and from the model, after it. It now waits for
|
|
47
|
+
running tools and their results, though not for a call waiting on a person's confirmation.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- A sandbox's file methods take a leading `/` as its root, as every workspace does: the process
|
|
52
|
+
sandbox and the container sandbox refused such a path unless it was already under their root.
|
|
53
|
+
`/etc/passwd` is now the workspace's own file, never the container's or the host's.
|
|
54
|
+
- The container sandbox rejects reading a missing file with `ENOENT`, as every workspace does, and
|
|
55
|
+
passes the workspace conformance suite.
|
|
56
|
+
- The file tools describe their files as the workspace's, not the sandbox's.
|
|
57
|
+
|
|
58
|
+
### Proof
|
|
59
|
+
|
|
60
|
+
- `tests/workspaces.test.ts`: every workspace the package ships passes the workspace conformance
|
|
61
|
+
suite: in memory, on disk, in the memory, SQLite, Postgres, and Redis stores, the process sandbox,
|
|
62
|
+
and a composite. A composite's shadowing, read-only mounts, and search across mounts.
|
|
63
|
+
- `tests/phone-agent.test.ts`: one phone agent on Twilio and on Telnyx: answered the same way,
|
|
64
|
+
recorded, streamed through the bridge to a realtime session that hears the caller, answers aloud,
|
|
65
|
+
texts a receipt, and transfers the call; its recording kept in memory, on disk, and in S3, deleted
|
|
66
|
+
at the provider, kept once on a retried callback, there the day before its retention ends and gone
|
|
67
|
+
after the day after's sweep. Without the claim, the expiry, the provider delete, the answer by
|
|
68
|
+
command, the messaging family, or `whenIdle()` waiting for tools, it fails.
|
|
69
|
+
- `tests/telephony-providers.test.ts`: both providers against stand-ins for their APIs, and Telnyx's
|
|
70
|
+
webhooks verified with a real Ed25519 key pair: signed, altered, stale, and unsigned.
|
|
71
|
+
- `tests/deep-agent-workspaces.test.ts`: one deep agent does the same work over every workspace and
|
|
72
|
+
each ends holding the same files; and an agent whose code is in a sandbox, notes in a store, and
|
|
73
|
+
docs read-only finds each where it belongs. Without the composite's shadowing, its read-only
|
|
74
|
+
mounts, the store's directory filter, or the agent taking its files from the workspace, a test
|
|
75
|
+
fails.
|
|
76
|
+
|
|
77
|
+
## [2.10.0] - 2026-10-16
|
|
78
|
+
|
|
79
|
+
Durable conversations, and a second realtime provider.
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **A2A tasks that outlive their replica.** A working task's record now holds an `A2aTaskLease` and
|
|
84
|
+
an `A2aTaskRun`. The lease names the replica running the task, which renews it every third of
|
|
85
|
+
`lease.ms` (30 seconds); the run says what the task was asked to do. `a2aHandler()` returns an
|
|
86
|
+
`A2aHandler`: the request handler, plus `recover()` and `close()`. `recover()` takes over working
|
|
87
|
+
tasks whose lease lapsed, and carries each run on in one of three ways:
|
|
88
|
+
- from the graph's last checkpoint;
|
|
89
|
+
- by answering its questions again, when a resume never reached the graph;
|
|
90
|
+
- from the start, when the run left no checkpoint.
|
|
91
|
+
|
|
92
|
+
`recoverEveryMs` runs it on a timer. `A2aTaskStore.claim()` takes a lease in one step, so two
|
|
93
|
+
replicas recovering at once take a task once. `MemoryA2aTaskStore` has it, and `storeA2aTasks()`
|
|
94
|
+
builds it on `putIfVersion()`. A replica that finds its lease taken stops its run, and never
|
|
95
|
+
records its result over the other's.
|
|
96
|
+
- **ACP turns that outlive the agent.** While a prompt runs, its session record holds an
|
|
97
|
+
`AcpTurnRecord`. When the editor reopens the session with `session/load`, the agent carries the
|
|
98
|
+
turn on, streaming its updates: from the graph's last checkpoint, by answering again, or from the
|
|
99
|
+
start. A turn stopped while the editor was deciding an approval asks for it again.
|
|
100
|
+
- **Realtime on the client's lifecycle.** A session given `ai.lifecycle` runs as `realtime.session`,
|
|
101
|
+
and each model turn as `realtime.turn`, carrying the session's `tenantId`, `userId` (the
|
|
102
|
+
`principal`'s by default), and `traceContext`. A turn is admitted as it starts, rate-limited,
|
|
103
|
+
authorized, and holding the average turn cost against the budget, and charged its cost when it
|
|
104
|
+
finishes. A turn the lifecycle refuses is cancelled before the model runs it, and the session ends
|
|
105
|
+
with `lifecycle_refused`. Connections are reported to the client's provider health and circuit
|
|
106
|
+
breaker through the new `reportHealth()`, on `OperationLifecycleLike` and `OperationLifecycle`,
|
|
107
|
+
with `ProviderHealthOutcome` and `LifecycleRuntime.providerHealth`.
|
|
108
|
+
- **Realtime conversations that outlive their process.** `conversationStore`, a
|
|
109
|
+
`RealtimeConversationStore`, keeps the record after each committed turn:
|
|
110
|
+
`MemoryRealtimeConversationStore`, or `storeRealtimeConversations()` on any `Store`. `resume`
|
|
111
|
+
carries a conversation on, from its record or its id: same id, items, metrics, and metadata. On
|
|
112
|
+
connecting, the provider is given what was said and what the tools returned before anything new,
|
|
113
|
+
through `replayRealtimeConversation()`. Echoes carry `REALTIME_REPLAY_PREFIX` and are not recorded
|
|
114
|
+
twice. `conversation.replayItems` caps the replay.
|
|
115
|
+
- **Gemini Live, behind the same realtime session.** `GeminiLiveTransport`, on the new
|
|
116
|
+
`nexus-ai-pro/realtime/gemini-live`, speaks Gemini's Live API, and `GeminiLiveProvider`, on
|
|
117
|
+
`nexus-ai-pro/realtime`, creates sessions on it for `createRealtimeAgent()`. The session's
|
|
118
|
+
configuration becomes Gemini's setup; audio, messages, and tool results become its realtime input,
|
|
119
|
+
turns, and tool responses; and its audio, transcripts, tool calls, interruptions, and usage become
|
|
120
|
+
the events the session already reads. It takes an API key on a server or an ephemeral token
|
|
121
|
+
anywhere, and reconnects a dropped socket with Gemini's latest resumption handle.
|
|
122
|
+
- **Helpers on another server, whose runs outlive their worker.** A `DeepSubagent` with `remote`, a
|
|
123
|
+
`DeepRemoteHelper` such as `createRemoteGraph()` returns, runs on another agent server, as a run
|
|
124
|
+
leased to the worker that runs it: when that worker stops, another takes it over. The agent starts
|
|
125
|
+
the run under a key made from the call that started it, so delegating again or collecting it, in
|
|
126
|
+
this process or another, finds the same run; a background task is marked on its thread first, so a
|
|
127
|
+
run that never reached the server is started by the process that collects it. Its answer comes from
|
|
128
|
+
a `DeepRemoteRunResult`.
|
|
129
|
+
- **Phone calls answered by a realtime agent, over SIP.** `answerOpenAISipCalls()`, on the new
|
|
130
|
+
`nexus-ai-pro/realtime/openai-sip`, handles OpenAI's `realtime.call.incoming` webhook: it verifies
|
|
131
|
+
each delivery, claims it so a retry answers its call once, and answers the call with the session
|
|
132
|
+
`answer()` chooses from the caller, which attaches to the call and runs it with its tools,
|
|
133
|
+
record, and lifecycle; or declines it with a SIP status. `OpenAISipCall` transfers and hangs up.
|
|
134
|
+
The call control is on `nexus-ai-pro/realtime/openai-server` too: `acceptOpenAIRealtimeCall()`,
|
|
135
|
+
`rejectOpenAIRealtimeCall()`, `referOpenAIRealtimeCall()`, `hangupOpenAIRealtimeCall()`, and
|
|
136
|
+
`verifyOpenAIWebhook()`, on Web Crypto. `OpenAIWebSocketTransportOptions.callId` attaches a
|
|
137
|
+
transport to a call.
|
|
138
|
+
- **A published method and budget for realtime turn latency and session load.** `npm run
|
|
139
|
+
bench:realtime`, in the release gate, holds what a session adds to a message (under 5 ms at p99 for
|
|
140
|
+
one call, under 20 ms with 250 calls at real time), that a turn late in a long call costs what an
|
|
141
|
+
early one does, that 250 calls at real time keep up, the heap each session holds, and that ended
|
|
142
|
+
sessions leave nothing. Budgets are in `realtime-budget.json`.
|
|
143
|
+
- **Realtime over real WebRTC in a browser.** `npm run test:browser:realtime`, in `npm test`, drives a
|
|
144
|
+
session in headless Chromium against a peer connection in the page that plays the provider: real
|
|
145
|
+
SDP, ICE, DTLS, and data channel, the microphone up, the model's voice down, a tool call, and a
|
|
146
|
+
session with no microphone.
|
|
147
|
+
- `retainRawAudio` on a realtime conversation's options, `inPlace` on
|
|
148
|
+
`RealtimeConversationReducerOptions`, and `addTransceiver()`, optional, on
|
|
149
|
+
`RealtimePeerConnectionLike`.
|
|
150
|
+
- **Video and images in a realtime session.** `sendVideoFrame()` streams camera or screen frames:
|
|
151
|
+
as realtime video on Gemini Live, as images in the conversation elsewhere, spaced by
|
|
152
|
+
`RealtimeVideoOptions.minIntervalMs`. `sendImage()` adds an image, with text, to the conversation.
|
|
153
|
+
`RealtimeTransport.sendVideo()`, optional, is what a transport with video input implements.
|
|
154
|
+
- `continueWith()` on a compiled graph: `continue()`, returning the final result.
|
|
155
|
+
`ProtocolGraph.continueWith()` and `state()`, both optional, are what A2A and ACP carry a run on
|
|
156
|
+
with.
|
|
157
|
+
|
|
158
|
+
### Fixed
|
|
159
|
+
|
|
160
|
+
- The WebRTC transport trimmed the SDP answer's final line break, and Chromium refuses a description
|
|
161
|
+
whose last line has none, so a session in Chrome or Edge never connected.
|
|
162
|
+
- A WebRTC session that sent no microphone offered no audio to receive, so it never heard the model.
|
|
163
|
+
- A realtime session copied its whole conversation record twice for every event, so each event cost
|
|
164
|
+
more as the call went on: a turn took 39 ms by the 500th, and 250 calls took half a core. It now
|
|
165
|
+
applies events in place and copies the record only for a `conversation.updated` listener.
|
|
166
|
+
- An A2A `SendMessage` with `returnImmediately`, whose store failed while the task ran, raised an
|
|
167
|
+
unhandled rejection, which ends a Node process by default. A store that cannot record a task's
|
|
168
|
+
failure now leaves the task working, with its lease, for the replica that takes it over.
|
|
169
|
+
- A realtime session's usage and estimated cost are the sum of its responses'. Before, each
|
|
170
|
+
response's usage replaced the last, so a session recorded, and settled, only its last turn.
|
|
171
|
+
|
|
172
|
+
### Proof
|
|
173
|
+
|
|
174
|
+
- `tests/conversation-crash.test.ts`: an A2A task, an ACP turn, and a realtime conversation are each
|
|
175
|
+
killed at every write a clean run makes, and taken over elsewhere. Each ends as the clean run did,
|
|
176
|
+
or its caller was told it never started; nothing is left half done, and nothing is recorded twice.
|
|
177
|
+
The suite found three defects before they shipped, each fixed: an unhandled rejection from a
|
|
178
|
+
failing store; a task submitted to a replica that stopped before starting it, which no replica
|
|
179
|
+
would ever start; and a reopened session's save that dropped the turn it was about to carry on.
|
|
180
|
+
- `tests/deep-agent-remote-helpers.test.ts`: against two replicas of a real agent server, the agent's
|
|
181
|
+
process and the worker running its remote helper both die; the second replica takes the run over,
|
|
182
|
+
and the agent, resumed in a new process, collects that same run: one run, its answer, and the
|
|
183
|
+
agent's note not written again. Without the key, the resumed agent starts a second run, and the
|
|
184
|
+
proof fails.
|
|
185
|
+
- `tests/realtime-sip.test.ts`: a signed incoming-call delivery is accepted with the agent's session,
|
|
186
|
+
chosen from the caller's number; the session attaches by the call's id, books a table through its
|
|
187
|
+
tool when the caller asks, and hangs up. Forged, altered, and stale deliveries answer nothing; a
|
|
188
|
+
retry after a failed accept answers the call, and a duplicate, on one replica or two, answers it
|
|
189
|
+
once. Without the signature check, the claim, or the release, a test fails.
|
|
190
|
+
- `tests/realtime-gemini.test.ts`: one agent definition (instructions, a tool, conversation settings)
|
|
191
|
+
runs on OpenAI's realtime API and on Gemini Live, each over its own scripted wire protocol. The two
|
|
192
|
+
records match item for item: what the person asked, the tool call and its result, and what the model
|
|
193
|
+
answered, with the same usage, and the tool runs once on each. Without the tool response, or with
|
|
194
|
+
the request for a response not dropped after it, the proof fails.
|
|
195
|
+
- `tests/realtime-lifecycle.test.ts`: through a real client with a five-cent budget, two
|
|
196
|
+
two-cent turns run, and the third is cancelled as it starts and ends the session; the budget holds
|
|
197
|
+
four cents, and every operation names the tenant and user. Connection failures open the client's
|
|
198
|
+
circuit. Without the admission at the turn's start, the proof fails.
|
|
199
|
+
- `tests/realtime-resume.test.ts`: a session's process is killed mid-conversation on the memory and
|
|
200
|
+
SQLite stores. Another resumes it by id on a new transport, and gives the provider what was said
|
|
201
|
+
before anything new. The conversation goes on under its own id with nothing recorded twice.
|
|
202
|
+
Without the replay, or without skipping the echoes, the proof fails.
|
|
203
|
+
- `tests/a2a-takeover.test.ts`: a task whose replica dies halfway through a tool is taken over, on
|
|
204
|
+
the memory, SQLite, Postgres, and Redis stores. The model call the dead replica finished is not
|
|
205
|
+
made again, and the email it never sent is sent once. Two replicas recovering at the same moment
|
|
206
|
+
take the task over once, and a lease renewed while a slow task works is never taken. Without the
|
|
207
|
+
lease check, or carried on from the start instead of the checkpoint, the proof fails.
|
|
208
|
+
- `tests/acp-turns.test.ts`: an agent stopped halfway through a tool is started again and its
|
|
209
|
+
session reopened; the turn goes on without repeating the finished model call, and sends the email
|
|
210
|
+
once. A turn stopped during an approval asks again. Without the recovery on load, the proof fails.
|
|
211
|
+
|
|
7
212
|
## [2.9.0] - 2026-10-15
|
|
8
213
|
|
|
9
214
|
Video, and everything shipped, proven.
|
|
@@ -2559,7 +2764,9 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
2559
2764
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
2560
2765
|
- CLI and audit findings no longer disclose detected secret values.
|
|
2561
2766
|
|
|
2562
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
2767
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.11.0...HEAD
|
|
2768
|
+
[2.11.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.10.0...v2.11.0
|
|
2769
|
+
[2.10.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.9.0...v2.10.0
|
|
2563
2770
|
[2.9.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.8.0...v2.9.0
|
|
2564
2771
|
[2.8.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.7.0...v2.8.0
|
|
2565
2772
|
[2.7.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.6.0...v2.7.0
|
package/README.md
CHANGED
|
@@ -294,14 +294,23 @@ Shipped so far:
|
|
|
294
294
|
priced per second; online evaluation that reads every run however many share a millisecond, and
|
|
295
295
|
budget reservations a dead worker leaves settled by what its call did; a deep agent's helper limit
|
|
296
296
|
across processes; a media index that remembers what it holds; and the experimental families run
|
|
297
|
-
against their live services every week
|
|
297
|
+
against their live services every week;
|
|
298
|
+
- 2.10 durable conversations, and a second realtime provider: A2A tasks, ACP turns, and realtime
|
|
299
|
+
sessions taken over by another worker when theirs dies, each proven killed at every write; realtime
|
|
300
|
+
on the client's lifecycle; Gemini Live behind the same session, with video and images; phone calls
|
|
301
|
+
answered over SIP; helpers on another server whose runs outlive their worker; and realtime over
|
|
302
|
+
real WebRTC in a browser, with published budgets for turn latency and session load;
|
|
303
|
+
- 2.11 workspaces, and telephony in full: an agent's files apart from where its code runs, in memory,
|
|
304
|
+
on disk, in any long-term store, read-only, or a composite of several; and a second phone provider,
|
|
305
|
+
Telnyx, beside Twilio, both answering, transferring, conferencing, recording under a retention
|
|
306
|
+
policy, and sending and receiving messages.
|
|
298
307
|
|
|
299
308
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
300
309
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
301
|
-
Next, 2.
|
|
302
|
-
|
|
303
|
-
embedding batch tier, media understanding,
|
|
304
|
-
each. The
|
|
310
|
+
Next, 2.12 makes observability lose nothing, with spans spooled when the collector is down and
|
|
311
|
+
deployment evaluation that reads the whole trace, and makes media something you can evaluate.
|
|
312
|
+
Pinecone, images, the Gemini embedding batch tier, media understanding, video, Gemini Live, SIP, and
|
|
313
|
+
both phone providers graduate once the live workflow passes against each. The
|
|
305
314
|
[roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
|
|
306
315
|
applications ask for it.
|
|
307
316
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The file methods of a directory on disk. */
|
|
2
|
+
export interface DirectoryFiles {
|
|
3
|
+
readFile(file: string): Promise<string>;
|
|
4
|
+
writeFile(file: string, content: string): Promise<void>;
|
|
5
|
+
listFiles(directory?: string): Promise<string[]>;
|
|
6
|
+
removeFile(file: string): Promise<void>;
|
|
7
|
+
glob(pattern: string, options?: {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}): Promise<string[]>;
|
|
11
|
+
grep(pattern: string, options?: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
glob?: string;
|
|
14
|
+
ignoreCase?: boolean;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}): Promise<Array<{
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
text: string;
|
|
20
|
+
}>>;
|
|
21
|
+
readBytes(file: string): Promise<Uint8Array>;
|
|
22
|
+
writeBytes(file: string, bytes: Uint8Array): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/** The files under `root`, refusing a path outside it with the error `refuse` makes. */
|
|
25
|
+
export declare function directoryFiles(rootDirectory: string, refuse: (path: string) => Error): DirectoryFiles & {
|
|
26
|
+
ready: Promise<DirectoryHandle>;
|
|
27
|
+
};
|
|
28
|
+
/** What a directory's file methods hold once Node's modules have loaded. */
|
|
29
|
+
export interface DirectoryHandle {
|
|
30
|
+
fs: typeof import('node:fs/promises');
|
|
31
|
+
dirname: typeof import('node:path').dirname;
|
|
32
|
+
join: typeof import('node:path').join;
|
|
33
|
+
relative: typeof import('node:path').relative;
|
|
34
|
+
root: string;
|
|
35
|
+
inside: (file: string) => string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { GREP_FILE_BYTES, GREP_TOTAL_BYTES, globMatcher, matchingLines, SKIPPED_DIRECTORIES, WALK_LIMIT, } from './glob.js';
|
|
2
|
+
export function directoryFiles(rootDirectory, refuse) {
|
|
3
|
+
const ready = (async () => {
|
|
4
|
+
const [fs, { dirname, isAbsolute, join, relative, resolve }] = await Promise.all([
|
|
5
|
+
import('node:fs/promises'),
|
|
6
|
+
import('node:path'),
|
|
7
|
+
]);
|
|
8
|
+
const root = resolve(rootDirectory);
|
|
9
|
+
await fs.mkdir(root, { recursive: true });
|
|
10
|
+
const outside = (fromRoot) => fromRoot.startsWith('..') || isAbsolute(fromRoot);
|
|
11
|
+
const inside = (file) => {
|
|
12
|
+
let resolved = resolve(root, file);
|
|
13
|
+
if (outside(relative(root, resolved)) && /^[\\/]/.test(file))
|
|
14
|
+
resolved = resolve(root, file.replace(/^[\\/]+/, ''));
|
|
15
|
+
if (outside(relative(root, resolved)))
|
|
16
|
+
throw refuse(file);
|
|
17
|
+
return resolved;
|
|
18
|
+
};
|
|
19
|
+
return { fs, dirname, join, relative, root, inside };
|
|
20
|
+
})();
|
|
21
|
+
ready.catch(() => undefined);
|
|
22
|
+
const walk = async (directory) => {
|
|
23
|
+
const { fs, join, relative, root, inside } = await ready;
|
|
24
|
+
const start = inside(directory);
|
|
25
|
+
const slash = (path) => path.replace(/\\/g, '/');
|
|
26
|
+
const files = [];
|
|
27
|
+
const queue = [start];
|
|
28
|
+
let walked = 0;
|
|
29
|
+
while (queue.length > 0 && walked < WALK_LIMIT) {
|
|
30
|
+
const current = queue.shift();
|
|
31
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
walked += 1;
|
|
34
|
+
if (walked > WALK_LIMIT)
|
|
35
|
+
break;
|
|
36
|
+
const full = join(current, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name))
|
|
39
|
+
queue.push(full);
|
|
40
|
+
}
|
|
41
|
+
else if (entry.isFile()) {
|
|
42
|
+
files.push({ full, fromRoot: slash(relative(root, full)), fromStart: slash(relative(start, full)) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return files.sort((a, b) => (a.fromRoot < b.fromRoot ? -1 : a.fromRoot > b.fromRoot ? 1 : 0));
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
ready,
|
|
50
|
+
async readFile(file) {
|
|
51
|
+
const { fs, inside } = await ready;
|
|
52
|
+
return fs.readFile(inside(file), 'utf8');
|
|
53
|
+
},
|
|
54
|
+
async writeFile(file, content) {
|
|
55
|
+
const { fs, dirname, inside } = await ready;
|
|
56
|
+
const target = inside(file);
|
|
57
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
58
|
+
await fs.writeFile(target, content, 'utf8');
|
|
59
|
+
},
|
|
60
|
+
async listFiles(directory = '.') {
|
|
61
|
+
const { fs, inside } = await ready;
|
|
62
|
+
return (await fs.readdir(inside(directory))).sort();
|
|
63
|
+
},
|
|
64
|
+
async removeFile(file) {
|
|
65
|
+
const { fs, root, inside } = await ready;
|
|
66
|
+
const target = inside(file);
|
|
67
|
+
if (target === root)
|
|
68
|
+
throw refuse(file);
|
|
69
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
70
|
+
},
|
|
71
|
+
async glob(pattern, options = {}) {
|
|
72
|
+
const matches = globMatcher(pattern);
|
|
73
|
+
return (await walk(options.cwd ?? '.'))
|
|
74
|
+
.filter((file) => matches(file.fromStart))
|
|
75
|
+
.slice(0, options.limit ?? 1_000)
|
|
76
|
+
.map((file) => file.fromRoot);
|
|
77
|
+
},
|
|
78
|
+
async grep(pattern, options = {}) {
|
|
79
|
+
const { fs } = await ready;
|
|
80
|
+
const expression = new RegExp(pattern, options.ignoreCase ? 'i' : '');
|
|
81
|
+
const only = options.glob ? globMatcher(options.glob) : undefined;
|
|
82
|
+
const limit = options.limit ?? 200;
|
|
83
|
+
const found = [];
|
|
84
|
+
let scanned = 0;
|
|
85
|
+
for (const file of await walk(options.cwd ?? '.')) {
|
|
86
|
+
if (only && !only(file.fromStart))
|
|
87
|
+
continue;
|
|
88
|
+
const size = (await fs.stat(file.full).catch(() => undefined))?.size;
|
|
89
|
+
if (size === undefined || size > GREP_FILE_BYTES)
|
|
90
|
+
continue;
|
|
91
|
+
scanned += size;
|
|
92
|
+
if (scanned > GREP_TOTAL_BYTES)
|
|
93
|
+
break;
|
|
94
|
+
const lines = matchingLines(await fs.readFile(file.full), expression, limit - found.length);
|
|
95
|
+
found.push(...lines.map((match) => ({ file: file.fromRoot, ...match })));
|
|
96
|
+
if (found.length >= limit)
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
},
|
|
101
|
+
async readBytes(file) {
|
|
102
|
+
const { fs, inside } = await ready;
|
|
103
|
+
return new Uint8Array(await fs.readFile(inside(file)));
|
|
104
|
+
},
|
|
105
|
+
async writeBytes(file, bytes) {
|
|
106
|
+
const { fs, dirname, inside } = await ready;
|
|
107
|
+
const target = inside(file);
|
|
108
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
109
|
+
await fs.writeFile(target, bytes);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentMiddleware } from '../create-agent.js';
|
|
2
|
-
import type {
|
|
3
|
-
/** Where `filesystemContext()` reads and writes: a `Sandbox`, or anything with the same file methods. */
|
|
4
|
-
export type FilesystemContextSource = Pick<
|
|
2
|
+
import type { Workspace } from '../workspace.js';
|
|
3
|
+
/** Where `filesystemContext()` reads and writes: a `Workspace` or a `Sandbox`, or anything with the same file methods. */
|
|
4
|
+
export type FilesystemContextSource = Pick<Workspace, 'readFile'> & Partial<Pick<Workspace, 'writeFile' | 'listFiles'>>;
|
|
5
5
|
/** Where large tool results go instead of the transcript. */
|
|
6
6
|
export interface ContextOffloadOptions {
|
|
7
7
|
/** Results longer than this, in characters, are offloaded. Defaults to 20,000. */
|
|
@@ -12,7 +12,8 @@ export function containerSandbox(options) {
|
|
|
12
12
|
if (unified.includes('\0'))
|
|
13
13
|
throw new SandboxPathError(file);
|
|
14
14
|
const parts = [];
|
|
15
|
-
|
|
15
|
+
const rooted = unified === workdir || unified.startsWith(`${workdir}/`);
|
|
16
|
+
for (const segment of (rooted ? unified : `${workdir}/${unified.replace(/^\/+/, '')}`).split('/')) {
|
|
16
17
|
if (segment === '' || segment === '.')
|
|
17
18
|
continue;
|
|
18
19
|
if (segment !== '..')
|
|
@@ -147,9 +148,10 @@ export function containerSandbox(options) {
|
|
|
147
148
|
};
|
|
148
149
|
const readBytes = async (file) => {
|
|
149
150
|
const result = await shell('cat "$1"', [inside(file)]);
|
|
150
|
-
if (result.code
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
if (result.code === 0)
|
|
152
|
+
return result.stdout;
|
|
153
|
+
const error = failed(`Reading ${file}`, result);
|
|
154
|
+
throw /no such file/i.test(result.stderr) ? Object.assign(error, { code: 'ENOENT', path: file }) : error;
|
|
153
155
|
};
|
|
154
156
|
return {
|
|
155
157
|
name: `container (${binary} ${options.image})`,
|
package/dist/agent/sandbox.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The sandbox contract: where an agent's commands and code run
|
|
2
|
+
* The sandbox contract: where an agent's commands and code run. A sandbox is a `Workspace` that can
|
|
3
|
+
* also run commands, and says how it is isolated, so its files are the files its commands see.
|
|
3
4
|
*
|
|
4
5
|
* The package ships the contract and a conformance suite, not the isolation: a container, a VM, a
|
|
5
6
|
* remote interpreter, or a hosted sandbox fills the `Sandbox` interface. `processSandbox()` is a
|
|
@@ -7,6 +8,7 @@
|
|
|
7
8
|
* `isolation`, and is never a security boundary.
|
|
8
9
|
*/
|
|
9
10
|
import type { ToolDefinition } from '../types/messages.js';
|
|
11
|
+
import { type Workspace, type WorkspaceGlobOptions, type WorkspaceGrepOptions, type WorkspaceMatch, WorkspacePathError } from './workspace.js';
|
|
10
12
|
/** Options for one command. */
|
|
11
13
|
export interface SandboxExecOptions {
|
|
12
14
|
/** The working directory, inside the sandbox. Defaults to its root. */
|
|
@@ -33,33 +35,12 @@ export interface SandboxExecResult {
|
|
|
33
35
|
/** How long it ran, in milliseconds. */
|
|
34
36
|
durationMs: number;
|
|
35
37
|
}
|
|
36
|
-
/** Options for `Sandbox.glob()
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
/** Options for `Sandbox.grep()`. */
|
|
44
|
-
export interface SandboxGrepOptions {
|
|
45
|
-
/** The directory to search, inside the sandbox. Defaults to its root. */
|
|
46
|
-
cwd?: string;
|
|
47
|
-
/** Searches only the files whose paths, relative to `cwd`, match this glob. */
|
|
48
|
-
glob?: string;
|
|
49
|
-
/** Matches without regard to case. */
|
|
50
|
-
ignoreCase?: boolean;
|
|
51
|
-
/** The most matching lines returned. Defaults to 200. */
|
|
52
|
-
limit?: number;
|
|
53
|
-
}
|
|
54
|
-
/** One line `Sandbox.grep()` matched. */
|
|
55
|
-
export interface SandboxMatch {
|
|
56
|
-
/** The file, relative to the sandbox's root. */
|
|
57
|
-
file: string;
|
|
58
|
-
/** The line's number, counting from 1. */
|
|
59
|
-
line: number;
|
|
60
|
-
/** The line, cut to 300 characters. */
|
|
61
|
-
text: string;
|
|
62
|
-
}
|
|
38
|
+
/** Options for `Sandbox.glob()`: a workspace's. */
|
|
39
|
+
export type SandboxGlobOptions = WorkspaceGlobOptions;
|
|
40
|
+
/** Options for `Sandbox.grep()`: a workspace's. */
|
|
41
|
+
export type SandboxGrepOptions = WorkspaceGrepOptions;
|
|
42
|
+
/** One line `Sandbox.grep()` matched: a workspace's match. */
|
|
43
|
+
export type SandboxMatch = WorkspaceMatch;
|
|
63
44
|
/** What a sandbox claims to isolate. The conformance suite checks the claims it can observe. */
|
|
64
45
|
export interface SandboxIsolation {
|
|
65
46
|
/** Commands cannot read or write the host's files. */
|
|
@@ -69,42 +50,20 @@ export interface SandboxIsolation {
|
|
|
69
50
|
/** Commands cannot see or signal the host's processes. */
|
|
70
51
|
processes: boolean;
|
|
71
52
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Where an agent's commands and code run: a workspace that can also run commands, and says what it
|
|
55
|
+
* isolates. The application provides one.
|
|
56
|
+
*/
|
|
57
|
+
export interface Sandbox extends Workspace {
|
|
76
58
|
/** What it isolates. */
|
|
77
59
|
readonly isolation: SandboxIsolation;
|
|
78
|
-
/** Runs a command line in the sandbox. */
|
|
60
|
+
/** Runs a command line in the sandbox, where its files are. */
|
|
79
61
|
exec(command: string, options?: SandboxExecOptions): Promise<SandboxExecResult>;
|
|
80
|
-
/** Reads a file, by a path inside the sandbox. */
|
|
81
|
-
readFile(file: string): Promise<string>;
|
|
82
|
-
/** Writes a file, creating its directory. */
|
|
83
|
-
writeFile(file: string, content: string): Promise<void>;
|
|
84
|
-
/** Lists a directory's entries. */
|
|
85
|
-
listFiles?(directory?: string): Promise<string[]>;
|
|
86
|
-
/** Removes a file or directory. */
|
|
87
|
-
removeFile?(file: string): Promise<void>;
|
|
88
|
-
/**
|
|
89
|
-
* The files under a directory whose paths match a glob, such as `src/**\/*.ts`, sorted. The glob
|
|
90
|
-
* matches paths relative to `cwd`; the paths returned are relative to the root, as `readFile`
|
|
91
|
-
* takes them. Returns at most `limit`.
|
|
92
|
-
*/
|
|
93
|
-
glob?(pattern: string, options?: SandboxGlobOptions): Promise<string[]>;
|
|
94
|
-
/**
|
|
95
|
-
* The lines matching a regular expression in the files under a directory, at most `limit`. Large
|
|
96
|
-
* files and binary files are skipped.
|
|
97
|
-
*/
|
|
98
|
-
grep?(pattern: string, options?: SandboxGrepOptions): Promise<SandboxMatch[]>;
|
|
99
|
-
/** Reads a file's bytes, for an image, audio, or a document. */
|
|
100
|
-
readBytes?(file: string): Promise<Uint8Array>;
|
|
101
62
|
/** Releases the sandbox. */
|
|
102
63
|
dispose?(): Promise<void>;
|
|
103
64
|
}
|
|
104
|
-
/** Raised when a path would leave the sandbox
|
|
105
|
-
export declare class SandboxPathError extends
|
|
106
|
-
/** The path that was refused. */
|
|
107
|
-
readonly path: string;
|
|
65
|
+
/** Raised when a path would leave the sandbox: a `WorkspacePathError` whose code is `SANDBOX_PATH`. */
|
|
66
|
+
export declare class SandboxPathError extends WorkspacePathError {
|
|
108
67
|
/** Always `SANDBOX_PATH`. */
|
|
109
68
|
readonly code = "SANDBOX_PATH";
|
|
110
69
|
constructor(
|