@ai-ecoverse/slicc-agent 6.19.15 → 7.1.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/README.md +17 -33
- package/dist/agent.js +10 -1
- package/dist/agent.js.map +2 -2
- package/dist/agents.js +2 -0
- package/dist/agents.js.map +2 -2
- package/dist/approvals/policy.js +2 -0
- package/dist/approvals/policy.js.map +2 -2
- package/dist/approvals/record.js +2 -0
- package/dist/approvals/record.js.map +2 -2
- package/dist/checkpoints.js +5 -0
- package/dist/checkpoints.js.map +7 -0
- package/dist/codemode/index.js +2 -0
- package/dist/codemode/index.js.map +2 -2
- package/dist/cone.js +2 -0
- package/dist/cone.js.map +2 -2
- package/dist/guests/guard.js +3 -0
- package/dist/guests/guard.js.map +2 -2
- package/dist/guests/review.js +4 -1
- package/dist/guests/review.js.map +2 -2
- package/dist/guests/scoop.js +3 -0
- package/dist/guests/scoop.js.map +2 -2
- package/dist/guests/seats.js +2 -0
- package/dist/guests/seats.js.map +2 -2
- package/dist/guests/tags.js +2 -0
- package/dist/guests/tags.js.map +2 -2
- package/dist/licks/boot.js +2 -0
- package/dist/licks/boot.js.map +2 -2
- package/dist/licks/licks.js +2 -0
- package/dist/licks/licks.js.map +2 -2
- package/dist/mcp/allow.js +2 -0
- package/dist/mcp/allow.js.map +2 -2
- package/dist/memory/extract.js +2 -0
- package/dist/memory/extract.js.map +2 -2
- package/dist/monitor/index.js +3 -1
- package/dist/monitor/index.js.map +2 -2
- package/dist/pause.js +26 -0
- package/dist/pause.js.map +7 -0
- package/dist/preferred.js +6 -6
- package/dist/preferred.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/providers.js.map +1 -1
- package/dist/scoops/extension.js +2 -1
- package/dist/scoops/extension.js.map +2 -2
- package/dist/scoops/service.js +2 -0
- package/dist/scoops/service.js.map +2 -2
- package/dist/secrets/legacy.js +2 -0
- package/dist/secrets/legacy.js.map +2 -2
- package/dist/selection.js +2 -0
- package/dist/selection.js.map +2 -2
- package/dist/services.js.map +2 -2
- package/dist/settings.js +33 -5
- package/dist/settings.js.map +3 -3
- package/dist/spectrum/budget.js +30 -0
- package/dist/spectrum/budget.js.map +7 -0
- package/dist/spectrum/index.js +7 -5
- package/dist/spectrum/index.js.map +2 -2
- package/dist/spectrum/monitor-port.js +24 -4
- package/dist/spectrum/monitor-port.js.map +2 -2
- package/dist/spectrum/tray-port.js +19 -3
- package/dist/spectrum/tray-port.js.map +2 -2
- package/dist/sprinkles/sprinkles.js +2 -0
- package/dist/sprinkles/sprinkles.js.map +2 -2
- package/dist/supergrok.js +73 -0
- package/dist/supergrok.js.map +7 -0
- package/dist/titles/labels.js +2 -0
- package/dist/titles/labels.js.map +2 -2
- package/dist/types/agent.d.ts +4 -0
- package/dist/types/checkpoints.d.ts +1 -0
- package/dist/types/pause.d.ts +6 -0
- package/dist/types/services.d.ts +5 -2
- package/dist/types/settings.d.ts +8 -1
- package/dist/types/spectrum/budget.d.ts +16 -0
- package/dist/types/spectrum/monitor-port.d.ts +2 -0
- package/dist/types/spectrum/tray-port.d.ts +2 -1
- package/dist/types/supergrok.d.ts +12 -0
- package/dist/workflow/run.js +5 -0
- package/dist/workflow/run.js.map +2 -2
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ await agent.prompt('What is in /home?');
|
|
|
27
27
|
```
|
|
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
|
-
- `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.
|
|
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. On pi durable 1.4, opening a database written by an earlier agent migrates it to durable's SQLite schema 2, once and in place; an agent on pi durable 1.3 or earlier (slicc-agent 6.19.13 and earlier) can't open it afterwards, so the agent can't be rolled back past this version. Requests through the Harness are attributed to SLICC (`identity: { attribution: { name: 'SLICC', url: 'https://www.sliccy.com' } }`), which pi-ai sends as OpenRouter's `HTTP-Referer` and `X-OpenRouter-Title`, NVIDIA's `X-BILLING-INVOKE-ORIGIN` and OpenCode's `x-opencode-client`; OpenRouter's preflight allows them from `https://seven.sliccy.ai`, and NVIDIA and OpenCode answer no browser preflight at all, so they go through a CORS-free transport either way. The Harness keeps one 1.3 behaviour: a task cut off by reloads is never parked (`restartLimit: Infinity`, since a reload ends the worker without closing the session).
|
|
31
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.
|
|
@@ -138,13 +138,13 @@ const owner = await startAgent({
|
|
|
138
138
|
|
|
139
139
|
`runAgentWorker(self, options)` is the same with its parts replaceable (model, providers, kernel attach, the account store, storage).
|
|
140
140
|
|
|
141
|
-
**Providers.** `sliccProviders()` registers Bedrock, Adobe and every pi-ai provider that takes a plain API key: Anthropic, OpenAI, Mistral, Groq, DeepSeek, Together, Hugging Face, Xiaomi and its three token plans, Cerebras, OpenRouter, Fireworks, Baseten, Moonshot AI and its China platform, xAI, Meta, Vercel AI Gateway, MiniMax and MiniMax CN, Z.AI and Z.AI Coding CN, NVIDIA, Kimi For Coding, Ant Ling, OpenCode Zen and Go, and the three Qwen token plans (`KEY_PROVIDERS` in `src/providers.ts`, each with its id, pi-ai factory, pi-ai env name, a description, a key URL and whether it answers browsers). Three more take a key and non-secret fields (`auth: 'fields'`), each checked in `connect` and mapped to pi-ai's env: Azure (`endpoint`, required: a resource name becomes `AZURE_OPENAI_RESOURCE_NAME`, an `https:` URL without a port or user name `AZURE_OPENAI_BASE_URL`; `deployments`, optional `model=deployment` pairs, `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`), Cloudflare Workers AI (`account`, 32 hex characters, `CLOUDFLARE_ACCOUNT_ID`) and Cloudflare AI Gateway (`account` and `gateway`, `CLOUDFLARE_GATEWAY_ID`). Bedrock's `region` is on the same mechanism. A bad value rejects `connect` with a sentence, a required field left out keeps its stored value or rejects with “{label} can’t be empty.”, and a stored value that no longer passes is left out of the env. A fields provider whose required fields are missing or no longer pass shows as not connected, gives pi-ai no credential and offers no models, even with its key already in the vault (spectrum puts the key before `connect` checks the fields). Bedrock's region is any AWS region name (`eusc-de-east-1` and `us-gov-west-1` included), and stored values are trimmed. OpenCode Zen and OpenCode Go need seven's vault 1.12.2 or later, whose built-in table has their `/inference` endpoints; their descriptions say so. Azure's `endpoint` field says `customBase: true`: when its value is a full URL, spectrum registers it with the vault (the page's `endpoints`) before `connect`, since the agent never registers model endpoints for pi-ai providers; a resource name needs nothing, as `*.openai.azure.com` is built into the vault. The worker routes requests to an Azure account's base URL as Azure's. Google and Google Vertex wait for slicc-bios to stop removing `@google/genai`; the sign-ins come later. A provider's preferred model is pi-coding-agent 1.
|
|
141
|
+
**Providers.** `sliccProviders()` registers Bedrock, Adobe and every pi-ai provider that takes a plain API key: Anthropic, OpenAI, Mistral, Groq, DeepSeek, Together, Hugging Face, Xiaomi and its three token plans, Cerebras, OpenRouter, Fireworks, Baseten, Moonshot AI and its China platform, xAI, Meta, Vercel AI Gateway, MiniMax and MiniMax CN, Z.AI and Z.AI Coding CN, NVIDIA, Kimi For Coding, Ant Ling, OpenCode Zen and Go, and the three Qwen token plans (`KEY_PROVIDERS` in `src/providers.ts`, each with its id, pi-ai factory, pi-ai env name, a description, a key URL and whether it answers browsers). Three more take a key and non-secret fields (`auth: 'fields'`), each checked in `connect` and mapped to pi-ai's env: Azure (`endpoint`, required: a resource name becomes `AZURE_OPENAI_RESOURCE_NAME`, an `https:` URL without a port or user name `AZURE_OPENAI_BASE_URL`; `deployments`, optional `model=deployment` pairs, `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`), Cloudflare Workers AI (`account`, 32 hex characters, `CLOUDFLARE_ACCOUNT_ID`) and Cloudflare AI Gateway (`account` and `gateway`, `CLOUDFLARE_GATEWAY_ID`). Bedrock's `region` is on the same mechanism. A bad value rejects `connect` with a sentence, a required field left out keeps its stored value or rejects with “{label} can’t be empty.”, and a stored value that no longer passes is left out of the env. A fields provider whose required fields are missing or no longer pass shows as not connected, gives pi-ai no credential and offers no models, even with its key already in the vault (spectrum puts the key before `connect` checks the fields). Bedrock's region is any AWS region name (`eusc-de-east-1` and `us-gov-west-1` included), and stored values are trimmed. OpenCode Zen and OpenCode Go need seven's vault 1.12.2 or later, whose built-in table has their `/inference` endpoints; their descriptions say so. Azure's `endpoint` field says `customBase: true`: when its value is a full URL, spectrum registers it with the vault (the page's `endpoints`) before `connect`, since the agent never registers model endpoints for pi-ai providers; a resource name needs nothing, as `*.openai.azure.com` is built into the vault. The worker routes requests to an Azure account's base URL as Azure's. Google and Google Vertex wait for slicc-bios to stop removing `@google/genai`; the sign-ins come later. A provider's preferred model is pi-coding-agent 1.4.0's `defaultModelPerProvider` (Bedrock keeps `us.anthropic.claude-sonnet-5-5`), and the first model when that one is missing. Providers that don't answer a browser from `https://seven.sliccy.ai` with the headers pi-ai and the AWS SDK send say `needs: 'cors-free-transport'`; the direct ones are Bedrock, Adobe, Anthropic, OpenAI, Azure, Mistral, Groq, DeepSeek, Together, Hugging Face, Baseten, both Moonshot AI platforms, xAI, Meta and the four Xiaomi providers. Z.AI Coding CN stays cors-free: the nightly check found it answering browsers on one day and not two days later. Chrome accepts `Authorization` under a bare `Access-Control-Allow-Headers: *` (as xAI, Meta and Adobe's proxy answer), checked in Chrome 156. The worker's transport fetch drops the Stainless SDKs' `x-stainless-*` telemetry headers from every request it sends, which is what Baseten's and Moonshot AI's preflights refuse.
|
|
142
142
|
|
|
143
|
-
**Reachability,
|
|
143
|
+
**Reachability, by hand.** `npm run reach` (`scripts/reach.mjs`, through `verdict.mjs`) checks every listing from `https://seven.sliccy.ai`. It captures the request pi-ai builds for each of the provider's APIs (with a dummy key and dummy fields, and the fetch faked; Bedrock's from the AWS SDK's own request, caught at its Node HTTP handler, which carries the same headers as in a browser: `amz-sdk-invocation-id`, `amz-sdk-request`, `authorization`, `content-type`, `x-amz-user-agent`). Then it sends that request's preflight and, when the preflight passes, the request itself with dummy credentials, whose answer must carry `Access-Control-Allow-Origin` too. Besides each chat API it probes what the provider's models need beyond a turn: the requests its model discovery sends under its base URL (Adobe's `/v1/config`), one classifier call per classifier API (Adobe's `/v1/ai/run`, OpenAI's `/v1/decisions`, OpenRouter's and Cloudflare's), and Adobe's `/v1/usage`. It prints `reach: <id> direct|cors-free|unknown|unprobed` per provider (`unknown` when the network gave no answer; `unprobed` when it captured no request at all, which counts as a disagreement, so a provider can't silently drop out of the check) and one `reach: PASS` line. A provider that stays `unknown` three runs in a row counts as a disagreement too, when `REACH_HISTORY` names a file that keeps the streaks between runs, and the line says how many runs in a row; `REACH_REPORT` names a file for the JSON report. A direct provider that passes only because Chrome lets `Authorization` through under a bare `Access-Control-Allow-Headers: *` reads `direct (wildcard auth)` (today Adobe, xAI and Meta); the PASS line and the report list them, without failing, so we know at once which break if Chrome ever enforces the Fetch spec. No workflow runs it: it calls the real providers' endpoints, so it is a check to run before changing a listing, and no CI job or integration test reaches a real provider (`test/integration/reach.test.mjs` fakes the network). A run without the PASS line fails.
|
|
144
144
|
|
|
145
145
|
The first provider is AWS Bedrock with a Bedrock API key as the bearer token; the default model is `us.anthropic.claude-sonnet-5-5`. Requests go to `us-west-2` unless the account's `region` field names another one (`AWS_REGION`). The account publishes that field as `fields: [{ name: 'region', label: 'Region', help, options, value }]`, and `connect` takes it. Bedrock's runtime endpoints answer browsers (`Access-Control-Allow-Origin: *`, allowing the AWS SDK's headers), so it needs no CORS-free transport.
|
|
146
146
|
|
|
147
|
-
**Adobe.** `adobeProvider()` reaches Adobe's LLM proxy (`ADOBE_PROXY`) with the user's IMS token as a bearer. Its models come from the proxy's `/v1/config` once a token is stored: Anthropic-style models go through pi-ai's `anthropic-messages` API at the proxy root, `api: 'openai'` models through `openai-completions` under `/v1`, and hidden ones are skipped. Every request to the proxy (turns on either API, Clef, `/v1/config` and `/v1/usage`) carries an `X-Session-Id` that is fixed for the worker and an `X-Slicc-Version` (slicc-agent's version, `adobeProvider({ session, version })`); the proxy refuses turns without them. They're added where the request is built, over the caller's and the model's headers, because pi-ai 1.0.4 keeps a provider's `headers` but never sends them. The account is a sign-in (`auth: 'oauth'`): `AgentSettings.signIn('adobe')` returns the IMS `clientId`, `scopes` and `imsEnvironment` from `/v1/config`, fetched in the worker through the transport. The page runs the IMS popup and seals the token in the vault itself (bios: `seal { kind: 'model', provider: 'adobe', name: 'ADOBE_IMS_TOKEN', source: 'oauth:adobe', expiresAt }`), so `createAgentModel(connection, { login })` takes a `login(providerId, signIn, { signal })` that resolves `{ sealed: true }`; the adapter then calls `connect('adobe')` with no secret. A `login` that still returns a token is refused with "Adobe sign-in needs seven's vault: update seven", and the token never reaches the worker. IMS gives no refresh token, so an expired record shows the account as `expired`, and Sign in (or Reconnect) runs `login` again. The worker derives that from each vault record (`records()`, any account kind, MCP rows included): a record that isn't `ready` or `refreshing` (such as `inert`), or whose `expiresAt` (epoch milliseconds, the vault's `EpochMs`) has passed, reads `expired`, and a timer flips it when the clock reaches `expiresAt`, so an open page needn't wait for the vault to change. A model request that still gets a 401 after the vault's one refresh marks the record it was sent with `expired` too, until that record changes (a new sign-in gives it a new `expiresAt`) or the account is connected again (`connect`), not when an unrelated record changes, so the error card's **Log in** lands on an account that offers Sign in. While it runs, the account shows as `signing-in` (spectrum ≥ 1.34.0), and `SettingsPort.cancel(id)` aborts the signal; a cancelled sign-in never connects, even if the login resolves late. The adapter calls `login` before anything else in the click's task, so the page can open its popup while it still has the user's gesture, and `signIn()` then fetches the IMS options lazily. The weekly budget from `/v1/usage` is in the settings state
|
|
147
|
+
**Adobe.** `adobeProvider()` reaches Adobe's LLM proxy (`ADOBE_PROXY`) with the user's IMS token as a bearer. Its models come from the proxy's `/v1/config` once a token is stored: Anthropic-style models go through pi-ai's `anthropic-messages` API at the proxy root, `api: 'openai'` models through `openai-completions` under `/v1`, and hidden ones are skipped. Every request to the proxy (turns on either API, Clef, `/v1/config` and `/v1/usage`) carries an `X-Session-Id` that is fixed for the worker and an `X-Slicc-Version` (slicc-agent's version, `adobeProvider({ session, version })`); the proxy refuses turns without them. They're added where the request is built, over the caller's and the model's headers, because pi-ai 1.0.4 keeps a provider's `headers` but never sends them. The account is a sign-in (`auth: 'oauth'`): `AgentSettings.signIn('adobe')` returns the IMS `clientId`, `scopes` and `imsEnvironment` from `/v1/config`, fetched in the worker through the transport. The page runs the IMS popup and seals the token in the vault itself (bios: `seal { kind: 'model', provider: 'adobe', name: 'ADOBE_IMS_TOKEN', source: 'oauth:adobe', expiresAt }`), so `createAgentModel(connection, { login })` takes a `login(providerId, signIn, { signal })` that resolves `{ sealed: true }`; the adapter then calls `connect('adobe')` with no secret. A `login` that still returns a token is refused with "Adobe sign-in needs seven's vault: update seven", and the token never reaches the worker. IMS gives no refresh token, so an expired record shows the account as `expired`, and Sign in (or Reconnect) runs `login` again. The worker derives that from each vault record (`records()`, any account kind, MCP rows included): a record that isn't `ready` or `refreshing` (such as `inert`), or whose `expiresAt` (epoch milliseconds, the vault's `EpochMs`) has passed, reads `expired`, and a timer flips it when the clock reaches `expiresAt`, so an open page needn't wait for the vault to change. A model request that still gets a 401 after the vault's one refresh marks the record it was sent with `expired` too, until that record changes (a new sign-in gives it a new `expiresAt`) or the account is connected again (`connect`), not when an unrelated record changes, so the error card's **Log in** lands on an account that offers Sign in. While it runs, the account shows as `signing-in` (spectrum ≥ 1.34.0), and `SettingsPort.cancel(id)` aborts the signal; a cancelled sign-in never connects, even if the login resolves late. The adapter calls `login` before anything else in the click's task, so the page can open its popup while it still has the user's gesture, and `signIn()` then fetches the IMS options lazily. The weekly budget from `/v1/usage` is in the settings state (refreshed on connect and every five minutes); see **Budgets**. The proxy answers browsers from `seven.sliccy.ai`, so Adobe needs no CORS-free transport. An expired session's error card (a 401, or a refusal whose type or message says the token is invalid or expired) offers **Log in**, reads “Log in again to continue.” (the raw error goes to the console once, already scrubbed, never onto the card) and names its account (`account: 'adobe'`, spectrum ≥ 1.88.0), so Log in calls `settings.connect('adobe')` inside the click and spectrum's card follows the sign-in (its own copy while signing in, then “Signed in to Adobe again.”, from the account's `provider`), or shows the failure as a sentence (a `login` that rejects with “the sign-in timed out” reads “The sign-in timed out.”; a cancel stays an `AbortError`, which shows nothing). A rejected or missing key's card, and the “isn't connected” error, name their account too, so spectrum opens Settings at that row; any other refusal, such as a 403 `missing_required_header`, shows the proxy's message, with the HTTP status and error type below it (`403 missing_required_header`) and **Retry**.
|
|
148
148
|
|
|
149
149
|
**Classifiers.** The Adobe provider also registers Cloudflare's Clef classifiers as `adobe/clef` and `adobe/clef-flash` (`adobeClassifiers()`): `type: 'classifier'` models on pi-ai's `cloudflare-workers-ai-system-one` API with `baseUrl: <proxy>/v1/ai`, so `models.classify(model, { state, questions })` posts `{ model: '@cf/cloudflare/clef', input }` to the proxy's `/v1/ai/run` with the IMS token as the bearer. The provider maps its short ids to the Workers AI names, and pi parses the answers and prices the usage ($0.24 per million input tokens for Clef, $0.09 for Clef Flash, nothing for output), which counts against the same weekly budget. They are classifiers only: `models.getModels()`, the settings `models` and the picker list chat models, and `models.getAvailableOfType('classifier', 'adobe')` returns them only while an Adobe token is stored.
|
|
150
150
|
|
|
@@ -154,6 +154,8 @@ The first provider is AWS Bedrock with a Bedrock API key as the bearer token; th
|
|
|
154
154
|
|
|
155
155
|
**Blocked providers.** `needs: 'cors-free-transport'` is the static fact; `blocked: 'cors-free-transport'`, on both `ProviderOption` and the account, says the current transport can't reach that provider right now: the worker's kernel transport is unavailable, or it is the page's own fetch without a linked exit (`transportReach(client)` in `src/net.ts`, from the transport's traits). The worker follows `client.onTraits`, so a provider unblocks as soon as an extension or a linked slicc gives a CORS-free route, and blocks again when it goes, with no reload. A blocked provider is still offered and can be added; a connected one keeps its key and its row, but its models are left out of `models` and `labels`, so the picker offers none until the route is back.
|
|
156
156
|
|
|
157
|
+
**Budgets.** The settings state's `budgets` lists each subscription's weekly window as `{ provider, percent, window: 'weekly', resets }`: Adobe's from `/v1/usage`, then SuperGrok's, and `budget` is the first of them (Adobe's when both are there). SuperGrok's (`xaiUsage` in `src/supergrok.ts`, as in SLICC 6) is `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` with the xAI record's placeholder as the bearer. It's read only for an xAI sign-in (a record with an `expiresAt`, not `expired`; a pasted key has none) and only while the cors-free transport is up (`reach.free()`), since that host doesn't answer browsers. It's read at most once per five-minute poll for the same record, with an 8 s timeout. The route is undocumented, so the answer is read defensively: a body over 64 KiB, malformed JSON, or anything but a `USAGE_PERIOD_TYPE_WEEKLY` `currentPeriod` with a finite `creditUsagePercent` and a valid `end` gives no budget, and the percent is clamped to 0–100 and rounded to one decimal. The vault fills the token only where xAI's endpoints allow: until its built-in table has the billing route, it refuses the request before anything is sent, and xAI shows no budget. The `tray` and `monitor` adapters show the budget of the selected model's provider (the active agent's model), or the first budget when that provider has none, and pass its `provider` (spectrum labels `adobe` “Adobe” and `xai` “SuperGrok”). The Monitor's budget vital carries `provider` and `window` too, which spectrum's heading is built from, so its own label stays “Budget (weekly)”; its alert names the provider: “SuperGrok budget 96% used”. `test/integration/budget.test.mjs` checks the switch, the refused endpoint and an oversized answer.
|
|
158
|
+
|
|
157
159
|
**Held sign-in windows** (`src/spectrum/held.ts`, slicc-bios#772). A sign-in popup opens in the click on `about:blank#slicc-sign-in-<nonce>` (a random UUID), and the page asks seven's owner to hold it with `{ type: 'hold', marker: <nonce> }` on the `slicc-held` BroadcastChannel. The owner holds the target with that exact URL, so no CDP client can list or drive it, and answers `{ type, marker }`: `held` once it's held on every connected browser host, `none` when no host is connected (nothing can reach the popup), and `unheld` when a host is connected but the target wasn't found. The popup goes to the IdP or device page only on `held` or `none`. On `unheld` or after 6 s without an answer, the popup closes without navigating and the sign-in fails with “SLICC couldn't protect the sign-in window. Try again.”; on Cancel it never navigates either.
|
|
158
160
|
|
|
159
161
|
**Subscription sign-ins.** xAI, Kimi For Coding and ChatGPT (`openai-codex`, shown as ChatGPT) sign in with pi-ai's own device-code flows (`DEVICE_SIGN_INS` in `src/sign-ins.ts`); xAI and Kimi keep API-key entry too, so their accounts are `auth: 'oauth'` with `secret: 'api-key'` (spectrum's “Use an API key instead”) and a `keyUrl`. Spectrum's Sign in calls `connect(id, undefined, undefined, { method: 'sign-in' })` in the click, and a pasted key `put`s first and calls `connect(id, undefined, fields, { method: 'key' })` (spectrum's `ConnectOptions`); without the fourth argument an `oauth` account signs in. The adapter opens a held popup (`slicc-sign-in`, see **Held sign-in windows**) before anything else in the click's task, then asks the worker's `signIn(id)`, which adds the account, runs the provider's `auth.oauth.login` and answers `{ url, code }` once the provider gives its device code (an `https:` page only). While it waits the account is `signing-in` with `signIn: { url, code? }`, kept in the page and never in the replicated settings state; the prompt appears and the popup goes to `url` only once the popup is held, and spectrum shows `code` with Copy and a link to `url` when the browser blocked the popup. The adapter's `connect` call then waits in the worker for the token (a sign-in no `connect` claims within a minute is dropped) and hands the vault `grant { provider, name, response, settings? }`, `name` being the account's `vaultName` and `response` the token response built from pi-ai's credential, as bytes zeroed after the call; the token is never stored, logged, licked or put in the state, and pi-ai's own error texts, which can quote a token response, never leave the worker. A refusal reads “The sign-in was refused.”, a timeout, network failure or malformed answer “The sign-in didn't finish. Try again.”, the vault's `EEXIST` “This name holds another provider's key.”, and Cancel aborts at once with an `AbortError`. Signing in again is a rotation that keeps the placeholder, and over a pasted key the vault keeps that key restorable. The vault refreshes the token through `egress`; after `invalid_grant` its record is `expired` and the account offers Sign in again. ChatGPT's API reads the account id from the access token's JWT, which a placeholder isn't: the grant carries `settings: { CHATGPT_ACCOUNT_ID }` (every sign-in), the credential's env gets it from the record, and the worker hands pi-ai a carrier key `slicc.<claims>.<placeholder>` that the worker's fetch turns back into `Bearer <placeholder>` before the vault fills it, so pi-ai sends `chatgpt-account-id` itself; its turns use SSE, not WebSockets. Kimi's auth host answers browsers but its API needs the CORS-free transport; xAI's API answers browsers but its device endpoints don't, so an xAI sign-in needs the CORS-free transport while its key doesn't; ChatGPT's auth host answers browsers and its API doesn't. The flows load lazily from pi-ai: xAI's and Kimi's modules import nothing from Node, and openai-codex's imports `node:http` for its browser callback, which the device flow never runs, so slicc-bios's `node:*` stubs let it load.
|
|
@@ -162,7 +164,7 @@ The first provider is AWS Bedrock with a Bedrock API key as the bearer token; th
|
|
|
162
164
|
|
|
163
165
|
### In slicc-spectrum
|
|
164
166
|
|
|
165
|
-
`@ai-ecoverse/slicc-spectrum` is an optional peer dependency, `^1.103.0`. 1.102.0 types a guest tool approval's `args` and `argsTruncated`, and 1.103.0 shows a device sign-in's `signIn` code and link in Settings. 1.94.0 is the first release with the provider picker (`providers()`, `remove`, `vaultName`, `customBase`, a field's `options` and `value`); 1.94.1 shows a field with `options` as an `sp-picker` on its `value`; 1.95.0 shows a `blocked` provider in the picker as unpickable with “Needs the SLICC extension or CLI”; 1.96.0 registers an MCP token's server URL (`Account.url`) with the vault itself, after a “Connect to {url}?” confirm, and offers Reconnect when the URL moves; and 1.98.0 saves a field's value as its `vaultSetting` on the vault record (`VaultPort.settings`, V3). `test/integration/settings-ui.test.mjs` checks each in a real `<slicc-settings>`: Bedrock's region picker on the stored region, a blocked provider that can't be picked until a CORS-free route appears, the moved note once `mcp.json` points a connected server elsewhere, and that adding Cloudflare Workers AI or Azure (by resource name) puts the key with its tenant fields as the record's vault `settings` (`CLOUDFLARE_ACCOUNT_ID`, `AZURE_OPENAI_RESOURCE_NAME`).
|
|
167
|
+
`@ai-ecoverse/slicc-spectrum` is an optional peer dependency, `^1.103.0`. 1.102.0 types a guest tool approval's `args` and `argsTruncated`, and 1.103.0 shows a device sign-in's `signIn` code and link in Settings. 1.107.0 heads the budget with its provider: the tray's `budget.provider` and the Monitor vital's `provider` and `window` (older releases ignore them). 1.94.0 is the first release with the provider picker (`providers()`, `remove`, `vaultName`, `customBase`, a field's `options` and `value`); 1.94.1 shows a field with `options` as an `sp-picker` on its `value`; 1.95.0 shows a `blocked` provider in the picker as unpickable with “Needs the SLICC extension or CLI”; 1.96.0 registers an MCP token's server URL (`Account.url`) with the vault itself, after a “Connect to {url}?” confirm, and offers Reconnect when the URL moves; and 1.98.0 saves a field's value as its `vaultSetting` on the vault record (`VaultPort.settings`, V3). `test/integration/settings-ui.test.mjs` checks each in a real `<slicc-settings>`: Bedrock's region picker on the stored region, a blocked provider that can't be picked until a CORS-free route appears, the moved note once `mcp.json` points a connected server elsewhere, and that adding Cloudflare Workers AI or Azure (by resource name) puts the key with its tenant fields as the record's vault `settings` (`CLOUDFLARE_ACCOUNT_ID`, `AZURE_OPENAI_RESOURCE_NAME`).
|
|
166
168
|
|
|
167
169
|
`@ai-ecoverse/slicc-agent/spectrum` turns an `AgentConnection` into slicc-spectrum's `AgentPort`, so `<slicc-app>` shows the real agent:
|
|
168
170
|
|
|
@@ -613,7 +615,7 @@ pi has no git integration and no changes list; pi coding-agent leaves that to th
|
|
|
613
615
|
### The live monitor
|
|
614
616
|
|
|
615
617
|
Spectrum's Monitor panel (`MonitorPort`, opened from the Budget chip) shows the agent's task graph, its usage and the kernel's process table. Most of it comes from pi durable as is:
|
|
616
|
-
- **Tasks:** `harness.watchTaskGraph()` gives every live task with its kind, owner, conversation, state and abort mark.
|
|
618
|
+
- **Tasks:** `harness.watchTaskGraph()` gives every live task with its kind, owner, conversation, state and abort mark, a sleeping task's `until` and a blocked task's reason. The adapter shows a sleeping task with how long it still sleeps (`sleeps 30s`) and a blocked one (`blocked: restart limit`, `blocked: no definition`, …) as a warning. Generation retry backoffs sleep on pi durable 1.4, so a retrying agent's generation task sleeps until its next attempt.
|
|
617
619
|
- **Usage:** `harness.usage()` gives the session's tokens and cost. The cost is pi-ai's, recorded with each response.
|
|
618
620
|
- **Failures, retries and usage changes:** one `subscribeCommits` listener catches them, the way `watchEvents` derives `task_failed`.
|
|
619
621
|
- **Agent state:** each agent's `pi.live`, `pi.inbox` and `pi.agent` come from the views the host already mounts. `inspect()` runs only on a resync.
|
|
@@ -643,7 +645,7 @@ The process table is the worker's root client's `ps()`. Each process is named af
|
|
|
643
645
|
- **Users mode:** the human's panel is root's, so it sees every process. A cone's bound client would see only its own, by the kernel's hidepid.
|
|
644
646
|
|
|
645
647
|
**The port.** `createAgentModel()` returns a `MonitorAdapter`. Its first `snapshot` listener opens the lease (with a resync), and its last one closes it.
|
|
646
|
-
- **Vitals:** the
|
|
648
|
+
- **Vitals:** the selected provider's budget (see **Budgets**), spend (or tokens when every model is free) with its hourly rate, active agents, running tools and tasks, processes, process memory, CPU and the fullest context.
|
|
647
649
|
- **Alerts:**
|
|
648
650
|
|
|
649
651
|
| Alert | Warn | Error |
|
|
@@ -848,7 +850,7 @@ Fixes to pi stay in this repository. [`patches/patches.json`](patches/patches.js
|
|
|
848
850
|
|
|
849
851
|
`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.
|
|
850
852
|
|
|
851
|
-
**One Chord.** `@earendil-works/chord` is pinned to the version pi-durable pins (1.
|
|
853
|
+
**One Chord.** `@earendil-works/chord` is pinned to the version pi-durable pins (1.4.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 Chords 1.2 and 1.3 for the version tests only, which the check skips because it's dev-only.
|
|
852
854
|
|
|
853
855
|
**pi-ai in lockstep with the vault.** The vault fills a model key only under the endpoints of its built-in table, which it generates from its own pi-ai pin and publishes as `"piAi"` in its package.json (slicc-vault 1.12.2 and later). `npm run lint:vault-pi` (part of `npm run lint`) fails when the installed `@ai-ecoverse/slicc-vault`'s `piAi` has another minor than our `@earendil-works/pi-ai`, or when it has none (an older vault). Renovate's `patched dependencies` group carries the `pi-ai-lockstep` label, as the vault's pi-ai updates do, so the two are found and bumped together.
|
|
854
856
|
|
|
@@ -864,28 +866,10 @@ npm run lint
|
|
|
864
866
|
|
|
865
867
|
`npm test` builds `dist/`, bundles a test worker that runs the agent on pi's faux provider, and runs the integration tests in Chromium through the [slicc-shared-web harness](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness). Unit tests live in `test/unit/`, which stays out of git; the pre-commit hook requires every changed line in `src/` to be covered. No test calls a real model.
|
|
866
868
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
###
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
-
|
|
874
|
-
- **Plan:** packs the base and the pull request merged onto it with `npm pack --dry-run --ignore-scripts`. A change touches behaviour when it changes a file either side ships (`dist`, `bin`, `packages/vfs-root` and whatever `files` lists, so a shipped prompt or agent definition counts), a build input (`src/`, `bin/`, `build.mjs`, `tsconfig*.json`, `patches/`, `package.json` and its lock, `.releaserc*`, `scripts/publish.mjs`), `release.yml` or `ci.yml`, or the reviewer itself.
|
|
875
|
-
- **Not behaviour:** anything else, such as `evals/`, `test/`, `README.md`, `AGENTS.md`, `renovate.json`, `lefthook.yml` and `scripts/check-*.mjs`. It gets a green `not behaviour: <files>`.
|
|
876
|
-
- **Evidence comes by id from the job that made it:** the jobs that run the pull request's code (`pack candidate` builds it; both `suite` legs run its tests) share the run's artifact store with the jobs that don't, and code in them can reach the runner's artifact token. So each producer job hands on its artifact's `artifact-id` and `artifact-digest`, consumers download by `artifact-ids` with `digest-mismatch: error`, no upload uses `overwrite`, and the base build has its own job, `pack base`, which runs only main's code and restores no Actions cache, because the candidate job shares the run's cache scope. `tools/cert-review/artifacts.mjs` lists the run's artifacts and fails on any in `agent-tgz-` or `cert-review-` that its producer didn't upload with that id, name and digest. `same` runs it before it compares the tarballs, and it uploads its evidence before it posts, so "not behaviour (identical dist)" can't be bought with a planted artifact. `record` runs it on everything, and a failure makes the record INCOMPLETE. **Protected:** the base build, the delta, the same-dist evidence and the review's verdict and evidence. **Not protected, by design:** the candidate tarball and both suites' results (the base leg runs the pull request's tests on main's tree), because code on a runner can always lie about its own results. They're taken by exact name, at most one each.
|
|
877
|
-
- **Same dist:** a behaviour change is packed for real on both sides. If the tarballs carry the same files, entry types, modes and bytes once each side's exact version and `gitHead` are normalised (source maps without their `mappings`), and `package.json` differs only in `devDependencies`, `version` and `gitHead`, the status is `not behaviour (identical dist)` with both sha256. A change to the reviewer, `release.yml`, `ci.yml`, `.releaserc*` or `scripts/publish.mjs` always gets the full review.
|
|
878
|
-
- **When it runs:** on every same-repo pull request that isn't a draft: `opened`, `ready_for_review`, `reopened` and every push. Draft status replaces the old `merge-ok` label: **mark a pull request ready = it gets reviewed, and merges on green**; a pull request held for anything stays a draft. A draft that changes shipped behaviour is red with `draft: not reviewed, mark it ready for review` and gets no review; `converted_to_draft` cancels a running review (one concurrency group per pull request) and sets that red status, so an older result on the same head can't satisfy the check. Renovate's pull requests aren't drafts, so they're reviewed on every push like anyone's: a devDependency bump settles as `not behaviour (identical dist)` by itself, and a bump that changes what ships gets the full review. Labels other than `cert-review-approved` don't start or cancel anything.
|
|
879
|
-
- **The full review:**
|
|
880
|
-
- The integration suite runs on the merged tree and on main's tree, on `vars.CERT_RUNNER`, with no secrets in those steps. The agent's tests bundle `src/`, so they run on the tree the tarball was packed from.
|
|
881
|
-
- **The NEGATIVE:** the pull request's `test/` diff is applied to main's tree, and its integration tests must fail there for a fix (a test file that fails to load is never a NEGATIVE). Unit tests stay out of git and out of it.
|
|
882
|
-
- A Claude review on Bedrock reads only that evidence. It has read-only tools, and the key exists only in its step. It writes a verdict file.
|
|
883
|
-
- Code turns the result into the line. Missing evidence (a suite that didn't finish, a missing artifact, a review that left nothing) is `cert-ci: INCOMPLETE`, never certified on the model's judgement. The status reads `certified @ai-ecoverse/slicc-agent PR #<n> at <head>, build <v> sha256=<hex> (…; downstream: not run (slicc-bios#480))`, and a sticky comment carries the review.
|
|
884
|
-
- **Downstream:** a bios/seven boot of the candidate isn't part of it yet. It waits for slicc-bios#480, and the line says so.
|
|
885
|
-
- **The merge group:** looks up each pull request's `cert-review` on the head being merged. Every pull request needs a green one (certified, or not behaviour), or the group is red.
|
|
886
|
-
- **The self-guard:** the shared `cert-self-guard` action, pinned. It holds the reviewer's own paths: `.github/workflows/cert*.yml`, `tools/cert-review/**`, `.github/actions/cert-self-guard/**`, and `.github/runner.mjs`, which picks CI's runner and is loaded from the base. A change to them is red with `reviewer change: needs coordinator review` until the agent coordinator adds `cert-review-approved` to that head. Node's glob `**` skips dot segments, so the guard's paths list the dot forms of each reviewer directory too (`REVIEWER_GLOBS` in `lib.mjs`), and the job's `cert` step reads the guard's receipt and fails closed: a reviewer path the globs still miss (anything under `tools/cert-review/` or the guard's directory, by prefix) without the label on that head is a FAIL, and a PASS without a receipt, or with a member that lacks its `paths` or `touched` list, is INCOMPLETE. Workers never apply that label, which is a procedural rule: every agent acts as the same GitHub account. The guard refuses a bot's label, checks admin or maintain permission and binds the label to the head, so a later push removes it.
|
|
887
|
-
- **Bootstrap:** the pull request that adds this workflow can't pass its own check, because its base has no tools yet.
|
|
888
|
-
- **Releases:** after `publish`, `release / verify on npm` (light runner, no secrets) re-hashes the npm tarball against its SLSA provenance and writes one line into the GitHub release's notes, between `<!-- cert-verify -->` markers. It waits up to an hour for npm to serve the version. A version npm still doesn't serve after that is a "not served yet, re-run this job" failure, never a supply-chain alert. npm serves the attestations a little after the version, so a 404 or a missing `dist.attestations`, an `npm view` or network error while looking for them, and `npm audit signatures`' E404 on them are retried within the same hour. That's a supply-chain alert only once the version has been served for 30 min (counted from the first tarball 200) and its packument has been still for 10 min, or the hour has run out. Any other status, or attestations that don't match, fail at once.
|
|
889
|
-
- The line starts with `certified` only when every pull request it covers was certified, or not behaviour, at its merge. That's read as the newest finished `cert-review` at or before `merged_at`, so a later re-review changes nothing.
|
|
890
|
-
- Otherwise it reads `verified … (pr-certified: not all: …)`, as for pull requests merged before the check was required.
|
|
891
|
-
- Another sha for the same version is a supply-chain alert.
|
|
869
|
+
Locally the harness runs Chrome headless; with `CI` set it runs headed, as on the pipeline's runners, and `SLICC_HEADLESS=1` or `0` overrides either way.
|
|
870
|
+
|
|
871
|
+
### CI and releases
|
|
872
|
+
|
|
873
|
+
`.github/workflows/ci.yml` is the only CI and release workflow: it calls [slicc-shared-web's `pipeline.yml`](https://github.com/ai-ecoverse/slicc-shared-web#pipeline), pinned to the same version as the `@ai-ecoverse/slicc-shared-web` devDependency. A pull request runs `npm run lint` (slicc-lint plus `lint:patches`, `lint:chord` and `lint:vault-pi`), then the integration suite (`test/integration/*.test.mjs`) split over four shards on headed Chrome, then the Claude and Codex reviews on Bedrock with this repo's review prompt; Renovate's `patched dependencies` group (pi and the AWS WebSocket middleware, kept in lockstep with the vault's pi-ai) gets the reviews too, other Renovate pull requests don't. The merge queue runs lint and the integration suite. `ci / integration` and `ci / review` are the required checks.
|
|
874
|
+
|
|
875
|
+
A push to `main` runs semantic-release in the pipeline's `release` job, on a GitHub-hosted runner: it computes the version, tags the commit, creates the GitHub release and publishes to npm with provenance through npm's trusted publishing, which names `ci.yml` (`npm run trust` syncs it with fledgling). `main` only takes merges that the merge queue tested on that same commit, so the release runs no tests. Only `fix`, `feat` and `perf` commits release; `ci`, `chore`, `docs`, `test` and `refactor` don't.
|
package/dist/agent.js
CHANGED
|
@@ -22,6 +22,13 @@ async function prompt(root, text, context) {
|
|
|
22
22
|
const entry = await root.commit((tx) => tx.entry(AssistantEntry, answer), context);
|
|
23
23
|
return answerText(entry?.model?.[0]?.content);
|
|
24
24
|
}
|
|
25
|
+
const SLICC_ATTRIBUTION = { name: "SLICC", url: "https://www.sliccy.com" };
|
|
26
|
+
const IDENTITY = { attribution: SLICC_ATTRIBUTION };
|
|
27
|
+
const RESTARTS = { restartLimit: Number.POSITIVE_INFINITY };
|
|
28
|
+
function withRestarts(settings) {
|
|
29
|
+
if (typeof settings === "function") return () => ({ ...RESTARTS, ...settings() });
|
|
30
|
+
return { ...RESTARTS, ...settings };
|
|
31
|
+
}
|
|
25
32
|
async function openAgent(options) {
|
|
26
33
|
const context = options.context ?? BACKGROUND_CONTEXT;
|
|
27
34
|
const registry = options.registry ?? createRegistry();
|
|
@@ -37,7 +44,8 @@ async function openAgent(options) {
|
|
|
37
44
|
{
|
|
38
45
|
models: options.models,
|
|
39
46
|
registry,
|
|
40
|
-
|
|
47
|
+
identity: IDENTITY,
|
|
48
|
+
settings: withRestarts(options.settings),
|
|
41
49
|
...options.env ? { env: options.env } : {},
|
|
42
50
|
...options.conversationCreated ? { conversationCreated: options.conversationCreated } : {}
|
|
43
51
|
},
|
|
@@ -63,6 +71,7 @@ async function openAgent(options) {
|
|
|
63
71
|
};
|
|
64
72
|
}
|
|
65
73
|
export {
|
|
74
|
+
SLICC_ATTRIBUTION,
|
|
66
75
|
answerText,
|
|
67
76
|
openAgent
|
|
68
77
|
};
|
package/dist/agent.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/agent.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport {\n AssistantEntry,\n type Conversation,\n createRegistry,\n type EntryId,\n Harness,\n type HarnessOptions,\n MemoryStorage,\n type ModelRef,\n type Registry,\n type Storage,\n} from '@earendil-works/pi-durable';\nimport { type Agents, openAgents } from './agents.ts';\nimport type { Cones } from './cone.ts';\nimport { type GuestToolGate, guestToolGate } from './guests/guard.ts';\nimport { type GuestReviews, guestReviews } from './guests/review.ts';\nimport { type ScoopReviews, scoopReviews } from './guests/scoop.ts';\n\nexport interface AgentOptions {\n models: Models;\n model: ModelRef;\n storage?: Storage;\n registry?: Registry;\n settings?: HarnessOptions['settings'];\n env?: HarnessOptions['env'];\n conversationCreated?: HarnessOptions['conversationCreated'];\n context?: Context;\n beforeResume?: (harness: Harness, context: Context) => Promise<unknown>;\n}\n\nexport interface Agent extends Cones {\n readonly harness: Harness;\n readonly root: Conversation;\n readonly agents: Agents;\n readonly reviews: GuestReviews;\n readonly gate: GuestToolGate;\n readonly scoopReviews: ScoopReviews;\n onCone(listener: (conversation: Conversation) => void | Promise<void>): () => void;\n prompt(text: string): Promise<string>;\n close(): Promise<void>;\n}\n\ninterface Block {\n type: string;\n text?: string;\n}\n\nexport function answerText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return (content as Block[])\n .filter((block) => block.type === 'text')\n .map((block) => block.text ?? '')\n .join('');\n}\n\nasync function prompt(root: Conversation, text: string, context: Context): Promise<string> {\n const submission = await root.submit({ type: 'input', content: text }, context);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) throw new Error(`unanswered: ${reason}`);\n const entry = await root.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return answerText(entry?.model?.[0]?.content);\n}\n\nexport async function openAgent(options: AgentOptions): Promise<Agent> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const registry = options.registry ?? createRegistry();\n let opened: Harness | undefined;\n const gate = guestToolGate(() => opened);\n registry.install(gate.extension);\n const reviews = guestReviews();\n registry.install(reviews.extension);\n const scooped = scoopReviews();\n registry.install(scooped.extension);\n const harness = await Harness.open(\n options.storage ?? new MemoryStorage(),\n {\n models: options.models,\n registry,\n
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,OAIK;AACP,SAAsB,kBAAkB;AAExC,SAA6B,qBAAqB;AAClD,SAA4B,oBAAoB;AAChD,SAA4B,oBAAoB;AA+BzC,SAAS,WAAW,SAA0B;AACnD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,SAAQ,QACL,OAAO,CAAC,UAAU,MAAM,SAAS,MAAM,EACvC,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,EAC/B,KAAK,EAAE;AACZ;AAEA,eAAe,OAAO,MAAoB,MAAc,SAAmC;AACzF,QAAM,aAAa,MAAM,KAAK,OAAO,EAAE,MAAM,SAAS,SAAS,KAAK,GAAG,OAAO;AAC9E,QAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,MAAI,WAAW,OAAW,OAAM,IAAI,MAAM,eAAe,MAAM,EAAE;AACjE,QAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACjF,SAAO,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO;AAC9C;AAEA,eAAsB,UAAU,SAAuC;AACrE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,WAAW,QAAQ,YAAY,eAAe;AACpD,MAAI;AACJ,QAAM,OAAO,cAAc,MAAM,MAAM;AACvC,WAAS,QAAQ,KAAK,SAAS;AAC/B,QAAM,UAAU,aAAa;AAC7B,WAAS,QAAQ,QAAQ,SAAS;AAClC,QAAM,UAAU,aAAa;AAC7B,WAAS,QAAQ,QAAQ,SAAS;AAClC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ,WAAW,IAAI,cAAc;AAAA,IACrC;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB;AAAA,MACA,
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport {\n AssistantEntry,\n type Conversation,\n createRegistry,\n type EntryId,\n Harness,\n type HarnessOptions,\n MemoryStorage,\n type ModelRef,\n type Registry,\n type Storage,\n} from '@earendil-works/pi-durable';\nimport { type Agents, openAgents } from './agents.ts';\nimport type { Cones } from './cone.ts';\nimport { type GuestToolGate, guestToolGate } from './guests/guard.ts';\nimport { type GuestReviews, guestReviews } from './guests/review.ts';\nimport { type ScoopReviews, scoopReviews } from './guests/scoop.ts';\n\nexport interface AgentOptions {\n models: Models;\n model: ModelRef;\n storage?: Storage;\n registry?: Registry;\n settings?: HarnessOptions['settings'];\n env?: HarnessOptions['env'];\n conversationCreated?: HarnessOptions['conversationCreated'];\n context?: Context;\n beforeResume?: (harness: Harness, context: Context) => Promise<unknown>;\n}\n\nexport interface Agent extends Cones {\n readonly harness: Harness;\n readonly root: Conversation;\n readonly agents: Agents;\n readonly reviews: GuestReviews;\n readonly gate: GuestToolGate;\n readonly scoopReviews: ScoopReviews;\n onCone(listener: (conversation: Conversation) => void | Promise<void>): () => void;\n prompt(text: string): Promise<string>;\n close(): Promise<void>;\n}\n\ninterface Block {\n type: string;\n text?: string;\n}\n\nexport function answerText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return (content as Block[])\n .filter((block) => block.type === 'text')\n .map((block) => block.text ?? '')\n .join('');\n}\n\nasync function prompt(root: Conversation, text: string, context: Context): Promise<string> {\n const submission = await root.submit({ type: 'input', content: text }, context);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) throw new Error(`unanswered: ${reason}`);\n const entry = await root.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return answerText(entry?.model?.[0]?.content);\n}\n\nexport const SLICC_ATTRIBUTION = { name: 'SLICC', url: 'https://www.sliccy.com' };\nconst IDENTITY = { attribution: SLICC_ATTRIBUTION };\nconst RESTARTS = { restartLimit: Number.POSITIVE_INFINITY };\n\nfunction withRestarts(settings: HarnessOptions['settings']): HarnessOptions['settings'] {\n if (typeof settings === 'function') return () => ({ ...RESTARTS, ...settings() });\n return { ...RESTARTS, ...settings };\n}\n\nexport async function openAgent(options: AgentOptions): Promise<Agent> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const registry = options.registry ?? createRegistry();\n let opened: Harness | undefined;\n const gate = guestToolGate(() => opened);\n registry.install(gate.extension);\n const reviews = guestReviews();\n registry.install(reviews.extension);\n const scooped = scoopReviews();\n registry.install(scooped.extension);\n const harness = await Harness.open(\n options.storage ?? new MemoryStorage(),\n {\n models: options.models,\n registry,\n identity: IDENTITY,\n settings: withRestarts(options.settings),\n ...(options.env ? { env: options.env } : {}),\n ...(options.conversationCreated ? { conversationCreated: options.conversationCreated } : {}),\n },\n context\n );\n opened = harness;\n const root = await harness.root(context, { agent: { model: options.model } });\n const agents = await openAgents(harness, root, context);\n await options.beforeResume?.(harness, context);\n harness.resume();\n return {\n harness,\n root,\n agents,\n reviews,\n gate,\n scoopReviews: scooped,\n cone: () => agents.cone(),\n switchCone: (conversation, switching) => agents.switchCone(conversation, switching),\n onCone: (listener) => agents.onCone(listener),\n prompt: (text) => prompt(agents.cone(), text, context),\n close: () => harness.close(context),\n };\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,OAIK;AACP,SAAsB,kBAAkB;AAExC,SAA6B,qBAAqB;AAClD,SAA4B,oBAAoB;AAChD,SAA4B,oBAAoB;AA+BzC,SAAS,WAAW,SAA0B;AACnD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,SAAQ,QACL,OAAO,CAAC,UAAU,MAAM,SAAS,MAAM,EACvC,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,EAC/B,KAAK,EAAE;AACZ;AAEA,eAAe,OAAO,MAAoB,MAAc,SAAmC;AACzF,QAAM,aAAa,MAAM,KAAK,OAAO,EAAE,MAAM,SAAS,SAAS,KAAK,GAAG,OAAO;AAC9E,QAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,MAAI,WAAW,OAAW,OAAM,IAAI,MAAM,eAAe,MAAM,EAAE;AACjE,QAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACjF,SAAO,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO;AAC9C;AAEO,MAAM,oBAAoB,EAAE,MAAM,SAAS,KAAK,yBAAyB;AAChF,MAAM,WAAW,EAAE,aAAa,kBAAkB;AAClD,MAAM,WAAW,EAAE,cAAc,OAAO,kBAAkB;AAE1D,SAAS,aAAa,UAAkE;AACtF,MAAI,OAAO,aAAa,WAAY,QAAO,OAAO,EAAE,GAAG,UAAU,GAAG,SAAS,EAAE;AAC/E,SAAO,EAAE,GAAG,UAAU,GAAG,SAAS;AACpC;AAEA,eAAsB,UAAU,SAAuC;AACrE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,WAAW,QAAQ,YAAY,eAAe;AACpD,MAAI;AACJ,QAAM,OAAO,cAAc,MAAM,MAAM;AACvC,WAAS,QAAQ,KAAK,SAAS;AAC/B,QAAM,UAAU,aAAa;AAC7B,WAAS,QAAQ,QAAQ,SAAS;AAClC,QAAM,UAAU,aAAa;AAC7B,WAAS,QAAQ,QAAQ,SAAS;AAClC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ,WAAW,IAAI,cAAc;AAAA,IACrC;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB;AAAA,MACA,UAAU;AAAA,MACV,UAAU,aAAa,QAAQ,QAAQ;AAAA,MACvC,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,MAC1C,GAAI,QAAQ,sBAAsB,EAAE,qBAAqB,QAAQ,oBAAoB,IAAI,CAAC;AAAA,IAC5F;AAAA,IACA;AAAA,EACF;AACA,WAAS;AACT,QAAM,OAAO,MAAM,QAAQ,KAAK,SAAS,EAAE,OAAO,EAAE,OAAO,QAAQ,MAAM,EAAE,CAAC;AAC5E,QAAM,SAAS,MAAM,WAAW,SAAS,MAAM,OAAO;AACtD,QAAM,QAAQ,eAAe,SAAS,OAAO;AAC7C,UAAQ,OAAO;AACf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,MAAM,MAAM,OAAO,KAAK;AAAA,IACxB,YAAY,CAAC,cAAc,cAAc,OAAO,WAAW,cAAc,SAAS;AAAA,IAClF,QAAQ,CAAC,aAAa,OAAO,OAAO,QAAQ;AAAA,IAC5C,QAAQ,CAAC,SAAS,OAAO,OAAO,KAAK,GAAG,MAAM,OAAO;AAAA,IACrD,OAAO,MAAM,QAAQ,MAAM,OAAO;AAAA,EACpC;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/agents.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineDoc
|
|
3
3
|
} from "@earendil-works/pi-durable";
|
|
4
|
+
import { deltasOnly } from "./checkpoints.js";
|
|
4
5
|
import { ConeDoc } from "./cone.js";
|
|
5
6
|
const FIRST_CONE = "cone";
|
|
6
7
|
function live(record) {
|
|
@@ -8,6 +9,7 @@ function live(record) {
|
|
|
8
9
|
}
|
|
9
10
|
const AgentsDoc = defineDoc({
|
|
10
11
|
kind: "slicc.agents",
|
|
12
|
+
checkpointWhen: deltasOnly,
|
|
11
13
|
version: 1,
|
|
12
14
|
scope: "session",
|
|
13
15
|
initial: () => ({ cones: {}, active: FIRST_CONE, next: 2, scoops: {} })
|
package/dist/agents.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/agents.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n type ConversationId,\n defineDoc,\n type Harness,\n type Tx,\n} from '@earendil-works/pi-durable';\nimport { ConeDoc } from './cone.ts';\nimport type { RoleMemory } from './memory/store.ts';\n\nexport const FIRST_CONE = 'cone';\n\nexport type ConeRecord = { name: string; conversation: number; title?: string; titled?: number };\n\nexport type Mark = { in: number; at: number };\n\nexport type ScoopRecord = {\n name: string;\n folder: string;\n cone: string;\n conversation: number;\n anchor: number;\n role: string | null;\n memory?: RoleMemory;\n origin?: string;\n context?: { project: boolean; global: boolean };\n kind: 'async' | 'sync';\n parent: string;\n depth: number;\n roots: { write: string[]; read: string[] } | null;\n created: Mark | null;\n dropped: Mark | null;\n gone: boolean;\n frozen?: string;\n removed?: boolean;\n thawedAs?: string;\n title?: string;\n titled?: number;\n};\n\nexport type FrozenRecord = {\n cone: string;\n name: string;\n conversation: number;\n title: string;\n model: string;\n messages: number;\n frozenAt: number;\n scoops: string[];\n v6?: { origin: string; key: string; suffix?: string };\n};\n\nexport type V6Import = {\n at: number;\n origin: string;\n cones: Record<string, string>;\n tails: Record<string, string>;\n};\n\nexport function live(record: ScoopRecord): boolean {\n return record.dropped === null && !record.gone && !record.frozen;\n}\n\nexport type AgentsState = {\n cones: Record<string, ConeRecord>;\n active: string;\n next: number;\n scoops: Record<string, ScoopRecord>;\n frozen?: Record<string, FrozenRecord>;\n frozenNext?: number;\n imports?: Record<string, V6Import>;\n};\n\nexport const AgentsDoc = defineDoc<AgentsState>({\n kind: 'slicc.agents',\n version: 1,\n scope: 'session',\n initial: () => ({ cones: {}, active: FIRST_CONE, next: 2, scoops: {} }),\n});\n\nexport const SCOOP_PREFIX = 'scoop:';\n\nexport function scoopId(folder: string): string {\n return `${SCOOP_PREFIX}${folder}`;\n}\n\nexport function slug(name: string): string {\n const cleaned = name\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/^[-._]+|[-._]+$/g, '')\n .slice(0, 48);\n return cleaned || 'scoop';\n}\n\nexport function freeFolder(state: Pick<AgentsState, 'scoops'>, name: string): string {\n const base = slug(name);\n if (!state.scoops[scoopId(base)]) return base;\n for (let n = 2; ; n++) if (!state.scoops[scoopId(`${base}-${n}`)]) return `${base}-${n}`;\n}\n\nexport interface Agents {\n state(): Readonly<AgentsState>;\n cone(): Conversation;\n activeCone(): string;\n conversation(agentId: string, context: Context): Promise<Conversation | undefined>;\n selectCone(id: string, context: Context): Promise<void>;\n createCone(name: string, context: Context): Promise<string>;\n switchCone(conversation: Conversation, context: Context): Promise<void>;\n setConversation(coneId: string, conversation: Conversation, context: Context): Promise<void>;\n update<T>(change: (tx: Tx, state: AgentsState) => T | Promise<T>, context: Context): Promise<T>;\n onChange(listener: (state: Readonly<AgentsState>) => void): () => void;\n onCone(listener: (conversation: Conversation) => void | Promise<void>): () => void;\n}\n\nasync function initial(harness: Harness, root: Conversation, context: Context) {\n return harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n if (!doc.cones[FIRST_CONE]) {\n const legacy = (await tx.doc(ConeDoc)).conversation;\n doc.cones[FIRST_CONE] = { name: 'sliccy', conversation: legacy ?? root.id };\n }\n if (!doc.cones[doc.active]) doc.active = FIRST_CONE;\n return JSON.parse(JSON.stringify(doc)) as AgentsState;\n }, context);\n}\n\nexport async function openAgents(\n harness: Harness,\n root: Conversation,\n context: Context\n): Promise<Agents> {\n let state = await initial(harness, root, context);\n const handles = new Map<number, Conversation>([[root.id, root]]);\n const handle = async (id: number, using: Context) => {\n const known = handles.get(id);\n if (known) return known;\n const found = await harness.conversation(id as ConversationId, using);\n if (found) handles.set(id, found);\n return found;\n };\n let cone =\n (await handle((state.cones[state.active] as ConeRecord).conversation, context)) ?? root;\n const changes = new Set<(state: Readonly<AgentsState>) => void>();\n const cones = new Set<(conversation: Conversation) => void | Promise<void>>();\n const notifyCone = async (next: Conversation) => {\n cone = next;\n await Promise.all([...cones].map((listener) => listener(next)));\n };\n const update: Agents['update'] = async (change, using) => {\n const [result, next] = await harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n const value = await change(tx, doc as AgentsState);\n return [value, JSON.parse(JSON.stringify(doc)) as AgentsState] as const;\n }, using);\n state = next;\n for (const listener of changes) listener(state);\n return result;\n };\n const agents: Agents = {\n state: () => state,\n cone: () => cone,\n activeCone: () => state.active,\n async conversation(agentId, using) {\n const id = state.cones[agentId]?.conversation ?? state.scoops[agentId]?.conversation;\n return id === undefined ? undefined : handle(id, using);\n },\n async selectCone(id, using) {\n const record = state.cones[id];\n if (!record) throw new Error(`no cone ${id}`);\n if (state.active === id) return;\n await update((_tx, doc) => {\n doc.active = id;\n }, using);\n await notifyCone((await handle(record.conversation, using)) as Conversation);\n },\n async createCone(name, using) {\n const wanted = name.trim();\n if (!wanted) throw new Error('A cone needs a name.');\n if (Object.values(state.cones).some((record) => record.name === wanted))\n throw new Error(`A cone named ${wanted} already exists.`);\n const conversation = await harness.createConversation(\n {\n ownership: { kind: 'ownerless' },\n agent: { model: (await cone.agent(using)).model ?? null },\n },\n using\n );\n handles.set(conversation.id, conversation);\n return update((_tx, doc) => {\n const id = `cone-${doc.next}`;\n doc.next += 1;\n doc.cones[id] = { name: wanted, conversation: conversation.id };\n return id;\n }, using);\n },\n switchCone(conversation, using) {\n return agents.setConversation(state.active, conversation, using);\n },\n async setConversation(coneId, conversation, using) {\n handles.set(conversation.id, conversation);\n await update(async (tx, doc) => {\n const cone = doc.cones[coneId] as ConeRecord;\n cone.conversation = conversation.id;\n delete cone.title;\n delete cone.titled;\n if (coneId === doc.active) (await tx.doc(ConeDoc)).conversation = conversation.id;\n }, using);\n if (coneId === state.active) await notifyCone(conversation);\n },\n update,\n onChange(listener) {\n changes.add(listener);\n return () => changes.delete(listener);\n },\n onCone(listener) {\n cones.add(listener);\n return () => cones.delete(listener);\n },\n };\n return agents;\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n type ConversationId,\n defineDoc,\n type Harness,\n type Tx,\n} from '@earendil-works/pi-durable';\nimport { deltasOnly } from './checkpoints.ts';\nimport { ConeDoc } from './cone.ts';\nimport type { RoleMemory } from './memory/store.ts';\n\nexport const FIRST_CONE = 'cone';\n\nexport type ConeRecord = { name: string; conversation: number; title?: string; titled?: number };\n\nexport type Mark = { in: number; at: number };\n\nexport type ScoopRecord = {\n name: string;\n folder: string;\n cone: string;\n conversation: number;\n anchor: number;\n role: string | null;\n memory?: RoleMemory;\n origin?: string;\n context?: { project: boolean; global: boolean };\n kind: 'async' | 'sync';\n parent: string;\n depth: number;\n roots: { write: string[]; read: string[] } | null;\n created: Mark | null;\n dropped: Mark | null;\n gone: boolean;\n frozen?: string;\n removed?: boolean;\n thawedAs?: string;\n title?: string;\n titled?: number;\n};\n\nexport type FrozenRecord = {\n cone: string;\n name: string;\n conversation: number;\n title: string;\n model: string;\n messages: number;\n frozenAt: number;\n scoops: string[];\n v6?: { origin: string; key: string; suffix?: string };\n};\n\nexport type V6Import = {\n at: number;\n origin: string;\n cones: Record<string, string>;\n tails: Record<string, string>;\n};\n\nexport function live(record: ScoopRecord): boolean {\n return record.dropped === null && !record.gone && !record.frozen;\n}\n\nexport type AgentsState = {\n cones: Record<string, ConeRecord>;\n active: string;\n next: number;\n scoops: Record<string, ScoopRecord>;\n frozen?: Record<string, FrozenRecord>;\n frozenNext?: number;\n imports?: Record<string, V6Import>;\n};\n\nexport const AgentsDoc = defineDoc<AgentsState>({\n kind: 'slicc.agents',\n checkpointWhen: deltasOnly,\n version: 1,\n scope: 'session',\n initial: () => ({ cones: {}, active: FIRST_CONE, next: 2, scoops: {} }),\n});\n\nexport const SCOOP_PREFIX = 'scoop:';\n\nexport function scoopId(folder: string): string {\n return `${SCOOP_PREFIX}${folder}`;\n}\n\nexport function slug(name: string): string {\n const cleaned = name\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/^[-._]+|[-._]+$/g, '')\n .slice(0, 48);\n return cleaned || 'scoop';\n}\n\nexport function freeFolder(state: Pick<AgentsState, 'scoops'>, name: string): string {\n const base = slug(name);\n if (!state.scoops[scoopId(base)]) return base;\n for (let n = 2; ; n++) if (!state.scoops[scoopId(`${base}-${n}`)]) return `${base}-${n}`;\n}\n\nexport interface Agents {\n state(): Readonly<AgentsState>;\n cone(): Conversation;\n activeCone(): string;\n conversation(agentId: string, context: Context): Promise<Conversation | undefined>;\n selectCone(id: string, context: Context): Promise<void>;\n createCone(name: string, context: Context): Promise<string>;\n switchCone(conversation: Conversation, context: Context): Promise<void>;\n setConversation(coneId: string, conversation: Conversation, context: Context): Promise<void>;\n update<T>(change: (tx: Tx, state: AgentsState) => T | Promise<T>, context: Context): Promise<T>;\n onChange(listener: (state: Readonly<AgentsState>) => void): () => void;\n onCone(listener: (conversation: Conversation) => void | Promise<void>): () => void;\n}\n\nasync function initial(harness: Harness, root: Conversation, context: Context) {\n return harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n if (!doc.cones[FIRST_CONE]) {\n const legacy = (await tx.doc(ConeDoc)).conversation;\n doc.cones[FIRST_CONE] = { name: 'sliccy', conversation: legacy ?? root.id };\n }\n if (!doc.cones[doc.active]) doc.active = FIRST_CONE;\n return JSON.parse(JSON.stringify(doc)) as AgentsState;\n }, context);\n}\n\nexport async function openAgents(\n harness: Harness,\n root: Conversation,\n context: Context\n): Promise<Agents> {\n let state = await initial(harness, root, context);\n const handles = new Map<number, Conversation>([[root.id, root]]);\n const handle = async (id: number, using: Context) => {\n const known = handles.get(id);\n if (known) return known;\n const found = await harness.conversation(id as ConversationId, using);\n if (found) handles.set(id, found);\n return found;\n };\n let cone =\n (await handle((state.cones[state.active] as ConeRecord).conversation, context)) ?? root;\n const changes = new Set<(state: Readonly<AgentsState>) => void>();\n const cones = new Set<(conversation: Conversation) => void | Promise<void>>();\n const notifyCone = async (next: Conversation) => {\n cone = next;\n await Promise.all([...cones].map((listener) => listener(next)));\n };\n const update: Agents['update'] = async (change, using) => {\n const [result, next] = await harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n const value = await change(tx, doc as AgentsState);\n return [value, JSON.parse(JSON.stringify(doc)) as AgentsState] as const;\n }, using);\n state = next;\n for (const listener of changes) listener(state);\n return result;\n };\n const agents: Agents = {\n state: () => state,\n cone: () => cone,\n activeCone: () => state.active,\n async conversation(agentId, using) {\n const id = state.cones[agentId]?.conversation ?? state.scoops[agentId]?.conversation;\n return id === undefined ? undefined : handle(id, using);\n },\n async selectCone(id, using) {\n const record = state.cones[id];\n if (!record) throw new Error(`no cone ${id}`);\n if (state.active === id) return;\n await update((_tx, doc) => {\n doc.active = id;\n }, using);\n await notifyCone((await handle(record.conversation, using)) as Conversation);\n },\n async createCone(name, using) {\n const wanted = name.trim();\n if (!wanted) throw new Error('A cone needs a name.');\n if (Object.values(state.cones).some((record) => record.name === wanted))\n throw new Error(`A cone named ${wanted} already exists.`);\n const conversation = await harness.createConversation(\n {\n ownership: { kind: 'ownerless' },\n agent: { model: (await cone.agent(using)).model ?? null },\n },\n using\n );\n handles.set(conversation.id, conversation);\n return update((_tx, doc) => {\n const id = `cone-${doc.next}`;\n doc.next += 1;\n doc.cones[id] = { name: wanted, conversation: conversation.id };\n return id;\n }, using);\n },\n switchCone(conversation, using) {\n return agents.setConversation(state.active, conversation, using);\n },\n async setConversation(coneId, conversation, using) {\n handles.set(conversation.id, conversation);\n await update(async (tx, doc) => {\n const cone = doc.cones[coneId] as ConeRecord;\n cone.conversation = conversation.id;\n delete cone.title;\n delete cone.titled;\n if (coneId === doc.active) (await tx.doc(ConeDoc)).conversation = conversation.id;\n }, using);\n if (coneId === state.active) await notifyCone(conversation);\n },\n update,\n onChange(listener) {\n changes.add(listener);\n return () => changes.delete(listener);\n },\n onCone(listener) {\n cones.add(listener);\n return () => cones.delete(listener);\n },\n };\n return agents;\n}\n"],
|
|
5
|
+
"mappings": "AACA;AAAA,EAGE;AAAA,OAGK;AACP,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AAGjB,MAAM,aAAa;AAiDnB,SAAS,KAAK,QAA8B;AACjD,SAAO,OAAO,YAAY,QAAQ,CAAC,OAAO,QAAQ,CAAC,OAAO;AAC5D;AAYO,MAAM,YAAY,UAAuB;AAAA,EAC9C,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,OAAO,CAAC,GAAG,QAAQ,YAAY,MAAM,GAAG,QAAQ,CAAC,EAAE;AACvE,CAAC;AAEM,MAAM,eAAe;AAErB,SAAS,QAAQ,QAAwB;AAC9C,SAAO,GAAG,YAAY,GAAG,MAAM;AACjC;AAEO,SAAS,KAAK,MAAsB;AACzC,QAAM,UAAU,KACb,YAAY,EACZ,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,oBAAoB,EAAE,EAC9B,MAAM,GAAG,EAAE;AACd,SAAO,WAAW;AACpB;AAEO,SAAS,WAAW,OAAoC,MAAsB;AACnF,QAAM,OAAO,KAAK,IAAI;AACtB,MAAI,CAAC,MAAM,OAAO,QAAQ,IAAI,CAAC,EAAG,QAAO;AACzC,WAAS,IAAI,KAAK,IAAK,KAAI,CAAC,MAAM,OAAO,QAAQ,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,EAAG,QAAO,GAAG,IAAI,IAAI,CAAC;AACxF;AAgBA,eAAe,QAAQ,SAAkB,MAAoB,SAAkB;AAC7E,SAAO,QAAQ,OAAO,OAAO,OAAO;AAClC,UAAM,MAAM,MAAM,GAAG,IAAI,SAAS;AAClC,QAAI,CAAC,IAAI,MAAM,UAAU,GAAG;AAC1B,YAAM,UAAU,MAAM,GAAG,IAAI,OAAO,GAAG;AACvC,UAAI,MAAM,UAAU,IAAI,EAAE,MAAM,UAAU,cAAc,UAAU,KAAK,GAAG;AAAA,IAC5E;AACA,QAAI,CAAC,IAAI,MAAM,IAAI,MAAM,EAAG,KAAI,SAAS;AACzC,WAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AAAA,EACvC,GAAG,OAAO;AACZ;AAEA,eAAsB,WACpB,SACA,MACA,SACiB;AACjB,MAAI,QAAQ,MAAM,QAAQ,SAAS,MAAM,OAAO;AAChD,QAAM,UAAU,oBAAI,IAA0B,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAC/D,QAAM,SAAS,OAAO,IAAY,UAAmB;AACnD,UAAM,QAAQ,QAAQ,IAAI,EAAE;AAC5B,QAAI,MAAO,QAAO;AAClB,UAAM,QAAQ,MAAM,QAAQ,aAAa,IAAsB,KAAK;AACpE,QAAI,MAAO,SAAQ,IAAI,IAAI,KAAK;AAChC,WAAO;AAAA,EACT;AACA,MAAI,OACD,MAAM,OAAQ,MAAM,MAAM,MAAM,MAAM,EAAiB,cAAc,OAAO,KAAM;AACrF,QAAM,UAAU,oBAAI,IAA4C;AAChE,QAAM,QAAQ,oBAAI,IAA0D;AAC5E,QAAM,aAAa,OAAO,SAAuB;AAC/C,WAAO;AACP,UAAM,QAAQ,IAAI,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,aAAa,SAAS,IAAI,CAAC,CAAC;AAAA,EAChE;AACA,QAAM,SAA2B,OAAO,QAAQ,UAAU;AACxD,UAAM,CAAC,QAAQ,IAAI,IAAI,MAAM,QAAQ,OAAO,OAAO,OAAO;AACxD,YAAM,MAAM,MAAM,GAAG,IAAI,SAAS;AAClC,YAAM,QAAQ,MAAM,OAAO,IAAI,GAAkB;AACjD,aAAO,CAAC,OAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC,CAAgB;AAAA,IAC/D,GAAG,KAAK;AACR,YAAQ;AACR,eAAW,YAAY,QAAS,UAAS,KAAK;AAC9C,WAAO;AAAA,EACT;AACA,QAAM,SAAiB;AAAA,IACrB,OAAO,MAAM;AAAA,IACb,MAAM,MAAM;AAAA,IACZ,YAAY,MAAM,MAAM;AAAA,IACxB,MAAM,aAAa,SAAS,OAAO;AACjC,YAAM,KAAK,MAAM,MAAM,OAAO,GAAG,gBAAgB,MAAM,OAAO,OAAO,GAAG;AACxE,aAAO,OAAO,SAAY,SAAY,OAAO,IAAI,KAAK;AAAA,IACxD;AAAA,IACA,MAAM,WAAW,IAAI,OAAO;AAC1B,YAAM,SAAS,MAAM,MAAM,EAAE;AAC7B,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,WAAW,EAAE,EAAE;AAC5C,UAAI,MAAM,WAAW,GAAI;AACzB,YAAM,OAAO,CAAC,KAAK,QAAQ;AACzB,YAAI,SAAS;AAAA,MACf,GAAG,KAAK;AACR,YAAM,WAAY,MAAM,OAAO,OAAO,cAAc,KAAK,CAAkB;AAAA,IAC7E;AAAA,IACA,MAAM,WAAW,MAAM,OAAO;AAC5B,YAAM,SAAS,KAAK,KAAK;AACzB,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,sBAAsB;AACnD,UAAI,OAAO,OAAO,MAAM,KAAK,EAAE,KAAK,CAAC,WAAW,OAAO,SAAS,MAAM;AACpE,cAAM,IAAI,MAAM,gBAAgB,MAAM,kBAAkB;AAC1D,YAAM,eAAe,MAAM,QAAQ;AAAA,QACjC;AAAA,UACE,WAAW,EAAE,MAAM,YAAY;AAAA,UAC/B,OAAO,EAAE,QAAQ,MAAM,KAAK,MAAM,KAAK,GAAG,SAAS,KAAK;AAAA,QAC1D;AAAA,QACA;AAAA,MACF;AACA,cAAQ,IAAI,aAAa,IAAI,YAAY;AACzC,aAAO,OAAO,CAAC,KAAK,QAAQ;AAC1B,cAAM,KAAK,QAAQ,IAAI,IAAI;AAC3B,YAAI,QAAQ;AACZ,YAAI,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,cAAc,aAAa,GAAG;AAC9D,eAAO;AAAA,MACT,GAAG,KAAK;AAAA,IACV;AAAA,IACA,WAAW,cAAc,OAAO;AAC9B,aAAO,OAAO,gBAAgB,MAAM,QAAQ,cAAc,KAAK;AAAA,IACjE;AAAA,IACA,MAAM,gBAAgB,QAAQ,cAAc,OAAO;AACjD,cAAQ,IAAI,aAAa,IAAI,YAAY;AACzC,YAAM,OAAO,OAAO,IAAI,QAAQ;AAC9B,cAAMA,QAAO,IAAI,MAAM,MAAM;AAC7B,QAAAA,MAAK,eAAe,aAAa;AACjC,eAAOA,MAAK;AACZ,eAAOA,MAAK;AACZ,YAAI,WAAW,IAAI,OAAQ,EAAC,MAAM,GAAG,IAAI,OAAO,GAAG,eAAe,aAAa;AAAA,MACjF,GAAG,KAAK;AACR,UAAI,WAAW,MAAM,OAAQ,OAAM,WAAW,YAAY;AAAA,IAC5D;AAAA,IACA;AAAA,IACA,SAAS,UAAU;AACjB,cAAQ,IAAI,QAAQ;AACpB,aAAO,MAAM,QAAQ,OAAO,QAAQ;AAAA,IACtC;AAAA,IACA,OAAO,UAAU;AACf,YAAM,IAAI,QAAQ;AAClB,aAAO,MAAM,MAAM,OAAO,QAAQ;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
6
|
"names": ["cone"]
|
|
7
7
|
}
|
package/dist/approvals/policy.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
} from "@earendil-works/chord";
|
|
4
4
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
5
5
|
import { defineDoc } from "@earendil-works/pi-durable";
|
|
6
|
+
import { deltasOnly } from "../checkpoints.js";
|
|
6
7
|
import {
|
|
7
8
|
changedGuests,
|
|
8
9
|
DEFAULT_GUEST_POLICY
|
|
@@ -13,6 +14,7 @@ const DEFAULT_APPROVAL_POLICY = {
|
|
|
13
14
|
};
|
|
14
15
|
const ApprovalPolicyDoc = defineDoc({
|
|
15
16
|
kind: "slicc.approval-policy",
|
|
17
|
+
checkpointWhen: deltasOnly,
|
|
16
18
|
version: 1,
|
|
17
19
|
scope: "session",
|
|
18
20
|
initial: () => ({ overrides: {} })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/policy.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type Context,\n type JsonValue,\n type ReplicatedState,\n replicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { defineDoc, type Harness, type JsonObject } from '@earendil-works/pi-durable';\nimport {\n changedGuests,\n DEFAULT_GUEST_POLICY,\n type GuestPolicy,\n type GuestPolicyChange,\n} from '../guests/policy.ts';\nimport type { ApprovalKind } from './record.ts';\n\nexport type SudoPolicy = { approver: 'user' | 'agent' };\n\nexport type ApprovalPolicy = { guests: GuestPolicy; sudo: SudoPolicy };\n\nexport type ApprovalPolicyChange = { guests?: GuestPolicyChange; sudo?: Partial<SudoPolicy> };\n\nexport type ApprovalPolicySet =\n | { policy: ApprovalPolicy; error: null }\n | { policy: null; error: string };\n\nexport const DEFAULT_APPROVAL_POLICY: ApprovalPolicy = {\n guests: DEFAULT_GUEST_POLICY,\n sudo: { approver: 'user' },\n};\n\ntype Overrides = Partial<Record<keyof ApprovalPolicy, JsonObject>>;\n\nexport const ApprovalPolicyDoc = defineDoc<{ overrides: Overrides }>({\n kind: 'slicc.approval-policy',\n version: 1,\n scope: 'session',\n initial: () => ({ overrides: {} }),\n});\n\nconst copy = <T>(value: T): T => JSON.parse(JSON.stringify(value));\n\nfunction changedSudo(next: ApprovalPolicy, change: unknown): string | null {\n if (!change || typeof change !== 'object' || Array.isArray(change))\n return 'The sudo policy is an object of policy entries.';\n for (const [name, value] of Object.entries(change)) {\n if (name !== 'approver') return `There is no sudo policy entry ${name}.`;\n if (value !== 'user' && value !== 'agent')\n return 'A sudo request is approved by user or agent.';\n next.sudo.approver = value;\n }\n return null;\n}\n\nexport function changedPolicy(base: ApprovalPolicy, change: unknown): ApprovalPolicySet {\n if (!change || typeof change !== 'object' || Array.isArray(change))\n return { policy: null, error: 'An approval policy change is an object of sections.' };\n const next = copy(base);\n for (const [name, value] of Object.entries(change)) {\n const error =\n name === 'guests'\n ? changedGuests(next.guests, value)\n : name === 'sudo'\n ? changedSudo(next, value)\n : `There is no approval policy section ${name}.`;\n if (error) return { policy: null, error };\n }\n return { policy: next, error: null };\n}\n\nexport function resolvedPolicy(overrides: Overrides): ApprovalPolicy {\n const guests = (overrides.guests ?? {}) as Partial<GuestPolicy>;\n return copy({\n guests: {\n ...DEFAULT_GUEST_POLICY,\n ...guests,\n always: { ...DEFAULT_GUEST_POLICY.always, ...guests.always },\n },\n sudo: { ...DEFAULT_APPROVAL_POLICY.sudo, ...overrides.sudo },\n });\n}\n\nexport function overridden(\n overrides: Overrides,\n change: Record<string, Record<string, unknown>>,\n policy: ApprovalPolicy\n): Overrides {\n const next = copy(overrides);\n for (const [section, entries] of Object.entries(change)) {\n const key = section as keyof ApprovalPolicy;\n const into = next[key] ?? {};\n const resolved = policy[key] as unknown as JsonObject;\n for (const name of Object.keys(entries))\n into[name] = (\n name === 'always'\n ? { ...(into.always as object), ...(entries.always as object) }\n : resolved[name]\n ) as JsonValue;\n next[key] = into;\n }\n return next;\n}\n\nexport interface ApprovalPolicies {\n readonly state: ReplicatedState<ApprovalPolicy>;\n current(): ApprovalPolicy;\n set(change: unknown, context: Context): Promise<ApprovalPolicySet>;\n}\n\nexport const alwaysFor =\n (policy: Pick<ApprovalPolicies, 'current'>) =>\n (kind: ApprovalKind): boolean => {\n const { always } = policy.current().guests;\n return kind === 'guest-tool' ? always.tools : always.messages;\n };\n\nexport async function attachApprovalPolicy(\n harness: Pick<Harness, 'commit'>,\n context: Context = BACKGROUND_CONTEXT\n): Promise<ApprovalPolicies> {\n let overrides = await harness.commit(\n async (tx) => copy((await tx.doc(ApprovalPolicyDoc)).overrides ?? {}),\n context\n );\n const state = replicatedState<ApprovalPolicy>(resolvedPolicy(overrides));\n let line: Promise<unknown> = Promise.resolve();\n return {\n state,\n current: () => copy(state.value as ApprovalPolicy),\n set(change, using) {\n const run = line.then(async () => {\n const made = changedPolicy(state.value as ApprovalPolicy, change);\n if (!made.policy) return made;\n const next = overridden(\n overrides,\n change as Record<string, Record<string, unknown>>,\n made.policy\n );\n await harness.commit(async (tx) => {\n (await tx.doc(ApprovalPolicyDoc)).overrides = copy(next);\n }, using);\n overrides = next;\n const policy = resolvedPolicy(next);\n state.replace(using, copy(policy));\n return { policy: copy(policy), error: null };\n });\n line = run.catch(() => undefined);\n return run;\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import {\n type Context,\n type JsonValue,\n type ReplicatedState,\n replicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { defineDoc, type Harness, type JsonObject } from '@earendil-works/pi-durable';\nimport { deltasOnly } from '../checkpoints.ts';\nimport {\n changedGuests,\n DEFAULT_GUEST_POLICY,\n type GuestPolicy,\n type GuestPolicyChange,\n} from '../guests/policy.ts';\nimport type { ApprovalKind } from './record.ts';\n\nexport type SudoPolicy = { approver: 'user' | 'agent' };\n\nexport type ApprovalPolicy = { guests: GuestPolicy; sudo: SudoPolicy };\n\nexport type ApprovalPolicyChange = { guests?: GuestPolicyChange; sudo?: Partial<SudoPolicy> };\n\nexport type ApprovalPolicySet =\n | { policy: ApprovalPolicy; error: null }\n | { policy: null; error: string };\n\nexport const DEFAULT_APPROVAL_POLICY: ApprovalPolicy = {\n guests: DEFAULT_GUEST_POLICY,\n sudo: { approver: 'user' },\n};\n\ntype Overrides = Partial<Record<keyof ApprovalPolicy, JsonObject>>;\n\nexport const ApprovalPolicyDoc = defineDoc<{ overrides: Overrides }>({\n kind: 'slicc.approval-policy',\n checkpointWhen: deltasOnly,\n version: 1,\n scope: 'session',\n initial: () => ({ overrides: {} }),\n});\n\nconst copy = <T>(value: T): T => JSON.parse(JSON.stringify(value));\n\nfunction changedSudo(next: ApprovalPolicy, change: unknown): string | null {\n if (!change || typeof change !== 'object' || Array.isArray(change))\n return 'The sudo policy is an object of policy entries.';\n for (const [name, value] of Object.entries(change)) {\n if (name !== 'approver') return `There is no sudo policy entry ${name}.`;\n if (value !== 'user' && value !== 'agent')\n return 'A sudo request is approved by user or agent.';\n next.sudo.approver = value;\n }\n return null;\n}\n\nexport function changedPolicy(base: ApprovalPolicy, change: unknown): ApprovalPolicySet {\n if (!change || typeof change !== 'object' || Array.isArray(change))\n return { policy: null, error: 'An approval policy change is an object of sections.' };\n const next = copy(base);\n for (const [name, value] of Object.entries(change)) {\n const error =\n name === 'guests'\n ? changedGuests(next.guests, value)\n : name === 'sudo'\n ? changedSudo(next, value)\n : `There is no approval policy section ${name}.`;\n if (error) return { policy: null, error };\n }\n return { policy: next, error: null };\n}\n\nexport function resolvedPolicy(overrides: Overrides): ApprovalPolicy {\n const guests = (overrides.guests ?? {}) as Partial<GuestPolicy>;\n return copy({\n guests: {\n ...DEFAULT_GUEST_POLICY,\n ...guests,\n always: { ...DEFAULT_GUEST_POLICY.always, ...guests.always },\n },\n sudo: { ...DEFAULT_APPROVAL_POLICY.sudo, ...overrides.sudo },\n });\n}\n\nexport function overridden(\n overrides: Overrides,\n change: Record<string, Record<string, unknown>>,\n policy: ApprovalPolicy\n): Overrides {\n const next = copy(overrides);\n for (const [section, entries] of Object.entries(change)) {\n const key = section as keyof ApprovalPolicy;\n const into = next[key] ?? {};\n const resolved = policy[key] as unknown as JsonObject;\n for (const name of Object.keys(entries))\n into[name] = (\n name === 'always'\n ? { ...(into.always as object), ...(entries.always as object) }\n : resolved[name]\n ) as JsonValue;\n next[key] = into;\n }\n return next;\n}\n\nexport interface ApprovalPolicies {\n readonly state: ReplicatedState<ApprovalPolicy>;\n current(): ApprovalPolicy;\n set(change: unknown, context: Context): Promise<ApprovalPolicySet>;\n}\n\nexport const alwaysFor =\n (policy: Pick<ApprovalPolicies, 'current'>) =>\n (kind: ApprovalKind): boolean => {\n const { always } = policy.current().guests;\n return kind === 'guest-tool' ? always.tools : always.messages;\n };\n\nexport async function attachApprovalPolicy(\n harness: Pick<Harness, 'commit'>,\n context: Context = BACKGROUND_CONTEXT\n): Promise<ApprovalPolicies> {\n let overrides = await harness.commit(\n async (tx) => copy((await tx.doc(ApprovalPolicyDoc)).overrides ?? {}),\n context\n );\n const state = replicatedState<ApprovalPolicy>(resolvedPolicy(overrides));\n let line: Promise<unknown> = Promise.resolve();\n return {\n state,\n current: () => copy(state.value as ApprovalPolicy),\n set(change, using) {\n const run = line.then(async () => {\n const made = changedPolicy(state.value as ApprovalPolicy, change);\n if (!made.policy) return made;\n const next = overridden(\n overrides,\n change as Record<string, Record<string, unknown>>,\n made.policy\n );\n await harness.commit(async (tx) => {\n (await tx.doc(ApprovalPolicyDoc)).overrides = copy(next);\n }, using);\n overrides = next;\n const policy = resolvedPolicy(next);\n state.replace(using, copy(policy));\n return { policy: copy(policy), error: null };\n });\n line = run.catch(() => undefined);\n return run;\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/approvals/record.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/record.ts"],
|
|
4
|
-
"sourcesContent": ["import { defineDoc } from '@earendil-works/pi-durable';\n\nexport type ApprovalKind = 'guest-message' | 'guest-tool' | 'sudo';\n\nexport type ApprovalState = 'pending' | 'approved' | 'rejected' | 'unanswered' | 'withdrawn';\n\nexport type ApprovalTier = 'user' | 'agent' | 'cone' | `scoop:${string}`;\n\nexport type ApprovalAnswer = 'once' | 'always' | 'deny';\n\nexport type DecidedBy = 'owner' | 'follower' | 'judge' | 'host' | `scoop:${string}`;\n\nexport type ApprovalRequester = {\n kind: 'guest' | 'agent' | 'user';\n name: string;\n seat?: string;\n agentId?: string;\n where?: string;\n device?: string;\n};\n\nexport type ApprovalVerdict = {\n decision: 'allow' | 'deny' | 'unsure';\n reason: string;\n risk?: number;\n aligned?: number;\n model?: string;\n};\n\nexport type ApprovalAttachment = { name: string; mimeType: string; size: number };\n\nexport type GuestMessageDetail = { text: string; attachments: ApprovalAttachment[] };\n\nexport type GuestToolDetail = { tool: string; summary: string; arguments?: string; task?: number };\n\nexport type SudoDetail = {\n command: string;\n target?: string;\n cwd?: string;\n alwaysScope?: 'cwd';\n};\n\ntype Base = {\n id: string;\n seat?: string;\n agentId: string;\n requester: ApprovalRequester;\n tier: string;\n state: ApprovalState;\n decidedBy: DecidedBy | null;\n device?: string;\n answer?: 'once' | 'always';\n reviewer?: string;\n reason?: string;\n verdict?: ApprovalVerdict;\n always: boolean;\n createdAt: number;\n expiresAt: number;\n decidedAt?: number;\n};\n\nexport type GuestMessageApproval = Base & { kind: 'guest-message'; detail: GuestMessageDetail };\nexport type GuestToolApproval = Base & { kind: 'guest-tool'; detail: GuestToolDetail };\nexport type SudoApproval = Base & { kind: 'sudo'; detail: SudoDetail };\n\nexport type Approval = GuestMessageApproval | GuestToolApproval | SudoApproval;\n\ntype DraftOf<T extends Approval> = Pick<T, 'kind' | 'detail' | 'agentId' | 'requester'> & {\n seat?: string;\n tier: string;\n always?: boolean;\n expiresAt?: number;\n};\n\nexport type ApprovalDraft =\n | DraftOf<GuestMessageApproval>\n | DraftOf<GuestToolApproval>\n | DraftOf<SudoApproval>;\n\nexport type ApprovalsState = { items: Approval[] };\n\nexport const ApprovalsDoc = defineDoc<ApprovalsState>({\n kind: 'slicc.approvals',\n version: 1,\n scope: 'session',\n initial: () => ({ items: [] }),\n});\n\nexport const APPROVAL_MS = 300_000;\nexport const APPROVALS_KEPT = 100;\nexport const MAX_DELAY = 2_147_483_647;\nexport const REASON_LENGTH = 500;\n\nexport const REASONS = {\n timeout: 'timeout',\n unknownTier: 'unknown-tier',\n unroutable: 'unroutable',\n unknownAnswer: 'unknown-answer',\n restart: 'restart',\n revoked: 'revoked',\n aborted: 'aborted',\n disconnected: 'disconnected',\n noScoop: 'no-scoop',\n scoopStopped: 'scoop-stopped',\n scoopAsked: 'scoop-asked',\n scoopUndecided: 'scoop-undecided',\n} as const;\n\nexport const guestKind = (kind: ApprovalKind): boolean => kind !== 'sudo';\n\nexport const scoopTier = (tier: unknown): tier is `scoop:${string}` =>\n typeof tier === 'string' && /^scoop:\\S+$/.test(tier);\n\nexport function knownTier(tier: unknown): tier is ApprovalTier {\n if (tier === 'user' || tier === 'agent' || tier === 'cone') return true;\n return scoopTier(tier);\n}\n\nexport function routable(kind: ApprovalKind, tier: ApprovalTier): boolean {\n return !(kind === 'guest-tool' && tier === 'cone');\n}\n\nexport function answerOf(value: unknown): ApprovalAnswer | null {\n return value === 'once' || value === 'always' || value === 'deny' ? value : null;\n}\n\nexport const personal = (by: DecidedBy): boolean => by === 'owner' || by === 'follower';\n\nexport function answerer(by: DecidedBy, item: Pick<Approval, 'tier' | 'reviewer'>): boolean {\n if (personal(by)) return item.tier === 'user';\n if (by === 'judge') return item.tier === 'agent';\n if (scoopTier(by)) return scoopTier(item.tier) && item.reviewer === by;\n return true;\n}\n\nexport type SudoApprover = 'user' | 'agent';\n\nexport const sudoApprover = (value: unknown): SudoApprover =>\n value === 'agent' ? 'agent' : 'user';\n\nexport const tierOf = (kind: ApprovalKind, tier: string, sudo: SudoApprover): string =>\n kind === 'sudo' ? sudo : tier;\n\nexport function gone(state: ApprovalState): string {\n if (state === 'unanswered') return 'This request timed out.';\n if (state === 'withdrawn') return 'This request was withdrawn.';\n return 'Someone else already answered this.';\n}\n\nexport function cleanReason(reason: unknown): string | undefined {\n if (typeof reason !== 'string') return undefined;\n const text = reason.trim().slice(0, REASON_LENGTH);\n return text || undefined;\n}\n\nconst decided = (item: Approval) => item.decidedAt ?? item.createdAt;\n\nexport function prune(items: Approval[], kept = APPROVALS_KEPT): Approval[] {\n const settled = items.filter((item) => item.state !== 'pending');\n settled.sort((a, b) => decided(a) - decided(b));\n const drop = new Set(settled.slice(0, Math.max(0, settled.length - kept)));\n return items.filter((item) => !drop.has(item));\n}\n\nexport type Settle = { state: Exclude<ApprovalState, 'pending'>; by: DecidedBy; reason?: string };\n\nexport type Ruling = { error?: string; settle?: Settle; answer?: 'once' | 'always' };\n\nconst why = (reason: string | undefined) => (reason ? { reason } : {});\n\nexport function ruled(\n item: Approval,\n answer: unknown,\n reason: unknown,\n by: DecidedBy,\n at: number\n): Ruling {\n if (item.state !== 'pending') return { error: gone(item.state) };\n if (item.expiresAt <= at)\n return {\n error: gone('unanswered'),\n settle: { state: 'unanswered', by: 'host', reason: REASONS.timeout },\n };\n if (!answerer(by, item)) return { error: \"This request isn't yours to answer.\" };\n const known = answerOf(answer);\n if (known === 'always' && scoopTier(by)) return { error: 'A scoop answers once or deny.' };\n if (known === 'always' && !item.always)\n return { error: \"Always isn't offered for this request.\" };\n if (!known) return { settle: { state: 'rejected', by, reason: REASONS.unknownAnswer } };\n const given = why(cleanReason(reason));\n if (known === 'deny') return { settle: { state: 'rejected', by, ...given } };\n return { settle: { state: 'approved', by, ...given }, answer: known };\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import { defineDoc } from '@earendil-works/pi-durable';\nimport { deltasOnly } from '../checkpoints.ts';\n\nexport type ApprovalKind = 'guest-message' | 'guest-tool' | 'sudo';\n\nexport type ApprovalState = 'pending' | 'approved' | 'rejected' | 'unanswered' | 'withdrawn';\n\nexport type ApprovalTier = 'user' | 'agent' | 'cone' | `scoop:${string}`;\n\nexport type ApprovalAnswer = 'once' | 'always' | 'deny';\n\nexport type DecidedBy = 'owner' | 'follower' | 'judge' | 'host' | `scoop:${string}`;\n\nexport type ApprovalRequester = {\n kind: 'guest' | 'agent' | 'user';\n name: string;\n seat?: string;\n agentId?: string;\n where?: string;\n device?: string;\n};\n\nexport type ApprovalVerdict = {\n decision: 'allow' | 'deny' | 'unsure';\n reason: string;\n risk?: number;\n aligned?: number;\n model?: string;\n};\n\nexport type ApprovalAttachment = { name: string; mimeType: string; size: number };\n\nexport type GuestMessageDetail = { text: string; attachments: ApprovalAttachment[] };\n\nexport type GuestToolDetail = { tool: string; summary: string; arguments?: string; task?: number };\n\nexport type SudoDetail = {\n command: string;\n target?: string;\n cwd?: string;\n alwaysScope?: 'cwd';\n};\n\ntype Base = {\n id: string;\n seat?: string;\n agentId: string;\n requester: ApprovalRequester;\n tier: string;\n state: ApprovalState;\n decidedBy: DecidedBy | null;\n device?: string;\n answer?: 'once' | 'always';\n reviewer?: string;\n reason?: string;\n verdict?: ApprovalVerdict;\n always: boolean;\n createdAt: number;\n expiresAt: number;\n decidedAt?: number;\n};\n\nexport type GuestMessageApproval = Base & { kind: 'guest-message'; detail: GuestMessageDetail };\nexport type GuestToolApproval = Base & { kind: 'guest-tool'; detail: GuestToolDetail };\nexport type SudoApproval = Base & { kind: 'sudo'; detail: SudoDetail };\n\nexport type Approval = GuestMessageApproval | GuestToolApproval | SudoApproval;\n\ntype DraftOf<T extends Approval> = Pick<T, 'kind' | 'detail' | 'agentId' | 'requester'> & {\n seat?: string;\n tier: string;\n always?: boolean;\n expiresAt?: number;\n};\n\nexport type ApprovalDraft =\n | DraftOf<GuestMessageApproval>\n | DraftOf<GuestToolApproval>\n | DraftOf<SudoApproval>;\n\nexport type ApprovalsState = { items: Approval[] };\n\nexport const ApprovalsDoc = defineDoc<ApprovalsState>({\n kind: 'slicc.approvals',\n checkpointWhen: deltasOnly,\n version: 1,\n scope: 'session',\n initial: () => ({ items: [] }),\n});\n\nexport const APPROVAL_MS = 300_000;\nexport const APPROVALS_KEPT = 100;\nexport const MAX_DELAY = 2_147_483_647;\nexport const REASON_LENGTH = 500;\n\nexport const REASONS = {\n timeout: 'timeout',\n unknownTier: 'unknown-tier',\n unroutable: 'unroutable',\n unknownAnswer: 'unknown-answer',\n restart: 'restart',\n revoked: 'revoked',\n aborted: 'aborted',\n disconnected: 'disconnected',\n noScoop: 'no-scoop',\n scoopStopped: 'scoop-stopped',\n scoopAsked: 'scoop-asked',\n scoopUndecided: 'scoop-undecided',\n} as const;\n\nexport const guestKind = (kind: ApprovalKind): boolean => kind !== 'sudo';\n\nexport const scoopTier = (tier: unknown): tier is `scoop:${string}` =>\n typeof tier === 'string' && /^scoop:\\S+$/.test(tier);\n\nexport function knownTier(tier: unknown): tier is ApprovalTier {\n if (tier === 'user' || tier === 'agent' || tier === 'cone') return true;\n return scoopTier(tier);\n}\n\nexport function routable(kind: ApprovalKind, tier: ApprovalTier): boolean {\n return !(kind === 'guest-tool' && tier === 'cone');\n}\n\nexport function answerOf(value: unknown): ApprovalAnswer | null {\n return value === 'once' || value === 'always' || value === 'deny' ? value : null;\n}\n\nexport const personal = (by: DecidedBy): boolean => by === 'owner' || by === 'follower';\n\nexport function answerer(by: DecidedBy, item: Pick<Approval, 'tier' | 'reviewer'>): boolean {\n if (personal(by)) return item.tier === 'user';\n if (by === 'judge') return item.tier === 'agent';\n if (scoopTier(by)) return scoopTier(item.tier) && item.reviewer === by;\n return true;\n}\n\nexport type SudoApprover = 'user' | 'agent';\n\nexport const sudoApprover = (value: unknown): SudoApprover =>\n value === 'agent' ? 'agent' : 'user';\n\nexport const tierOf = (kind: ApprovalKind, tier: string, sudo: SudoApprover): string =>\n kind === 'sudo' ? sudo : tier;\n\nexport function gone(state: ApprovalState): string {\n if (state === 'unanswered') return 'This request timed out.';\n if (state === 'withdrawn') return 'This request was withdrawn.';\n return 'Someone else already answered this.';\n}\n\nexport function cleanReason(reason: unknown): string | undefined {\n if (typeof reason !== 'string') return undefined;\n const text = reason.trim().slice(0, REASON_LENGTH);\n return text || undefined;\n}\n\nconst decided = (item: Approval) => item.decidedAt ?? item.createdAt;\n\nexport function prune(items: Approval[], kept = APPROVALS_KEPT): Approval[] {\n const settled = items.filter((item) => item.state !== 'pending');\n settled.sort((a, b) => decided(a) - decided(b));\n const drop = new Set(settled.slice(0, Math.max(0, settled.length - kept)));\n return items.filter((item) => !drop.has(item));\n}\n\nexport type Settle = { state: Exclude<ApprovalState, 'pending'>; by: DecidedBy; reason?: string };\n\nexport type Ruling = { error?: string; settle?: Settle; answer?: 'once' | 'always' };\n\nconst why = (reason: string | undefined) => (reason ? { reason } : {});\n\nexport function ruled(\n item: Approval,\n answer: unknown,\n reason: unknown,\n by: DecidedBy,\n at: number\n): Ruling {\n if (item.state !== 'pending') return { error: gone(item.state) };\n if (item.expiresAt <= at)\n return {\n error: gone('unanswered'),\n settle: { state: 'unanswered', by: 'host', reason: REASONS.timeout },\n };\n if (!answerer(by, item)) return { error: \"This request isn't yours to answer.\" };\n const known = answerOf(answer);\n if (known === 'always' && scoopTier(by)) return { error: 'A scoop answers once or deny.' };\n if (known === 'always' && !item.always)\n return { error: \"Always isn't offered for this request.\" };\n if (!known) return { settle: { state: 'rejected', by, reason: REASONS.unknownAnswer } };\n const given = why(cleanReason(reason));\n if (known === 'deny') return { settle: { state: 'rejected', by, ...given } };\n return { settle: { state: 'approved', by, ...given }, answer: known };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/codemode/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
defineExtension,
|
|
12
12
|
defineTool
|
|
13
13
|
} from "@earendil-works/pi-durable";
|
|
14
|
+
import { deltasOnly } from "../checkpoints.js";
|
|
14
15
|
import { scrubItems } from "../secrets/scrub.js";
|
|
15
16
|
const CODEMODE = "codemode";
|
|
16
17
|
const DEFAULT_MAX_OUTPUT_TOKENS = 1e4;
|
|
@@ -18,6 +19,7 @@ const MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;
|
|
|
18
19
|
const CHARS_PER_TOKEN = 4;
|
|
19
20
|
const CodemodeDoc = defineDoc({
|
|
20
21
|
kind: "slicc.codemode",
|
|
22
|
+
checkpointWhen: deltasOnly,
|
|
21
23
|
version: 1,
|
|
22
24
|
scope: "conversation",
|
|
23
25
|
history: "rewindable",
|