@ai-ecoverse/slicc-agent 5.10.1 → 6.0.1
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 +12 -8
- package/dist/approvals/sudo-client.js +9 -9
- package/dist/approvals/sudo-client.js.map +2 -2
- package/dist/bedrock.js +72 -0
- package/dist/bedrock.js.map +7 -0
- package/dist/client.js +46 -27
- package/dist/client.js.map +3 -3
- package/dist/guests/client.js +11 -24
- package/dist/guests/client.js.map +2 -2
- package/dist/guests/serve.js +3 -1
- package/dist/guests/serve.js.map +2 -2
- package/dist/host.js +59 -56
- package/dist/host.js.map +2 -2
- package/dist/owner.js +8 -3
- package/dist/owner.js.map +2 -2
- package/dist/patches.js +2 -0
- package/dist/patches.js.map +2 -2
- package/dist/serving.js +10 -25
- package/dist/serving.js.map +2 -2
- package/dist/settings.js +2 -1
- package/dist/settings.js.map +2 -2
- package/dist/types/bedrock.d.ts +14 -0
- package/dist/types/client.d.ts +8 -3
- package/dist/types/guests/serve.d.ts +3 -2
- package/dist/types/host.d.ts +3 -3
- package/dist/types/patches.d.ts +1 -0
- package/dist/types/serving.d.ts +14 -15
- package/dist/types/wire.d.ts +5 -2
- package/dist/wire.js +50 -8
- package/dist/wire.js.map +2 -2
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -28,15 +28,19 @@ await agent.prompt('What is in /home?');
|
|
|
28
28
|
|
|
29
29
|
- `openAgent({ models, model, storage?, registry?, settings? })` opens a durable Harness (in memory unless `storage` is given) and its root conversation, and resumes work a previous worker left unfinished.
|
|
30
30
|
- `openOpfsSqliteStorage({ directory?, file? })` keeps the session in wasm SQLite ([`@sqlite.org/sqlite-wasm`](https://www.npmjs.com/package/@sqlite.org/sqlite-wasm)) in an `opfs-sahpool` pool, by default `/.slicc/agent/` in OPFS. It needs a dedicated worker. Only one worker holds a pool at a time: the opener takes a Web Lock per directory and waits until every file of the pool can be opened before sqlite-wasm installs it, because a failed install deletes the pool's directory. `openMemorySqliteStorage()` keeps the same database in memory.
|
|
31
|
-
- `hostAgent(agent)` serves the agent from one Chord host named `agent` (`serveAgent` in [`src/host.ts`](src/host.ts)). Its remote contracts, in [`src/services.ts`](src/services.ts), are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state, plus `deliveries`: how each user entry was delivered, `run`, `steer` or `follow-up`, derived by the host from where each send it accepted was placed: after a tool result it steered, queued first it's a follow-up or a late steer per its `whenBusy`, placed at once it started a run), `slicc.agent.settings`, `slicc.agent.remote` (remote control), `slicc.agent.pages` (Panels) and `slicc.agent.guest` (Guest seats). `serveConnections(self, host)` hands it every `MessagePort` the page sends, and each port becomes one Chord link whose principal is a seat: `owner` (a page of the owner, with its own Panels), `control` (a follower with Control), `view` (a follower without) or `guest` (a guest seat, with its id, label and epoch), plus the link's number, so every link gets its own objects. Each factory picks a variant per seat. The contracts take their methods' arguments as one JSON array, and the client in [`src/serving.ts`](src/serving.ts) turns them back into the `AgentControl`, `AgentSettings`, `AgentRemote` and `AgentPages` interfaces and their replicated states, so callers don't see Chord.
|
|
31
|
+
- `hostAgent(agent)` serves the agent from one Chord host named `agent` (`serveAgent` in [`src/host.ts`](src/host.ts)). Its remote contracts, in [`src/services.ts`](src/services.ts), are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state, plus `deliveries`: how each user entry was delivered, `run`, `steer` or `follow-up`, derived by the host from where each send it accepted was placed: after a tool result it steered, queued first it's a follow-up or a late steer per its `whenBusy`, placed at once it started a run), `slicc.agent.settings`, `slicc.agent.remote` (remote control), `slicc.agent.pages` (Panels) and `slicc.agent.guest` (Guest seats). `serveConnections(self, host)` hands it every `MessagePort` the page sends, and each port becomes one Chord link whose principal is a seat: `owner` (a page of the owner, with its own Panels), `control` (a follower with Control), `view` (a follower without) or `guest` (a guest seat, with its id, label and epoch), plus the link's number, so every link gets its own objects. The principal is typed (`AgentPrincipal` in [`src/serving.ts`](src/serving.ts)); the host's own code acts as `{ seat: 'host' }`, which no link gets. Each factory picks a variant per seat. Every contract places its objects on the host `agent` (`hostNamed`), so no caller names a host. The contracts take their methods' arguments as one JSON array, and the client in [`src/serving.ts`](src/serving.ts) turns them back into the `AgentControl`, `AgentSettings`, `AgentRemote` and `AgentPages` interfaces and their replicated states, so callers don't see Chord.
|
|
32
32
|
- `@ai-ecoverse/slicc-agent/page` has what the page needs (`startAgent`, `connectAgent`) without the agent's runtime: no pi-durable, pi-ai, providers or SQLite, which load only in the worker. `@ai-ecoverse/slicc-agent/spectrum` is just as light, and a test keeps both that way.
|
|
33
33
|
- `startAgent({ worker })` takes the Web Lock `slicc-agent`, so one tab at a time owns the agent worker, and waits while another tab holds it. `connect()` returns an `AgentConnection` (`control`, `transcript`, `prompt()`, and `webhook(name, { id?, headers?, body })`, which hands a webhook delivery, such as one from the tray hub, to the licks without a chord `Context`), `restart()` replaces the worker (durable resumes from SQLite), and `release()` stops it and lets the next tab take over. `port()` returns a raw `MessagePort` into the worker instead, for another tab: hand it over (through a SharedWorker, say), and that tab calls `connectAgent(port)`. A caller that already holds the lock passes `held: true`. A connection's `closed` resolves when its port ends (with the error, if any), and from then on its states keep their last values.
|
|
34
34
|
- **Followers over a link** (PR 18): `port({ readOnly: true })` and `connect({ readOnly: true })` give a view-only connection, and `port({ pages: false })` a full one that never takes `panel` requests (see Panels).
|
|
35
35
|
- **What a view-only connection can do:** it sees the transcript, the agents, settings state and remote-control `watch`, and may call `wait` and `changesOpen`. It's still an owner device, so it has the owner-device methods (`OWNER_DEVICE_METHODS` in [`src/services.ts`](src/services.ts)) with the owner's powers: `decide`, `guestOpen`, `guestRevoke`, `guestAlwaysForget` and `approvalPolicySet`. Control gates driving, not approvals.
|
|
36
36
|
- **What it can't:** every other `AgentControl` method, settings action and remote-control `prompt` or `abort` is refused with `EACCES` ("this follower can watch, but not send, steer or abort"). Send and compact answer `accepted: false` with that code. The `view` seat's variants don't have those methods at all (`view` for control and settings, `watch` for remote control, `none` for Panels); the follower's client answers the missing ones with the refusals ([`src/readonly.ts`](src/readonly.ts)), so the worker never runs them. A `watch` stream is readable only over the link that opened it.
|
|
37
37
|
- **Carrying the connection over a byte stream:** `linkEndpoint({ write, close })` is a `PortEndpoint` that `connectAgent` accepts. Feed it the stream's bytes with `receive(bytes)` and its end with `end(error?)`. On the owner's side, `bridgePort(port, { write, close })` does the same for the `MessagePort` from `port()`.
|
|
38
|
-
- **The bytes:** the owner first posts a hello, its server id, then each Chord frame travels as its JSON text in UTF-8 (`PortLink` in [`src/wire.ts`](src/wire.ts)), and `{ close: true }` ends the port. On a byte stream each message becomes a frame: a `u32` big-endian length, then a tag (0 data, 1 hello, 2 close), then the payload. A frame is at most 16 MiB, and a malformed one ends the link.
|
|
39
|
-
- **Another agent version:**
|
|
38
|
+
- **The bytes:** the owner first posts a hello, its server id, then each Chord frame travels as its JSON text in UTF-8 (`PortLink` in [`src/wire.ts`](src/wire.ts)), and `{ close: true }` ends the port. The owner holds its own Chord frames until the client's Chord hello arrives, so it knows which Chord it talks to before it says anything. `PortLink.send` returns each frame's size, so Chord's flow control applies: once 256 KiB are unacknowledged, state waits at the owner, coalesced per member, instead of piling up in front of a slow WebRTC follower, while calls and results still pass. On a byte stream each message becomes a frame: a `u32` big-endian length, then a tag (0 data, 1 hello, 2 close), then the payload. A frame is at most 16 MiB, and a malformed one ends the link.
|
|
39
|
+
- **Another agent version:** `connectAgent` and `connectGuest` close the link and reject once with `code: 'EVERSION'` and the sentence "That SLICC runs another version of the agent, so this tab can't follow it. Update both SLICCs, then reload this page." It is final: a follower should show the sentence and not retry. That happens when:
|
|
40
|
+
- Chord refuses the handshake because the owner's contracts don't match (`upgrade_required` or `contract_mismatch`; Chord's error is the `cause`);
|
|
41
|
+
- Chord reports another link protocol (`onProtocolMismatch`, from Chord 1.3 on);
|
|
42
|
+
- the owner runs agent 5.x, whose Chord 1.2 hello carries no protocol version: the client leaves at once.
|
|
43
|
+
- **A 5.x follower or guest of a 6.x owner:** the owner sees its Chord 1.2 hello, never hands that link to Chord, and answers each of its holds with Chord 1.2's `gone` and `upgrade_required`, which 5.5.2 and later already turn into `EVERSION`. Agent 6.0 runs Chord 1.3, whose link protocol (4) refuses 1.2 peers, so 5.x and 6.x don't follow each other.
|
|
40
44
|
|
|
41
45
|
### Bash and files on slicc-kernel
|
|
42
46
|
|
|
@@ -623,7 +627,7 @@ Every approval, of a guest's prompt, of a tool call in a run a guest started, or
|
|
|
623
627
|
- **Requester** comes from the kernel's process tag: `{ kind: 'agent', agentId }` is the cone or scoop by its name, `{ kind: 'user', where }` is the kernel user with `where` (`terminal:…` reads "a terminal", `follower:<device>` the `where` the page passes, such as "on Pat's phone", or "a linked device", with the device id kept as `requester.device`), and anything else is the kernel user alone.
|
|
624
628
|
- **Detail:** the command as the kernel resolved it (`path` and the arguments, shell-quoted), `target`, `cwd`, and `alwaysScope: 'cwd'` when the request offers Always (`req.always === true`, which the page sets from its own checks), since the kernel's Always writes a rule for that directory.
|
|
625
629
|
- **Answers:** `approved` resolves its answer and anything else `deny`. Every device of the owner answers with the same powers: a follower, with or without Control, sees the same item, Always included when the request offers it, and its `always` counts as the owner tab's would. `device` on the item names the device that answered, from `owner.port({ device })`.
|
|
626
|
-
- **Ends:** when the kernel aborts `signal` (`withdrawn`) the item is withdrawn, and on `expired` the page keeps its link until the record's own timeout settles it (`unanswered`), so it never reads as withdrawn. When the page's link closes the worker withdraws its open sudo items (`disconnected`)
|
|
630
|
+
- **Ends:** when the kernel aborts `signal` (`withdrawn`) the item is withdrawn, and on `expired` the page keeps its link until the record's own timeout settles it (`unanswered`), so it never reads as withdrawn. When the page's link closes the worker withdraws its open sudo items (`disconnected`). A restart or release of the owner first ends the old worker, then denies the requests still waiting, so the old worker never sees their links close, and the next worker withdraws their items (`restart`). The kernel already allows one pending request per user.
|
|
627
631
|
- **Scoops:** a scoop's request sends its cone a `sudo-request` lick without actions once a person has to answer it (at once on the `user` tier, after the judge hands it on with the `agent` policy, never when the judge decides), so the cone knows the scoop waits and can't approve it itself. A cone that asks gets none: it waits in its own command.
|
|
628
632
|
- **With the host's own approvals:** bios has its own `approvals` port for sudo, and a page that spreads `{ ...bios, ...createAgentModel(connection) }` would lose it. Pass that port in instead, as `createAgentModel(connection, { approvals: biosPort })`. The returned port then lists both sets, oldest first, sends `decide` to whichever port listed the id, and passes on both ports' `approvals` and `ended` events. It has `decide` when either side can answer; asked for an item its owner can't answer, it rejects with "This request can't be answered in this tab." Without the option, it lists only the agent's items.
|
|
629
633
|
|
|
@@ -675,7 +679,7 @@ A guest seat (plan D13, PR 19) is a revocable seat on one cone: the guest sees t
|
|
|
675
679
|
|
|
676
680
|
- **Minting.** The tray hub mints the seat and its one-time URL (v6's routes, D10); the page hands the agent adapter its routes: `createAgentModel(connection, { guests: { mint({ label, expiresAt, gates, agentId }) → { id, url }, revoke(id) } })`. Its `guests` port (spectrum's `GuestPort`) mints through the hub, then opens the seat in the worker with `AgentControl.guestOpen({ id, label, agentId, expiresAt, gates })`; if the worker refuses, the hub's seat is revoked again. `list()` is the transcript's `guests` state (`{ id, label, agentId, createdAt, expiresAt, gates, connected, pending, alwaysTools }`), and the URL is returned once and never kept. `unavailable()` says "Inviting guests needs the tray hub." without a hub, and "Only the owner's devices can invite guests." on a guest. Every owner device can invite, with or without Control. `gates()` (spectrum's `GuestPort.gates?()`) lists the gates the approval policy allows for messages and tools, in display order, for the invite pickers; `scoop:*`, for "any scoop", shows only when the policy offers the scoop tier.
|
|
677
681
|
- **The record.** Seats live in the session document `slicc.guests`, so they survive reloads and restarts: id, label (control characters stripped, at most 64 characters), cone, expiry (at most the policy's `guests.maxSeatDays`, 30 by default), the two gates (`user`, `agent`, `cone`, `scoop:<name>` or `off`, fixed when the seat is opened and checked then against the policy's `allowedMessageGates` and `allowedToolGates`; by default the tools gate can't be `off` or `cone`, from any caller: "A guest's tool calls need a review: by a person, the judge or a scoop.") and an epoch. Revoking (`guestRevoke(id)`) keeps a tombstone, bumps the epoch, withdraws the guest's messages still queued in the cone, and ends every guest link of the seat (`invalidatePrincipal`). An expired seat ends the same way at its expiry. Tombstones go 365 days after they end, the longest a seat can be opened for.
|
|
678
|
-
- **Joining.** The hub resolves the guest's token to the seat's id and label, and the owner page opens `owner.port({ guest: { id, label } })` and bridges it like a follower's. Before Chord sees the link, the worker checks that the seat exists, isn't revoked or expired, and has that label; otherwise it closes the port. The link's principal is `{ seat: 'guest', link, guest: { id, name, epoch } }`, and anything else the port asks for (`readOnly: false`, `pages`) is ignored.
|
|
682
|
+
- **Joining.** The hub resolves the guest's token to the seat's id and label, and the owner page opens `owner.port({ guest: { id, label } })` and bridges it like a follower's. Before Chord sees the link, the worker checks that the seat exists, isn't revoked or expired, and has that label; otherwise it closes the port. The link's principal is `{ seat: 'guest', link, guest: { id, name, epoch } }`, and anything else the port asks for (`readOnly: false`, `pages`) is ignored. A guest link has at most four calls in flight (Chord's `maxCalls`): further ones are answered at once with `unavailable` ("Too many calls in progress on this connection (4)"), and the link stays open. A revoked guest leaves nothing behind on the host: Chord 1.3 drops each disposed object, where 1.2 kept one per guest principal (seat and epoch), with its link, until the worker restarted.
|
|
679
683
|
- **What a guest gets.** Every contract served from the agent's host refuses a guest principal (`forbidden`) unless its factory is opened with `openToGuests`, so a contract added later is closed to guests until someone opens it. The only open one is `slicc.agent.guest`: its `guest` variant has the shared cone's transcript (its user, assistant, tool and compaction entries; `pi.live`; `pi.agent`'s model and thinking level; and only the guest's own queued messages) and the seat, plus `propose({ text, requestId })`. Everyone else gets its empty `none` variant.
|
|
680
684
|
- **The guest's page** calls `connectGuest(endpoint)` from `@ai-ecoverse/slicc-agent/page`, which resolves an `AgentConnection` for that one cone with `guest` (the seat as the guest sees it); `connectAgent` on a guest port is forbidden. `createAgentModel(guest).agent` shows the transcript and sends through `propose`; every other control method, settings action, `prompt` and `webhook` rejects with `EACCES` ("a guest can only watch and send messages"). A port that isn't a valid seat rejects with `EACCES` "This guest link isn't valid any more."
|
|
681
685
|
- **Messages.** With the messages gate `off`, a guest's message is submitted to the cone as a follow-up placed as a `slicc.guest-input` entry (`{ seat, name, approval: null }`), and the model reads it after the line "[A guest message from "<label>", shared through a guest seat, NOT from the cone's owner. …]". The spectrum adapter shows it as a user message with `origin: 'guest'` and `from` the label, without that line, queued or placed. Under any other gate it waits as a pending `guest-message` approval (tier = the gate, requester from the seat), which times out like other approvals. The guest contract's `proposals` state lists the seat's own, and the guest's adapter shows a pending one as its queued message and an ended one as a notice: "Nobody approved this in time.", "It was declined." or "It was withdrawn." Once approved it is delivered exactly once (request id `guest:<seat>:<approval>`, and the seat record keeps the ids it delivered) as the same entry with `approval` set; a seat revoked or expired by then gets nothing. #253 makes the delivery durable across a crash between approval and submit, and adds the review UX.
|
|
@@ -713,7 +717,7 @@ The agent never holds a secret's value. Values live in the vault (`vault.sliccy.
|
|
|
713
717
|
|
|
714
718
|
Fixes to pi stay in this repository. [`patches/patches.json`](patches/patches.json) lists each one with its `kind`, `package`, `patchedVersion`, `reason`, `removeWhen` and `verify` command, and an optional `marker` (a file in the package and a string it must contain):
|
|
715
719
|
|
|
716
|
-
- `monkeypatch`: code that runs in users' browsers is patched at runtime. pi-mcp parses SSE with Node's `Buffer.byteLength`, which browsers don't have. `patchSse()` in [`src/mcp/sse.ts`](src/mcp/sse.ts) replaces `StreamableHttpTransport.prototype.consumeSse` with the same parser measuring bytes with `TextEncoder`, before any server connects. It checks that the code it replaces still looks as expected and throws, naming its manifest entry (named in [`src/patches.ts`](src/patches.ts)), if not.
|
|
720
|
+
- `monkeypatch`: code that runs in users' browsers is patched at runtime. pi-mcp parses SSE with Node's `Buffer.byteLength`, which browsers don't have. `patchSse()` in [`src/mcp/sse.ts`](src/mcp/sse.ts) replaces `StreamableHttpTransport.prototype.consumeSse` with the same parser measuring bytes with `TextEncoder`, before any server connects. It checks that the code it replaces still looks as expected and throws, naming its manifest entry (named in [`src/patches.ts`](src/patches.ts)), if not. The AWS SDK has the other one: in the browser, Bedrock's request handler is `WebSocketFetchHandler`, which passes https requests to `FetchHttpHandler` without their abort signal, so Stop couldn't cancel a Bedrock request that hadn't answered yet (#350). [`src/bedrock.ts`](src/bedrock.ts) wraps the Bedrock provider and, before its first request, replaces `WebSocketFetchHandler.prototype.handle` with one that forwards the signal. The patch must reach the copy the client imports. The agent therefore depends on `@aws-sdk/middleware-websocket` with a caret range, as the client does, so npm and seven's pnpm install resolve one copy for both. If Bedrock answers without going through the patched handler, the agent warns once in the worker console, naming the entry. pi-server's handshake-timer patch went with pi-server in 5.0.
|
|
717
721
|
- `patch-package`: repository-only fixes go in `patches/<package>+<version>.patch`, applied by a `postinstall` that runs [patch-package](https://github.com/ds300/patch-package). There are none yet.
|
|
718
722
|
- `environment`: something the host must provide.
|
|
719
723
|
- pi-codemode runs QuickJS in a `node:worker_threads` Worker. slicc-bios must serve a `node:worker_threads` stub that wraps a module Web Worker; inside the worker, the stub gives `parentPort` and `workerData` from the first message. `test/integration/shims/node-worker-threads.js` is that stub, and the integration bundles alias to it.
|
|
@@ -723,9 +727,9 @@ Fixes to pi stay in this repository. [`patches/patches.json`](patches/patches.js
|
|
|
723
727
|
- The prune takes the install from 99 to 58 packages and `node_modules` from 143 to 92 MB.
|
|
724
728
|
- The markers keep pi-ai's two Google APIs (Gemini and Vertex) lazy. Seven offers only the Bedrock and Adobe providers: if a Google provider is ever added, the bios override has to go first, and a failing marker is the reminder.
|
|
725
729
|
|
|
726
|
-
`npm run lint:patches` (part of `npm run lint`) fails when an installed version differs from `patchedVersion`, a marker is gone, a monkeypatch isn't named in `src/patches.ts`, a patch file and its entry don't match, or `renovate.json` doesn't route the package to the `patched dependencies` group with automerge off. That group covers every `@earendil-works/*` package
|
|
730
|
+
`npm run lint:patches` (part of `npm run lint`) fails when an installed version differs from `patchedVersion`, a marker is gone, a monkeypatch isn't named in `src/patches.ts`, a patch file and its entry don't match, or `renovate.json` doesn't route the package to the `patched dependencies` group with automerge off. That group covers every `@earendil-works/*` package and `@aws-sdk/middleware-websocket`, so a pi bump is always a reviewed PR: rerun each entry's `verify`, then move `patchedVersion` forward or drop the entry.
|
|
727
731
|
|
|
728
|
-
**
|
|
732
|
+
**One Chord.** `@earendil-works/chord` is pinned to the version pi-durable pins (1.3.0), so the link, the serving code and pi-durable's state layer share one copy. `npm run lint:chord` (part of `npm run lint`) fails if the lockfile has a second Chord that the agent would load, or none. The `slicc-agent-5` devDependency brings its own Chord 1.2 for the version tests only, which the check skips because it's dev-only.
|
|
729
733
|
|
|
730
734
|
## Develop
|
|
731
735
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createHost } from "@earendil-works/chord";
|
|
2
1
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
|
+
import { clientHost, holding } from "../client.js";
|
|
3
3
|
import { AgentSudoService, SUDO_METHODS } from "../services.js";
|
|
4
|
-
import {
|
|
4
|
+
import { bound } from "../serving.js";
|
|
5
5
|
import { PortLink } from "../wire.js";
|
|
6
6
|
const UNAVAILABLE = "The agent can't take this sudo request.";
|
|
7
7
|
const unavailable = (cause) => Object.assign(new Error(UNAVAILABLE), { code: "EAGAIN", ...cause ? { cause } : {} });
|
|
@@ -18,10 +18,10 @@ function halted(stop) {
|
|
|
18
18
|
}
|
|
19
19
|
async function open(endpoint, stop) {
|
|
20
20
|
const link = new PortLink(endpoint);
|
|
21
|
-
|
|
21
|
+
let host;
|
|
22
22
|
const close = async () => {
|
|
23
23
|
link.close("closed");
|
|
24
|
-
await host
|
|
24
|
+
await host?.close().catch(() => void 0);
|
|
25
25
|
};
|
|
26
26
|
const ended = link.ended.then((reason) => {
|
|
27
27
|
throw reason ?? new Error("the agent closed the link");
|
|
@@ -31,11 +31,11 @@ async function open(endpoint, stop) {
|
|
|
31
31
|
stopped.catch(() => void 0);
|
|
32
32
|
try {
|
|
33
33
|
await Promise.race([link.hello, ended, stopped]);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await Promise.race([
|
|
37
|
-
if (handle.
|
|
38
|
-
const root = (run, options) =>
|
|
34
|
+
const client = clientHost(link, "sudo");
|
|
35
|
+
host = client;
|
|
36
|
+
const [handle] = await Promise.race([holding(client, link, [AgentSudoService]), stopped]);
|
|
37
|
+
if (handle.variant !== "sudo") throw unavailable();
|
|
38
|
+
const root = (run, options) => client.root(run, options);
|
|
39
39
|
const sudo = bound(handle, root, SUDO_METHODS, missing);
|
|
40
40
|
return { sudo, approvals: handle.approvals, ended, close };
|
|
41
41
|
} catch (error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/sudo-client.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { Readable } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { clientHost, holding } from '../client.ts';\nimport { AgentSudoService, SUDO_METHODS } from '../services.ts';\nimport { bound, type Remote, type Root } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Approval } from './record.ts';\nimport type { AgentSudo, SudoAnswer, SudoRequest } from './sudo.ts';\n\nexport interface ApproveOptions {\n signal?: AbortSignal;\n where?: string;\n}\n\nexport const UNAVAILABLE = \"The agent can't take this sudo request.\";\n\nexport const unavailable = (cause?: unknown): Error =>\n Object.assign(new Error(UNAVAILABLE), { code: 'EAGAIN', ...(cause ? { cause } : {}) });\n\nconst missing = () => () => Promise.reject(unavailable());\n\nexport function answerOf(item: Approval | undefined): SudoAnswer | null {\n if (!item || item.state === 'pending') return null;\n return item.state === 'approved' && item.answer ? item.answer : 'deny';\n}\n\nfunction halted(stop: AbortSignal | undefined): Promise<never> {\n return new Promise((_, reject) => {\n if (stop?.aborted) reject(unavailable());\n stop?.addEventListener('abort', () => reject(unavailable()), { once: true });\n });\n}\n\nasync function open(endpoint: PortEndpoint, stop: AbortSignal | undefined) {\n const link = new PortLink(endpoint);\n let host: ReturnType<typeof clientHost> | undefined;\n const close = async () => {\n link.close('closed');\n await host?.close().catch(() => undefined);\n };\n const ended = link.ended.then((reason) => {\n throw reason ?? new Error('the agent closed the link');\n });\n ended.catch(() => undefined);\n const stopped = halted(stop);\n stopped.catch(() => undefined);\n try {\n await Promise.race([link.hello, ended, stopped]);\n const client = clientHost(link, 'sudo');\n host = client;\n const [handle] = (await Promise.race([holding(client, link, [AgentSudoService]), stopped])) as [\n Remote,\n ];\n if (handle.variant !== 'sudo') throw unavailable();\n const root: Root = (run, options) => client.root(run, options);\n const sudo = bound<Omit<AgentSudo, 'approvals'>>(handle, root, SUDO_METHODS, missing);\n return { sudo, approvals: handle.approvals as Readable<Approval[]>, ended, close };\n } catch (error) {\n await close();\n throw (error as { code?: unknown })?.code === 'EAGAIN' ? error : unavailable(error);\n }\n}\n\nfunction settled(\n approvals: Readable<Approval[]>,\n id: string,\n signals: { signal: AbortSignal | undefined; stop: AbortSignal | undefined },\n ended: Promise<never>\n): Promise<{ answer: SudoAnswer; aborted: unknown }> {\n const { signal, stop } = signals;\n return new Promise((resolve) => {\n let off: () => void = () => undefined;\n let done = false;\n const finish = (answer: SudoAnswer, aborted: unknown = null) => {\n if (done) return;\n done = true;\n off();\n signal?.removeEventListener('abort', abort);\n stop?.removeEventListener('abort', halt);\n resolve({ answer, aborted });\n };\n const abort = () => finish('deny', signal?.reason ?? 'withdrawn');\n const halt = () => finish('deny');\n if (signal?.aborted) return abort();\n if (stop?.aborted) return halt();\n signal?.addEventListener('abort', abort, { once: true });\n stop?.addEventListener('abort', halt, { once: true });\n ended.catch(() => finish('deny'));\n off = approvals.subscribe((items) => {\n const answer = answerOf(items?.find((item) => item.id === id));\n if (answer) finish(answer);\n });\n if (done) off();\n });\n}\n\nexport const WITHDRAW_MS = 5000;\n\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction timedOut(\n link: { approvals: Readable<Approval[]>; ended: Promise<never> },\n id: string,\n expiresAt: number\n): Promise<unknown> {\n let off: () => void = () => undefined;\n const left = new Promise<void>((resolve) => {\n off = link.approvals.subscribe((items) => {\n if (answerOf(items?.find((item) => item.id === id))) resolve();\n });\n });\n const wait = Math.max(0, expiresAt - Date.now()) + WITHDRAW_MS;\n return Promise.race([left, link.ended.catch(() => undefined), delay(wait)]).finally(() => off());\n}\n\nexport async function askSudo(\n endpoint: PortEndpoint,\n request: SudoRequest,\n options: ApproveOptions = {},\n stop?: AbortSignal\n): Promise<SudoAnswer> {\n const { signal } = options;\n const link = await open(endpoint, stop);\n try {\n if (signal?.aborted || stop?.aborted) return 'deny';\n const asked = await link.sudo.request(request, options.where ?? null, BACKGROUND_CONTEXT);\n if (asked.id === null) throw new Error(asked.error);\n const signals = { signal, stop };\n const { answer, aborted } = await settled(link.approvals, asked.id, signals, link.ended);\n if (aborted === 'expired') await timedOut(link, asked.id, request.expiresAt);\n else if (aborted)\n await Promise.race([\n link.sudo.withdraw(asked.id, String(aborted), BACKGROUND_CONTEXT),\n delay(WITHDRAW_MS),\n ]);\n return answer;\n } finally {\n await link.close();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/bedrock.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { lazyStream } from "@earendil-works/pi-ai/api/lazy";
|
|
2
|
+
import { BEDROCK_ABORT_PATCH } from "./patches.js";
|
|
3
|
+
import { BEDROCK } from "./preferred.js";
|
|
4
|
+
const patched = /* @__PURE__ */ Symbol.for("slicc.agent.bedrock-abort");
|
|
5
|
+
const DROPS = /this\.httpHandler\.handle\(\w+\)/;
|
|
6
|
+
const ANSWERED = /* @__PURE__ */ new Set(["stop", "length", "toolUse"]);
|
|
7
|
+
const UNPATCHED_COPY = `${BEDROCK_ABORT_PATCH}: Bedrock answered through a WebSocketFetchHandler the agent didn't patch, so Stop can't cancel its requests; the agent and @aws-sdk/client-bedrock-runtime load different copies of @aws-sdk/middleware-websocket`;
|
|
8
|
+
function patchWebSocketHandler(target) {
|
|
9
|
+
const proto = target.prototype;
|
|
10
|
+
const done = proto[patched];
|
|
11
|
+
if (done) return done;
|
|
12
|
+
const original = proto.handle;
|
|
13
|
+
if (typeof original !== "function" || !DROPS.test(String(original))) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`${BEDROCK_ABORT_PATCH}: WebSocketFetchHandler.prototype.handle no longer calls this.httpHandler.handle(request); revisit patches.json`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const probe = { sent: 0 };
|
|
19
|
+
proto.handle = async function handle(request, options) {
|
|
20
|
+
if (request.protocol === "ws:" || request.protocol === "wss:")
|
|
21
|
+
return original.call(this, request, options);
|
|
22
|
+
probe.sent += 1;
|
|
23
|
+
this.config = await this.configPromise;
|
|
24
|
+
return this.httpHandler.handle(request, options);
|
|
25
|
+
};
|
|
26
|
+
proto[patched] = probe;
|
|
27
|
+
return probe;
|
|
28
|
+
}
|
|
29
|
+
let loading;
|
|
30
|
+
function patchBedrockAbort(load = () => import("@aws-sdk/middleware-websocket")) {
|
|
31
|
+
loading ??= load().then(
|
|
32
|
+
({ WebSocketFetchHandler }) => patchWebSocketHandler(WebSocketFetchHandler),
|
|
33
|
+
(error) => {
|
|
34
|
+
loading = void 0;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
return loading;
|
|
39
|
+
}
|
|
40
|
+
function abortableBedrock(provider, patch = patchBedrockAbort, warn = (message) => console.warn(message)) {
|
|
41
|
+
if (provider.id !== BEDROCK) return provider;
|
|
42
|
+
let warned = false;
|
|
43
|
+
const checked = (stream, probe) => ({
|
|
44
|
+
[Symbol.asyncIterator]: () => stream[Symbol.asyncIterator](),
|
|
45
|
+
async result() {
|
|
46
|
+
const message = await stream.result();
|
|
47
|
+
if (!warned && probe.sent === 0 && ANSWERED.has(message.stopReason)) {
|
|
48
|
+
warned = true;
|
|
49
|
+
warn(UNPATCHED_COPY);
|
|
50
|
+
}
|
|
51
|
+
return message;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const streams = {
|
|
55
|
+
stream: (model, context, options) => lazyStream(model, async () => {
|
|
56
|
+
const probe = await patch();
|
|
57
|
+
return checked(provider.stream(model, context, options), probe);
|
|
58
|
+
}),
|
|
59
|
+
streamSimple: (model, context, options) => lazyStream(model, async () => {
|
|
60
|
+
const probe = await patch();
|
|
61
|
+
return checked(provider.streamSimple(model, context, options), probe);
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
return { ...provider, ...streams };
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
UNPATCHED_COPY,
|
|
68
|
+
abortableBedrock,
|
|
69
|
+
patchBedrockAbort,
|
|
70
|
+
patchWebSocketHandler
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=bedrock.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/bedrock.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n AssistantMessage,\n AssistantMessageEvent,\n AssistantMessageEventStream,\n Provider,\n ProviderStreams,\n} from '@earendil-works/pi-ai';\nimport { lazyStream } from '@earendil-works/pi-ai/api/lazy';\nimport { BEDROCK_ABORT_PATCH } from './patches.ts';\nimport { BEDROCK } from './preferred.ts';\n\nconst patched = Symbol.for('slicc.agent.bedrock-abort');\nconst DROPS = /this\\.httpHandler\\.handle\\(\\w+\\)/;\nconst ANSWERED = new Set(['stop', 'length', 'toolUse']);\n\nexport const UNPATCHED_COPY = `${BEDROCK_ABORT_PATCH}: Bedrock answered through a WebSocketFetchHandler the agent didn't patch, so Stop can't cancel its requests; the agent and @aws-sdk/client-bedrock-runtime load different copies of @aws-sdk/middleware-websocket`;\n\nexport interface Probe {\n sent: number;\n}\n\ninterface HttpRequest {\n protocol?: string;\n}\n\ninterface Handle {\n handle(request: HttpRequest, options?: unknown): Promise<unknown>;\n}\n\ninterface WebSocketHandler extends Handle {\n config: unknown;\n configPromise: Promise<unknown>;\n httpHandler: Handle;\n}\n\nexport function patchWebSocketHandler(target: { prototype: object }): Probe {\n const proto = target.prototype as WebSocketHandler & { [patched]?: Probe };\n const done = proto[patched];\n if (done) return done;\n const original = proto.handle;\n if (typeof original !== 'function' || !DROPS.test(String(original))) {\n throw new Error(\n `${BEDROCK_ABORT_PATCH}: WebSocketFetchHandler.prototype.handle no longer calls this.httpHandler.handle(request); revisit patches.json`\n );\n }\n const probe: Probe = { sent: 0 };\n proto.handle = async function handle(request, options) {\n if (request.protocol === 'ws:' || request.protocol === 'wss:')\n return original.call(this, request, options);\n probe.sent += 1;\n this.config = await this.configPromise;\n return this.httpHandler.handle(request, options);\n };\n proto[patched] = probe;\n return probe;\n}\n\nlet loading: Promise<Probe> | undefined;\n\nexport function patchBedrockAbort(\n load: () => Promise<{ WebSocketFetchHandler: { prototype: object } }> = () =>\n import('@aws-sdk/middleware-websocket')\n): Promise<Probe> {\n loading ??= load().then(\n ({ WebSocketFetchHandler }) => patchWebSocketHandler(WebSocketFetchHandler),\n (error: unknown) => {\n loading = undefined;\n throw error;\n }\n );\n return loading;\n}\n\nexport function abortableBedrock(\n provider: Provider,\n patch: () => Promise<Probe> = patchBedrockAbort,\n warn: (message: string) => void = (message) => console.warn(message)\n): Provider {\n if (provider.id !== BEDROCK) return provider;\n let warned = false;\n const checked = (stream: AssistantMessageEventStream, probe: Probe) => ({\n [Symbol.asyncIterator]: (): AsyncIterator<AssistantMessageEvent> =>\n stream[Symbol.asyncIterator](),\n async result(): Promise<AssistantMessage> {\n const message = await stream.result();\n if (!warned && probe.sent === 0 && ANSWERED.has(message.stopReason)) {\n warned = true;\n warn(UNPATCHED_COPY);\n }\n return message;\n },\n });\n const streams: Pick<ProviderStreams, 'stream' | 'streamSimple'> = {\n stream: (model, context, options) =>\n lazyStream(model, async () => {\n const probe = await patch();\n return checked(provider.stream(model, context, options), probe);\n }),\n streamSimple: (model, context, options) =>\n lazyStream(model, async () => {\n const probe = await patch();\n return checked(provider.streamSimple(model, context, options), probe);\n }),\n };\n return { ...provider, ...streams };\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AACpC,SAAS,eAAe;AAExB,MAAM,UAAU,uBAAO,IAAI,2BAA2B;AACtD,MAAM,QAAQ;AACd,MAAM,WAAW,oBAAI,IAAI,CAAC,QAAQ,UAAU,SAAS,CAAC;AAE/C,MAAM,iBAAiB,GAAG,mBAAmB;AAoB7C,SAAS,sBAAsB,QAAsC;AAC1E,QAAM,QAAQ,OAAO;AACrB,QAAM,OAAO,MAAM,OAAO;AAC1B,MAAI,KAAM,QAAO;AACjB,QAAM,WAAW,MAAM;AACvB,MAAI,OAAO,aAAa,cAAc,CAAC,MAAM,KAAK,OAAO,QAAQ,CAAC,GAAG;AACnE,UAAM,IAAI;AAAA,MACR,GAAG,mBAAmB;AAAA,IACxB;AAAA,EACF;AACA,QAAM,QAAe,EAAE,MAAM,EAAE;AAC/B,QAAM,SAAS,eAAe,OAAO,SAAS,SAAS;AACrD,QAAI,QAAQ,aAAa,SAAS,QAAQ,aAAa;AACrD,aAAO,SAAS,KAAK,MAAM,SAAS,OAAO;AAC7C,UAAM,QAAQ;AACd,SAAK,SAAS,MAAM,KAAK;AACzB,WAAO,KAAK,YAAY,OAAO,SAAS,OAAO;AAAA,EACjD;AACA,QAAM,OAAO,IAAI;AACjB,SAAO;AACT;AAEA,IAAI;AAEG,SAAS,kBACd,OAAwE,MACtE,OAAO,+BAA+B,GACxB;AAChB,cAAY,KAAK,EAAE;AAAA,IACjB,CAAC,EAAE,sBAAsB,MAAM,sBAAsB,qBAAqB;AAAA,IAC1E,CAAC,UAAmB;AAClB,gBAAU;AACV,YAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,iBACd,UACA,QAA8B,mBAC9B,OAAkC,CAAC,YAAY,QAAQ,KAAK,OAAO,GACzD;AACV,MAAI,SAAS,OAAO,QAAS,QAAO;AACpC,MAAI,SAAS;AACb,QAAM,UAAU,CAAC,QAAqC,WAAkB;AAAA,IACtE,CAAC,OAAO,aAAa,GAAG,MACtB,OAAO,OAAO,aAAa,EAAE;AAAA,IAC/B,MAAM,SAAoC;AACxC,YAAM,UAAU,MAAM,OAAO,OAAO;AACpC,UAAI,CAAC,UAAU,MAAM,SAAS,KAAK,SAAS,IAAI,QAAQ,UAAU,GAAG;AACnE,iBAAS;AACT,aAAK,cAAc;AAAA,MACrB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAA4D;AAAA,IAChE,QAAQ,CAAC,OAAO,SAAS,YACvB,WAAW,OAAO,YAAY;AAC5B,YAAM,QAAQ,MAAM,MAAM;AAC1B,aAAO,QAAQ,SAAS,OAAO,OAAO,SAAS,OAAO,GAAG,KAAK;AAAA,IAChE,CAAC;AAAA,IACH,cAAc,CAAC,OAAO,SAAS,YAC7B,WAAW,OAAO,YAAY;AAC5B,YAAM,QAAQ,MAAM,MAAM;AAC1B,aAAO,QAAQ,SAAS,aAAa,OAAO,SAAS,OAAO,GAAG,KAAK;AAAA,IACtE,CAAC;AAAA,EACL;AACA,SAAO,EAAE,GAAG,UAAU,GAAG,QAAQ;AACnC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
SETTINGS_METHODS,
|
|
17
17
|
TRANSCRIPT_STATES
|
|
18
18
|
} from "./services.js";
|
|
19
|
-
import {
|
|
20
|
-
import { PortLink } from "./wire.js";
|
|
19
|
+
import { bound, replicated, SELF } from "./serving.js";
|
|
20
|
+
import { otherVersion, PortLink } from "./wire.js";
|
|
21
21
|
function lasting(state) {
|
|
22
22
|
let last;
|
|
23
23
|
let frozen = false;
|
|
@@ -52,44 +52,62 @@ function lastingSettings(settings, hold) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
const VERSIONS = /* @__PURE__ */ new Set(["upgrade_required", "contract_mismatch"]);
|
|
55
|
-
const
|
|
56
|
-
const final = (error) => isChordError(error) && VERSIONS.has(error.code) ? Object.assign(new Error(OTHER_VERSION, { cause: error }), { code: "EVERSION" }) : error;
|
|
55
|
+
const final = (error) => isChordError(error) && VERSIONS.has(error.code) ? otherVersion(error) : error;
|
|
57
56
|
const ended = (link) => link.ended.then((reason) => {
|
|
58
57
|
throw reason ?? new Error("the agent closed the link");
|
|
59
58
|
});
|
|
59
|
+
function clientHost(link, name) {
|
|
60
|
+
const host = createHost({
|
|
61
|
+
name: `${name}:${crypto.randomUUID()}`,
|
|
62
|
+
kind: "client",
|
|
63
|
+
self: SELF
|
|
64
|
+
});
|
|
65
|
+
host.connect(link, { principal: SELF, onProtocolMismatch: () => link.mismatched() });
|
|
66
|
+
return host;
|
|
67
|
+
}
|
|
68
|
+
async function holding(host, link, contracts) {
|
|
69
|
+
const stop = new AbortController();
|
|
70
|
+
const getting = contracts.map(
|
|
71
|
+
(contract) => host.get(contract, {}, { signal: stop.signal })
|
|
72
|
+
);
|
|
73
|
+
try {
|
|
74
|
+
return await Promise.race([Promise.all(getting), ended(link)]);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
stop.abort();
|
|
77
|
+
for (const got of getting)
|
|
78
|
+
void got.then(
|
|
79
|
+
(ref) => ref.release(),
|
|
80
|
+
() => void 0
|
|
81
|
+
);
|
|
82
|
+
link.close("handshake failed");
|
|
83
|
+
void host.close().catch(() => void 0);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
60
87
|
async function connectAgent(endpoint, options = {}) {
|
|
61
88
|
const context = options.context ?? BACKGROUND_CONTEXT;
|
|
62
89
|
const link = new PortLink(endpoint);
|
|
63
90
|
const serverId = await link.hello;
|
|
64
|
-
const host =
|
|
65
|
-
host
|
|
66
|
-
const get = (contract) => host.get(contract, {}, { at: AGENT_HOST });
|
|
67
|
-
const handles = [
|
|
91
|
+
const host = clientHost(link, "follower");
|
|
92
|
+
const handles = await holding(host, link, [
|
|
68
93
|
AgentControl,
|
|
69
94
|
AgentTranscript,
|
|
70
95
|
AgentSettings,
|
|
71
96
|
AgentRemoteService,
|
|
72
97
|
AgentPagesService
|
|
73
|
-
].
|
|
98
|
+
]).catch((error) => {
|
|
99
|
+
throw final(error);
|
|
100
|
+
});
|
|
74
101
|
const [controlHandle, transcriptHandle, settingsHandle, remoteHandle, pagesHandle] = handles;
|
|
75
102
|
const release = () => {
|
|
76
103
|
for (const handle of handles) handle.release();
|
|
77
104
|
};
|
|
78
|
-
try {
|
|
79
|
-
await Promise.race([Promise.all(handles.map((handle) => handle.ready)), ended(link)]);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
release();
|
|
82
|
-
link.close("handshake failed");
|
|
83
|
-
throw final(error);
|
|
84
|
-
}
|
|
85
105
|
const root = (run, rootOptions) => host.root(run, rootOptions);
|
|
86
106
|
const control = bound(controlHandle, root, CONTROL_METHODS, controlAnswer);
|
|
87
107
|
const members = Object.fromEntries(
|
|
88
108
|
TRANSCRIPT_STATES.map((name) => [name, transcriptHandle[name]])
|
|
89
109
|
);
|
|
90
110
|
const settingsState = settingsHandle.state;
|
|
91
|
-
const keeping = [...Object.values(members), settingsState].map(kept);
|
|
92
|
-
await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), ended(link)]);
|
|
93
111
|
const settings = {
|
|
94
112
|
...bound(settingsHandle, root, SETTINGS_METHODS, settingsAnswer),
|
|
95
113
|
state: replicated(settingsState)
|
|
@@ -99,16 +117,15 @@ async function connectAgent(endpoint, options = {}) {
|
|
|
99
117
|
const state = (name) => replicated(members[name]);
|
|
100
118
|
const held = [];
|
|
101
119
|
const hold = (state2) => {
|
|
102
|
-
const
|
|
103
|
-
held.push(
|
|
104
|
-
return
|
|
120
|
+
const kept = lasting(state2);
|
|
121
|
+
held.push(kept);
|
|
122
|
+
return kept.state;
|
|
105
123
|
};
|
|
106
124
|
let open = true;
|
|
107
125
|
const finish = () => {
|
|
108
126
|
if (!open) return;
|
|
109
127
|
open = false;
|
|
110
|
-
for (const
|
|
111
|
-
for (const keep of keeping) keep.stop();
|
|
128
|
+
for (const kept of held) kept.freeze();
|
|
112
129
|
release();
|
|
113
130
|
};
|
|
114
131
|
const closed = link.ended.then((reason) => {
|
|
@@ -117,7 +134,7 @@ async function connectAgent(endpoint, options = {}) {
|
|
|
117
134
|
});
|
|
118
135
|
return {
|
|
119
136
|
serverId,
|
|
120
|
-
readOnly: controlHandle.
|
|
137
|
+
readOnly: controlHandle.variant === "view",
|
|
121
138
|
control,
|
|
122
139
|
transcript: hold(state("state")),
|
|
123
140
|
deliveries: hold(state("deliveries")),
|
|
@@ -134,8 +151,8 @@ async function connectAgent(endpoint, options = {}) {
|
|
|
134
151
|
vault: hold(state("vault")),
|
|
135
152
|
labels: hold(state("labels")),
|
|
136
153
|
guests: hold(state("guests")),
|
|
137
|
-
viewOnly: controlHandle.
|
|
138
|
-
owner: pagesHandle.
|
|
154
|
+
viewOnly: controlHandle.variant === "view",
|
|
155
|
+
owner: pagesHandle.variant === "pages",
|
|
139
156
|
approvals: hold(state("approvals")),
|
|
140
157
|
approvalPolicy: hold(state("approvalPolicy")),
|
|
141
158
|
settings: lastingSettings(settings, hold),
|
|
@@ -165,9 +182,11 @@ async function connectAgent(endpoint, options = {}) {
|
|
|
165
182
|
};
|
|
166
183
|
}
|
|
167
184
|
export {
|
|
168
|
-
|
|
185
|
+
clientHost,
|
|
169
186
|
connectAgent,
|
|
187
|
+
ended,
|
|
170
188
|
final,
|
|
189
|
+
holding,
|
|
171
190
|
lasting
|
|
172
191
|
};
|
|
173
192
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type Context,\n createHost,\n isChordError,\n type Readable,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ApprovalPolicy } from './approvals/policy.ts';\nimport type { Approval } from './approvals/record.ts';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { GuestSeat } from './guests/seats.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport { controlAnswer, pageAnswer, remoteAnswer, settingsAnswer } from './readonly.ts';\nimport type { AgentRemote } from './remote/host.ts';\nimport type { AgentPages } from './remote/pages.ts';\nimport type { VaultState } from './secrets/vault.ts';\nimport {\n AgentControl,\n AgentPagesService,\n AgentRemoteService,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n CONTROL_METHODS,\n type Command,\n type Delivered,\n type GuestProposal,\n type GuestShown,\n type MonitorFeed,\n PAGE_METHODS,\n type PrivateRevision,\n REMOTE_METHODS,\n SETTINGS_METHODS,\n type SendMode,\n type SettingsState,\n TRANSCRIPT_STATES,\n} from './services.ts';\nimport {
|
|
5
|
-
"mappings": "AAAA;AAAA,EAEE;AAAA,
|
|
6
|
-
"names": ["state"
|
|
4
|
+
"sourcesContent": ["import {\n type Context,\n createHost,\n type Host,\n isChordError,\n type Readable,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ApprovalPolicy } from './approvals/policy.ts';\nimport type { Approval } from './approvals/record.ts';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { GuestSeat } from './guests/seats.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport { controlAnswer, pageAnswer, remoteAnswer, settingsAnswer } from './readonly.ts';\nimport type { AgentRemote } from './remote/host.ts';\nimport type { AgentPages } from './remote/pages.ts';\nimport type { VaultState } from './secrets/vault.ts';\nimport {\n AgentControl,\n AgentPagesService,\n AgentRemoteService,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n CONTROL_METHODS,\n type Command,\n type Delivered,\n type GuestProposal,\n type GuestShown,\n type MonitorFeed,\n PAGE_METHODS,\n type PrivateRevision,\n REMOTE_METHODS,\n SETTINGS_METHODS,\n type SendMode,\n type SettingsState,\n TRANSCRIPT_STATES,\n} from './services.ts';\nimport { type AgentPrincipal, bound, type Remote, type Root, replicated, SELF } from './serving.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { otherVersion, type PortEndpoint, PortLink } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly privateRevision?: ReplicatedState<PrivateRevision>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly monitor?: ReplicatedState<MonitorFeed>;\n readonly vault?: ReplicatedState<VaultState>;\n readonly labels?: ReplicatedState<Record<string, string>>;\n readonly guests?: ReplicatedState<GuestSeat[]>;\n readonly viewOnly?: boolean;\n readonly guest?: GuestShown;\n readonly proposals?: ReplicatedState<GuestProposal[]>;\n readonly owner?: boolean;\n readonly approvals?: ReplicatedState<Approval[]>;\n readonly approvalPolicy?: ReplicatedState<ApprovalPolicy>;\n readonly settings: AgentSettings;\n readonly remote?: AgentRemote;\n readonly pages?: AgentPages;\n readonly closed: Promise<Error | undefined>;\n readonly readOnly?: boolean;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n webhook(\n name: string,\n delivery: { id?: string | null; headers?: Record<string, string>; body: string }\n ): Promise<{ delivered: boolean }>;\n close(): Promise<void>;\n}\n\nexport interface Lasting<T> {\n readonly state: ReplicatedState<T>;\n freeze(): void;\n}\n\nexport function lasting<T>(state: ReplicatedState<T>): Lasting<T> {\n let last: T | undefined;\n let frozen = false;\n const read = () => {\n if (frozen) return last;\n try {\n last = state.value;\n } catch {}\n return last;\n };\n return {\n state: {\n get value() {\n return read();\n },\n subscribe: (listener) => (frozen ? () => undefined : state.subscribe(listener)),\n },\n freeze() {\n read();\n frozen = true;\n },\n };\n}\n\nfunction lastingSettings(\n settings: AgentSettings,\n hold: <T>(state: ReplicatedState<T>) => ReplicatedState<T>\n): AgentSettings {\n return {\n state: hold(settings.state),\n connect: (providerId, secret, fields, context) =>\n settings.connect(providerId, secret, fields, context),\n disconnect: (providerId, context) => settings.disconnect(providerId, context),\n signIn: (providerId, context) => settings.signIn(providerId, context),\n dismissMoved: (context) => settings.dismissMoved(context),\n };\n}\n\nconst VERSIONS = new Set(['upgrade_required', 'contract_mismatch']);\n\nexport const final = (error: unknown): unknown =>\n isChordError(error) && VERSIONS.has(error.code) ? otherVersion(error) : error;\n\nexport const ended = (link: PortLink): Promise<never> =>\n link.ended.then((reason) => {\n throw reason ?? new Error('the agent closed the link');\n });\n\ntype Contract = Parameters<Host<AgentPrincipal>['get']>[0];\n\nexport function clientHost(link: PortLink, name: string): Host<AgentPrincipal> {\n const host = createHost<AgentPrincipal>({\n name: `${name}:${crypto.randomUUID()}`,\n kind: 'client',\n self: SELF,\n });\n host.connect(link, { principal: SELF, onProtocolMismatch: () => link.mismatched() });\n return host;\n}\n\nexport async function holding(\n host: Host<AgentPrincipal>,\n link: PortLink,\n contracts: readonly Contract[]\n): Promise<Remote[]> {\n const stop = new AbortController();\n const getting = contracts.map(\n (contract) =>\n host.get(contract, {}, { signal: stop.signal }) as Promise<unknown> as Promise<Remote>\n );\n try {\n return await Promise.race([Promise.all(getting), ended(link)]);\n } catch (error) {\n stop.abort();\n for (const got of getting)\n void got.then(\n (ref) => ref.release(),\n () => undefined\n );\n link.close('handshake failed');\n void host.close().catch(() => undefined);\n throw error;\n }\n}\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const link = new PortLink(endpoint);\n const serverId = await link.hello;\n const host = clientHost(link, 'follower');\n const handles = await holding(host, link, [\n AgentControl,\n AgentTranscript,\n AgentSettings,\n AgentRemoteService,\n AgentPagesService,\n ]).catch((error: unknown) => {\n throw final(error);\n });\n const [controlHandle, transcriptHandle, settingsHandle, remoteHandle, pagesHandle] = handles as [\n Remote,\n Remote,\n Remote,\n Remote,\n Remote,\n ];\n const release = () => {\n for (const handle of handles) handle.release();\n };\n const root: Root = (run, rootOptions) => host.root(run, rootOptions);\n const control = bound<AgentControl>(controlHandle, root, CONTROL_METHODS, controlAnswer);\n const members = Object.fromEntries(\n TRANSCRIPT_STATES.map((name) => [name, transcriptHandle[name] as Readable<unknown>])\n ) as Record<(typeof TRANSCRIPT_STATES)[number], Readable<unknown>>;\n const settingsState = settingsHandle.state as Readable<SettingsState>;\n const settings: AgentSettings = {\n ...bound<Omit<AgentSettings, 'state'>>(settingsHandle, root, SETTINGS_METHODS, settingsAnswer),\n state: replicated(settingsState),\n };\n const remote = bound<AgentRemote>(remoteHandle, root, REMOTE_METHODS, remoteAnswer);\n const pages = bound<AgentPages>(pagesHandle, root, PAGE_METHODS, pageAnswer);\n const state = <K extends keyof typeof members>(name: K) =>\n replicated(members[name] as Readable<never>);\n const held: Lasting<unknown>[] = [];\n const hold = <T>(state: ReplicatedState<T>): ReplicatedState<T> => {\n const kept = lasting(state);\n held.push(kept as Lasting<unknown>);\n return kept.state;\n };\n let open = true;\n const finish = () => {\n if (!open) return;\n open = false;\n for (const kept of held) kept.freeze();\n release();\n };\n const closed = link.ended.then((reason) => {\n finish();\n return reason;\n });\n return {\n serverId,\n readOnly: controlHandle.variant === 'view',\n control,\n transcript: hold<ConversationView>(state('state')),\n deliveries: hold<Record<string, Delivered>>(state('deliveries')),\n agents: hold<AgentsSummary>(state('agents')),\n views: hold<Record<string, ConversationView>>(state('views')),\n commands: hold<Command[]>(state('commands')),\n privateRevision: hold<PrivateRevision>(state('privateRevision')),\n sprinkles: hold<Sprinkle[]>(state('sprinkles')),\n memories: hold<MemoryEntry[]>(state('memories')),\n memoryScopes: hold<MemoryScope[]>(state('memoryScopes')),\n frozen: hold<FrozenCone[]>(state('frozen')),\n changes: hold<ChangesView>(state('changes')),\n monitor: hold<MonitorFeed>(state('monitor')),\n vault: hold<VaultState>(state('vault')),\n labels: hold<Record<string, string>>(state('labels')),\n guests: hold<GuestSeat[]>(state('guests')),\n viewOnly: controlHandle.variant === 'view',\n owner: pagesHandle.variant === 'pages',\n approvals: hold<Approval[]>(state('approvals')),\n approvalPolicy: hold<ApprovalPolicy>(state('approvalPolicy')),\n settings: lastingSettings(settings, hold),\n remote,\n pages,\n closed,\n webhook(name, delivery) {\n return control.webhook(\n name,\n { id: delivery.id ?? null, headers: delivery.headers ?? {}, body: delivery.body },\n context\n );\n },\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n finish();\n link.close('closed');\n await closed;\n await host.close();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EAEE;AAAA,EAEA;AAAA,OAGK;AACP,SAAS,0BAA0B;AAQnC,SAAS,eAAe,YAAY,cAAc,sBAAsB;AAIxE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EAMA;AAAA,EAEA;AAAA,EACA;AAAA,EAGA;AAAA,OACK;AACP,SAA8B,OAA+B,YAAY,YAAY;AAErF,SAAS,cAAiC,gBAAgB;AA4CnD,SAAS,QAAW,OAAuC;AAChE,MAAI;AACJ,MAAI,SAAS;AACb,QAAM,OAAO,MAAM;AACjB,QAAI,OAAQ,QAAO;AACnB,QAAI;AACF,aAAO,MAAM;AAAA,IACf,QAAQ;AAAA,IAAC;AACT,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,OAAO;AAAA,MACL,IAAI,QAAQ;AACV,eAAO,KAAK;AAAA,MACd;AAAA,MACA,WAAW,CAAC,aAAc,SAAS,MAAM,SAAY,MAAM,UAAU,QAAQ;AAAA,IAC/E;AAAA,IACA,SAAS;AACP,WAAK;AACL,eAAS;AAAA,IACX;AAAA,EACF;AACF;AAEA,SAAS,gBACP,UACA,MACe;AACf,SAAO;AAAA,IACL,OAAO,KAAK,SAAS,KAAK;AAAA,IAC1B,SAAS,CAAC,YAAY,QAAQ,QAAQ,YACpC,SAAS,QAAQ,YAAY,QAAQ,QAAQ,OAAO;AAAA,IACtD,YAAY,CAAC,YAAY,YAAY,SAAS,WAAW,YAAY,OAAO;AAAA,IAC5E,QAAQ,CAAC,YAAY,YAAY,SAAS,OAAO,YAAY,OAAO;AAAA,IACpE,cAAc,CAAC,YAAY,SAAS,aAAa,OAAO;AAAA,EAC1D;AACF;AAEA,MAAM,WAAW,oBAAI,IAAI,CAAC,oBAAoB,mBAAmB,CAAC;AAE3D,MAAM,QAAQ,CAAC,UACpB,aAAa,KAAK,KAAK,SAAS,IAAI,MAAM,IAAI,IAAI,aAAa,KAAK,IAAI;AAEnE,MAAM,QAAQ,CAAC,SACpB,KAAK,MAAM,KAAK,CAAC,WAAW;AAC1B,QAAM,UAAU,IAAI,MAAM,2BAA2B;AACvD,CAAC;AAII,SAAS,WAAW,MAAgB,MAAoC;AAC7E,QAAM,OAAO,WAA2B;AAAA,IACtC,MAAM,GAAG,IAAI,IAAI,OAAO,WAAW,CAAC;AAAA,IACpC,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AACD,OAAK,QAAQ,MAAM,EAAE,WAAW,MAAM,oBAAoB,MAAM,KAAK,WAAW,EAAE,CAAC;AACnF,SAAO;AACT;AAEA,eAAsB,QACpB,MACA,MACA,WACmB;AACnB,QAAM,OAAO,IAAI,gBAAgB;AACjC,QAAM,UAAU,UAAU;AAAA,IACxB,CAAC,aACC,KAAK,IAAI,UAAU,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,EAClD;AACA,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK,CAAC,QAAQ,IAAI,OAAO,GAAG,MAAM,IAAI,CAAC,CAAC;AAAA,EAC/D,SAAS,OAAO;AACd,SAAK,MAAM;AACX,eAAW,OAAO;AAChB,WAAK,IAAI;AAAA,QACP,CAAC,QAAQ,IAAI,QAAQ;AAAA,QACrB,MAAM;AAAA,MACR;AACF,SAAK,MAAM,kBAAkB;AAC7B,SAAK,KAAK,MAAM,EAAE,MAAM,MAAM,MAAS;AACvC,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,aACpB,UACA,UAAiC,CAAC,GACR;AAC1B,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,OAAO,IAAI,SAAS,QAAQ;AAClC,QAAM,WAAW,MAAM,KAAK;AAC5B,QAAM,OAAO,WAAW,MAAM,UAAU;AACxC,QAAM,UAAU,MAAM,QAAQ,MAAM,MAAM;AAAA,IACxC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EAAE,MAAM,CAAC,UAAmB;AAC3B,UAAM,MAAM,KAAK;AAAA,EACnB,CAAC;AACD,QAAM,CAAC,eAAe,kBAAkB,gBAAgB,cAAc,WAAW,IAAI;AAOrF,QAAM,UAAU,MAAM;AACpB,eAAW,UAAU,QAAS,QAAO,QAAQ;AAAA,EAC/C;AACA,QAAM,OAAa,CAAC,KAAK,gBAAgB,KAAK,KAAK,KAAK,WAAW;AACnE,QAAM,UAAU,MAAoB,eAAe,MAAM,iBAAiB,aAAa;AACvF,QAAM,UAAU,OAAO;AAAA,IACrB,kBAAkB,IAAI,CAAC,SAAS,CAAC,MAAM,iBAAiB,IAAI,CAAsB,CAAC;AAAA,EACrF;AACA,QAAM,gBAAgB,eAAe;AACrC,QAAM,WAA0B;AAAA,IAC9B,GAAG,MAAoC,gBAAgB,MAAM,kBAAkB,cAAc;AAAA,IAC7F,OAAO,WAAW,aAAa;AAAA,EACjC;AACA,QAAM,SAAS,MAAmB,cAAc,MAAM,gBAAgB,YAAY;AAClF,QAAM,QAAQ,MAAkB,aAAa,MAAM,cAAc,UAAU;AAC3E,QAAM,QAAQ,CAAiC,SAC7C,WAAW,QAAQ,IAAI,CAAoB;AAC7C,QAAM,OAA2B,CAAC;AAClC,QAAM,OAAO,CAAIA,WAAkD;AACjE,UAAM,OAAO,QAAQA,MAAK;AAC1B,SAAK,KAAK,IAAwB;AAClC,WAAO,KAAK;AAAA,EACd;AACA,MAAI,OAAO;AACX,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC,KAAM;AACX,WAAO;AACP,eAAW,QAAQ,KAAM,MAAK,OAAO;AACrC,YAAQ;AAAA,EACV;AACA,QAAM,SAAS,KAAK,MAAM,KAAK,CAAC,WAAW;AACzC,WAAO;AACP,WAAO;AAAA,EACT,CAAC;AACD,SAAO;AAAA,IACL;AAAA,IACA,UAAU,cAAc,YAAY;AAAA,IACpC;AAAA,IACA,YAAY,KAAuB,MAAM,OAAO,CAAC;AAAA,IACjD,YAAY,KAAgC,MAAM,YAAY,CAAC;AAAA,IAC/D,QAAQ,KAAoB,MAAM,QAAQ,CAAC;AAAA,IAC3C,OAAO,KAAuC,MAAM,OAAO,CAAC;AAAA,IAC5D,UAAU,KAAgB,MAAM,UAAU,CAAC;AAAA,IAC3C,iBAAiB,KAAsB,MAAM,iBAAiB,CAAC;AAAA,IAC/D,WAAW,KAAiB,MAAM,WAAW,CAAC;AAAA,IAC9C,UAAU,KAAoB,MAAM,UAAU,CAAC;AAAA,IAC/C,cAAc,KAAoB,MAAM,cAAc,CAAC;AAAA,IACvD,QAAQ,KAAmB,MAAM,QAAQ,CAAC;AAAA,IAC1C,SAAS,KAAkB,MAAM,SAAS,CAAC;AAAA,IAC3C,SAAS,KAAkB,MAAM,SAAS,CAAC;AAAA,IAC3C,OAAO,KAAiB,MAAM,OAAO,CAAC;AAAA,IACtC,QAAQ,KAA6B,MAAM,QAAQ,CAAC;AAAA,IACpD,QAAQ,KAAkB,MAAM,QAAQ,CAAC;AAAA,IACzC,UAAU,cAAc,YAAY;AAAA,IACpC,OAAO,YAAY,YAAY;AAAA,IAC/B,WAAW,KAAiB,MAAM,WAAW,CAAC;AAAA,IAC9C,gBAAgB,KAAqB,MAAM,gBAAgB,CAAC;AAAA,IAC5D,UAAU,gBAAgB,UAAU,IAAI;AAAA,IACxC;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,MAAM,UAAU;AACtB,aAAO,QAAQ;AAAA,QACb;AAAA,QACA,EAAE,IAAI,SAAS,MAAM,MAAM,SAAS,SAAS,WAAW,CAAC,GAAG,MAAM,SAAS,KAAK;AAAA,QAChF;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,OAAO,MAAM,WAAW,YAAY;AACxC,YAAM,OAAO,MAAM,QAAQ,KAAK,EAAE,MAAM,UAAU,WAAW,KAAK,GAAG,OAAO;AAC5E,UAAI,CAAC,KAAK,SAAU,OAAM,IAAI,MAAM,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,OAAO,EAAE;AAC/E,YAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,cAAc,OAAO;AAC7D,UAAI,QAAQ,WAAW,OAAQ,OAAM,IAAI,MAAM,eAAe,QAAQ,MAAM,EAAE;AAC9E,aAAO,QAAQ;AAAA,IACjB;AAAA,IACA,MAAM,QAAQ;AACZ,aAAO;AACP,WAAK,MAAM,QAAQ;AACnB,YAAM;AACN,YAAM,KAAK,MAAM;AAAA,IACnB;AAAA,EACF;AACF;",
|
|
6
|
+
"names": ["state"]
|
|
7
7
|
}
|
package/dist/guests/client.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { final, lasting } from "../client.js";
|
|
1
|
+
import { isChordError, replicatedState } from "@earendil-works/chord";
|
|
2
|
+
import { clientHost, final, holding, lasting } from "../client.js";
|
|
3
3
|
import {
|
|
4
4
|
AgentGuestService,
|
|
5
5
|
CONTROL_METHODS
|
|
6
6
|
} from "../services.js";
|
|
7
|
-
import {
|
|
7
|
+
import { plain, replicated } from "../serving.js";
|
|
8
8
|
import { PortLink } from "../wire.js";
|
|
9
9
|
const GUEST_ONLY = "a guest can only watch and send messages";
|
|
10
10
|
const SEAT_REFUSED = "This guest link isn't valid any more.";
|
|
@@ -26,33 +26,21 @@ async function connectGuest(endpoint) {
|
|
|
26
26
|
} catch {
|
|
27
27
|
throw refusal(SEAT_REFUSED);
|
|
28
28
|
}
|
|
29
|
-
const host =
|
|
30
|
-
host.
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const host = clientHost(link, "guest");
|
|
30
|
+
const [handle] = await holding(host, link, [AgentGuestService]).catch((error) => {
|
|
31
|
+
const code = error?.code;
|
|
32
|
+
if (code === "EVERSION") throw error;
|
|
33
|
+
throw isChordError(error) && error.code !== "forbidden" ? final(error) : refusal(SEAT_REFUSED);
|
|
34
|
+
});
|
|
35
|
+
if (handle.variant !== "guest") {
|
|
33
36
|
handle.release();
|
|
34
37
|
link.close("handshake failed");
|
|
35
38
|
await host.close();
|
|
36
|
-
throw
|
|
37
|
-
};
|
|
38
|
-
try {
|
|
39
|
-
await Promise.race([
|
|
40
|
-
handle.ready,
|
|
41
|
-
link.ended.then(() => {
|
|
42
|
-
throw refusal(SEAT_REFUSED);
|
|
43
|
-
})
|
|
44
|
-
]);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
return fail(
|
|
47
|
-
isChordError(error) && error.code === "forbidden" ? refusal(SEAT_REFUSED) : final(error)
|
|
48
|
-
);
|
|
39
|
+
throw refusal(SEAT_REFUSED);
|
|
49
40
|
}
|
|
50
|
-
if (handle.kind !== "guest") return fail(refusal(SEAT_REFUSED));
|
|
51
41
|
const transcriptMember = handle.transcript;
|
|
52
42
|
const seatMember = handle.seat;
|
|
53
43
|
const proposalsMember = handle.proposals;
|
|
54
|
-
const keeping = [transcriptMember, seatMember, proposalsMember].map(kept);
|
|
55
|
-
await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), link.ended]);
|
|
56
44
|
const through = (name) => async (...args) => {
|
|
57
45
|
const call = handle[name];
|
|
58
46
|
try {
|
|
@@ -95,7 +83,6 @@ async function connectGuest(endpoint) {
|
|
|
95
83
|
open = false;
|
|
96
84
|
transcript.freeze();
|
|
97
85
|
proposals.freeze();
|
|
98
|
-
for (const keep of keeping) keep.stop();
|
|
99
86
|
handle.release();
|
|
100
87
|
};
|
|
101
88
|
const closed = link.ended.then((reason) => {
|