@ai-ecoverse/slicc-agent 5.8.3 → 5.10.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 +18 -11
- package/bin/agent +3 -1
- package/dist/agent.js +4 -0
- package/dist/agent.js.map +2 -2
- package/dist/approvals/index.js +14 -0
- package/dist/approvals/index.js.map +2 -2
- package/dist/approvals/record.js +14 -6
- package/dist/approvals/record.js.map +2 -2
- package/dist/gelatiere/index.js +16 -19
- package/dist/gelatiere/index.js.map +2 -2
- package/dist/guests/client.js +0 -2
- package/dist/guests/client.js.map +2 -2
- package/dist/guests/guard.js +19 -5
- package/dist/guests/guard.js.map +2 -2
- package/dist/guests/index.js +1 -2
- package/dist/guests/index.js.map +2 -2
- package/dist/guests/policy.js +2 -4
- package/dist/guests/policy.js.map +2 -2
- package/dist/guests/scoop.js +268 -0
- package/dist/guests/scoop.js.map +7 -0
- package/dist/host.js +13 -4
- package/dist/host.js.map +2 -2
- package/dist/kernel/agent-users.js +11 -0
- package/dist/kernel/agent-users.js.map +2 -2
- package/dist/licks/boot.js +10 -1
- package/dist/licks/boot.js.map +2 -2
- package/dist/licks/config.js +0 -37
- package/dist/licks/config.js.map +3 -3
- package/dist/licks/crontab.js +141 -0
- package/dist/licks/crontab.js.map +7 -0
- package/dist/licks/extension.js +4 -69
- package/dist/licks/extension.js.map +2 -2
- package/dist/licks/index.js +3 -5
- package/dist/licks/index.js.map +2 -2
- package/dist/licks/sources.js +42 -14
- package/dist/licks/sources.js.map +2 -2
- package/dist/main.js +9 -5
- package/dist/main.js.map +2 -2
- package/dist/scoops/cli.js +47 -0
- package/dist/scoops/cli.js.map +2 -2
- package/dist/scoops/index.js +7 -5
- package/dist/scoops/index.js.map +2 -2
- package/dist/scoops/requests.js +22 -7
- package/dist/scoops/requests.js.map +2 -2
- package/dist/spectrum/monitor-port.js +0 -1
- package/dist/spectrum/monitor-port.js.map +2 -2
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/approvals/index.d.ts +5 -0
- package/dist/types/approvals/record.d.ts +8 -2
- package/dist/types/gelatiere/index.d.ts +2 -1
- package/dist/types/guests/guard.d.ts +8 -2
- package/dist/types/guests/policy.d.ts +0 -1
- package/dist/types/guests/scoop.d.ts +45 -0
- package/dist/types/host.d.ts +3 -0
- package/dist/types/kernel/agent-users.d.ts +2 -0
- package/dist/types/licks/boot.d.ts +12 -1
- package/dist/types/licks/config.d.ts +0 -7
- package/dist/types/licks/crontab.d.ts +37 -0
- package/dist/types/licks/extension.d.ts +1 -17
- package/dist/types/licks/index.d.ts +1 -1
- package/dist/types/licks/sources.d.ts +1 -3
- package/dist/types/main.d.ts +1 -0
- package/dist/types/scoops/cli.d.ts +2 -0
- package/dist/types/scoops/requests.d.ts +5 -1
- package/package.json +6 -6
- package/packages/vfs-root/skills/gelatiere/SKILL.md +2 -2
- package/packages/vfs-root/skills/licks/SKILL.md +16 -12
- package/dist/licks/cron.js +0 -120
- package/dist/licks/cron.js.map +0 -7
- package/dist/licks/schedules.js +0 -50
- package/dist/licks/schedules.js.map +0 -7
- package/dist/types/licks/cron.d.ts +0 -16
- package/dist/types/licks/schedules.d.ts +0 -25
package/README.md
CHANGED
|
@@ -100,9 +100,9 @@ With users, each cone and each scoop is a kernel user, and works as that user:
|
|
|
100
100
|
| skills for every agent | `/usr/share/skills/` (root, `0755`), as without users |
|
|
101
101
|
| an agent's own skills | `~/.pi/agent/skills` and `~/.agents/skills` in its home; only that agent sees them |
|
|
102
102
|
| memory | `/root/.pi/agent/memory/` and `/root/.pi/agent/agent-memory/` (root; agents use `memory_write` and `memory`) |
|
|
103
|
-
| licks | each cone's `~/.slicc`, whose
|
|
103
|
+
| licks | each cone's `~/.slicc`, whose `cone` target is that cone; root's `/root/.slicc`, whose `cone` is the active cone. Schedules are each user's kernel crontab: a cone's, a scoop's own, and root's |
|
|
104
104
|
| sprinkles | each cone's `~/sprinkles`, every cone's in the rail, with the id `<cone>/<name>` (the cone's id, such as `cone-2`) and the cone as `agentId`; `panel open sprinkle:<cone>/<name>` opens one, and spectrum scopes the rest by cone. welcome and suggestions keep their plain ids. spectrum ≥ 1.56.0 groups the rail by cone, with the selected cone first; an older spectrum lists them ungrouped. A sprinkle's files are read as its owner, inside the owner's home |
|
|
105
|
-
| the gelatiere | its procedure
|
|
105
|
+
| the gelatiere | its procedure in the first cone's home, written as that cone, and its nightly line in the gelatiere scoop's own crontab; `gelatiere install` and `suggest` run as the caller |
|
|
106
106
|
| scoop reports | the cone's `~/.slicc/reports/<handle>/` |
|
|
107
107
|
| sync transcripts | `/tmp/agent-sessions/<user>/` (`0700`), or `~/sessions/` with `--persist-session` |
|
|
108
108
|
| the suggestions store and catalog cache, the welcome flag and profile | `/var/lib/slicc/agent/shared/` (root, `0700`). The built-in welcome and suggestions sprinkles read it as `/shared/…`, through the worker and only inside that folder; a user's sprinkle reads `/shared/…` as its owner's home |
|
|
@@ -201,21 +201,28 @@ gone /home/notes/old.md
|
|
|
201
201
|
|
|
202
202
|
**Stop.** pi's `Conversation.abort()` withdraws every queued steer and follow-up, so a lick that wasn't placed yet is withdrawn with them, and the events merged behind it are dropped too: Stop leaves the cone quiet. Events that arrive after Stop are delivered as usual, and an idle cone starts a run for them.
|
|
203
203
|
|
|
204
|
-
**Configuration** lives in files under `~/.slicc` (`/home/.slicc`), which the user and the agent edit with any tool. The worker watches the folder and reconciles on every change. Each malformed line or file becomes one lick naming the file, the line and the problem, once per distinct problem, with `severity="error"`; the entry is ignored until it's fixed. Names are 1–64 of `a-z 0-9 . _ -`, starting with a letter or digit. A target is `cone` (the active cone) or `scoop:<handle>`; a lick for a scoop that is stopped or gone goes to the cone, and webhooks always go to the cone. Files starting with `.` or ending in `~` are skipped; any other file that isn't `<name>.json` in `watches/` or `webhooks/` is an error.
|
|
204
|
+
**Configuration** of watches and webhooks lives in files under `~/.slicc` (`/home/.slicc`), which the user and the agent edit with any tool. The worker watches the folder and reconciles on every change. Each malformed line or file becomes one lick naming the file, the line and the problem, once per distinct problem, with `severity="error"`; the entry is ignored until it's fixed. Names are 1–64 of `a-z 0-9 . _ -`, starting with a letter or digit. A target is `cone` (the active cone) or `scoop:<handle>`; a lick for a scoop that is stopped or gone goes to the cone, and webhooks always go to the cone. Files starting with `.` or ending in `~` are skipped; any other file that isn't `<name>.json` in `watches/` or `webhooks/` is an error.
|
|
205
205
|
|
|
206
|
-
- `~/.slicc/crontab`, one job per line: `<schedule> <name> [<target>] [<message>]`. The schedule is five fields (minute, hour, day of month, month, day of week, with `*`, ranges, steps, lists and `jan`–`dec`/`sun`–`sat` names; 7 is Sunday, and a job whose day of month and day of week are both set runs on either) or `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`. Times are the browser's local time. Blank lines and `#` comments are ignored.
|
|
207
|
-
```
|
|
208
|
-
*/15 9-17 * * mon-fri standup Summarize what changed in ~/notes since the last standup
|
|
209
|
-
@daily backup scoop:ops
|
|
210
|
-
```
|
|
211
206
|
- `~/.slicc/watches/<name>.json`: `{ "path": "~/notes", "glob": "**/*.md", "target": "cone", "message": "…", "debounce": 500 }`. `path` is absolute or starts with `~/`; `glob` (default `**`) is matched against paths relative to it, with `*`, `**`, `?`, `[…]` and `{a,b}`. `debounce` is 100–60000 ms. `node_modules` and `.git` are never scanned.
|
|
212
207
|
- `~/.slicc/webhooks/<name>.json`: `{ "target": "cone", "message": "…" }`. The URL comes from the tray hub (milestone E); until then, `AgentControl.webhook(name, { id, headers, body })` is how a delivery arrives.
|
|
213
208
|
|
|
209
|
+
**Schedules** are the kernel's crontab (slicc-kernel ≥ 1.110.0), not the agent's: each user's table in `/var/spool/cron/crontabs/<user>`, installed with `crontab -` and listed with `crontab -l`. The kernel runs each due job as its user, with `$SHELL -c` in `$HOME`, and a job that runs `agent send` comes back as a cron lick:
|
|
210
|
+
```
|
|
211
|
+
# name: standup
|
|
212
|
+
*/15 9-17 * * mon-fri agent send cone "Summarize what changed in ~/notes since the last standup"
|
|
213
|
+
# name: backup
|
|
214
|
+
@daily agent send scoop:ops
|
|
215
|
+
```
|
|
216
|
+
- **`agent send` under `SLICC_CRON=1`** (the kernel sets it for every job, with `SLICC_CRON_JOB`, `SLICC_CRON_DUE` and `SLICC_CRON_MISSED`) delivers `<lick channel="cron">` instead of a message: source and title are the job's `# name:` (the kernel's hash of the line without one), the text says when it was due (`standup fired at 2026-10-17 11:00`), the message is the body, and `count` is 1 plus the missed runs (`standup was due at 2026-10-17 14:00; 2 more fires were missed while seven was closed`). Its id is `<job>@<due>`, so a run is delivered once. The `agent` script passes the four variables in its request (protocol `slicc-agent/2`; `slicc-agent/1` requests still parse).
|
|
217
|
+
- **Which agent:** a job's environment is a login's, so `SLICC_AGENT` and the page's variables don't reach it. The `agent` script needs only `$USER`: it writes to `/var/lib/slicc/agent/requests/$USER/`, the folder of that kernel user, so the job's user decides who it is. In a cone's crontab, `agent send cone` wakes that cone and `agent send <handle>` one of its scoops (a scoop the cone doesn't have falls back to the cone); in root's, `cone` is the active cone; a scoop's own crontab can only wake the scoop itself. A frozen cone's job is skipped with the usual `warn` lick.
|
|
218
|
+
- **Missed runs, overlap, several tabs and logs** are the kernel's: missed runs fold into one, at most once per due time, one tab per storage root fires, and each run's output goes to `/var/log/cron/<user>/<name>.log`.
|
|
219
|
+
- **Moving `~/.slicc/crontab`:** the agent no longer schedules anything. At start and whenever the file changes, each `~/.slicc/crontab` (`<schedule> <name> [<target>] [<message>]` per line) is moved into the kernel crontab: its jobs become `# name: <name>` and `<schedule> agent send <target> '<message>'`, replacing jobs of the same names and keeping the rest of the table. A line aimed at a live scoop goes into that scoop's own user's crontab; the others go into the crontab of the file's owner (the cone's user, or root's for `/root/.slicc` and without kernel users). Then the file is kept as `~/.slicc/crontab.migrated` and removed. A line that doesn't parse, or a table `crontab` refuses (it exits 1 and keeps the old table), becomes a `cron` lick with `severity="error"` naming the line and the problem, and the file stays until it's fixed. Durable `slicc.cron` tasks from earlier versions are aborted at start.
|
|
220
|
+
|
|
214
221
|
**Triage.** A watch or webhook file can add `"triage": "<which events the agent should see>"`, a sentence of 1–1000 characters such as `"pushes to main that touch src/"`. Then each of its events (a debounced batch of changes, or a webhook request) first goes to Clef Flash (`adobe/clef-flash`, see Classifiers) with the channel, source, title, text, body (cut at 4000 characters) and up to 50 paths. One `choice` question asks whether the event matches that sentence: `act` or `ignore`. When the answer is `ignore` with a probability of at least 0.75 (`TRIAGE_SKIP`), the lick isn't delivered and starts no turn. Instead, a passive `slicc.lick-triage` entry goes into the target's transcript. It has no model message, so neither the model nor the chat sees it, and it records the would-be lick with the triage sentence, the decision, Clef's confidence and probabilities, the model and the cost. An event whose delivery id was already seen isn't triaged again. A skipped one is remembered only after its record is written, under a `requestId` taken from the delivery id, so a crash in between neither loses the record nor writes it twice. Triage fails open: without an Adobe sign-in, or when the proxy errors, times out (15 s) or returns any other answer, the lick is delivered as usual. Watch notices (stopped, overflow), config errors, frozen notices, crontab jobs and boot licks are never triaged. A small webhook event is about 220 input tokens, which is $0.00002 against the weekly budget; a 4000-character body is about $0.0001.
|
|
215
222
|
|
|
216
223
|
**Sources.**
|
|
217
224
|
- `fswatch`: the kernel environment's `watch()` reports changed paths; those matching the glob are collected for `debounce` ms and delivered as one lick with `changed` or `gone` per path. A watch that overflows or stops says so in a lick (a stopped one with `severity="error"`, an overflow with `severity="warn"`), and saving its file restarts it. Changes made while no worker runs aren't reported. **The agent's own changes are dropped**, so it never licks itself: the kernel environment records every path its file tools write, edit, move or remove, and the time its `bash` commands run; a change to such a path within 2 s, or any change while an agent command runs or within 2 s after it, counts as the agent's. A change the user makes while an agent command runs is therefore dropped too. Background processes the agent started (`&`) aren't tracked, so their changes do lick.
|
|
218
|
-
- `cron`:
|
|
225
|
+
- `cron`: `agent send` from a kernel cron job (see Schedules).
|
|
219
226
|
- `webhook`: each request is its own lick, with the headers and the body; a delivery `id` makes retries idempotent.
|
|
220
227
|
- `upgrade`: when the worker starts with a different slicc-agent version than the session recorded, with the release notes link. Every change of version is its own lick, also back and forth.
|
|
221
228
|
- `session-reload`: when the kernel's boot time (`btime` in `/proc/stat`) differs from the last worker start and the cone ran `bash` since then. The page reloaded, so the kernel's processes are gone; the lick lists background commands (`&`, `nohup`, `setsid`, `disown`) that are no longer running. Durable itself resumes an interrupted run.
|
|
@@ -378,7 +385,7 @@ The `memory` section, read before every request, holds the first 200 lines (at m
|
|
|
378
385
|
**Extraction.** Durable compacts a long conversation into a summary; before it does, a `beforeCompact` hook (which never declines or replaces the summary) schedules a `slicc.memory-extract` task for the cone. A cone that has been idle for `memory.idleMinutes` (30 by default, `0` turns the idle trigger off) gets the same task. The task runs only when there is something new since the last extraction (a cursor per conversation in the session document `slicc.memory-extract`), makes one model call, with the cone's model unless `memory.extractModel` names a cheaper one (`provider/model`), and writes the proposed entries through `memory_write`'s writer into the cone's memory file, within its 16 KB. The transcript it sends is redacted first, its prompt forbids secrets, and the writer redacts again. `"memory": { "extract": false }` in `~/.pi/agent/settings.json` turns extraction off. v6's curator scoop, three-way merge, curation ledger and dreamer aren't ported.
|
|
379
386
|
|
|
380
387
|
**The gelatiere.** SLICC's resident advisor, as in v6, is a scoop with the built-in `gelatiere` role. That role has its own memory (`memory: { scope: user, path: gelatiere }`), so it keeps its notes there.
|
|
381
|
-
- `gelatiere init` starts the scoop in the first cone, writes `~/.pi/agent/GELATIERE.md` (its procedure, which the user can edit) unless it exists, and
|
|
388
|
+
- `gelatiere init` starts the scoop in the first cone, writes `~/.pi/agent/GELATIERE.md` (its procedure, which the user can edit) unless it exists, and installs one job `# name: gelatiere` / `0 3 * * * agent send scoop:<handle> 'Nightly pass: …'` in the scoop's own crontab (root's without kernel users), replacing an older one. `gelatiere status` shows its schedule from there.
|
|
382
389
|
- **Procedure updates:** at every boot, an existing GELATIERE.md that is a shipped version (its sha-256 is in `SHIPPED`, or matches `GELATIERE.md.sha256`, written whenever the agent writes the file) is replaced by the built-in one. A file with the user's edits is kept, the new built-in one goes to `~/.pi/agent/GELATIERE.new.md`, and `gelatiere status` says so.
|
|
383
390
|
- On each pass it reads memory, the installed skills, the roles and the lick files, and writes at most three suggestions as JSON. `gelatiere suggest <file>` validates them and folds them into `/home/.gelatiere/suggestions.json` by id:
|
|
384
391
|
- kinds `skill`, `use-case`, `tip`, `skill-idea` and `issue`;
|
|
@@ -700,7 +707,7 @@ The agent never holds a secret's value. Values live in the vault (`vault.sliccy.
|
|
|
700
707
|
|
|
701
708
|
## The kernel
|
|
702
709
|
|
|
703
|
-
`@ai-ecoverse/slicc-kernel` is a peer dependency (`>=1.
|
|
710
|
+
`@ai-ecoverse/slicc-kernel` is a peer dependency (`>=1.110.0 <2`; until 3.31.0 the floor was 1.26.3, then 1.47.1, then 1.81.0, then 1.90.0): the agent only calls `attachKernel`, the client end of the page kernel's port, so it must match the kernel the page runs. Seven chooses that version: slicc-bios pins the certified kernel next to the agent in `src/packages/agent/package.json`, and pnpm fills the peer from that pin. The floor is 1.110.0, the first kernel with its own cron and `crontab`, which the agent's schedules need; it is ahead of the certified kernel, so slicc-bios must pin 1.110.0 or later with this agent. The kernel and the `wasm-*` commands are devDependencies, pinned to certified versions except the kernel (1.110.0, for its cron), and CI runs the unit and integration tests on them in one leg. If the port protocol ever breaks within 1.x, a patch release narrows the upper bound. Renovate opens PRs for the kernel, `wasi-pnpm`, the `wasm-*` packages and `slicc-cdp` but never merges them, because they move only after the homescoop coordinator certifies them; such a PR is merged once the cert lane has certified its version.
|
|
704
711
|
|
|
705
712
|
## Patched dependencies
|
|
706
713
|
|
package/bin/agent
CHANGED
|
@@ -6,13 +6,15 @@ case "$as" in subagent|memory|gelatiere|freezer|models|panel) ;; *) as=agent ;;
|
|
|
6
6
|
[ -d "$dir/in" ] && [ -d "$dir/out" ] || mkdir -p "$dir/in" "$dir/out" 2>/dev/null
|
|
7
7
|
who="${SLICC_AGENT:-}"
|
|
8
8
|
if [ -z "$who" ] && [ -n "${SLICC_TAB:-}" ]; then who="tab:$SLICC_TAB"; fi
|
|
9
|
+
cron=""
|
|
10
|
+
if [ "${SLICC_CRON:-}" = 1 ]; then cron="${SLICC_CRON_JOB:-} ${SLICC_CRON_DUE:-} ${SLICC_CRON_MISSED:-0}"; fi
|
|
9
11
|
|
|
10
12
|
submit() {
|
|
11
13
|
local id="$1"
|
|
12
14
|
shift
|
|
13
15
|
local tmp="$dir/in/.tmp-$id"
|
|
14
16
|
{
|
|
15
|
-
printf '%s\0' "slicc-agent/
|
|
17
|
+
printf '%s\0' "slicc-agent/2" "$as" "$who" "$PWD" "$$" "$cron" "$#"
|
|
16
18
|
if [ $# -gt 0 ]; then printf '%s\0' "$@"; fi
|
|
17
19
|
if [ "$stdin" = 1 ]; then cat; fi
|
|
18
20
|
} > "$tmp" && mv "$tmp" "$dir/in/$id"
|
package/dist/agent.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { openAgents } from "./agents.js";
|
|
9
9
|
import { guestToolGate } from "./guests/guard.js";
|
|
10
10
|
import { guestReviews } from "./guests/review.js";
|
|
11
|
+
import { scoopReviews } from "./guests/scoop.js";
|
|
11
12
|
function answerText(content) {
|
|
12
13
|
if (typeof content === "string") return content;
|
|
13
14
|
if (!Array.isArray(content)) return "";
|
|
@@ -29,6 +30,8 @@ async function openAgent(options) {
|
|
|
29
30
|
registry.install(gate.extension);
|
|
30
31
|
const reviews = guestReviews();
|
|
31
32
|
registry.install(reviews.extension);
|
|
33
|
+
const scooped = scoopReviews();
|
|
34
|
+
registry.install(scooped.extension);
|
|
32
35
|
const harness = await Harness.open(
|
|
33
36
|
options.storage ?? new MemoryStorage(),
|
|
34
37
|
{
|
|
@@ -51,6 +54,7 @@ async function openAgent(options) {
|
|
|
51
54
|
agents,
|
|
52
55
|
reviews,
|
|
53
56
|
gate,
|
|
57
|
+
scoopReviews: scooped,
|
|
54
58
|
cone: () => agents.cone(),
|
|
55
59
|
switchCone: (conversation, switching) => agents.switchCone(conversation, switching),
|
|
56
60
|
onCone: (listener) => agents.onCone(listener),
|
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';\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 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 harness = await Harness.open(\n options.storage ?? new MemoryStorage(),\n {\n models: options.models,\n registry,\n ...(options.settings ? { settings: 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 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;
|
|
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 ...(options.settings ? { settings: 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;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,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,MACzD,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/approvals/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
REASONS,
|
|
12
12
|
routable,
|
|
13
13
|
ruled,
|
|
14
|
+
scoopTier,
|
|
14
15
|
sudoApprover,
|
|
15
16
|
tierOf
|
|
16
17
|
} from "./record.js";
|
|
@@ -176,6 +177,11 @@ async function attachApprovals(options, context = BACKGROUND_CONTEXT) {
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
arm(startup.items);
|
|
180
|
+
const toScoop = (item) => {
|
|
181
|
+
if (options.scoops && item.state === "pending" && scoopTier(item.tier))
|
|
182
|
+
void options.scoops.route(item).catch(() => void 0);
|
|
183
|
+
};
|
|
184
|
+
for (const item of startup.items) toScoop(item);
|
|
179
185
|
const withdrawWhere = (match, reason) => withdrawIn(match, reason, now);
|
|
180
186
|
const judging = async (judge, item, using) => {
|
|
181
187
|
const signal = using.abortSignal;
|
|
@@ -213,6 +219,7 @@ async function attachApprovals(options, context = BACKGROUND_CONTEXT) {
|
|
|
213
219
|
}, using);
|
|
214
220
|
if (created && options.judge && made.state === "pending" && made.tier === "agent")
|
|
215
221
|
void judging(options.judge, made, using);
|
|
222
|
+
if (created) toScoop(made);
|
|
216
223
|
return made;
|
|
217
224
|
},
|
|
218
225
|
decide(id, answer, reason, decider, using) {
|
|
@@ -231,6 +238,13 @@ async function attachApprovals(options, context = BACKGROUND_CONTEXT) {
|
|
|
231
238
|
}, using);
|
|
232
239
|
},
|
|
233
240
|
settled: (id, using) => settledIn(state, id, using),
|
|
241
|
+
assign(id, reviewer, using) {
|
|
242
|
+
return serial((doc) => {
|
|
243
|
+
const item = doc.items.find((entry) => entry.id === id);
|
|
244
|
+
if (item?.state === "pending" && scoopTier(item.tier)) item.reviewer ??= reviewer;
|
|
245
|
+
return item ? copy([item])[0] : void 0;
|
|
246
|
+
}, using);
|
|
247
|
+
},
|
|
234
248
|
async withdraw(id, reason, using) {
|
|
235
249
|
const [hit] = await serial(
|
|
236
250
|
withdrawWhere((item) => item.id === id, cleanReason(reason)),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { DocumentState, Harness } from '@earendil-works/pi-durable';\nimport type { ApprovalJudge, Judgement } from './judge.ts';\nimport {\n APPROVAL_MS,\n type Approval,\n type ApprovalDraft,\n type ApprovalKind,\n type ApprovalState,\n ApprovalsDoc,\n type ApprovalsState,\n cleanReason,\n type DecidedBy,\n guestKind,\n knownTier,\n MAX_DELAY,\n prune,\n REASONS,\n routable,\n ruled,\n type Settle,\n sudoApprover,\n tierOf,\n} from './record.ts';\n\nexport * from './intent.ts';\nexport * from './judge.ts';\nexport * from './record.ts';\n\nexport type ApprovalDecided =\n | { done: true; state: ApprovalState; error: null }\n | { done: false; state: null; error: string };\n\nexport type Decider = DecidedBy | { by: DecidedBy; device?: string };\n\nconst deciderOf = (decider: Decider) => (typeof decider === 'string' ? { by: decider } : decider);\n\nexport interface Approvals {\n readonly state: ReplicatedState<Approval[]>;\n request(draft: ApprovalDraft & { id?: string }, context: Context): Promise<Approval>;\n decide(\n id: string,\n answer: unknown,\n reason: string | null,\n by: Decider,\n context: Context\n ): Promise<ApprovalDecided>;\n settled(id: string, context: Context): Promise<Approval>;\n withdraw(id: string, reason: string | null, context: Context): Promise<Approval | undefined>;\n withdrawSeat(seat: string, context: Context): Promise<number>;\n expire(id: string, context: Context): Promise<Approval | undefined>;\n close(): void;\n}\n\nexport interface ApprovalsAttach {\n harness: Pick<Harness, 'commit' | 'documentState'>;\n now?: () => number;\n ttlMs?: number;\n judge?: ApprovalJudge;\n guestAlways?: (kind: ApprovalKind) => boolean;\n sudoApprover?: () => unknown;\n}\n\nconst copy = (items: readonly Approval[]): Approval[] => JSON.parse(JSON.stringify(items));\n\nfunction mark(item: Approval, how: Settle, at: number) {\n item.state = how.state;\n item.decidedBy = how.by;\n item.decidedAt = at;\n if (how.reason) item.reason = how.reason;\n}\n\nconst offered = (options: ApprovalsAttach, draft: ApprovalDraft): boolean =>\n guestKind(draft.kind) ? options.guestAlways?.(draft.kind) === true : draft.always === true;\n\nfunction fresh(\n draft: ApprovalDraft & { id?: string },\n at: number,\n ttl: number,\n options: ApprovalsAttach\n): Approval {\n const item = {\n ...draft,\n tier: tierOf(draft.kind, draft.tier, sudoApprover(options.sudoApprover?.())),\n id: draft.id ?? `apr-${crypto.randomUUID()}`,\n state: 'pending',\n decidedBy: null,\n always: offered(options, draft),\n createdAt: at,\n expiresAt: draft.expiresAt ?? at + ttl,\n } as Approval;\n if (!knownTier(item.tier))\n mark(item, { state: 'unanswered', by: 'host', reason: REASONS.unknownTier }, at);\n else if (!routable(draft.kind, item.tier))\n mark(item, { state: 'unanswered', by: 'host', reason: REASONS.unroutable }, at);\n return item;\n}\n\nconst withdrawIn =\n (match: (item: Approval) => boolean, reason: string | undefined, now: () => number) =>\n (doc: ApprovalsState) => {\n const hit = doc.items.filter((item) => item.state === 'pending' && match(item));\n for (const item of hit)\n mark(item, { state: 'withdrawn', by: 'host', ...(reason ? { reason } : {}) }, now());\n return hit.map((item) => copy([item])[0] as Approval);\n };\n\nconst judgedIn = (id: string, judgement: Judgement, now: () => number) => (doc: ApprovalsState) => {\n const item = doc.items.find((entry) => entry.id === id);\n if (item?.tier !== 'agent') return undefined;\n const late = item.state === 'unanswered' && item.reason === REASONS.timeout && !item.verdict;\n if (item.state !== 'pending' && !(late && judgement.verdict)) return undefined;\n if (judgement.verdict) item.verdict = judgement.verdict;\n if (late) return copy([item])[0] as Approval;\n if (judgement.outcome === 'ask') item.tier = 'user';\n else {\n const at = now();\n const ruling = ruled(\n item,\n judgement.outcome === 'approve' ? 'once' : 'deny',\n null,\n 'judge',\n at\n );\n if (ruling.answer) item.answer = ruling.answer;\n if (ruling.settle) mark(item, ruling.settle, at);\n }\n return copy([item])[0] as Approval;\n};\n\nfunction following(\n committed: DocumentState<ApprovalsState>,\n changed: (using: Context, items: Approval[]) => void\n) {\n const items = () => (committed.value as ApprovalsState).items;\n let shown = items();\n return (using: Context) => {\n const now = items();\n if (now === shown) return;\n shown = now;\n changed(using, copy(now));\n };\n}\n\nfunction settledIn(\n state: ReplicatedState<Approval[]>,\n id: string,\n using: Context\n): Promise<Approval> {\n return new Promise((resolve, reject) => {\n const signal = using.abortSignal;\n let done = false;\n let off: (() => void) | undefined;\n const finish = (end: () => void) => {\n done = true;\n off?.();\n signal?.removeEventListener('abort', abort);\n end();\n };\n const abort = () => finish(() => reject(signal?.reason));\n const check = (items: readonly Approval[] | undefined) => {\n const item = items?.find((entry) => entry.id === id);\n if (!item) finish(() => reject(new Error(`There is no approval ${id}.`)));\n else if (item.state !== 'pending') finish(() => resolve(item));\n };\n if (signal?.aborted) return abort();\n check(state.value);\n if (done) return;\n signal?.addEventListener('abort', abort, { once: true });\n off = state.subscribe((items) => check(items));\n });\n}\n\nexport async function attachApprovals(\n options: ApprovalsAttach,\n context: Context = BACKGROUND_CONTEXT\n): Promise<Approvals> {\n const { harness } = options;\n const now = options.now ?? Date.now;\n const ttl = options.ttlMs ?? APPROVAL_MS;\n const timers = new Map<string, ReturnType<typeof setTimeout>>();\n let line: Promise<unknown> = Promise.resolve();\n let closed = false;\n const settle = (item: Approval, how: Settle) => mark(item, how, now());\n const write = <R>(\n change: (doc: ApprovalsState) => R,\n using: Context\n ): Promise<{ result: R; items: Approval[] }> =>\n harness.commit(async (tx) => {\n const doc = await tx.doc(ApprovalsDoc);\n const result = change(doc);\n doc.items = prune(doc.items);\n return { result, items: copy(doc.items) };\n }, using);\n const startup = await write((doc) => {\n for (const item of doc.items) {\n if (item.state !== 'pending') continue;\n if (item.kind === 'sudo')\n settle(item, { state: 'withdrawn', by: 'host', reason: REASONS.restart });\n else if (item.expiresAt <= now())\n settle(item, { state: 'unanswered', by: 'host', reason: REASONS.timeout });\n else if (options.judge && item.tier === 'agent') item.tier = 'user';\n }\n }, context);\n const state = replicatedState<Approval[]>(startup.items);\n const committed = (await harness.documentState(\n ApprovalsDoc,\n context\n )) as DocumentState<ApprovalsState>;\n const follow = following(committed, (using, items) => {\n state.replace(using, items);\n arm(items);\n });\n committed.subscribe((_value, using) => follow(using));\n const serial = <R>(change: (doc: ApprovalsState) => R, using: Context): Promise<R> => {\n const run = line.then(async () => {\n const { result } = await write(change, using);\n follow(using);\n return result;\n });\n line = run.catch(() => undefined);\n return run;\n };\n const expire = (id: string) => {\n timers.delete(id);\n const due = (state.value as Approval[]).find((entry) => entry.id === id);\n if (due?.state === 'pending' && due.expiresAt > now()) return arm(state.value as Approval[]);\n void serial((doc) => {\n const item = doc.items.find((entry) => entry.id === id);\n if (item?.state === 'pending' && item.expiresAt <= now())\n settle(item, { state: 'unanswered', by: 'host', reason: REASONS.timeout });\n }, BACKGROUND_CONTEXT).catch(() => undefined);\n };\n function arm(items: readonly Approval[]) {\n const pending = new Set<string>();\n for (const item of items) {\n if (item.state !== 'pending') continue;\n pending.add(item.id);\n if (timers.has(item.id) || closed) continue;\n timers.set(\n item.id,\n setTimeout(() => expire(item.id), Math.min(MAX_DELAY, Math.max(0, item.expiresAt - now())))\n );\n }\n for (const [id, timer] of timers)\n if (!pending.has(id)) {\n clearTimeout(timer);\n timers.delete(id);\n }\n }\n arm(startup.items);\n const withdrawWhere = (match: (item: Approval) => boolean, reason: string | undefined) =>\n withdrawIn(match, reason, now);\n const judging = async (judge: ApprovalJudge, item: Approval, using: Context) => {\n const signal = using.abortSignal;\n let gone = false;\n const abort = () => {\n gone = true;\n void serial(\n withdrawWhere((entry) => entry.id === item.id && entry.tier === 'agent', REASONS.aborted),\n BACKGROUND_CONTEXT\n ).catch(() => undefined);\n };\n if (signal?.aborted) return abort();\n signal?.addEventListener('abort', abort, { once: true });\n const judgement = await judge\n .judge(item, using)\n .catch((error: unknown): Judgement => ({ outcome: 'ask', error: String(error) }));\n signal?.removeEventListener('abort', abort);\n if (gone || closed) return;\n const after = await serial(judgedIn(item.id, judgement, now), BACKGROUND_CONTEXT).catch(\n () => undefined\n );\n if (after) await judge.record?.(after, judgement, BACKGROUND_CONTEXT).catch(() => undefined);\n };\n return {\n state,\n async request(draft, using) {\n let created = true;\n const made = await serial((doc) => {\n const kept = draft.id && doc.items.find((entry) => entry.id === draft.id);\n if (kept) {\n created = false;\n return copy([kept])[0] as Approval;\n }\n const item = fresh(draft, now(), ttl, options);\n doc.items.push(item);\n return copy([item])[0] as Approval;\n }, using);\n if (created && options.judge && made.state === 'pending' && made.tier === 'agent')\n void judging(options.judge, made, using);\n return made;\n },\n decide(id, answer, reason, decider, using) {\n const { by, device } = deciderOf(decider);\n return serial((doc): ApprovalDecided => {\n const item = doc.items.find((entry) => entry.id === id);\n const ruling = item\n ? ruled(item, answer, reason, by, now())\n : { error: `There is no approval ${id}.` };\n if (item && ruling.settle) {\n if (ruling.answer) item.answer = ruling.answer;\n if (device && ruling.settle.by === by) item.device = device;\n settle(item, ruling.settle);\n }\n if ('error' in ruling && ruling.error)\n return { done: false, state: null, error: ruling.error };\n return { done: true, state: (item as Approval).state, error: null };\n }, using);\n },\n settled: (id, using) => settledIn(state, id, using),\n async withdraw(id, reason, using) {\n const [hit] = await serial(\n withdrawWhere((item) => item.id === id, cleanReason(reason)),\n using\n );\n return hit;\n },\n async withdrawSeat(seat, using) {\n return (\n await serial(\n withdrawWhere((item) => item.seat === seat, REASONS.revoked),\n using\n )\n ).length;\n },\n async expire(id, using) {\n return serial((doc) => {\n const item = doc.items.find((entry) => entry.id === id);\n if (item?.state === 'pending' && item.expiresAt <= now())\n settle(item, { state: 'unanswered', by: 'host', reason: REASONS.timeout });\n return item ? (copy([item])[0] as Approval) : undefined;\n }, using);\n },\n close() {\n closed = true;\n for (const timer of timers.values()) clearTimeout(timer);\n timers.clear();\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAA6C,uBAAuB;AACpE,SAAS,0BAA0B;AAGnC;AAAA,EACE;AAAA,EAKA;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,OACK;AAEP,cAAc;AACd,cAAc;AACd,cAAc;AAQd,MAAM,YAAY,CAAC,YAAsB,OAAO,YAAY,WAAW,EAAE,IAAI,QAAQ,IAAI;
|
|
4
|
+
"sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { DocumentState, Harness } from '@earendil-works/pi-durable';\nimport type { ApprovalJudge, Judgement } from './judge.ts';\nimport {\n APPROVAL_MS,\n type Approval,\n type ApprovalDraft,\n type ApprovalKind,\n type ApprovalState,\n ApprovalsDoc,\n type ApprovalsState,\n cleanReason,\n type DecidedBy,\n guestKind,\n knownTier,\n MAX_DELAY,\n prune,\n REASONS,\n routable,\n ruled,\n type Settle,\n scoopTier,\n sudoApprover,\n tierOf,\n} from './record.ts';\n\nexport * from './intent.ts';\nexport * from './judge.ts';\nexport * from './record.ts';\n\nexport type ApprovalDecided =\n | { done: true; state: ApprovalState; error: null }\n | { done: false; state: null; error: string };\n\nexport type Decider = DecidedBy | { by: DecidedBy; device?: string };\n\nconst deciderOf = (decider: Decider) => (typeof decider === 'string' ? { by: decider } : decider);\n\nexport interface Approvals {\n readonly state: ReplicatedState<Approval[]>;\n request(draft: ApprovalDraft & { id?: string }, context: Context): Promise<Approval>;\n decide(\n id: string,\n answer: unknown,\n reason: string | null,\n by: Decider,\n context: Context\n ): Promise<ApprovalDecided>;\n settled(id: string, context: Context): Promise<Approval>;\n assign(id: string, reviewer: string, context: Context): Promise<Approval | undefined>;\n withdraw(id: string, reason: string | null, context: Context): Promise<Approval | undefined>;\n withdrawSeat(seat: string, context: Context): Promise<number>;\n expire(id: string, context: Context): Promise<Approval | undefined>;\n close(): void;\n}\n\nexport interface ScoopRouter {\n route(item: Approval): Promise<void>;\n}\n\nexport interface ApprovalsAttach {\n harness: Pick<Harness, 'commit' | 'documentState'>;\n now?: () => number;\n ttlMs?: number;\n judge?: ApprovalJudge;\n scoops?: ScoopRouter;\n guestAlways?: (kind: ApprovalKind) => boolean;\n sudoApprover?: () => unknown;\n}\n\nconst copy = (items: readonly Approval[]): Approval[] => JSON.parse(JSON.stringify(items));\n\nfunction mark(item: Approval, how: Settle, at: number) {\n item.state = how.state;\n item.decidedBy = how.by;\n item.decidedAt = at;\n if (how.reason) item.reason = how.reason;\n}\n\nconst offered = (options: ApprovalsAttach, draft: ApprovalDraft): boolean =>\n guestKind(draft.kind) ? options.guestAlways?.(draft.kind) === true : draft.always === true;\n\nfunction fresh(\n draft: ApprovalDraft & { id?: string },\n at: number,\n ttl: number,\n options: ApprovalsAttach\n): Approval {\n const item = {\n ...draft,\n tier: tierOf(draft.kind, draft.tier, sudoApprover(options.sudoApprover?.())),\n id: draft.id ?? `apr-${crypto.randomUUID()}`,\n state: 'pending',\n decidedBy: null,\n always: offered(options, draft),\n createdAt: at,\n expiresAt: draft.expiresAt ?? at + ttl,\n } as Approval;\n if (!knownTier(item.tier))\n mark(item, { state: 'unanswered', by: 'host', reason: REASONS.unknownTier }, at);\n else if (!routable(draft.kind, item.tier))\n mark(item, { state: 'unanswered', by: 'host', reason: REASONS.unroutable }, at);\n return item;\n}\n\nconst withdrawIn =\n (match: (item: Approval) => boolean, reason: string | undefined, now: () => number) =>\n (doc: ApprovalsState) => {\n const hit = doc.items.filter((item) => item.state === 'pending' && match(item));\n for (const item of hit)\n mark(item, { state: 'withdrawn', by: 'host', ...(reason ? { reason } : {}) }, now());\n return hit.map((item) => copy([item])[0] as Approval);\n };\n\nconst judgedIn = (id: string, judgement: Judgement, now: () => number) => (doc: ApprovalsState) => {\n const item = doc.items.find((entry) => entry.id === id);\n if (item?.tier !== 'agent') return undefined;\n const late = item.state === 'unanswered' && item.reason === REASONS.timeout && !item.verdict;\n if (item.state !== 'pending' && !(late && judgement.verdict)) return undefined;\n if (judgement.verdict) item.verdict = judgement.verdict;\n if (late) return copy([item])[0] as Approval;\n if (judgement.outcome === 'ask') item.tier = 'user';\n else {\n const at = now();\n const ruling = ruled(\n item,\n judgement.outcome === 'approve' ? 'once' : 'deny',\n null,\n 'judge',\n at\n );\n if (ruling.answer) item.answer = ruling.answer;\n if (ruling.settle) mark(item, ruling.settle, at);\n }\n return copy([item])[0] as Approval;\n};\n\nfunction following(\n committed: DocumentState<ApprovalsState>,\n changed: (using: Context, items: Approval[]) => void\n) {\n const items = () => (committed.value as ApprovalsState).items;\n let shown = items();\n return (using: Context) => {\n const now = items();\n if (now === shown) return;\n shown = now;\n changed(using, copy(now));\n };\n}\n\nfunction settledIn(\n state: ReplicatedState<Approval[]>,\n id: string,\n using: Context\n): Promise<Approval> {\n return new Promise((resolve, reject) => {\n const signal = using.abortSignal;\n let done = false;\n let off: (() => void) | undefined;\n const finish = (end: () => void) => {\n done = true;\n off?.();\n signal?.removeEventListener('abort', abort);\n end();\n };\n const abort = () => finish(() => reject(signal?.reason));\n const check = (items: readonly Approval[] | undefined) => {\n const item = items?.find((entry) => entry.id === id);\n if (!item) finish(() => reject(new Error(`There is no approval ${id}.`)));\n else if (item.state !== 'pending') finish(() => resolve(item));\n };\n if (signal?.aborted) return abort();\n check(state.value);\n if (done) return;\n signal?.addEventListener('abort', abort, { once: true });\n off = state.subscribe((items) => check(items));\n });\n}\n\nexport async function attachApprovals(\n options: ApprovalsAttach,\n context: Context = BACKGROUND_CONTEXT\n): Promise<Approvals> {\n const { harness } = options;\n const now = options.now ?? Date.now;\n const ttl = options.ttlMs ?? APPROVAL_MS;\n const timers = new Map<string, ReturnType<typeof setTimeout>>();\n let line: Promise<unknown> = Promise.resolve();\n let closed = false;\n const settle = (item: Approval, how: Settle) => mark(item, how, now());\n const write = <R>(\n change: (doc: ApprovalsState) => R,\n using: Context\n ): Promise<{ result: R; items: Approval[] }> =>\n harness.commit(async (tx) => {\n const doc = await tx.doc(ApprovalsDoc);\n const result = change(doc);\n doc.items = prune(doc.items);\n return { result, items: copy(doc.items) };\n }, using);\n const startup = await write((doc) => {\n for (const item of doc.items) {\n if (item.state !== 'pending') continue;\n if (item.kind === 'sudo')\n settle(item, { state: 'withdrawn', by: 'host', reason: REASONS.restart });\n else if (item.expiresAt <= now())\n settle(item, { state: 'unanswered', by: 'host', reason: REASONS.timeout });\n else if (options.judge && item.tier === 'agent') item.tier = 'user';\n }\n }, context);\n const state = replicatedState<Approval[]>(startup.items);\n const committed = (await harness.documentState(\n ApprovalsDoc,\n context\n )) as DocumentState<ApprovalsState>;\n const follow = following(committed, (using, items) => {\n state.replace(using, items);\n arm(items);\n });\n committed.subscribe((_value, using) => follow(using));\n const serial = <R>(change: (doc: ApprovalsState) => R, using: Context): Promise<R> => {\n const run = line.then(async () => {\n const { result } = await write(change, using);\n follow(using);\n return result;\n });\n line = run.catch(() => undefined);\n return run;\n };\n const expire = (id: string) => {\n timers.delete(id);\n const due = (state.value as Approval[]).find((entry) => entry.id === id);\n if (due?.state === 'pending' && due.expiresAt > now()) return arm(state.value as Approval[]);\n void serial((doc) => {\n const item = doc.items.find((entry) => entry.id === id);\n if (item?.state === 'pending' && item.expiresAt <= now())\n settle(item, { state: 'unanswered', by: 'host', reason: REASONS.timeout });\n }, BACKGROUND_CONTEXT).catch(() => undefined);\n };\n function arm(items: readonly Approval[]) {\n const pending = new Set<string>();\n for (const item of items) {\n if (item.state !== 'pending') continue;\n pending.add(item.id);\n if (timers.has(item.id) || closed) continue;\n timers.set(\n item.id,\n setTimeout(() => expire(item.id), Math.min(MAX_DELAY, Math.max(0, item.expiresAt - now())))\n );\n }\n for (const [id, timer] of timers)\n if (!pending.has(id)) {\n clearTimeout(timer);\n timers.delete(id);\n }\n }\n arm(startup.items);\n const toScoop = (item: Approval) => {\n if (options.scoops && item.state === 'pending' && scoopTier(item.tier))\n void options.scoops.route(item).catch(() => undefined);\n };\n for (const item of startup.items) toScoop(item);\n const withdrawWhere = (match: (item: Approval) => boolean, reason: string | undefined) =>\n withdrawIn(match, reason, now);\n const judging = async (judge: ApprovalJudge, item: Approval, using: Context) => {\n const signal = using.abortSignal;\n let gone = false;\n const abort = () => {\n gone = true;\n void serial(\n withdrawWhere((entry) => entry.id === item.id && entry.tier === 'agent', REASONS.aborted),\n BACKGROUND_CONTEXT\n ).catch(() => undefined);\n };\n if (signal?.aborted) return abort();\n signal?.addEventListener('abort', abort, { once: true });\n const judgement = await judge\n .judge(item, using)\n .catch((error: unknown): Judgement => ({ outcome: 'ask', error: String(error) }));\n signal?.removeEventListener('abort', abort);\n if (gone || closed) return;\n const after = await serial(judgedIn(item.id, judgement, now), BACKGROUND_CONTEXT).catch(\n () => undefined\n );\n if (after) await judge.record?.(after, judgement, BACKGROUND_CONTEXT).catch(() => undefined);\n };\n return {\n state,\n async request(draft, using) {\n let created = true;\n const made = await serial((doc) => {\n const kept = draft.id && doc.items.find((entry) => entry.id === draft.id);\n if (kept) {\n created = false;\n return copy([kept])[0] as Approval;\n }\n const item = fresh(draft, now(), ttl, options);\n doc.items.push(item);\n return copy([item])[0] as Approval;\n }, using);\n if (created && options.judge && made.state === 'pending' && made.tier === 'agent')\n void judging(options.judge, made, using);\n if (created) toScoop(made);\n return made;\n },\n decide(id, answer, reason, decider, using) {\n const { by, device } = deciderOf(decider);\n return serial((doc): ApprovalDecided => {\n const item = doc.items.find((entry) => entry.id === id);\n const ruling = item\n ? ruled(item, answer, reason, by, now())\n : { error: `There is no approval ${id}.` };\n if (item && ruling.settle) {\n if (ruling.answer) item.answer = ruling.answer;\n if (device && ruling.settle.by === by) item.device = device;\n settle(item, ruling.settle);\n }\n if ('error' in ruling && ruling.error)\n return { done: false, state: null, error: ruling.error };\n return { done: true, state: (item as Approval).state, error: null };\n }, using);\n },\n settled: (id, using) => settledIn(state, id, using),\n assign(id, reviewer, using) {\n return serial((doc) => {\n const item = doc.items.find((entry) => entry.id === id);\n if (item?.state === 'pending' && scoopTier(item.tier)) item.reviewer ??= reviewer;\n return item ? (copy([item])[0] as Approval) : undefined;\n }, using);\n },\n async withdraw(id, reason, using) {\n const [hit] = await serial(\n withdrawWhere((item) => item.id === id, cleanReason(reason)),\n using\n );\n return hit;\n },\n async withdrawSeat(seat, using) {\n return (\n await serial(\n withdrawWhere((item) => item.seat === seat, REASONS.revoked),\n using\n )\n ).length;\n },\n async expire(id, using) {\n return serial((doc) => {\n const item = doc.items.find((entry) => entry.id === id);\n if (item?.state === 'pending' && item.expiresAt <= now())\n settle(item, { state: 'unanswered', by: 'host', reason: REASONS.timeout });\n return item ? (copy([item])[0] as Approval) : undefined;\n }, using);\n },\n close() {\n closed = true;\n for (const timer of timers.values()) clearTimeout(timer);\n timers.clear();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/approvals/record.js
CHANGED
|
@@ -17,12 +17,17 @@ const REASONS = {
|
|
|
17
17
|
restart: "restart",
|
|
18
18
|
revoked: "revoked",
|
|
19
19
|
aborted: "aborted",
|
|
20
|
-
disconnected: "disconnected"
|
|
20
|
+
disconnected: "disconnected",
|
|
21
|
+
noScoop: "no-scoop",
|
|
22
|
+
scoopStopped: "scoop-stopped",
|
|
23
|
+
scoopAsked: "scoop-asked",
|
|
24
|
+
scoopUndecided: "scoop-undecided"
|
|
21
25
|
};
|
|
22
26
|
const guestKind = (kind) => kind !== "sudo";
|
|
27
|
+
const scoopTier = (tier) => typeof tier === "string" && /^scoop:\S+$/.test(tier);
|
|
23
28
|
function knownTier(tier) {
|
|
24
29
|
if (tier === "user" || tier === "agent" || tier === "cone") return true;
|
|
25
|
-
return
|
|
30
|
+
return scoopTier(tier);
|
|
26
31
|
}
|
|
27
32
|
function routable(kind, tier) {
|
|
28
33
|
return !(kind === "guest-tool" && tier === "cone");
|
|
@@ -31,9 +36,10 @@ function answerOf(value) {
|
|
|
31
36
|
return value === "once" || value === "always" || value === "deny" ? value : null;
|
|
32
37
|
}
|
|
33
38
|
const personal = (by) => by === "owner" || by === "follower";
|
|
34
|
-
function answerer(by,
|
|
35
|
-
if (personal(by)) return tier === "user";
|
|
36
|
-
if (by === "judge") return tier === "agent";
|
|
39
|
+
function answerer(by, item) {
|
|
40
|
+
if (personal(by)) return item.tier === "user";
|
|
41
|
+
if (by === "judge") return item.tier === "agent";
|
|
42
|
+
if (scoopTier(by)) return scoopTier(item.tier) && item.reviewer === by;
|
|
37
43
|
return true;
|
|
38
44
|
}
|
|
39
45
|
const sudoApprover = (value) => value === "agent" ? "agent" : "user";
|
|
@@ -63,8 +69,9 @@ function ruled(item, answer, reason, by, at) {
|
|
|
63
69
|
error: gone("unanswered"),
|
|
64
70
|
settle: { state: "unanswered", by: "host", reason: REASONS.timeout }
|
|
65
71
|
};
|
|
66
|
-
if (!answerer(by, item
|
|
72
|
+
if (!answerer(by, item)) return { error: "This request isn't yours to answer." };
|
|
67
73
|
const known = answerOf(answer);
|
|
74
|
+
if (known === "always" && scoopTier(by)) return { error: "A scoop answers once or deny." };
|
|
68
75
|
if (known === "always" && !item.always)
|
|
69
76
|
return { error: "Always isn't offered for this request." };
|
|
70
77
|
if (!known) return { settle: { state: "rejected", by, reason: REASONS.unknownAnswer } };
|
|
@@ -89,6 +96,7 @@ export {
|
|
|
89
96
|
prune,
|
|
90
97
|
routable,
|
|
91
98
|
ruled,
|
|
99
|
+
scoopTier,
|
|
92
100
|
sudoApprover,
|
|
93
101
|
tierOf
|
|
94
102
|
};
|
|
@@ -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'
|
|
5
|
-
"mappings": "AAAA,SAAS,iBAAiB;
|
|
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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/gelatiere/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
2
|
import { live } from "../agents.js";
|
|
3
|
+
import { ROOT_USER } from "../layout.js";
|
|
4
|
+
import { installJobs, readTable, scheduleOf, sendCommand } from "../licks/crontab.js";
|
|
3
5
|
import { skillFromText } from "../skills/skills.js";
|
|
4
6
|
import {
|
|
5
7
|
CATALOG_BASE,
|
|
@@ -40,13 +42,7 @@ const USAGE = `usage:
|
|
|
40
42
|
|
|
41
43
|
The store is ${STORE}; the suggestions sprinkle shows it.
|
|
42
44
|
`;
|
|
43
|
-
|
|
44
|
-
return `${SCHEDULE} ${ROLE} scoop:${handle} Nightly pass: read ~/.pi/agent/GELATIERE.md and follow it.`;
|
|
45
|
-
}
|
|
46
|
-
const JOB = new RegExp(`^((?:\\S+\\s+){4}\\S+|@\\w+)\\s+${ROLE}(?:\\s|$)`);
|
|
47
|
-
function scheduleOf(entry) {
|
|
48
|
-
return JOB.exec(entry.trim())?.[1]?.replace(/\s+/g, " ");
|
|
49
|
-
}
|
|
45
|
+
const PASS = "Nightly pass: read ~/.pi/agent/GELATIERE.md and follow it.";
|
|
50
46
|
function handleOf(agents) {
|
|
51
47
|
const found = Object.entries(agents.state().scoops).find(
|
|
52
48
|
([, record]) => record.role === ROLE && live(record)
|
|
@@ -62,14 +58,15 @@ async function callerOf(g, context) {
|
|
|
62
58
|
}
|
|
63
59
|
const fail = (out) => ({ code: 1, out: `gelatiere: ${out}
|
|
64
60
|
` });
|
|
61
|
+
async function cronUserOf(g, handle) {
|
|
62
|
+
return await g.options.cronUser?.(handle) ?? ROOT_USER;
|
|
63
|
+
}
|
|
65
64
|
async function schedule(g, handle, context) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
await env.writeFile(g.crontab, `${[...kept, line(handle)].join("\n")}
|
|
72
|
-
`, context);
|
|
65
|
+
const command = sendCommand(`scoop:${handle}`, PASS);
|
|
66
|
+
const user = await cronUserOf(g, handle);
|
|
67
|
+
const job = { name: ROLE, schedule: SCHEDULE, command };
|
|
68
|
+
const problems = await installJobs(g.options.env, user, [job], context);
|
|
69
|
+
return problems.length ? problems.map((problem) => problem.error).join("; ") : void 0;
|
|
73
70
|
}
|
|
74
71
|
const SHIPPED = [
|
|
75
72
|
"53bc19daa486dec24116d8cf0558a498d8476dc4e18874f8a3e9ccb9defb15bd",
|
|
@@ -132,7 +129,8 @@ async function init(g, context) {
|
|
|
132
129
|
if (spawned.code !== 0) return { code: 1, out: `gelatiere: ${spawned.out}` };
|
|
133
130
|
handle = spawned.out.trim();
|
|
134
131
|
}
|
|
135
|
-
await schedule(g, handle, context);
|
|
132
|
+
const refused = await schedule(g, handle, context);
|
|
133
|
+
if (refused) return fail(`couldn't install the crontab line of scoop ${handle}: ${refused}`);
|
|
136
134
|
return {
|
|
137
135
|
code: 0,
|
|
138
136
|
out: `the gelatiere is scoop ${handle}; it runs at ${SCHEDULE} (crontab) and with gelatiere run, following ${g.procedure}
|
|
@@ -374,8 +372,8 @@ async function status(g, context) {
|
|
|
374
372
|
const { env, agents } = g.options;
|
|
375
373
|
const store = await readStore(env, context, g.store);
|
|
376
374
|
const handle = handleOf(agents);
|
|
377
|
-
const
|
|
378
|
-
const scheduled =
|
|
375
|
+
const table = handle ? await readTable(env, await cronUserOf(g, handle), context) : "";
|
|
376
|
+
const scheduled = typeof table === "string" ? scheduleOf(table, ROLE) : void 0;
|
|
379
377
|
const counts = store ? `${open(store).length} open, ${store.filter((item) => item.takenAt).length} taken, ${store.filter((item) => item.dismissedAt).length} dismissed` : "none yet";
|
|
380
378
|
const pending = await g.files.readTextFile(g.pending, context);
|
|
381
379
|
const lines = [
|
|
@@ -419,7 +417,6 @@ function attachGelatiere(options) {
|
|
|
419
417
|
serial,
|
|
420
418
|
options,
|
|
421
419
|
now,
|
|
422
|
-
crontab: `${home}/.slicc/crontab`,
|
|
423
420
|
procedure: `${home}/.pi/agent/GELATIERE.md`,
|
|
424
421
|
pending: `${home}/.pi/agent/GELATIERE.new.md`,
|
|
425
422
|
files: options.files ?? env,
|
|
@@ -475,6 +472,7 @@ function lateGelatiere() {
|
|
|
475
472
|
}
|
|
476
473
|
export {
|
|
477
474
|
INSTALLER,
|
|
475
|
+
PASS,
|
|
478
476
|
PROCEDURE,
|
|
479
477
|
ROLE,
|
|
480
478
|
SCHEDULE,
|
|
@@ -483,7 +481,6 @@ export {
|
|
|
483
481
|
USAGE,
|
|
484
482
|
attachGelatiere,
|
|
485
483
|
lateGelatiere,
|
|
486
|
-
scheduleOf,
|
|
487
484
|
upskillScript
|
|
488
485
|
};
|
|
489
486
|
//# sourceMappingURL=index.js.map
|