@ai-ecoverse/slicc-agent 6.14.2 → 6.15.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 CHANGED
@@ -158,7 +158,7 @@ The first provider is AWS Bedrock with a Bedrock API key as the bearer token; th
158
158
 
159
159
  ### In slicc-spectrum
160
160
 
161
- `@ai-ecoverse/slicc-spectrum` is an optional peer dependency, `^1.96.0`. 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”; and 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. `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, and the moved note once `mcp.json` points a connected server elsewhere.
161
+ `@ai-ecoverse/slicc-spectrum` is an optional peer dependency, `^1.98.0`. 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`).
162
162
 
163
163
  `@ai-ecoverse/slicc-agent/spectrum` turns an `AgentConnection` into slicc-spectrum's `AgentPort`, so `<slicc-app>` shows the real agent:
164
164
 
@@ -723,12 +723,18 @@ All the limits on approvals and guests live in one owner-changeable policy, the
723
723
 
724
724
  ### Remote control
725
725
 
726
- `remoteControl(connection, { agent? })` from `@ai-ecoverse/slicc-agent/page` gives a linked CLI's `slicc prompt`, `slicc watch` and `slicc abort` (the `agent` stream of the slicc link protocol, §5.11) as plain objects the page writes to the link as NDJSON. `agent()` names the agent, by default the cone the worker treats as current.
726
+ `remoteControl(connection, { agent? })` from `@ai-ecoverse/slicc-agent/page` gives a linked CLI's `slicc prompt`, `slicc watch`, `slicc wait` and `slicc abort` (the `agent` stream of the slicc link protocol, §5.11) as plain objects the page writes to the link as NDJSON. `agent()` names the agent, by default the cone the worker treats as current.
727
727
 
728
728
  - **`prompt({ text, whenBusy = 'followUp', thinking = false, attachments? })`** sends the prompt, with attachments as on `SendRequest` (see Attachments), and resolves `{ delivered, events }`. `delivered` is `run`, `steer` or `follow-up`. A `reject` while busy throws an `Error` with `code: 'EBUSY'`, and any other refusal `EIO`. `events` starts at the prompt's own `user` event and ends with `{ t: 'end', reason, message? }` after the run that holds it. Leaving the loop closes the stream but never aborts the run.
729
- - **`watch({ since, thinking })`** resolves `{ head, gap?, events }`. Without `since` it follows from the next event; with it, it first replays `since`'s own text or thinking whole, then everything after it, or answers `gap: true` when the transcript no longer holds it.
729
+ - **`watch({ since, thinking, busy })`** resolves `{ head, gap?, busy?, events }`. Without `since` it follows from the next event; with it, it first replays `since`'s own text or thinking whole, then everything after it, or answers `gap: true` when the transcript no longer holds it.
730
+ - **`busy`** (the OPEN's `"busy": true`, for slicc-cli's `prompt --allsettled` and `wait`) tells when the agent has nothing left to do. The cone can end a run while its scoops still work, and run again on their `scoop-notify` licks.
731
+ - **The answer** carries `busy: true | false`, the state after any replay. Without the option it has no `busy`, and neither does an older agent's, so the page's OPENED leaves it out and the node can tell.
732
+ - **Then** a `{ t: 'busy', busy, why? }` line follows each flip, in order with the other events. It has no id and is never replayed. `why` (`cone`, `scoop:<name>`, `pending-lick`) comes only with `true`.
733
+ - **Busy** means, for the cone the stream belongs to (a scoop's own cone when `agent()` names a scoop): the cone has a run or a queued input (`pi.live`, `pi.inbox`); a live scoop of that cone has a run, a queued input or a feed (`ScoopWorkDoc.feeds`); or a lick is still to reach the cone or one of those scoops: a scoop `wait` still to deliver (`ScoopWorkDoc.waits`), or a lick pending in the licks outbox, a lick held there until its conversation's model is known (#475) included. A cron or file-watch lick counts once it is queued, not before. If the model never comes, `busy` stays true.
734
+ - **Computed** in the worker from the Session's commits (`subscribeCommits`, one consistent state per commit: the cone's and its scoops' `pi.live` and `pi.inbox`, `slicc.agents`, `slicc.scoops.work` and the licks outbox `slicc.licks`), never polled, and a line goes out only on a flip.
735
+ - **Ordering:** a `run` `start` never comes before its `busy: true` (one is sent first when needed), and a `busy: false` goes out only after the stream has delivered every event of the commits before it, so the last `busy: false` follows the last `run` `end`. A `busy: true` sent ahead of a run whose work had already ended by then is followed by its `false` the same way.
730
736
  - **`abort()`** resolves `{ aborted }`, `false` when nothing was running.
731
- - **Events** are `user`, `text`, `thinking` (only with `thinking`), `tool`, `tool-result`, `run`, `notice` and `error`. They are mapped in the worker from pi-durable's agent events (`watchEvents`: `run_start`/`run_end` with their inputs, message deltas, tool ends, retries, compactions) and polled by the page over the `slicc.agent.remote` service.
737
+ - **Events** are `user`, `text`, `thinking` (only with `thinking`), `tool`, `tool-result`, `run`, `notice`, `error` and `busy` (only on a `watch` with `busy`). They are mapped in the worker from pi-durable's agent events (`watchEvents`: `run_start`/`run_end` with their inputs, message deltas, tool ends, retries, compactions) and polled by the page over the `slicc.agent.remote` service.
732
738
  - **Ids:** entry ids for committed entries, `a<timestamp>.<block>` for an assistant message's blocks (stable from the first delta to the committed entry; `~n` when timestamps collide), and unique ids for transient notices.
733
739
  - **What leaves the worker:** tool summaries of at most 200 characters (the tool's name and main argument), result summaries (a line count, or an error's first line), secrets redacted; never tool output.
734
740
  - **Bounds:** a stream nobody polls for two minutes is dropped, and one more than 10 000 events behind ends with `{ t: 'end', reason: 'error' }`.
@@ -824,7 +830,7 @@ Every same-repo pull request gets one `cert-review` status from `.github/workflo
824
830
  - **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>`.
825
831
  - **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.
826
832
  - **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.
827
- - **When it runs:** on `merge-ok`, and again on every push while the label is on. Before that a behaviour change is red with `waits for merge-ok`. A same-repo pull request by `renovate[bot]` is reviewed without the label, on every push, so a devDependency bump settles as `not behaviour (identical dist)` without anyone labelling it, and a bump that changes what ships gets the full review.
833
+ - **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.
828
834
  - **The full review:**
829
835
  - 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.
830
836
  - **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.
@@ -0,0 +1,226 @@
1
+ import {
2
+ InboxDoc,
3
+ LiveDoc
4
+ } from "@earendil-works/pi-durable";
5
+ import { AgentsDoc, live } from "../agents.js";
6
+ import { LicksOutbox } from "../licks/licks.js";
7
+ import { coneOf, ScoopWorkDoc } from "../scoops/service.js";
8
+ const KINDS = {
9
+ live: LiveDoc.definition.kind,
10
+ inbox: InboxDoc.definition.kind,
11
+ agents: AgentsDoc.definition.kind,
12
+ work: ScoopWorkDoc.definition.kind,
13
+ outbox: LicksOutbox.definition.kind
14
+ };
15
+ function running(value) {
16
+ return Boolean(value?.run);
17
+ }
18
+ function waiting(value) {
19
+ return (value?.items ?? []).some((item) => item.mode !== "write");
20
+ }
21
+ function mark(set, id, on) {
22
+ if (on) set.add(id);
23
+ else set.delete(id);
24
+ }
25
+ function snapshotBusy(snapshot) {
26
+ return Boolean(snapshot.run) || snapshot.inbox.some((item) => item.mode !== "write");
27
+ }
28
+ class BusyTracker {
29
+ #conversation;
30
+ #home;
31
+ #running = /* @__PURE__ */ new Set();
32
+ #waiting = /* @__PURE__ */ new Set();
33
+ #agents;
34
+ #work;
35
+ #outbox;
36
+ #cone;
37
+ constructor(conversation) {
38
+ this.#conversation = conversation;
39
+ this.#home = conversation;
40
+ }
41
+ seed(agents, work, outbox) {
42
+ this.#agents = agents;
43
+ this.#work = work;
44
+ this.#outbox = outbox;
45
+ const state = this.#agents;
46
+ if (!state) return;
47
+ const own = Object.values(state.scoops).find(
48
+ (record) => record.conversation === this.#conversation
49
+ );
50
+ this.#cone = coneOf(state, this.#conversation) ?? own?.cone;
51
+ this.#home = own ? state.cones[own.cone]?.conversation ?? this.#conversation : this.#conversation;
52
+ }
53
+ conversations() {
54
+ const scoops = this.#scoops().map(([, record]) => record.conversation);
55
+ return [.../* @__PURE__ */ new Set([this.#home, this.#conversation, ...scoops])];
56
+ }
57
+ conversation(id, live2, inbox) {
58
+ mark(this.#running, id, running(live2));
59
+ mark(this.#waiting, id, waiting(inbox));
60
+ }
61
+ apply(publication) {
62
+ let touched = false;
63
+ for (const change of publication.changes) {
64
+ if (change.type !== "document" || change.record.key !== void 0) continue;
65
+ const { kind } = change.record;
66
+ const id = change.conversationId;
67
+ if (kind === KINDS.live && id !== void 0) mark(this.#running, id, running(change.value));
68
+ else if (kind === KINDS.inbox && id !== void 0)
69
+ mark(this.#waiting, id, waiting(change.value));
70
+ else if (kind === KINDS.agents && id === void 0)
71
+ this.#agents = change.value ?? void 0;
72
+ else if (kind === KINDS.work && id === void 0)
73
+ this.#work = change.value ?? void 0;
74
+ else if (kind === KINDS.outbox && id === void 0)
75
+ this.#outbox = change.value ?? void 0;
76
+ else continue;
77
+ touched = true;
78
+ }
79
+ return touched;
80
+ }
81
+ #scoops() {
82
+ const cone = this.#cone;
83
+ if (cone === void 0) return [];
84
+ return Object.entries(this.#agents?.scoops ?? {}).filter(
85
+ ([, record]) => record.cone === cone && live(record)
86
+ );
87
+ }
88
+ #busy(id) {
89
+ return this.#running.has(id) || this.#waiting.has(id);
90
+ }
91
+ #lands(target, channel) {
92
+ const state = this.#agents;
93
+ if (!state) return void 0;
94
+ const active = state.cones[state.active]?.conversation;
95
+ if (target === "cone") return active;
96
+ if (target.startsWith("cone:"))
97
+ return state.cones[target.slice("cone:".length)]?.conversation ?? active;
98
+ if (channel === "webhook") return active;
99
+ const scoop = state.scoops[target];
100
+ return scoop && live(scoop) ? scoop.conversation : active;
101
+ }
102
+ #pending(ours) {
103
+ const cone = this.#cone;
104
+ if (cone !== void 0 && Object.values(this.#work?.waits ?? {}).some((w) => w.cone === cone))
105
+ return true;
106
+ return Object.entries(this.#outbox?.queues ?? {}).some(([key, queue]) => {
107
+ if (!queue.pending) return false;
108
+ const lands = this.#lands(key.split("|")[0], queue.pending.lick.channel);
109
+ return lands !== void 0 && ours.has(lands);
110
+ });
111
+ }
112
+ value() {
113
+ const why = [];
114
+ if (this.#busy(this.#home)) why.push("cone");
115
+ const scoops = this.#scoops();
116
+ const feeds = Object.values(this.#work?.feeds ?? {});
117
+ for (const [id, record] of scoops)
118
+ if (this.#busy(record.conversation) || feeds.some((feed) => feed.scoop === id))
119
+ why.push(`scoop:${record.name}`);
120
+ const ours = /* @__PURE__ */ new Set([this.#home, ...scoops.map(([, record]) => record.conversation)]);
121
+ if (this.#pending(ours)) why.push("pending-lick");
122
+ return { busy: why.length > 0, why };
123
+ }
124
+ }
125
+ async function trackBusy(harness, conversation, context) {
126
+ const tracker = new BusyTracker(conversation);
127
+ const buffer = [];
128
+ let ready = false;
129
+ let listener = () => void 0;
130
+ const stop = harness.subscribeCommits((publication) => {
131
+ if (!ready) buffer.push(publication);
132
+ else if (tracker.apply(publication)) listener();
133
+ });
134
+ try {
135
+ const [agents, work, outbox] = await Promise.all([
136
+ harness.snapshot(AgentsDoc, context),
137
+ harness.snapshot(ScoopWorkDoc, context),
138
+ harness.snapshot(LicksOutbox, context)
139
+ ]);
140
+ tracker.seed(agents, work, outbox);
141
+ await Promise.all(
142
+ tracker.conversations().map(async (id) => {
143
+ const [liveDoc, inboxDoc] = await Promise.all([
144
+ harness.snapshot(LiveDoc, id, context),
145
+ harness.snapshot(InboxDoc, id, context)
146
+ ]);
147
+ tracker.conversation(id, liveDoc, inboxDoc);
148
+ })
149
+ );
150
+ } catch (error) {
151
+ stop();
152
+ throw error;
153
+ }
154
+ for (const publication of buffer.splice(0)) tracker.apply(publication);
155
+ ready = true;
156
+ return {
157
+ tracker,
158
+ listen(next) {
159
+ listener = next;
160
+ },
161
+ stop
162
+ };
163
+ }
164
+ function busyLine(state) {
165
+ return state.busy ? { t: "busy", busy: true, why: state.why } : { t: "busy", busy: false };
166
+ }
167
+ class BusyGate {
168
+ #tracker;
169
+ #push;
170
+ #sent;
171
+ #timer;
172
+ constructor(tracker, push, sent) {
173
+ this.#tracker = tracker;
174
+ this.#push = push;
175
+ this.#sent = sent;
176
+ }
177
+ before(events) {
178
+ if (this.#sent || !events.some((event) => event.t === "run" && event.state === "start"))
179
+ return events;
180
+ this.#sent = true;
181
+ const now = this.#tracker.value();
182
+ this.changed();
183
+ return [busyLine(now.busy ? now : { busy: true, why: ["cone"] }), ...events];
184
+ }
185
+ changed() {
186
+ if (this.#timer !== void 0 || this.#tracker.value().busy === this.#sent) return;
187
+ this.#timer = setTimeout(() => {
188
+ this.#timer = void 0;
189
+ const now = this.#tracker.value();
190
+ if (now.busy === this.#sent) return;
191
+ this.#sent = now.busy;
192
+ this.#push([busyLine(now)]);
193
+ }, 0);
194
+ }
195
+ stop() {
196
+ clearTimeout(this.#timer);
197
+ this.#timer = void 0;
198
+ }
199
+ }
200
+ async function watchBusy(harness, watched, push, context) {
201
+ const busy = await trackBusy(harness, watched.conversation.id, context).catch(async (error) => {
202
+ await watched.stop();
203
+ throw error;
204
+ });
205
+ const opened = busy.tracker.value().busy || snapshotBusy(watched.snapshot);
206
+ const gate = new BusyGate(busy.tracker, push, opened);
207
+ busy.listen(() => gate.changed());
208
+ gate.changed();
209
+ return {
210
+ opened,
211
+ gate,
212
+ stop() {
213
+ busy.stop();
214
+ gate.stop();
215
+ }
216
+ };
217
+ }
218
+ export {
219
+ BusyGate,
220
+ BusyTracker,
221
+ busyLine,
222
+ snapshotBusy,
223
+ trackBusy,
224
+ watchBusy
225
+ };
226
+ //# sourceMappingURL=busy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/remote/busy.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type CommitPublication,\n type ConversationId,\n type Harness,\n InboxDoc,\n type InboxState,\n LiveDoc,\n type LiveState,\n type SnapshotEvent,\n} from '@earendil-works/pi-durable';\nimport { AgentsDoc, type AgentsState, live } from '../agents.ts';\nimport { LicksOutbox } from '../licks/licks.ts';\nimport { coneOf, ScoopWorkDoc } from '../scoops/service.ts';\nimport type { RemoteEvent } from './events.ts';\n\nexport interface BusyState {\n busy: boolean;\n why: string[];\n}\n\ntype Work = {\n feeds: Record<string, { scoop: string }>;\n waits: Record<string, { cone: string }>;\n};\n\ntype Outbox = {\n queues: Record<string, { pending: { lick: { channel: string } } | null }>;\n};\n\nconst KINDS = {\n live: LiveDoc.definition.kind,\n inbox: InboxDoc.definition.kind,\n agents: AgentsDoc.definition.kind,\n work: ScoopWorkDoc.definition.kind,\n outbox: LicksOutbox.definition.kind,\n};\n\nfunction running(value: unknown): boolean {\n return Boolean((value as LiveState | null)?.run);\n}\n\nfunction waiting(value: unknown): boolean {\n return ((value as InboxState | null)?.items ?? []).some((item) => item.mode !== 'write');\n}\n\nfunction mark(set: Set<number>, id: number, on: boolean): void {\n if (on) set.add(id);\n else set.delete(id);\n}\n\nexport function snapshotBusy(snapshot: SnapshotEvent): boolean {\n return Boolean(snapshot.run) || snapshot.inbox.some((item) => item.mode !== 'write');\n}\n\nexport class BusyTracker {\n readonly #conversation: number;\n #home: number;\n readonly #running = new Set<number>();\n readonly #waiting = new Set<number>();\n #agents: Readonly<AgentsState> | undefined;\n #work: Readonly<Work> | undefined;\n #outbox: Readonly<Outbox> | undefined;\n #cone: string | undefined;\n\n constructor(conversation: number) {\n this.#conversation = conversation;\n this.#home = conversation;\n }\n\n seed(agents: unknown, work: unknown, outbox: unknown): void {\n this.#agents = agents as AgentsState | undefined;\n this.#work = work as Work | undefined;\n this.#outbox = outbox as Outbox | undefined;\n const state = this.#agents;\n if (!state) return;\n const own = Object.values(state.scoops).find(\n (record) => record.conversation === this.#conversation\n );\n this.#cone = coneOf(state, this.#conversation) ?? own?.cone;\n this.#home = own\n ? (state.cones[own.cone]?.conversation ?? this.#conversation)\n : this.#conversation;\n }\n\n conversations(): number[] {\n const scoops = this.#scoops().map(([, record]) => record.conversation);\n return [...new Set([this.#home, this.#conversation, ...scoops])];\n }\n\n conversation(id: number, live: unknown, inbox: unknown): void {\n mark(this.#running, id, running(live));\n mark(this.#waiting, id, waiting(inbox));\n }\n\n apply(publication: CommitPublication): boolean {\n let touched = false;\n for (const change of publication.changes) {\n if (change.type !== 'document' || change.record.key !== undefined) continue;\n const { kind } = change.record;\n const id = change.conversationId;\n if (kind === KINDS.live && id !== undefined) mark(this.#running, id, running(change.value));\n else if (kind === KINDS.inbox && id !== undefined)\n mark(this.#waiting, id, waiting(change.value));\n else if (kind === KINDS.agents && id === undefined)\n this.#agents = (change.value ?? undefined) as AgentsState | undefined;\n else if (kind === KINDS.work && id === undefined)\n this.#work = (change.value ?? undefined) as Work | undefined;\n else if (kind === KINDS.outbox && id === undefined)\n this.#outbox = (change.value ?? undefined) as Outbox | undefined;\n else continue;\n touched = true;\n }\n return touched;\n }\n\n #scoops() {\n const cone = this.#cone;\n if (cone === undefined) return [];\n return Object.entries(this.#agents?.scoops ?? {}).filter(\n ([, record]) => record.cone === cone && live(record)\n );\n }\n\n #busy(id: number): boolean {\n return this.#running.has(id) || this.#waiting.has(id);\n }\n\n #lands(target: string, channel: string): number | undefined {\n const state = this.#agents;\n if (!state) return undefined;\n const active = state.cones[state.active]?.conversation;\n if (target === 'cone') return active;\n if (target.startsWith('cone:'))\n return state.cones[target.slice('cone:'.length)]?.conversation ?? active;\n if (channel === 'webhook') return active;\n const scoop = state.scoops[target];\n return scoop && live(scoop) ? scoop.conversation : active;\n }\n\n #pending(ours: Set<number>): boolean {\n const cone = this.#cone;\n if (cone !== undefined && Object.values(this.#work?.waits ?? {}).some((w) => w.cone === cone))\n return true;\n return Object.entries(this.#outbox?.queues ?? {}).some(([key, queue]) => {\n if (!queue.pending) return false;\n const lands = this.#lands(key.split('|')[0] as string, queue.pending.lick.channel);\n return lands !== undefined && ours.has(lands);\n });\n }\n\n value(): BusyState {\n const why: string[] = [];\n if (this.#busy(this.#home)) why.push('cone');\n const scoops = this.#scoops();\n const feeds = Object.values(this.#work?.feeds ?? {});\n for (const [id, record] of scoops)\n if (this.#busy(record.conversation) || feeds.some((feed) => feed.scoop === id))\n why.push(`scoop:${record.name}`);\n const ours = new Set([this.#home, ...scoops.map(([, record]) => record.conversation)]);\n if (this.#pending(ours)) why.push('pending-lick');\n return { busy: why.length > 0, why };\n }\n}\n\nexport async function trackBusy(\n harness: Harness,\n conversation: number,\n context: Context\n): Promise<{ tracker: BusyTracker; listen(listener: () => void): void; stop(): void }> {\n const tracker = new BusyTracker(conversation);\n const buffer: CommitPublication[] = [];\n let ready = false;\n let listener: () => void = () => undefined;\n const stop = harness.subscribeCommits((publication) => {\n if (!ready) buffer.push(publication);\n else if (tracker.apply(publication)) listener();\n });\n try {\n const [agents, work, outbox] = await Promise.all([\n harness.snapshot(AgentsDoc, context),\n harness.snapshot(ScoopWorkDoc, context),\n harness.snapshot(LicksOutbox, context),\n ]);\n tracker.seed(agents, work, outbox);\n await Promise.all(\n tracker.conversations().map(async (id) => {\n const [liveDoc, inboxDoc] = await Promise.all([\n harness.snapshot(LiveDoc, id as ConversationId, context),\n harness.snapshot(InboxDoc, id as ConversationId, context),\n ]);\n tracker.conversation(id, liveDoc, inboxDoc);\n })\n );\n } catch (error) {\n stop();\n throw error;\n }\n for (const publication of buffer.splice(0)) tracker.apply(publication);\n ready = true;\n return {\n tracker,\n listen(next) {\n listener = next;\n },\n stop,\n };\n}\n\nexport function busyLine(state: BusyState): RemoteEvent {\n return state.busy ? { t: 'busy', busy: true, why: state.why } : { t: 'busy', busy: false };\n}\n\nexport class BusyGate {\n readonly #tracker: BusyTracker;\n readonly #push: (events: RemoteEvent[]) => void;\n #sent: boolean;\n #timer: ReturnType<typeof setTimeout> | undefined;\n\n constructor(tracker: BusyTracker, push: (events: RemoteEvent[]) => void, sent: boolean) {\n this.#tracker = tracker;\n this.#push = push;\n this.#sent = sent;\n }\n\n before(events: RemoteEvent[]): RemoteEvent[] {\n if (this.#sent || !events.some((event) => event.t === 'run' && event.state === 'start'))\n return events;\n this.#sent = true;\n const now = this.#tracker.value();\n this.changed();\n return [busyLine(now.busy ? now : { busy: true, why: ['cone'] }), ...events];\n }\n\n changed(): void {\n if (this.#timer !== undefined || this.#tracker.value().busy === this.#sent) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const now = this.#tracker.value();\n if (now.busy === this.#sent) return;\n this.#sent = now.busy;\n this.#push([busyLine(now)]);\n }, 0);\n }\n\n stop(): void {\n clearTimeout(this.#timer);\n this.#timer = undefined;\n }\n}\n\nexport async function watchBusy(\n harness: Harness,\n watched: { conversation: { id: number }; snapshot: SnapshotEvent; stop(): Promise<unknown> },\n push: (events: RemoteEvent[]) => void,\n context: Context\n): Promise<{ opened: boolean; gate: BusyGate; stop(): void }> {\n const busy = await trackBusy(harness, watched.conversation.id, context).catch(async (error) => {\n await watched.stop();\n throw error;\n });\n const opened = busy.tracker.value().busy || snapshotBusy(watched.snapshot);\n const gate = new BusyGate(busy.tracker, push, opened);\n busy.listen(() => gate.changed());\n gate.changed();\n return {\n opened,\n gate,\n stop() {\n busy.stop();\n gate.stop();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["live"]
7
+ }
@@ -61,14 +61,14 @@ function remoteControl(connection, options = {}) {
61
61
  events: events(service, result.stream)
62
62
  };
63
63
  },
64
- async watch({ since = null, thinking = false } = {}) {
64
+ async watch({ since = null, thinking = false, busy = false } = {}) {
65
65
  const service = need();
66
- const result = opened(
67
- await service.watch({ agentId: agent(), since, thinking }, BACKGROUND_CONTEXT)
68
- );
66
+ const request = { agentId: agent(), since, thinking, ...busy ? { busy: true } : {} };
67
+ const result = opened(await service.watch(request, BACKGROUND_CONTEXT));
69
68
  return {
70
69
  head: result.head ?? "",
71
70
  ...result.gap ? { gap: true } : {},
71
+ ...typeof result.busy === "boolean" ? { busy: result.busy } : {},
72
72
  events: events(service, result.stream)
73
73
  };
74
74
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/remote/client.ts"],
4
- "sourcesContent": ["import { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { SendAttachment } from '../attachments.ts';\nimport type { AgentConnection } from '../client.ts';\nimport type { Delivered, SendMode } from '../services.ts';\nimport type { RemoteEvent } from './events.ts';\nimport type { AgentRemote, RemoteOpened } from './host.ts';\n\nexport type { RemoteEvent } from './events.ts';\n\nexport const POLL_MS = 25000;\n\nexport interface RemoteOptions {\n agent?: () => string | null;\n}\n\nexport interface RemoteControl {\n prompt(request: {\n text: string;\n whenBusy?: SendMode;\n thinking?: boolean;\n attachments?: SendAttachment[];\n }): Promise<{ delivered: Delivered; events: AsyncIterable<RemoteEvent> }>;\n watch(request?: {\n since?: string | null;\n thinking?: boolean;\n }): Promise<{ head: string; gap?: true; events: AsyncIterable<RemoteEvent> }>;\n abort(): Promise<{ aborted: boolean }>;\n}\n\nfunction failure(code: string, message: string): Error {\n return Object.assign(new Error(`${code}: ${message}`), { code });\n}\n\nfunction opened(result: RemoteOpened): Extract<RemoteOpened, { ok: true }> {\n if (!result.ok) throw failure(result.code, result.message);\n return result;\n}\n\nfunction events(remote: AgentRemote, stream: string): AsyncIterable<RemoteEvent> {\n return {\n [Symbol.asyncIterator]() {\n let buffer: RemoteEvent[] = [];\n let done = false;\n let closed = false;\n const close = async () => {\n if (closed) return;\n closed = true;\n await remote.close(stream, BACKGROUND_CONTEXT).catch(() => undefined);\n };\n return {\n async next(): Promise<IteratorResult<RemoteEvent>> {\n while (!buffer.length && !done && !closed) {\n const batch = await remote.next(stream, POLL_MS, BACKGROUND_CONTEXT);\n buffer = batch.events;\n done = batch.done;\n }\n const value = buffer.shift();\n if (value) return { value, done: false };\n await close();\n return { value: undefined, done: true };\n },\n async return(): Promise<IteratorResult<RemoteEvent>> {\n await close();\n return { value: undefined, done: true };\n },\n };\n },\n };\n}\n\nexport function remoteControl(\n connection: Pick<AgentConnection, 'remote'>,\n options: RemoteOptions = {}\n): RemoteControl {\n const remote = connection.remote;\n const agent = () => options.agent?.() ?? null;\n const need = (): AgentRemote => {\n if (!remote) throw failure('ENOSYS', 'this agent has no remote control');\n return remote;\n };\n return {\n async prompt({ text, whenBusy = 'followUp', thinking = false, attachments }) {\n const service = need();\n const request = {\n agentId: agent(),\n text,\n whenBusy,\n thinking,\n ...(attachments?.length ? { attachments } : {}),\n };\n const result = opened(await service.prompt(request, BACKGROUND_CONTEXT));\n return {\n delivered: result.delivered as Delivered,\n events: events(service, result.stream),\n };\n },\n async watch({ since = null, thinking = false } = {}) {\n const service = need();\n const result = opened(\n await service.watch({ agentId: agent(), since, thinking }, BACKGROUND_CONTEXT)\n );\n return {\n head: result.head ?? '',\n ...(result.gap ? { gap: true as const } : {}),\n events: events(service, result.stream),\n };\n },\n async abort() {\n return need().abort(agent(), BACKGROUND_CONTEXT);\n },\n };\n}\n"],
5
- "mappings": "AAAA,SAAS,0BAA0B;AAS5B,MAAM,UAAU;AAoBvB,SAAS,QAAQ,MAAc,SAAwB;AACrD,SAAO,OAAO,OAAO,IAAI,MAAM,GAAG,IAAI,KAAK,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC;AACjE;AAEA,SAAS,OAAO,QAA2D;AACzE,MAAI,CAAC,OAAO,GAAI,OAAM,QAAQ,OAAO,MAAM,OAAO,OAAO;AACzD,SAAO;AACT;AAEA,SAAS,OAAO,QAAqB,QAA4C;AAC/E,SAAO;AAAA,IACL,CAAC,OAAO,aAAa,IAAI;AACvB,UAAI,SAAwB,CAAC;AAC7B,UAAI,OAAO;AACX,UAAI,SAAS;AACb,YAAM,QAAQ,YAAY;AACxB,YAAI,OAAQ;AACZ,iBAAS;AACT,cAAM,OAAO,MAAM,QAAQ,kBAAkB,EAAE,MAAM,MAAM,MAAS;AAAA,MACtE;AACA,aAAO;AAAA,QACL,MAAM,OAA6C;AACjD,iBAAO,CAAC,OAAO,UAAU,CAAC,QAAQ,CAAC,QAAQ;AACzC,kBAAM,QAAQ,MAAM,OAAO,KAAK,QAAQ,SAAS,kBAAkB;AACnE,qBAAS,MAAM;AACf,mBAAO,MAAM;AAAA,UACf;AACA,gBAAM,QAAQ,OAAO,MAAM;AAC3B,cAAI,MAAO,QAAO,EAAE,OAAO,MAAM,MAAM;AACvC,gBAAM,MAAM;AACZ,iBAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,QACxC;AAAA,QACA,MAAM,SAA+C;AACnD,gBAAM,MAAM;AACZ,iBAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,cACd,YACA,UAAyB,CAAC,GACX;AACf,QAAM,SAAS,WAAW;AAC1B,QAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK;AACzC,QAAM,OAAO,MAAmB;AAC9B,QAAI,CAAC,OAAQ,OAAM,QAAQ,UAAU,kCAAkC;AACvE,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,MAAM,OAAO,EAAE,MAAM,WAAW,YAAY,WAAW,OAAO,YAAY,GAAG;AAC3E,YAAM,UAAU,KAAK;AACrB,YAAM,UAAU;AAAA,QACd,SAAS,MAAM;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA,GAAI,aAAa,SAAS,EAAE,YAAY,IAAI,CAAC;AAAA,MAC/C;AACA,YAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,SAAS,kBAAkB,CAAC;AACvE,aAAO;AAAA,QACL,WAAW,OAAO;AAAA,QAClB,QAAQ,OAAO,SAAS,OAAO,MAAM;AAAA,MACvC;AAAA,IACF;AAAA,IACA,MAAM,MAAM,EAAE,QAAQ,MAAM,WAAW,MAAM,IAAI,CAAC,GAAG;AACnD,YAAM,UAAU,KAAK;AACrB,YAAM,SAAS;AAAA,QACb,MAAM,QAAQ,MAAM,EAAE,SAAS,MAAM,GAAG,OAAO,SAAS,GAAG,kBAAkB;AAAA,MAC/E;AACA,aAAO;AAAA,QACL,MAAM,OAAO,QAAQ;AAAA,QACrB,GAAI,OAAO,MAAM,EAAE,KAAK,KAAc,IAAI,CAAC;AAAA,QAC3C,QAAQ,OAAO,SAAS,OAAO,MAAM;AAAA,MACvC;AAAA,IACF;AAAA,IACA,MAAM,QAAQ;AACZ,aAAO,KAAK,EAAE,MAAM,MAAM,GAAG,kBAAkB;AAAA,IACjD;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { SendAttachment } from '../attachments.ts';\nimport type { AgentConnection } from '../client.ts';\nimport type { Delivered, SendMode } from '../services.ts';\nimport type { RemoteEvent } from './events.ts';\nimport type { AgentRemote, RemoteOpened } from './host.ts';\n\nexport type { RemoteEvent } from './events.ts';\n\nexport const POLL_MS = 25000;\n\nexport interface RemoteOptions {\n agent?: () => string | null;\n}\n\nexport interface RemoteControl {\n prompt(request: {\n text: string;\n whenBusy?: SendMode;\n thinking?: boolean;\n attachments?: SendAttachment[];\n }): Promise<{ delivered: Delivered; events: AsyncIterable<RemoteEvent> }>;\n watch(request?: {\n since?: string | null;\n thinking?: boolean;\n busy?: boolean;\n }): Promise<{ head: string; gap?: true; busy?: boolean; events: AsyncIterable<RemoteEvent> }>;\n abort(): Promise<{ aborted: boolean }>;\n}\n\nfunction failure(code: string, message: string): Error {\n return Object.assign(new Error(`${code}: ${message}`), { code });\n}\n\nfunction opened(result: RemoteOpened): Extract<RemoteOpened, { ok: true }> {\n if (!result.ok) throw failure(result.code, result.message);\n return result;\n}\n\nfunction events(remote: AgentRemote, stream: string): AsyncIterable<RemoteEvent> {\n return {\n [Symbol.asyncIterator]() {\n let buffer: RemoteEvent[] = [];\n let done = false;\n let closed = false;\n const close = async () => {\n if (closed) return;\n closed = true;\n await remote.close(stream, BACKGROUND_CONTEXT).catch(() => undefined);\n };\n return {\n async next(): Promise<IteratorResult<RemoteEvent>> {\n while (!buffer.length && !done && !closed) {\n const batch = await remote.next(stream, POLL_MS, BACKGROUND_CONTEXT);\n buffer = batch.events;\n done = batch.done;\n }\n const value = buffer.shift();\n if (value) return { value, done: false };\n await close();\n return { value: undefined, done: true };\n },\n async return(): Promise<IteratorResult<RemoteEvent>> {\n await close();\n return { value: undefined, done: true };\n },\n };\n },\n };\n}\n\nexport function remoteControl(\n connection: Pick<AgentConnection, 'remote'>,\n options: RemoteOptions = {}\n): RemoteControl {\n const remote = connection.remote;\n const agent = () => options.agent?.() ?? null;\n const need = (): AgentRemote => {\n if (!remote) throw failure('ENOSYS', 'this agent has no remote control');\n return remote;\n };\n return {\n async prompt({ text, whenBusy = 'followUp', thinking = false, attachments }) {\n const service = need();\n const request = {\n agentId: agent(),\n text,\n whenBusy,\n thinking,\n ...(attachments?.length ? { attachments } : {}),\n };\n const result = opened(await service.prompt(request, BACKGROUND_CONTEXT));\n return {\n delivered: result.delivered as Delivered,\n events: events(service, result.stream),\n };\n },\n async watch({ since = null, thinking = false, busy = false } = {}) {\n const service = need();\n const request = { agentId: agent(), since, thinking, ...(busy ? { busy: true } : {}) };\n const result = opened(await service.watch(request, BACKGROUND_CONTEXT));\n return {\n head: result.head ?? '',\n ...(result.gap ? { gap: true as const } : {}),\n ...(typeof result.busy === 'boolean' ? { busy: result.busy } : {}),\n events: events(service, result.stream),\n };\n },\n async abort() {\n return need().abort(agent(), BACKGROUND_CONTEXT);\n },\n };\n}\n"],
5
+ "mappings": "AAAA,SAAS,0BAA0B;AAS5B,MAAM,UAAU;AAqBvB,SAAS,QAAQ,MAAc,SAAwB;AACrD,SAAO,OAAO,OAAO,IAAI,MAAM,GAAG,IAAI,KAAK,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC;AACjE;AAEA,SAAS,OAAO,QAA2D;AACzE,MAAI,CAAC,OAAO,GAAI,OAAM,QAAQ,OAAO,MAAM,OAAO,OAAO;AACzD,SAAO;AACT;AAEA,SAAS,OAAO,QAAqB,QAA4C;AAC/E,SAAO;AAAA,IACL,CAAC,OAAO,aAAa,IAAI;AACvB,UAAI,SAAwB,CAAC;AAC7B,UAAI,OAAO;AACX,UAAI,SAAS;AACb,YAAM,QAAQ,YAAY;AACxB,YAAI,OAAQ;AACZ,iBAAS;AACT,cAAM,OAAO,MAAM,QAAQ,kBAAkB,EAAE,MAAM,MAAM,MAAS;AAAA,MACtE;AACA,aAAO;AAAA,QACL,MAAM,OAA6C;AACjD,iBAAO,CAAC,OAAO,UAAU,CAAC,QAAQ,CAAC,QAAQ;AACzC,kBAAM,QAAQ,MAAM,OAAO,KAAK,QAAQ,SAAS,kBAAkB;AACnE,qBAAS,MAAM;AACf,mBAAO,MAAM;AAAA,UACf;AACA,gBAAM,QAAQ,OAAO,MAAM;AAC3B,cAAI,MAAO,QAAO,EAAE,OAAO,MAAM,MAAM;AACvC,gBAAM,MAAM;AACZ,iBAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,QACxC;AAAA,QACA,MAAM,SAA+C;AACnD,gBAAM,MAAM;AACZ,iBAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,cACd,YACA,UAAyB,CAAC,GACX;AACf,QAAM,SAAS,WAAW;AAC1B,QAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK;AACzC,QAAM,OAAO,MAAmB;AAC9B,QAAI,CAAC,OAAQ,OAAM,QAAQ,UAAU,kCAAkC;AACvE,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,MAAM,OAAO,EAAE,MAAM,WAAW,YAAY,WAAW,OAAO,YAAY,GAAG;AAC3E,YAAM,UAAU,KAAK;AACrB,YAAM,UAAU;AAAA,QACd,SAAS,MAAM;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA,GAAI,aAAa,SAAS,EAAE,YAAY,IAAI,CAAC;AAAA,MAC/C;AACA,YAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,SAAS,kBAAkB,CAAC;AACvE,aAAO;AAAA,QACL,WAAW,OAAO;AAAA,QAClB,QAAQ,OAAO,SAAS,OAAO,MAAM;AAAA,MACvC;AAAA,IACF;AAAA,IACA,MAAM,MAAM,EAAE,QAAQ,MAAM,WAAW,OAAO,OAAO,MAAM,IAAI,CAAC,GAAG;AACjE,YAAM,UAAU,KAAK;AACrB,YAAM,UAAU,EAAE,SAAS,MAAM,GAAG,OAAO,UAAU,GAAI,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,EAAG;AACrF,YAAM,SAAS,OAAO,MAAM,QAAQ,MAAM,SAAS,kBAAkB,CAAC;AACtE,aAAO;AAAA,QACL,MAAM,OAAO,QAAQ;AAAA,QACrB,GAAI,OAAO,MAAM,EAAE,KAAK,KAAc,IAAI,CAAC;AAAA,QAC3C,GAAI,OAAO,OAAO,SAAS,YAAY,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;AAAA,QAChE,QAAQ,OAAO,SAAS,OAAO,MAAM;AAAA,MACvC;AAAA,IACF;AAAA,IACA,MAAM,QAAQ;AACZ,aAAO,KAAK,EAAE,MAAM,MAAM,GAAG,kBAAkB;AAAA,IACjD;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/remote/events.ts"],
4
- "sourcesContent": ["import type { AgentEvent, EntryRecord } from '@earendil-works/pi-durable';\nimport { entryLick } from '../licks/state.ts';\nimport { mcpLabel } from '../mcp/label.ts';\nimport { redactSecrets } from '../memory/format.ts';\nimport type { Delivered } from '../services.ts';\n\nexport const SUMMARY = 200;\n\nexport type EndReason = 'stop' | 'aborted' | 'error';\n\nexport type RemoteEvent =\n | { t: 'user'; id: string; text: string; delivered: Delivered }\n | { t: 'text'; id: string; delta: string }\n | { t: 'thinking'; id: string; delta: string }\n | { t: 'tool'; id: string; call: string; name: string; summary: string }\n | { t: 'tool-result'; id: string; call: string; ok: boolean; summary: string }\n | { t: 'run'; state: 'start' | 'end'; reason?: EndReason }\n | { t: 'notice'; id: string; text: string }\n | { t: 'error'; id: string; message: string; detail?: string }\n | { t: 'end'; reason: EndReason; message?: string };\n\ntype Json = Record<string, unknown>;\n\ninterface Block {\n type: string;\n text?: string;\n thinking?: string;\n id?: string;\n name?: string;\n arguments?: Json;\n}\n\ninterface ModelMessage {\n role?: string;\n content?: string | Block[];\n timestamp?: number;\n stopReason?: string;\n errorMessage?: string;\n toolCallId?: string;\n toolName?: string;\n isError?: boolean;\n}\n\nexport function clip(text: string, limit = SUMMARY): string {\n const flat = redactSecrets(text).text.replace(/\\s+/g, ' ').trim();\n return flat.length > limit ? `${flat.slice(0, limit - 1)}\u2026` : flat;\n}\n\nfunction textOf(content: ModelMessage['content']): string {\n if (typeof content === 'string') return content;\n return (content ?? []).map((block) => (block.type === 'text' ? (block.text ?? '') : '')).join('');\n}\n\nconst SUBJECTS = ['command', 'path', 'pattern', 'url', 'query', 'code', 'name'];\n\nexport function toolSummary(name: string, args: Json | undefined): string {\n const subject = SUBJECTS.map((key) => args?.[key]).find((value) => typeof value === 'string');\n const detail = typeof subject === 'string' ? subject : JSON.stringify(args ?? {});\n return clip(`${mcpLabel(name) ?? name} ${detail}`);\n}\n\nexport function resultSummary(message: ModelMessage): string {\n const text = textOf(message.content);\n if (message.isError)\n return clip(\n text.split('\\n').find((line) => line.trim() && !/^<\\/?harness>$/.test(line.trim())) ??\n 'failed'\n );\n const lines = text ? text.split('\\n').length : 0;\n return lines === 1 ? '1 line' : `${lines} lines`;\n}\n\nexport function keyOf(timestamp: number | undefined, at: number): string {\n return at > 0 ? `a${timestamp ?? 0}~${at}` : `a${timestamp ?? 0}`;\n}\n\nfunction reasonOf(stopReason: string | undefined): EndReason {\n if (stopReason === 'aborted') return 'aborted';\n return stopReason === 'error' ? 'error' : 'stop';\n}\n\nexport class Mapper {\n readonly #thinking: boolean;\n readonly #emitted = new Map<string, number>();\n readonly #seen = new Set<number>();\n #current: string | undefined;\n #previous: string | undefined;\n #ending: readonly number[] = [];\n readonly #stamps = new Map<number, number[]>();\n reason: EndReason = 'stop';\n error: string | undefined;\n\n constructor(thinking: boolean) {\n this.#thinking = thinking;\n }\n\n #key(message: ModelMessage | undefined, entry?: number): string {\n const stamp = message?.timestamp ?? 0;\n const list = this.#stamps.get(stamp) ?? [];\n this.#stamps.set(stamp, list);\n if (entry === undefined) return keyOf(stamp, list.length);\n if (!list.includes(entry)) list.push(entry);\n return keyOf(stamp, list.indexOf(entry));\n }\n\n #delta(t: 'text' | 'thinking', id: string, full: string): RemoteEvent[] {\n if (t === 'thinking' && !this.#thinking) return [];\n const done = this.#emitted.get(id) ?? 0;\n if (full.length <= done) return [];\n this.#emitted.set(id, full.length);\n return [{ t, id, delta: full.slice(done) }];\n }\n\n partial(message: ModelMessage | undefined): RemoteEvent[] {\n if (!message || !Array.isArray(message.content)) return [];\n const key = this.#key(message);\n this.#current = key;\n return message.content.flatMap((block, at) => {\n if (block.type === 'text') return this.#delta('text', `${key}.${at}`, block.text ?? '');\n if (block.type === 'thinking')\n return this.#delta('thinking', `${key}.${at}`, block.thinking ?? '');\n return [];\n });\n }\n\n #assistant(message: ModelMessage, entry: EntryRecord): RemoteEvent[] {\n const key = this.#key(message, entry.id);\n const blocks = Array.isArray(message.content) ? message.content : [];\n const out = blocks.flatMap((block, at): RemoteEvent[] => {\n const id = `${key}.${at}`;\n if (block.type === 'text') return this.#delta('text', id, block.text ?? '');\n if (block.type === 'thinking') return this.#delta('thinking', id, block.thinking ?? '');\n if (block.type !== 'toolCall') return [];\n const name = block.name ?? 'tool';\n return [\n { t: 'tool', id, call: block.id ?? '', name, summary: toolSummary(name, block.arguments) },\n ];\n });\n if (typeof message.content === 'string')\n out.push(...this.#delta('text', `${key}.0`, message.content));\n this.reason = reasonOf(message.stopReason);\n this.error =\n message.stopReason === 'error' ? clip(message.errorMessage ?? 'error', 1000) : undefined;\n if (this.error) out.push({ t: 'error', id: String(entry.id), message: this.error });\n return out;\n }\n\n #delivered(entry: EntryRecord): Delivered {\n if (entry.byTaskId === undefined) return 'run';\n return this.#previous === 'pi.tool-result' ? 'steer' : 'follow-up';\n }\n\n entry(entry: EntryRecord, delivered: Delivered = this.#delivered(entry)): RemoteEvent[] {\n if (this.#seen.has(entry.id)) return [];\n this.#seen.add(entry.id);\n this.#previous = entry.kind;\n const id = String(entry.id);\n const message = entry.model?.[0] as ModelMessage | undefined;\n if (entry.kind === 'pi.user' && message) {\n const lick = entryLick(entry);\n if (lick) return [{ t: 'notice', id, text: clip(`lick ${lick.channel}: ${lick.title}`) }];\n return [{ t: 'user', id, text: redactSecrets(textOf(message.content)).text, delivered }];\n }\n if (entry.kind === 'pi.assistant' && message) return this.#assistant(message, entry);\n if (entry.kind === 'pi.tool-result' && message)\n return [\n {\n t: 'tool-result',\n id,\n call: message.toolCallId ?? '',\n ok: !message.isError,\n summary: resultSummary(message),\n },\n ];\n if (entry.kind === 'pi.compaction') return [{ t: 'notice', id, text: 'compacted' }];\n if (entry.kind === 'slicc.rewound') return [{ t: 'notice', id, text: 'rewound' }];\n return [];\n }\n\n #ended(reason: EndReason): RemoteEvent[] {\n if (!this.#ending.length) return [];\n this.#ending = [];\n this.reason = reason;\n return [{ t: 'run', state: 'end', reason }];\n }\n\n #settled(record: { id: number; status: string; reason?: unknown }): RemoteEvent[] {\n if (!this.#ending.includes(record.id) || ['queued', 'placed'].includes(record.status))\n return [];\n if (record.status !== 'unanswered') return this.#ended(this.reason);\n if (record.reason === 'aborted') return this.#ended('aborted');\n this.error ??= clip(String(record.reason ?? 'error'), 1000);\n return this.#ended('error');\n }\n\n last(): number {\n return Math.max(0, ...this.#seen);\n }\n\n event(event: AgentEvent): RemoteEvent[] {\n switch (event.type) {\n case 'run_start': {\n const ended = this.#ended(this.reason);\n this.reason = 'stop';\n this.error = undefined;\n return [...ended, { t: 'run', state: 'start' }];\n }\n case 'run_end':\n this.#ending = event.inputs;\n return [];\n case 'submission':\n return this.#settled(event.record);\n case 'message_start':\n if ((event.message as ModelMessage).role === 'assistant')\n this.#current = this.#key(event.message as ModelMessage);\n return [];\n case 'message_update':\n return event.changes.flatMap((change) => {\n if (change.type !== 'text_delta' && change.type !== 'thinking_delta') return [];\n const id = `${this.#current}.${change.contentIndex}`;\n const t = change.type === 'text_delta' ? 'text' : 'thinking';\n const done = this.#emitted.get(id) ?? 0;\n if (t === 'thinking' && !this.#thinking) return [];\n this.#emitted.set(id, done + change.delta.length);\n return [{ t, id, delta: change.delta }];\n });\n case 'message_end':\n return this.entry(event.entry);\n case 'tool_execution_end':\n return event.entry\n ? this.entry(event.entry)\n : [\n {\n t: 'tool-result',\n id: `x${event.toolCallId}`,\n call: event.toolCallId,\n ok: false,\n summary: 'the tool did not finish',\n },\n ];\n case 'auto_retry_start':\n return [\n {\n t: 'notice',\n id: `retry-${event.at}`,\n text: clip(`retrying (attempt ${event.attempt}): ${event.errorMessage}`),\n },\n ];\n case 'compaction_start':\n return [{ t: 'notice', id: `compaction-${event.taskId}`, text: 'compacting' }];\n case 'task_failed':\n return [{ t: 'error', id: `task-${event.taskId}`, message: clip(event.message, 1000) }];\n default:\n return [];\n }\n }\n}\n\nexport function replayOf(\n entries: readonly EntryRecord[],\n partial: ModelMessage | undefined,\n thinking: boolean\n): RemoteEvent[] {\n const mapper = new Mapper(thinking);\n const out: RemoteEvent[] = [];\n for (const entry of entries) out.push(...mapper.entry(entry));\n out.push(...mapper.partial(partial));\n return out;\n}\n\nexport function idOf(event: RemoteEvent): string | undefined {\n return 'id' in event ? event.id : undefined;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AgentEvent, EntryRecord } from '@earendil-works/pi-durable';\nimport { entryLick } from '../licks/state.ts';\nimport { mcpLabel } from '../mcp/label.ts';\nimport { redactSecrets } from '../memory/format.ts';\nimport type { Delivered } from '../services.ts';\n\nexport const SUMMARY = 200;\n\nexport type EndReason = 'stop' | 'aborted' | 'error';\n\nexport type RemoteEvent =\n | { t: 'user'; id: string; text: string; delivered: Delivered }\n | { t: 'text'; id: string; delta: string }\n | { t: 'thinking'; id: string; delta: string }\n | { t: 'tool'; id: string; call: string; name: string; summary: string }\n | { t: 'tool-result'; id: string; call: string; ok: boolean; summary: string }\n | { t: 'run'; state: 'start' | 'end'; reason?: EndReason }\n | { t: 'notice'; id: string; text: string }\n | { t: 'error'; id: string; message: string; detail?: string }\n | { t: 'busy'; busy: boolean; why?: string[] }\n | { t: 'end'; reason: EndReason; message?: string };\n\ntype Json = Record<string, unknown>;\n\ninterface Block {\n type: string;\n text?: string;\n thinking?: string;\n id?: string;\n name?: string;\n arguments?: Json;\n}\n\ninterface ModelMessage {\n role?: string;\n content?: string | Block[];\n timestamp?: number;\n stopReason?: string;\n errorMessage?: string;\n toolCallId?: string;\n toolName?: string;\n isError?: boolean;\n}\n\nexport function clip(text: string, limit = SUMMARY): string {\n const flat = redactSecrets(text).text.replace(/\\s+/g, ' ').trim();\n return flat.length > limit ? `${flat.slice(0, limit - 1)}\u2026` : flat;\n}\n\nfunction textOf(content: ModelMessage['content']): string {\n if (typeof content === 'string') return content;\n return (content ?? []).map((block) => (block.type === 'text' ? (block.text ?? '') : '')).join('');\n}\n\nconst SUBJECTS = ['command', 'path', 'pattern', 'url', 'query', 'code', 'name'];\n\nexport function toolSummary(name: string, args: Json | undefined): string {\n const subject = SUBJECTS.map((key) => args?.[key]).find((value) => typeof value === 'string');\n const detail = typeof subject === 'string' ? subject : JSON.stringify(args ?? {});\n return clip(`${mcpLabel(name) ?? name} ${detail}`);\n}\n\nexport function resultSummary(message: ModelMessage): string {\n const text = textOf(message.content);\n if (message.isError)\n return clip(\n text.split('\\n').find((line) => line.trim() && !/^<\\/?harness>$/.test(line.trim())) ??\n 'failed'\n );\n const lines = text ? text.split('\\n').length : 0;\n return lines === 1 ? '1 line' : `${lines} lines`;\n}\n\nexport function keyOf(timestamp: number | undefined, at: number): string {\n return at > 0 ? `a${timestamp ?? 0}~${at}` : `a${timestamp ?? 0}`;\n}\n\nfunction reasonOf(stopReason: string | undefined): EndReason {\n if (stopReason === 'aborted') return 'aborted';\n return stopReason === 'error' ? 'error' : 'stop';\n}\n\nexport class Mapper {\n readonly #thinking: boolean;\n readonly #emitted = new Map<string, number>();\n readonly #seen = new Set<number>();\n #current: string | undefined;\n #previous: string | undefined;\n #ending: readonly number[] = [];\n readonly #stamps = new Map<number, number[]>();\n reason: EndReason = 'stop';\n error: string | undefined;\n\n constructor(thinking: boolean) {\n this.#thinking = thinking;\n }\n\n #key(message: ModelMessage | undefined, entry?: number): string {\n const stamp = message?.timestamp ?? 0;\n const list = this.#stamps.get(stamp) ?? [];\n this.#stamps.set(stamp, list);\n if (entry === undefined) return keyOf(stamp, list.length);\n if (!list.includes(entry)) list.push(entry);\n return keyOf(stamp, list.indexOf(entry));\n }\n\n #delta(t: 'text' | 'thinking', id: string, full: string): RemoteEvent[] {\n if (t === 'thinking' && !this.#thinking) return [];\n const done = this.#emitted.get(id) ?? 0;\n if (full.length <= done) return [];\n this.#emitted.set(id, full.length);\n return [{ t, id, delta: full.slice(done) }];\n }\n\n partial(message: ModelMessage | undefined): RemoteEvent[] {\n if (!message || !Array.isArray(message.content)) return [];\n const key = this.#key(message);\n this.#current = key;\n return message.content.flatMap((block, at) => {\n if (block.type === 'text') return this.#delta('text', `${key}.${at}`, block.text ?? '');\n if (block.type === 'thinking')\n return this.#delta('thinking', `${key}.${at}`, block.thinking ?? '');\n return [];\n });\n }\n\n #assistant(message: ModelMessage, entry: EntryRecord): RemoteEvent[] {\n const key = this.#key(message, entry.id);\n const blocks = Array.isArray(message.content) ? message.content : [];\n const out = blocks.flatMap((block, at): RemoteEvent[] => {\n const id = `${key}.${at}`;\n if (block.type === 'text') return this.#delta('text', id, block.text ?? '');\n if (block.type === 'thinking') return this.#delta('thinking', id, block.thinking ?? '');\n if (block.type !== 'toolCall') return [];\n const name = block.name ?? 'tool';\n return [\n { t: 'tool', id, call: block.id ?? '', name, summary: toolSummary(name, block.arguments) },\n ];\n });\n if (typeof message.content === 'string')\n out.push(...this.#delta('text', `${key}.0`, message.content));\n this.reason = reasonOf(message.stopReason);\n this.error =\n message.stopReason === 'error' ? clip(message.errorMessage ?? 'error', 1000) : undefined;\n if (this.error) out.push({ t: 'error', id: String(entry.id), message: this.error });\n return out;\n }\n\n #delivered(entry: EntryRecord): Delivered {\n if (entry.byTaskId === undefined) return 'run';\n return this.#previous === 'pi.tool-result' ? 'steer' : 'follow-up';\n }\n\n entry(entry: EntryRecord, delivered: Delivered = this.#delivered(entry)): RemoteEvent[] {\n if (this.#seen.has(entry.id)) return [];\n this.#seen.add(entry.id);\n this.#previous = entry.kind;\n const id = String(entry.id);\n const message = entry.model?.[0] as ModelMessage | undefined;\n if (entry.kind === 'pi.user' && message) {\n const lick = entryLick(entry);\n if (lick) return [{ t: 'notice', id, text: clip(`lick ${lick.channel}: ${lick.title}`) }];\n return [{ t: 'user', id, text: redactSecrets(textOf(message.content)).text, delivered }];\n }\n if (entry.kind === 'pi.assistant' && message) return this.#assistant(message, entry);\n if (entry.kind === 'pi.tool-result' && message)\n return [\n {\n t: 'tool-result',\n id,\n call: message.toolCallId ?? '',\n ok: !message.isError,\n summary: resultSummary(message),\n },\n ];\n if (entry.kind === 'pi.compaction') return [{ t: 'notice', id, text: 'compacted' }];\n if (entry.kind === 'slicc.rewound') return [{ t: 'notice', id, text: 'rewound' }];\n return [];\n }\n\n #ended(reason: EndReason): RemoteEvent[] {\n if (!this.#ending.length) return [];\n this.#ending = [];\n this.reason = reason;\n return [{ t: 'run', state: 'end', reason }];\n }\n\n #settled(record: { id: number; status: string; reason?: unknown }): RemoteEvent[] {\n if (!this.#ending.includes(record.id) || ['queued', 'placed'].includes(record.status))\n return [];\n if (record.status !== 'unanswered') return this.#ended(this.reason);\n if (record.reason === 'aborted') return this.#ended('aborted');\n this.error ??= clip(String(record.reason ?? 'error'), 1000);\n return this.#ended('error');\n }\n\n last(): number {\n return Math.max(0, ...this.#seen);\n }\n\n event(event: AgentEvent): RemoteEvent[] {\n switch (event.type) {\n case 'run_start': {\n const ended = this.#ended(this.reason);\n this.reason = 'stop';\n this.error = undefined;\n return [...ended, { t: 'run', state: 'start' }];\n }\n case 'run_end':\n this.#ending = event.inputs;\n return [];\n case 'submission':\n return this.#settled(event.record);\n case 'message_start':\n if ((event.message as ModelMessage).role === 'assistant')\n this.#current = this.#key(event.message as ModelMessage);\n return [];\n case 'message_update':\n return event.changes.flatMap((change) => {\n if (change.type !== 'text_delta' && change.type !== 'thinking_delta') return [];\n const id = `${this.#current}.${change.contentIndex}`;\n const t = change.type === 'text_delta' ? 'text' : 'thinking';\n const done = this.#emitted.get(id) ?? 0;\n if (t === 'thinking' && !this.#thinking) return [];\n this.#emitted.set(id, done + change.delta.length);\n return [{ t, id, delta: change.delta }];\n });\n case 'message_end':\n return this.entry(event.entry);\n case 'tool_execution_end':\n return event.entry\n ? this.entry(event.entry)\n : [\n {\n t: 'tool-result',\n id: `x${event.toolCallId}`,\n call: event.toolCallId,\n ok: false,\n summary: 'the tool did not finish',\n },\n ];\n case 'auto_retry_start':\n return [\n {\n t: 'notice',\n id: `retry-${event.at}`,\n text: clip(`retrying (attempt ${event.attempt}): ${event.errorMessage}`),\n },\n ];\n case 'compaction_start':\n return [{ t: 'notice', id: `compaction-${event.taskId}`, text: 'compacting' }];\n case 'task_failed':\n return [{ t: 'error', id: `task-${event.taskId}`, message: clip(event.message, 1000) }];\n default:\n return [];\n }\n }\n}\n\nexport function replayOf(\n entries: readonly EntryRecord[],\n partial: ModelMessage | undefined,\n thinking: boolean\n): RemoteEvent[] {\n const mapper = new Mapper(thinking);\n const out: RemoteEvent[] = [];\n for (const entry of entries) out.push(...mapper.entry(entry));\n out.push(...mapper.partial(partial));\n return out;\n}\n\nexport function idOf(event: RemoteEvent): string | undefined {\n return 'id' in event ? event.id : undefined;\n}\n"],
5
+ "mappings": "AACA,SAAS,iBAAiB;AAC1B,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAGvB,MAAM,UAAU;AAsChB,SAAS,KAAK,MAAc,QAAQ,SAAiB;AAC1D,QAAM,OAAO,cAAc,IAAI,EAAE,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAChE,SAAO,KAAK,SAAS,QAAQ,GAAG,KAAK,MAAM,GAAG,QAAQ,CAAC,CAAC,WAAM;AAChE;AAEA,SAAS,OAAO,SAA0C;AACxD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,UAAQ,WAAW,CAAC,GAAG,IAAI,CAAC,UAAW,MAAM,SAAS,SAAU,MAAM,QAAQ,KAAM,EAAG,EAAE,KAAK,EAAE;AAClG;AAEA,MAAM,WAAW,CAAC,WAAW,QAAQ,WAAW,OAAO,SAAS,QAAQ,MAAM;AAEvE,SAAS,YAAY,MAAc,MAAgC;AACxE,QAAM,UAAU,SAAS,IAAI,CAAC,QAAQ,OAAO,GAAG,CAAC,EAAE,KAAK,CAAC,UAAU,OAAO,UAAU,QAAQ;AAC5F,QAAM,SAAS,OAAO,YAAY,WAAW,UAAU,KAAK,UAAU,QAAQ,CAAC,CAAC;AAChF,SAAO,KAAK,GAAG,SAAS,IAAI,KAAK,IAAI,IAAI,MAAM,EAAE;AACnD;AAEO,SAAS,cAAc,SAA+B;AAC3D,QAAM,OAAO,OAAO,QAAQ,OAAO;AACnC,MAAI,QAAQ;AACV,WAAO;AAAA,MACL,KAAK,MAAM,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,KAAK,CAAC,iBAAiB,KAAK,KAAK,KAAK,CAAC,CAAC,KAChF;AAAA,IACJ;AACF,QAAM,QAAQ,OAAO,KAAK,MAAM,IAAI,EAAE,SAAS;AAC/C,SAAO,UAAU,IAAI,WAAW,GAAG,KAAK;AAC1C;AAEO,SAAS,MAAM,WAA+B,IAAoB;AACvE,SAAO,KAAK,IAAI,IAAI,aAAa,CAAC,IAAI,EAAE,KAAK,IAAI,aAAa,CAAC;AACjE;AAEA,SAAS,SAAS,YAA2C;AAC3D,MAAI,eAAe,UAAW,QAAO;AACrC,SAAO,eAAe,UAAU,UAAU;AAC5C;AAEO,MAAM,OAAO;AAAA,EACT;AAAA,EACA,WAAW,oBAAI,IAAoB;AAAA,EACnC,QAAQ,oBAAI,IAAY;AAAA,EACjC;AAAA,EACA;AAAA,EACA,UAA6B,CAAC;AAAA,EACrB,UAAU,oBAAI,IAAsB;AAAA,EAC7C,SAAoB;AAAA,EACpB;AAAA,EAEA,YAAY,UAAmB;AAC7B,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,KAAK,SAAmC,OAAwB;AAC9D,UAAM,QAAQ,SAAS,aAAa;AACpC,UAAM,OAAO,KAAK,QAAQ,IAAI,KAAK,KAAK,CAAC;AACzC,SAAK,QAAQ,IAAI,OAAO,IAAI;AAC5B,QAAI,UAAU,OAAW,QAAO,MAAM,OAAO,KAAK,MAAM;AACxD,QAAI,CAAC,KAAK,SAAS,KAAK,EAAG,MAAK,KAAK,KAAK;AAC1C,WAAO,MAAM,OAAO,KAAK,QAAQ,KAAK,CAAC;AAAA,EACzC;AAAA,EAEA,OAAO,GAAwB,IAAY,MAA6B;AACtE,QAAI,MAAM,cAAc,CAAC,KAAK,UAAW,QAAO,CAAC;AACjD,UAAM,OAAO,KAAK,SAAS,IAAI,EAAE,KAAK;AACtC,QAAI,KAAK,UAAU,KAAM,QAAO,CAAC;AACjC,SAAK,SAAS,IAAI,IAAI,KAAK,MAAM;AACjC,WAAO,CAAC,EAAE,GAAG,IAAI,OAAO,KAAK,MAAM,IAAI,EAAE,CAAC;AAAA,EAC5C;AAAA,EAEA,QAAQ,SAAkD;AACxD,QAAI,CAAC,WAAW,CAAC,MAAM,QAAQ,QAAQ,OAAO,EAAG,QAAO,CAAC;AACzD,UAAM,MAAM,KAAK,KAAK,OAAO;AAC7B,SAAK,WAAW;AAChB,WAAO,QAAQ,QAAQ,QAAQ,CAAC,OAAO,OAAO;AAC5C,UAAI,MAAM,SAAS,OAAQ,QAAO,KAAK,OAAO,QAAQ,GAAG,GAAG,IAAI,EAAE,IAAI,MAAM,QAAQ,EAAE;AACtF,UAAI,MAAM,SAAS;AACjB,eAAO,KAAK,OAAO,YAAY,GAAG,GAAG,IAAI,EAAE,IAAI,MAAM,YAAY,EAAE;AACrE,aAAO,CAAC;AAAA,IACV,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,SAAuB,OAAmC;AACnE,UAAM,MAAM,KAAK,KAAK,SAAS,MAAM,EAAE;AACvC,UAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO,IAAI,QAAQ,UAAU,CAAC;AACnE,UAAM,MAAM,OAAO,QAAQ,CAAC,OAAO,OAAsB;AACvD,YAAM,KAAK,GAAG,GAAG,IAAI,EAAE;AACvB,UAAI,MAAM,SAAS,OAAQ,QAAO,KAAK,OAAO,QAAQ,IAAI,MAAM,QAAQ,EAAE;AAC1E,UAAI,MAAM,SAAS,WAAY,QAAO,KAAK,OAAO,YAAY,IAAI,MAAM,YAAY,EAAE;AACtF,UAAI,MAAM,SAAS,WAAY,QAAO,CAAC;AACvC,YAAM,OAAO,MAAM,QAAQ;AAC3B,aAAO;AAAA,QACL,EAAE,GAAG,QAAQ,IAAI,MAAM,MAAM,MAAM,IAAI,MAAM,SAAS,YAAY,MAAM,MAAM,SAAS,EAAE;AAAA,MAC3F;AAAA,IACF,CAAC;AACD,QAAI,OAAO,QAAQ,YAAY;AAC7B,UAAI,KAAK,GAAG,KAAK,OAAO,QAAQ,GAAG,GAAG,MAAM,QAAQ,OAAO,CAAC;AAC9D,SAAK,SAAS,SAAS,QAAQ,UAAU;AACzC,SAAK,QACH,QAAQ,eAAe,UAAU,KAAK,QAAQ,gBAAgB,SAAS,GAAI,IAAI;AACjF,QAAI,KAAK,MAAO,KAAI,KAAK,EAAE,GAAG,SAAS,IAAI,OAAO,MAAM,EAAE,GAAG,SAAS,KAAK,MAAM,CAAC;AAClF,WAAO;AAAA,EACT;AAAA,EAEA,WAAW,OAA+B;AACxC,QAAI,MAAM,aAAa,OAAW,QAAO;AACzC,WAAO,KAAK,cAAc,mBAAmB,UAAU;AAAA,EACzD;AAAA,EAEA,MAAM,OAAoB,YAAuB,KAAK,WAAW,KAAK,GAAkB;AACtF,QAAI,KAAK,MAAM,IAAI,MAAM,EAAE,EAAG,QAAO,CAAC;AACtC,SAAK,MAAM,IAAI,MAAM,EAAE;AACvB,SAAK,YAAY,MAAM;AACvB,UAAM,KAAK,OAAO,MAAM,EAAE;AAC1B,UAAM,UAAU,MAAM,QAAQ,CAAC;AAC/B,QAAI,MAAM,SAAS,aAAa,SAAS;AACvC,YAAM,OAAO,UAAU,KAAK;AAC5B,UAAI,KAAM,QAAO,CAAC,EAAE,GAAG,UAAU,IAAI,MAAM,KAAK,QAAQ,KAAK,OAAO,KAAK,KAAK,KAAK,EAAE,EAAE,CAAC;AACxF,aAAO,CAAC,EAAE,GAAG,QAAQ,IAAI,MAAM,cAAc,OAAO,QAAQ,OAAO,CAAC,EAAE,MAAM,UAAU,CAAC;AAAA,IACzF;AACA,QAAI,MAAM,SAAS,kBAAkB,QAAS,QAAO,KAAK,WAAW,SAAS,KAAK;AACnF,QAAI,MAAM,SAAS,oBAAoB;AACrC,aAAO;AAAA,QACL;AAAA,UACE,GAAG;AAAA,UACH;AAAA,UACA,MAAM,QAAQ,cAAc;AAAA,UAC5B,IAAI,CAAC,QAAQ;AAAA,UACb,SAAS,cAAc,OAAO;AAAA,QAChC;AAAA,MACF;AACF,QAAI,MAAM,SAAS,gBAAiB,QAAO,CAAC,EAAE,GAAG,UAAU,IAAI,MAAM,YAAY,CAAC;AAClF,QAAI,MAAM,SAAS,gBAAiB,QAAO,CAAC,EAAE,GAAG,UAAU,IAAI,MAAM,UAAU,CAAC;AAChF,WAAO,CAAC;AAAA,EACV;AAAA,EAEA,OAAO,QAAkC;AACvC,QAAI,CAAC,KAAK,QAAQ,OAAQ,QAAO,CAAC;AAClC,SAAK,UAAU,CAAC;AAChB,SAAK,SAAS;AACd,WAAO,CAAC,EAAE,GAAG,OAAO,OAAO,OAAO,OAAO,CAAC;AAAA,EAC5C;AAAA,EAEA,SAAS,QAAyE;AAChF,QAAI,CAAC,KAAK,QAAQ,SAAS,OAAO,EAAE,KAAK,CAAC,UAAU,QAAQ,EAAE,SAAS,OAAO,MAAM;AAClF,aAAO,CAAC;AACV,QAAI,OAAO,WAAW,aAAc,QAAO,KAAK,OAAO,KAAK,MAAM;AAClE,QAAI,OAAO,WAAW,UAAW,QAAO,KAAK,OAAO,SAAS;AAC7D,SAAK,UAAU,KAAK,OAAO,OAAO,UAAU,OAAO,GAAG,GAAI;AAC1D,WAAO,KAAK,OAAO,OAAO;AAAA,EAC5B;AAAA,EAEA,OAAe;AACb,WAAO,KAAK,IAAI,GAAG,GAAG,KAAK,KAAK;AAAA,EAClC;AAAA,EAEA,MAAM,OAAkC;AACtC,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK,aAAa;AAChB,cAAM,QAAQ,KAAK,OAAO,KAAK,MAAM;AACrC,aAAK,SAAS;AACd,aAAK,QAAQ;AACb,eAAO,CAAC,GAAG,OAAO,EAAE,GAAG,OAAO,OAAO,QAAQ,CAAC;AAAA,MAChD;AAAA,MACA,KAAK;AACH,aAAK,UAAU,MAAM;AACrB,eAAO,CAAC;AAAA,MACV,KAAK;AACH,eAAO,KAAK,SAAS,MAAM,MAAM;AAAA,MACnC,KAAK;AACH,YAAK,MAAM,QAAyB,SAAS;AAC3C,eAAK,WAAW,KAAK,KAAK,MAAM,OAAuB;AACzD,eAAO,CAAC;AAAA,MACV,KAAK;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC,WAAW;AACvC,cAAI,OAAO,SAAS,gBAAgB,OAAO,SAAS,iBAAkB,QAAO,CAAC;AAC9E,gBAAM,KAAK,GAAG,KAAK,QAAQ,IAAI,OAAO,YAAY;AAClD,gBAAM,IAAI,OAAO,SAAS,eAAe,SAAS;AAClD,gBAAM,OAAO,KAAK,SAAS,IAAI,EAAE,KAAK;AACtC,cAAI,MAAM,cAAc,CAAC,KAAK,UAAW,QAAO,CAAC;AACjD,eAAK,SAAS,IAAI,IAAI,OAAO,OAAO,MAAM,MAAM;AAChD,iBAAO,CAAC,EAAE,GAAG,IAAI,OAAO,OAAO,MAAM,CAAC;AAAA,QACxC,CAAC;AAAA,MACH,KAAK;AACH,eAAO,KAAK,MAAM,MAAM,KAAK;AAAA,MAC/B,KAAK;AACH,eAAO,MAAM,QACT,KAAK,MAAM,MAAM,KAAK,IACtB;AAAA,UACE;AAAA,YACE,GAAG;AAAA,YACH,IAAI,IAAI,MAAM,UAAU;AAAA,YACxB,MAAM,MAAM;AAAA,YACZ,IAAI;AAAA,YACJ,SAAS;AAAA,UACX;AAAA,QACF;AAAA,MACN,KAAK;AACH,eAAO;AAAA,UACL;AAAA,YACE,GAAG;AAAA,YACH,IAAI,SAAS,MAAM,EAAE;AAAA,YACrB,MAAM,KAAK,qBAAqB,MAAM,OAAO,MAAM,MAAM,YAAY,EAAE;AAAA,UACzE;AAAA,QACF;AAAA,MACF,KAAK;AACH,eAAO,CAAC,EAAE,GAAG,UAAU,IAAI,cAAc,MAAM,MAAM,IAAI,MAAM,aAAa,CAAC;AAAA,MAC/E,KAAK;AACH,eAAO,CAAC,EAAE,GAAG,SAAS,IAAI,QAAQ,MAAM,MAAM,IAAI,SAAS,KAAK,MAAM,SAAS,GAAI,EAAE,CAAC;AAAA,MACxF;AACE,eAAO,CAAC;AAAA,IACZ;AAAA,EACF;AACF;AAEO,SAAS,SACd,SACA,SACA,UACe;AACf,QAAM,SAAS,IAAI,OAAO,QAAQ;AAClC,QAAM,MAAqB,CAAC;AAC5B,aAAW,SAAS,QAAS,KAAI,KAAK,GAAG,OAAO,MAAM,KAAK,CAAC;AAC5D,MAAI,KAAK,GAAG,OAAO,QAAQ,OAAO,CAAC;AACnC,SAAO;AACT;AAEO,SAAS,KAAK,OAAwC;AAC3D,SAAO,QAAQ,QAAQ,MAAM,KAAK;AACpC;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import {
3
3
  watchEvents
4
4
  } from "@earendil-works/pi-durable";
5
5
  import { holdText } from "../secrets/vault.js";
6
+ import { watchBusy } from "./busy.js";
6
7
  import { idOf, Mapper, replayOf } from "./events.js";
7
8
  const VAULT_BACK = "the vault is answering again";
8
9
  function vaultNotices(vault, push) {
@@ -169,11 +170,12 @@ function createRemote(host) {
169
170
  conversation.id,
170
171
  cancel.context
171
172
  );
172
- const open = (onEvents) => {
173
+ const open = (onEvents, ended) => {
173
174
  let off = () => void 0;
174
175
  const stream = new Stream(
175
176
  async () => {
176
177
  off();
178
+ ended?.();
177
179
  await events.stop();
178
180
  cancel.cancel();
179
181
  },
@@ -240,9 +242,11 @@ function createRemote(host) {
240
242
  const mapper = new Mapper(request.thinking);
241
243
  for (const entry of watched.snapshot.entries) mapper.entry(entry);
242
244
  mapper.partial(partial);
243
- const { id, stream } = watched.open(
244
- (batch) => stream.push(batch.flatMap((event) => followed(mapper, event)))
245
- );
245
+ const busy = request.busy ? await watchBusy(host.harness, watched, (events) => stream.push(events), context) : void 0;
246
+ const { id, stream } = watched.open((batch) => {
247
+ const mapped = batch.flatMap((event) => followed(mapper, event));
248
+ stream.push(busy ? busy.gate.before(mapped) : mapped);
249
+ }, busy?.stop);
246
250
  if (at >= 0) {
247
251
  const own = all[at];
248
252
  const sinceId = idOf(own);
@@ -251,7 +255,14 @@ function createRemote(host) {
251
255
  const replay = own.t === "text" || own.t === "thinking" ? [own] : [];
252
256
  stream.push([...replay, ...all.slice(last + 1)]);
253
257
  }
254
- return { ok: true, stream: id, delivered: null, head, gap };
258
+ return {
259
+ ok: true,
260
+ stream: id,
261
+ delivered: null,
262
+ head,
263
+ gap,
264
+ ...busy ? { busy: busy.opened } : {}
265
+ };
255
266
  },
256
267
  async next(id, waitMs, context) {
257
268
  const stream = streams.get(id);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/remote/host.ts"],
4
- "sourcesContent": ["import type { Context, ReplicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT, withCancel } from '@earendil-works/chord/context';\nimport {\n type AgentEvent,\n type AgentEventStream,\n type Conversation,\n type EntryRecord,\n type Harness,\n type SnapshotEvent,\n type SubmissionId,\n watchEvents,\n} from '@earendil-works/pi-durable';\nimport type { SendAttachment } from '../attachments.ts';\nimport { holdText, type VaultState } from '../secrets/vault.ts';\nimport type { Delivered, SendMode, SendRequest, SendResponse } from '../services.ts';\nimport { type EndReason, idOf, Mapper, type RemoteEvent, replayOf } from './events.ts';\n\nexport const VAULT_BACK = 'the vault is answering again';\n\nexport function vaultNotices(\n vault: ReplicatedState<VaultState> | undefined,\n push: (events: RemoteEvent[]) => void\n): () => void {\n if (!vault) return () => undefined;\n let shown = vault.value ? holdText(vault.value) : undefined;\n let serial = 0;\n const notice = (text: string) => push([{ t: 'notice', id: `vault-${++serial}`, text }]);\n if (shown) notice(shown);\n return vault.subscribe((value) => {\n const next = holdText(value);\n if (next === shown) return;\n shown = next;\n notice(next ?? VAULT_BACK);\n });\n}\n\nexport const MAX_QUEUED = 10000;\nexport const IDLE_MS = 120000;\n\nexport type RemoteOpened =\n | { ok: true; stream: string; delivered: Delivered | null; head: string | null; gap: boolean }\n | { ok: false; code: string; message: string };\n\nexport interface RemoteBatch {\n events: RemoteEvent[];\n done: boolean;\n}\n\nexport interface AgentRemote {\n prompt(\n request: {\n agentId: string | null;\n text: string;\n whenBusy: SendMode;\n thinking: boolean;\n attachments?: SendAttachment[];\n },\n context: Context\n ): Promise<RemoteOpened>;\n watch(\n request: { agentId: string | null; since: string | null; thinking: boolean },\n context: Context\n ): Promise<RemoteOpened>;\n next(stream: string, waitMs: number, context: Context): Promise<RemoteBatch>;\n close(stream: string, context: Context): Promise<void>;\n abort(agentId: string | null, context: Context): Promise<{ aborted: boolean }>;\n}\n\nexport interface RemoteHost {\n harness: Harness;\n conversation(agentId: string | null, context: Context): Promise<Conversation>;\n send(request: SendRequest, context: Context): Promise<SendResponse>;\n vault?: ReplicatedState<VaultState>;\n now?: () => number;\n limit?: number;\n sweepMs?: number;\n}\n\nexport class Stream<T = RemoteEvent> {\n readonly queue: T[] = [];\n done = false;\n touched: number;\n #wake: (() => void) | undefined;\n readonly #stop: () => Promise<unknown>;\n readonly #limit: number;\n readonly #overflow: readonly T[];\n\n constructor(stop: () => Promise<unknown>, now: number, limit: number, overflow: readonly T[]) {\n this.#stop = stop;\n this.touched = now;\n this.#limit = limit;\n this.#overflow = overflow;\n }\n\n push(events: readonly T[]): void {\n if (this.done || !events.length) return;\n this.queue.push(...events);\n if (this.queue.length > this.#limit) {\n this.queue.length = 0;\n this.queue.push(...this.#overflow);\n void this.finish();\n }\n this.#wake?.();\n }\n\n async finish(): Promise<void> {\n if (this.done) return;\n this.done = true;\n this.#wake?.();\n await this.#stop().catch(() => undefined);\n }\n\n wait(ms: number, signal?: AbortSignal): Promise<void> {\n if (this.queue.length || this.done) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(done, ms);\n signal?.addEventListener('abort', done, { once: true });\n function done() {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n this.#wake = () => {\n this.#wake = undefined;\n done();\n };\n });\n }\n}\n\nfunction deliveredOf(\n status: { status: string } | undefined,\n whenBusy: SendMode,\n busy: boolean\n): Delivered {\n if (status?.status === 'queued') return whenBusy === 'followUp' ? 'follow-up' : 'steer';\n return busy ? 'steer' : 'run';\n}\n\nfunction headOf(events: readonly RemoteEvent[]): string | null {\n for (let at = events.length - 1; at >= 0; at--) {\n const id = idOf(events[at] as RemoteEvent);\n if (id) return id;\n }\n return null;\n}\n\nexport function followed(mapper: Mapper, event: AgentEvent): RemoteEvent[] {\n return event.type === 'snapshot' ? resync(mapper, event) : mapper.event(event);\n}\n\nexport function resync(mapper: Mapper, snapshot: SnapshotEvent): RemoteEvent[] {\n const last = mapper.last();\n return [\n ...snapshot.entries.filter((entry) => entry.id > last).flatMap((entry) => mapper.entry(entry)),\n ...mapper.partial(snapshot.generation?.message as never),\n ];\n}\n\nfunction placedEntry(value: unknown): number | undefined {\n const entry = (value as { entry?: unknown } | undefined)?.entry;\n return typeof entry === 'number' ? entry : undefined;\n}\n\nexport class Follower {\n readonly #mapper: Mapper;\n readonly #entries: Map<number, EntryRecord>;\n readonly #submission: SubmissionId;\n readonly #delivered: Delivered;\n readonly #out: RemoteEvent[] = [];\n #user: number | undefined;\n #started = false;\n\n constructor(\n mapper: Mapper,\n entries: readonly EntryRecord[],\n submission: SubmissionId,\n delivered: Delivered,\n record: unknown\n ) {\n this.#mapper = mapper;\n this.#entries = new Map(entries.map((entry) => [entry.id, entry]));\n this.#submission = submission;\n this.#delivered = delivered;\n this.#user = placedEntry(record);\n }\n\n drain(): RemoteEvent[] {\n return this.#out.splice(0);\n }\n\n #begin(): void {\n const entry = this.#user === undefined ? undefined : this.#entries.get(this.#user);\n if (!entry) return;\n this.#started = true;\n this.#out.push(...this.#mapper.entry(entry, this.#delivered));\n }\n\n #end(reason: EndReason, message?: string): true {\n this.#out.push({ t: 'end', reason, ...(message ? { message } : {}) });\n return true;\n }\n\n #ours(event: AgentEvent): boolean {\n if (event.type !== 'submission' || event.record.id !== this.#submission) return false;\n this.#user = placedEntry(event.record) ?? this.#user;\n return this.#user === undefined && !['queued', 'placed'].includes(event.record.status);\n }\n\n take(batch: readonly AgentEvent[]): boolean {\n for (const event of batch) {\n if (event.type === 'message_end') this.#entries.set(event.entry.id, event.entry);\n if (this.#ours(event)) return this.#end('aborted');\n if (!this.#started) {\n this.#begin();\n continue;\n }\n const mapped = this.#mapper.event(event).filter((item) => item.t !== 'user');\n this.#out.push(...mapped);\n if (mapped.some((item) => item.t === 'run' && item.state === 'end'))\n return this.#end(this.#mapper.reason, this.#mapper.error);\n }\n return false;\n }\n}\n\nexport function createRemote(host: RemoteHost): AgentRemote & { dispose(): Promise<void> } {\n const now = host.now ?? Date.now;\n const streams = new Map<string, Stream>();\n let serial = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const sweep = () => {\n for (const [id, stream] of streams)\n if (now() - stream.touched > IDLE_MS) {\n streams.delete(id);\n void stream.finish();\n }\n if (!streams.size && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n const track = (id: string, stream: Stream) => {\n streams.set(id, stream);\n timer ??= setInterval(sweep, host.sweepMs ?? IDLE_MS / 4);\n };\n const watching = async (agentId: string | null, context: Context) => {\n const conversation = await host.conversation(agentId, context);\n const cancel = withCancel(BACKGROUND_CONTEXT);\n const events: AgentEventStream = await watchEvents(\n host.harness,\n conversation.id,\n cancel.context\n );\n const open = (onEvents: (batch: readonly AgentEvent[]) => void) => {\n let off = () => undefined as void;\n const stream = new Stream<RemoteEvent>(\n async () => {\n off();\n await events.stop();\n cancel.cancel();\n },\n now(),\n host.limit ?? MAX_QUEUED,\n [{ t: 'end', reason: 'error', message: 'the stream fell too far behind' }]\n );\n const id = `s${++serial}-${crypto.randomUUID()}`;\n track(id, stream);\n off = vaultNotices(host.vault, (notices) => stream.push(notices));\n events.start(async (batch) => onEvents(batch));\n return { id, stream };\n };\n return { conversation, snapshot: events.snapshot, open, stop: () => events.stop() };\n };\n return {\n async prompt(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const busy = Boolean(watched.snapshot.run);\n const response = await host.send(\n {\n text: request.text,\n whenBusy: request.whenBusy,\n requestId: null,\n ...(request.attachments?.length ? { attachments: request.attachments } : {}),\n ...(request.agentId ? { agentId: request.agentId } : {}),\n },\n context\n );\n if (!response.accepted) {\n await watched.stop();\n return {\n ok: false,\n code: response.error.code === 'busy' ? 'EBUSY' : 'EIO',\n message: response.error.message,\n };\n }\n const submission = Number(response.submissionId) as SubmissionId;\n const record = await (await host.harness.submission(submission, context))?.status(context);\n const delivered = deliveredOf(record, request.whenBusy, busy);\n const follower = new Follower(\n new Mapper(request.thinking),\n watched.snapshot.entries,\n submission,\n delivered,\n record\n );\n const { id, stream } = watched.open((batch) => {\n const ended = follower.take(batch);\n stream.push(follower.drain());\n if (ended) void stream.finish();\n });\n return { ok: true, stream: id, delivered, head: null, gap: false };\n },\n async watch(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const partial = watched.snapshot.generation?.message as never;\n const all = replayOf(watched.snapshot.entries, partial, request.thinking);\n const head = headOf(all);\n const at = request.since === null ? -1 : all.findIndex((e) => idOf(e) === request.since);\n const gap = request.since !== null && at < 0;\n const mapper = new Mapper(request.thinking);\n for (const entry of watched.snapshot.entries) mapper.entry(entry);\n mapper.partial(partial);\n const { id, stream } = watched.open((batch) =>\n stream.push(batch.flatMap((event) => followed(mapper, event)))\n );\n if (at >= 0) {\n const own = all[at] as RemoteEvent;\n const sinceId = idOf(own);\n let last = at;\n while (last + 1 < all.length && idOf(all[last + 1] as RemoteEvent) === sinceId) last++;\n const replay = own.t === 'text' || own.t === 'thinking' ? [own] : [];\n stream.push([...replay, ...all.slice(last + 1)]);\n }\n return { ok: true, stream: id, delivered: null, head, gap };\n },\n async next(id, waitMs, context) {\n const stream = streams.get(id);\n if (!stream) return { events: [], done: true };\n stream.touched = now();\n await stream.wait(Math.max(0, Math.min(waitMs, 30000)));\n context.abortSignal?.throwIfAborted();\n const events = stream.queue.splice(0);\n const done = stream.done && stream.queue.length === 0;\n if (done) streams.delete(id);\n return { events, done };\n },\n async close(id) {\n const stream = streams.get(id);\n streams.delete(id);\n await stream?.finish();\n },\n async abort(agentId, context) {\n const conversation = await host.conversation(agentId, context);\n const watched = await watchEvents(host.harness, conversation.id, context);\n const busy = Boolean(watched.snapshot.run);\n await watched.stop();\n if (busy) await conversation.abort(context);\n return { aborted: busy };\n },\n async dispose() {\n clearInterval(timer);\n timer = undefined;\n const all = [...streams.values()];\n streams.clear();\n await Promise.all(all.map((stream) => stream.finish()));\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context, ReplicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT, withCancel } from '@earendil-works/chord/context';\nimport {\n type AgentEvent,\n type AgentEventStream,\n type Conversation,\n type EntryRecord,\n type Harness,\n type SnapshotEvent,\n type SubmissionId,\n watchEvents,\n} from '@earendil-works/pi-durable';\nimport type { SendAttachment } from '../attachments.ts';\nimport { holdText, type VaultState } from '../secrets/vault.ts';\nimport type { Delivered, SendMode, SendRequest, SendResponse } from '../services.ts';\nimport { watchBusy } from './busy.ts';\nimport { type EndReason, idOf, Mapper, type RemoteEvent, replayOf } from './events.ts';\n\nexport const VAULT_BACK = 'the vault is answering again';\n\nexport function vaultNotices(\n vault: ReplicatedState<VaultState> | undefined,\n push: (events: RemoteEvent[]) => void\n): () => void {\n if (!vault) return () => undefined;\n let shown = vault.value ? holdText(vault.value) : undefined;\n let serial = 0;\n const notice = (text: string) => push([{ t: 'notice', id: `vault-${++serial}`, text }]);\n if (shown) notice(shown);\n return vault.subscribe((value) => {\n const next = holdText(value);\n if (next === shown) return;\n shown = next;\n notice(next ?? VAULT_BACK);\n });\n}\n\nexport const MAX_QUEUED = 10000;\nexport const IDLE_MS = 120000;\n\nexport type RemoteOpened =\n | {\n ok: true;\n stream: string;\n delivered: Delivered | null;\n head: string | null;\n gap: boolean;\n busy?: boolean;\n }\n | { ok: false; code: string; message: string };\n\nexport interface RemoteBatch {\n events: RemoteEvent[];\n done: boolean;\n}\n\nexport interface AgentRemote {\n prompt(\n request: {\n agentId: string | null;\n text: string;\n whenBusy: SendMode;\n thinking: boolean;\n attachments?: SendAttachment[];\n },\n context: Context\n ): Promise<RemoteOpened>;\n watch(\n request: { agentId: string | null; since: string | null; thinking: boolean; busy?: boolean },\n context: Context\n ): Promise<RemoteOpened>;\n next(stream: string, waitMs: number, context: Context): Promise<RemoteBatch>;\n close(stream: string, context: Context): Promise<void>;\n abort(agentId: string | null, context: Context): Promise<{ aborted: boolean }>;\n}\n\nexport interface RemoteHost {\n harness: Harness;\n conversation(agentId: string | null, context: Context): Promise<Conversation>;\n send(request: SendRequest, context: Context): Promise<SendResponse>;\n vault?: ReplicatedState<VaultState>;\n now?: () => number;\n limit?: number;\n sweepMs?: number;\n}\n\nexport class Stream<T = RemoteEvent> {\n readonly queue: T[] = [];\n done = false;\n touched: number;\n #wake: (() => void) | undefined;\n readonly #stop: () => Promise<unknown>;\n readonly #limit: number;\n readonly #overflow: readonly T[];\n\n constructor(stop: () => Promise<unknown>, now: number, limit: number, overflow: readonly T[]) {\n this.#stop = stop;\n this.touched = now;\n this.#limit = limit;\n this.#overflow = overflow;\n }\n\n push(events: readonly T[]): void {\n if (this.done || !events.length) return;\n this.queue.push(...events);\n if (this.queue.length > this.#limit) {\n this.queue.length = 0;\n this.queue.push(...this.#overflow);\n void this.finish();\n }\n this.#wake?.();\n }\n\n async finish(): Promise<void> {\n if (this.done) return;\n this.done = true;\n this.#wake?.();\n await this.#stop().catch(() => undefined);\n }\n\n wait(ms: number, signal?: AbortSignal): Promise<void> {\n if (this.queue.length || this.done) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(done, ms);\n signal?.addEventListener('abort', done, { once: true });\n function done() {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n this.#wake = () => {\n this.#wake = undefined;\n done();\n };\n });\n }\n}\n\nfunction deliveredOf(\n status: { status: string } | undefined,\n whenBusy: SendMode,\n busy: boolean\n): Delivered {\n if (status?.status === 'queued') return whenBusy === 'followUp' ? 'follow-up' : 'steer';\n return busy ? 'steer' : 'run';\n}\n\nfunction headOf(events: readonly RemoteEvent[]): string | null {\n for (let at = events.length - 1; at >= 0; at--) {\n const id = idOf(events[at] as RemoteEvent);\n if (id) return id;\n }\n return null;\n}\n\nexport function followed(mapper: Mapper, event: AgentEvent): RemoteEvent[] {\n return event.type === 'snapshot' ? resync(mapper, event) : mapper.event(event);\n}\n\nexport function resync(mapper: Mapper, snapshot: SnapshotEvent): RemoteEvent[] {\n const last = mapper.last();\n return [\n ...snapshot.entries.filter((entry) => entry.id > last).flatMap((entry) => mapper.entry(entry)),\n ...mapper.partial(snapshot.generation?.message as never),\n ];\n}\n\nfunction placedEntry(value: unknown): number | undefined {\n const entry = (value as { entry?: unknown } | undefined)?.entry;\n return typeof entry === 'number' ? entry : undefined;\n}\n\nexport class Follower {\n readonly #mapper: Mapper;\n readonly #entries: Map<number, EntryRecord>;\n readonly #submission: SubmissionId;\n readonly #delivered: Delivered;\n readonly #out: RemoteEvent[] = [];\n #user: number | undefined;\n #started = false;\n\n constructor(\n mapper: Mapper,\n entries: readonly EntryRecord[],\n submission: SubmissionId,\n delivered: Delivered,\n record: unknown\n ) {\n this.#mapper = mapper;\n this.#entries = new Map(entries.map((entry) => [entry.id, entry]));\n this.#submission = submission;\n this.#delivered = delivered;\n this.#user = placedEntry(record);\n }\n\n drain(): RemoteEvent[] {\n return this.#out.splice(0);\n }\n\n #begin(): void {\n const entry = this.#user === undefined ? undefined : this.#entries.get(this.#user);\n if (!entry) return;\n this.#started = true;\n this.#out.push(...this.#mapper.entry(entry, this.#delivered));\n }\n\n #end(reason: EndReason, message?: string): true {\n this.#out.push({ t: 'end', reason, ...(message ? { message } : {}) });\n return true;\n }\n\n #ours(event: AgentEvent): boolean {\n if (event.type !== 'submission' || event.record.id !== this.#submission) return false;\n this.#user = placedEntry(event.record) ?? this.#user;\n return this.#user === undefined && !['queued', 'placed'].includes(event.record.status);\n }\n\n take(batch: readonly AgentEvent[]): boolean {\n for (const event of batch) {\n if (event.type === 'message_end') this.#entries.set(event.entry.id, event.entry);\n if (this.#ours(event)) return this.#end('aborted');\n if (!this.#started) {\n this.#begin();\n continue;\n }\n const mapped = this.#mapper.event(event).filter((item) => item.t !== 'user');\n this.#out.push(...mapped);\n if (mapped.some((item) => item.t === 'run' && item.state === 'end'))\n return this.#end(this.#mapper.reason, this.#mapper.error);\n }\n return false;\n }\n}\n\nexport function createRemote(host: RemoteHost): AgentRemote & { dispose(): Promise<void> } {\n const now = host.now ?? Date.now;\n const streams = new Map<string, Stream>();\n let serial = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const sweep = () => {\n for (const [id, stream] of streams)\n if (now() - stream.touched > IDLE_MS) {\n streams.delete(id);\n void stream.finish();\n }\n if (!streams.size && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n const track = (id: string, stream: Stream) => {\n streams.set(id, stream);\n timer ??= setInterval(sweep, host.sweepMs ?? IDLE_MS / 4);\n };\n const watching = async (agentId: string | null, context: Context) => {\n const conversation = await host.conversation(agentId, context);\n const cancel = withCancel(BACKGROUND_CONTEXT);\n const events: AgentEventStream = await watchEvents(\n host.harness,\n conversation.id,\n cancel.context\n );\n const open = (onEvents: (batch: readonly AgentEvent[]) => void, ended?: () => void) => {\n let off = () => undefined as void;\n const stream = new Stream<RemoteEvent>(\n async () => {\n off();\n ended?.();\n await events.stop();\n cancel.cancel();\n },\n now(),\n host.limit ?? MAX_QUEUED,\n [{ t: 'end', reason: 'error', message: 'the stream fell too far behind' }]\n );\n const id = `s${++serial}-${crypto.randomUUID()}`;\n track(id, stream);\n off = vaultNotices(host.vault, (notices) => stream.push(notices));\n events.start(async (batch) => onEvents(batch));\n return { id, stream };\n };\n return { conversation, snapshot: events.snapshot, open, stop: () => events.stop() };\n };\n return {\n async prompt(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const busy = Boolean(watched.snapshot.run);\n const response = await host.send(\n {\n text: request.text,\n whenBusy: request.whenBusy,\n requestId: null,\n ...(request.attachments?.length ? { attachments: request.attachments } : {}),\n ...(request.agentId ? { agentId: request.agentId } : {}),\n },\n context\n );\n if (!response.accepted) {\n await watched.stop();\n return {\n ok: false,\n code: response.error.code === 'busy' ? 'EBUSY' : 'EIO',\n message: response.error.message,\n };\n }\n const submission = Number(response.submissionId) as SubmissionId;\n const record = await (await host.harness.submission(submission, context))?.status(context);\n const delivered = deliveredOf(record, request.whenBusy, busy);\n const follower = new Follower(\n new Mapper(request.thinking),\n watched.snapshot.entries,\n submission,\n delivered,\n record\n );\n const { id, stream } = watched.open((batch) => {\n const ended = follower.take(batch);\n stream.push(follower.drain());\n if (ended) void stream.finish();\n });\n return { ok: true, stream: id, delivered, head: null, gap: false };\n },\n async watch(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const partial = watched.snapshot.generation?.message as never;\n const all = replayOf(watched.snapshot.entries, partial, request.thinking);\n const head = headOf(all);\n const at = request.since === null ? -1 : all.findIndex((e) => idOf(e) === request.since);\n const gap = request.since !== null && at < 0;\n const mapper = new Mapper(request.thinking);\n for (const entry of watched.snapshot.entries) mapper.entry(entry);\n mapper.partial(partial);\n const busy = request.busy\n ? await watchBusy(host.harness, watched, (events) => stream.push(events), context)\n : undefined;\n const { id, stream } = watched.open((batch) => {\n const mapped = batch.flatMap((event) => followed(mapper, event));\n stream.push(busy ? busy.gate.before(mapped) : mapped);\n }, busy?.stop);\n if (at >= 0) {\n const own = all[at] as RemoteEvent;\n const sinceId = idOf(own);\n let last = at;\n while (last + 1 < all.length && idOf(all[last + 1] as RemoteEvent) === sinceId) last++;\n const replay = own.t === 'text' || own.t === 'thinking' ? [own] : [];\n stream.push([...replay, ...all.slice(last + 1)]);\n }\n return {\n ok: true,\n stream: id,\n delivered: null,\n head,\n gap,\n ...(busy ? { busy: busy.opened } : {}),\n };\n },\n async next(id, waitMs, context) {\n const stream = streams.get(id);\n if (!stream) return { events: [], done: true };\n stream.touched = now();\n await stream.wait(Math.max(0, Math.min(waitMs, 30000)));\n context.abortSignal?.throwIfAborted();\n const events = stream.queue.splice(0);\n const done = stream.done && stream.queue.length === 0;\n if (done) streams.delete(id);\n return { events, done };\n },\n async close(id) {\n const stream = streams.get(id);\n streams.delete(id);\n await stream?.finish();\n },\n async abort(agentId, context) {\n const conversation = await host.conversation(agentId, context);\n const watched = await watchEvents(host.harness, conversation.id, context);\n const busy = Boolean(watched.snapshot.run);\n await watched.stop();\n if (busy) await conversation.abort(context);\n return { aborted: busy };\n },\n async dispose() {\n clearInterval(timer);\n timer = undefined;\n const all = [...streams.values()];\n streams.clear();\n await Promise.all(all.map((stream) => stream.finish()));\n },\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB,kBAAkB;AAC/C;AAAA,EAQE;AAAA,OACK;AAEP,SAAS,gBAAiC;AAE1C,SAAS,iBAAiB;AAC1B,SAAyB,MAAM,QAA0B,gBAAgB;AAElE,MAAM,aAAa;AAEnB,SAAS,aACd,OACA,MACY;AACZ,MAAI,CAAC,MAAO,QAAO,MAAM;AACzB,MAAI,QAAQ,MAAM,QAAQ,SAAS,MAAM,KAAK,IAAI;AAClD,MAAI,SAAS;AACb,QAAM,SAAS,CAAC,SAAiB,KAAK,CAAC,EAAE,GAAG,UAAU,IAAI,SAAS,EAAE,MAAM,IAAI,KAAK,CAAC,CAAC;AACtF,MAAI,MAAO,QAAO,KAAK;AACvB,SAAO,MAAM,UAAU,CAAC,UAAU;AAChC,UAAM,OAAO,SAAS,KAAK;AAC3B,QAAI,SAAS,MAAO;AACpB,YAAQ;AACR,WAAO,QAAQ,UAAU;AAAA,EAC3B,CAAC;AACH;AAEO,MAAM,aAAa;AACnB,MAAM,UAAU;AAgDhB,MAAM,OAAwB;AAAA,EAC1B,QAAa,CAAC;AAAA,EACvB,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACS;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAA8B,KAAa,OAAe,UAAwB;AAC5F,SAAK,QAAQ;AACb,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,KAAK,QAA4B;AAC/B,QAAI,KAAK,QAAQ,CAAC,OAAO,OAAQ;AACjC,SAAK,MAAM,KAAK,GAAG,MAAM;AACzB,QAAI,KAAK,MAAM,SAAS,KAAK,QAAQ;AACnC,WAAK,MAAM,SAAS;AACpB,WAAK,MAAM,KAAK,GAAG,KAAK,SAAS;AACjC,WAAK,KAAK,OAAO;AAAA,IACnB;AACA,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,MAAM,SAAwB;AAC5B,QAAI,KAAK,KAAM;AACf,SAAK,OAAO;AACZ,SAAK,QAAQ;AACb,UAAM,KAAK,MAAM,EAAE,MAAM,MAAM,MAAS;AAAA,EAC1C;AAAA,EAEA,KAAK,IAAY,QAAqC;AACpD,QAAI,KAAK,MAAM,UAAU,KAAK,KAAM,QAAO,QAAQ,QAAQ;AAC3D,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,YAAM,QAAQ,WAAW,MAAM,EAAE;AACjC,cAAQ,iBAAiB,SAAS,MAAM,EAAE,MAAM,KAAK,CAAC;AACtD,eAAS,OAAO;AACd,qBAAa,KAAK;AAClB,gBAAQ,oBAAoB,SAAS,IAAI;AACzC,gBAAQ;AAAA,MACV;AACA,WAAK,QAAQ,MAAM;AACjB,aAAK,QAAQ;AACb,aAAK;AAAA,MACP;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAS,YACP,QACA,UACA,MACW;AACX,MAAI,QAAQ,WAAW,SAAU,QAAO,aAAa,aAAa,cAAc;AAChF,SAAO,OAAO,UAAU;AAC1B;AAEA,SAAS,OAAO,QAA+C;AAC7D,WAAS,KAAK,OAAO,SAAS,GAAG,MAAM,GAAG,MAAM;AAC9C,UAAM,KAAK,KAAK,OAAO,EAAE,CAAgB;AACzC,QAAI,GAAI,QAAO;AAAA,EACjB;AACA,SAAO;AACT;AAEO,SAAS,SAAS,QAAgB,OAAkC;AACzE,SAAO,MAAM,SAAS,aAAa,OAAO,QAAQ,KAAK,IAAI,OAAO,MAAM,KAAK;AAC/E;AAEO,SAAS,OAAO,QAAgB,UAAwC;AAC7E,QAAM,OAAO,OAAO,KAAK;AACzB,SAAO;AAAA,IACL,GAAG,SAAS,QAAQ,OAAO,CAAC,UAAU,MAAM,KAAK,IAAI,EAAE,QAAQ,CAAC,UAAU,OAAO,MAAM,KAAK,CAAC;AAAA,IAC7F,GAAG,OAAO,QAAQ,SAAS,YAAY,OAAgB;AAAA,EACzD;AACF;AAEA,SAAS,YAAY,OAAoC;AACvD,QAAM,QAAS,OAA2C;AAC1D,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEO,MAAM,SAAS;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAsB,CAAC;AAAA,EAChC;AAAA,EACA,WAAW;AAAA,EAEX,YACE,QACA,SACA,YACA,WACA,QACA;AACA,SAAK,UAAU;AACf,SAAK,WAAW,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;AACjE,SAAK,cAAc;AACnB,SAAK,aAAa;AAClB,SAAK,QAAQ,YAAY,MAAM;AAAA,EACjC;AAAA,EAEA,QAAuB;AACrB,WAAO,KAAK,KAAK,OAAO,CAAC;AAAA,EAC3B;AAAA,EAEA,SAAe;AACb,UAAM,QAAQ,KAAK,UAAU,SAAY,SAAY,KAAK,SAAS,IAAI,KAAK,KAAK;AACjF,QAAI,CAAC,MAAO;AACZ,SAAK,WAAW;AAChB,SAAK,KAAK,KAAK,GAAG,KAAK,QAAQ,MAAM,OAAO,KAAK,UAAU,CAAC;AAAA,EAC9D;AAAA,EAEA,KAAK,QAAmB,SAAwB;AAC9C,SAAK,KAAK,KAAK,EAAE,GAAG,OAAO,QAAQ,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC,EAAG,CAAC;AACpE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAA4B;AAChC,QAAI,MAAM,SAAS,gBAAgB,MAAM,OAAO,OAAO,KAAK,YAAa,QAAO;AAChF,SAAK,QAAQ,YAAY,MAAM,MAAM,KAAK,KAAK;AAC/C,WAAO,KAAK,UAAU,UAAa,CAAC,CAAC,UAAU,QAAQ,EAAE,SAAS,MAAM,OAAO,MAAM;AAAA,EACvF;AAAA,EAEA,KAAK,OAAuC;AAC1C,eAAW,SAAS,OAAO;AACzB,UAAI,MAAM,SAAS,cAAe,MAAK,SAAS,IAAI,MAAM,MAAM,IAAI,MAAM,KAAK;AAC/E,UAAI,KAAK,MAAM,KAAK,EAAG,QAAO,KAAK,KAAK,SAAS;AACjD,UAAI,CAAC,KAAK,UAAU;AAClB,aAAK,OAAO;AACZ;AAAA,MACF;AACA,YAAM,SAAS,KAAK,QAAQ,MAAM,KAAK,EAAE,OAAO,CAAC,SAAS,KAAK,MAAM,MAAM;AAC3E,WAAK,KAAK,KAAK,GAAG,MAAM;AACxB,UAAI,OAAO,KAAK,CAAC,SAAS,KAAK,MAAM,SAAS,KAAK,UAAU,KAAK;AAChE,eAAO,KAAK,KAAK,KAAK,QAAQ,QAAQ,KAAK,QAAQ,KAAK;AAAA,IAC5D;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,aAAa,MAA8D;AACzF,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,QAAM,UAAU,oBAAI,IAAoB;AACxC,MAAI,SAAS;AACb,MAAI;AACJ,QAAM,QAAQ,MAAM;AAClB,eAAW,CAAC,IAAI,MAAM,KAAK;AACzB,UAAI,IAAI,IAAI,OAAO,UAAU,SAAS;AACpC,gBAAQ,OAAO,EAAE;AACjB,aAAK,OAAO,OAAO;AAAA,MACrB;AACF,QAAI,CAAC,QAAQ,QAAQ,UAAU,QAAW;AACxC,oBAAc,KAAK;AACnB,cAAQ;AAAA,IACV;AAAA,EACF;AACA,QAAM,QAAQ,CAAC,IAAY,WAAmB;AAC5C,YAAQ,IAAI,IAAI,MAAM;AACtB,cAAU,YAAY,OAAO,KAAK,WAAW,UAAU,CAAC;AAAA,EAC1D;AACA,QAAM,WAAW,OAAO,SAAwB,YAAqB;AACnE,UAAM,eAAe,MAAM,KAAK,aAAa,SAAS,OAAO;AAC7D,UAAM,SAAS,WAAW,kBAAkB;AAC5C,UAAM,SAA2B,MAAM;AAAA,MACrC,KAAK;AAAA,MACL,aAAa;AAAA,MACb,OAAO;AAAA,IACT;AACA,UAAM,OAAO,CAAC,UAAkD,UAAuB;AACrF,UAAI,MAAM,MAAM;AAChB,YAAM,SAAS,IAAI;AAAA,QACjB,YAAY;AACV,cAAI;AACJ,kBAAQ;AACR,gBAAM,OAAO,KAAK;AAClB,iBAAO,OAAO;AAAA,QAChB;AAAA,QACA,IAAI;AAAA,QACJ,KAAK,SAAS;AAAA,QACd,CAAC,EAAE,GAAG,OAAO,QAAQ,SAAS,SAAS,iCAAiC,CAAC;AAAA,MAC3E;AACA,YAAM,KAAK,IAAI,EAAE,MAAM,IAAI,OAAO,WAAW,CAAC;AAC9C,YAAM,IAAI,MAAM;AAChB,YAAM,aAAa,KAAK,OAAO,CAAC,YAAY,OAAO,KAAK,OAAO,CAAC;AAChE,aAAO,MAAM,OAAO,UAAU,SAAS,KAAK,CAAC;AAC7C,aAAO,EAAE,IAAI,OAAO;AAAA,IACtB;AACA,WAAO,EAAE,cAAc,UAAU,OAAO,UAAU,MAAM,MAAM,MAAM,OAAO,KAAK,EAAE;AAAA,EACpF;AACA,SAAO;AAAA,IACL,MAAM,OAAO,SAAS,SAAS;AAC7B,YAAM;AACN,YAAM,UAAU,MAAM,SAAS,QAAQ,SAAS,OAAO;AACvD,YAAM,OAAO,QAAQ,QAAQ,SAAS,GAAG;AACzC,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,UACE,MAAM,QAAQ;AAAA,UACd,UAAU,QAAQ;AAAA,UAClB,WAAW;AAAA,UACX,GAAI,QAAQ,aAAa,SAAS,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;AAAA,UAC1E,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,QACxD;AAAA,QACA;AAAA,MACF;AACA,UAAI,CAAC,SAAS,UAAU;AACtB,cAAM,QAAQ,KAAK;AACnB,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,MAAM,SAAS,MAAM,SAAS,SAAS,UAAU;AAAA,UACjD,SAAS,SAAS,MAAM;AAAA,QAC1B;AAAA,MACF;AACA,YAAM,aAAa,OAAO,SAAS,YAAY;AAC/C,YAAM,SAAS,OAAO,MAAM,KAAK,QAAQ,WAAW,YAAY,OAAO,IAAI,OAAO,OAAO;AACzF,YAAM,YAAY,YAAY,QAAQ,QAAQ,UAAU,IAAI;AAC5D,YAAM,WAAW,IAAI;AAAA,QACnB,IAAI,OAAO,QAAQ,QAAQ;AAAA,QAC3B,QAAQ,SAAS;AAAA,QACjB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,YAAM,EAAE,IAAI,OAAO,IAAI,QAAQ,KAAK,CAAC,UAAU;AAC7C,cAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,eAAO,KAAK,SAAS,MAAM,CAAC;AAC5B,YAAI,MAAO,MAAK,OAAO,OAAO;AAAA,MAChC,CAAC;AACD,aAAO,EAAE,IAAI,MAAM,QAAQ,IAAI,WAAW,MAAM,MAAM,KAAK,MAAM;AAAA,IACnE;AAAA,IACA,MAAM,MAAM,SAAS,SAAS;AAC5B,YAAM;AACN,YAAM,UAAU,MAAM,SAAS,QAAQ,SAAS,OAAO;AACvD,YAAM,UAAU,QAAQ,SAAS,YAAY;AAC7C,YAAM,MAAM,SAAS,QAAQ,SAAS,SAAS,SAAS,QAAQ,QAAQ;AACxE,YAAM,OAAO,OAAO,GAAG;AACvB,YAAM,KAAK,QAAQ,UAAU,OAAO,KAAK,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,MAAM,QAAQ,KAAK;AACvF,YAAM,MAAM,QAAQ,UAAU,QAAQ,KAAK;AAC3C,YAAM,SAAS,IAAI,OAAO,QAAQ,QAAQ;AAC1C,iBAAW,SAAS,QAAQ,SAAS,QAAS,QAAO,MAAM,KAAK;AAChE,aAAO,QAAQ,OAAO;AACtB,YAAM,OAAO,QAAQ,OACjB,MAAM,UAAU,KAAK,SAAS,SAAS,CAAC,WAAW,OAAO,KAAK,MAAM,GAAG,OAAO,IAC/E;AACJ,YAAM,EAAE,IAAI,OAAO,IAAI,QAAQ,KAAK,CAAC,UAAU;AAC7C,cAAM,SAAS,MAAM,QAAQ,CAAC,UAAU,SAAS,QAAQ,KAAK,CAAC;AAC/D,eAAO,KAAK,OAAO,KAAK,KAAK,OAAO,MAAM,IAAI,MAAM;AAAA,MACtD,GAAG,MAAM,IAAI;AACb,UAAI,MAAM,GAAG;AACX,cAAM,MAAM,IAAI,EAAE;AAClB,cAAM,UAAU,KAAK,GAAG;AACxB,YAAI,OAAO;AACX,eAAO,OAAO,IAAI,IAAI,UAAU,KAAK,IAAI,OAAO,CAAC,CAAgB,MAAM,QAAS;AAChF,cAAM,SAAS,IAAI,MAAM,UAAU,IAAI,MAAM,aAAa,CAAC,GAAG,IAAI,CAAC;AACnE,eAAO,KAAK,CAAC,GAAG,QAAQ,GAAG,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC;AAAA,MACjD;AACA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,WAAW;AAAA,QACX;AAAA,QACA;AAAA,QACA,GAAI,OAAO,EAAE,MAAM,KAAK,OAAO,IAAI,CAAC;AAAA,MACtC;AAAA,IACF;AAAA,IACA,MAAM,KAAK,IAAI,QAAQ,SAAS;AAC9B,YAAM,SAAS,QAAQ,IAAI,EAAE;AAC7B,UAAI,CAAC,OAAQ,QAAO,EAAE,QAAQ,CAAC,GAAG,MAAM,KAAK;AAC7C,aAAO,UAAU,IAAI;AACrB,YAAM,OAAO,KAAK,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,GAAK,CAAC,CAAC;AACtD,cAAQ,aAAa,eAAe;AACpC,YAAM,SAAS,OAAO,MAAM,OAAO,CAAC;AACpC,YAAM,OAAO,OAAO,QAAQ,OAAO,MAAM,WAAW;AACpD,UAAI,KAAM,SAAQ,OAAO,EAAE;AAC3B,aAAO,EAAE,QAAQ,KAAK;AAAA,IACxB;AAAA,IACA,MAAM,MAAM,IAAI;AACd,YAAM,SAAS,QAAQ,IAAI,EAAE;AAC7B,cAAQ,OAAO,EAAE;AACjB,YAAM,QAAQ,OAAO;AAAA,IACvB;AAAA,IACA,MAAM,MAAM,SAAS,SAAS;AAC5B,YAAM,eAAe,MAAM,KAAK,aAAa,SAAS,OAAO;AAC7D,YAAM,UAAU,MAAM,YAAY,KAAK,SAAS,aAAa,IAAI,OAAO;AACxE,YAAM,OAAO,QAAQ,QAAQ,SAAS,GAAG;AACzC,YAAM,QAAQ,KAAK;AACnB,UAAI,KAAM,OAAM,aAAa,MAAM,OAAO;AAC1C,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,IACA,MAAM,UAAU;AACd,oBAAc,KAAK;AACnB,cAAQ;AACR,YAAM,MAAM,CAAC,GAAG,QAAQ,OAAO,CAAC;AAChC,cAAQ,MAAM;AACd,YAAM,QAAQ,IAAI,IAAI,IAAI,CAAC,WAAW,OAAO,OAAO,CAAC,CAAC;AAAA,IACxD;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,41 @@
1
+ import type { Context } from '@earendil-works/chord';
2
+ import { type CommitPublication, type Harness, type SnapshotEvent } from '@earendil-works/pi-durable';
3
+ import type { RemoteEvent } from './events.ts';
4
+ export interface BusyState {
5
+ busy: boolean;
6
+ why: string[];
7
+ }
8
+ export declare function snapshotBusy(snapshot: SnapshotEvent): boolean;
9
+ export declare class BusyTracker {
10
+ #private;
11
+ constructor(conversation: number);
12
+ seed(agents: unknown, work: unknown, outbox: unknown): void;
13
+ conversations(): number[];
14
+ conversation(id: number, live: unknown, inbox: unknown): void;
15
+ apply(publication: CommitPublication): boolean;
16
+ value(): BusyState;
17
+ }
18
+ export declare function trackBusy(harness: Harness, conversation: number, context: Context): Promise<{
19
+ tracker: BusyTracker;
20
+ listen(listener: () => void): void;
21
+ stop(): void;
22
+ }>;
23
+ export declare function busyLine(state: BusyState): RemoteEvent;
24
+ export declare class BusyGate {
25
+ #private;
26
+ constructor(tracker: BusyTracker, push: (events: RemoteEvent[]) => void, sent: boolean);
27
+ before(events: RemoteEvent[]): RemoteEvent[];
28
+ changed(): void;
29
+ stop(): void;
30
+ }
31
+ export declare function watchBusy(harness: Harness, watched: {
32
+ conversation: {
33
+ id: number;
34
+ };
35
+ snapshot: SnapshotEvent;
36
+ stop(): Promise<unknown>;
37
+ }, push: (events: RemoteEvent[]) => void, context: Context): Promise<{
38
+ opened: boolean;
39
+ gate: BusyGate;
40
+ stop(): void;
41
+ }>;
@@ -20,9 +20,11 @@ export interface RemoteControl {
20
20
  watch(request?: {
21
21
  since?: string | null;
22
22
  thinking?: boolean;
23
+ busy?: boolean;
23
24
  }): Promise<{
24
25
  head: string;
25
26
  gap?: true;
27
+ busy?: boolean;
26
28
  events: AsyncIterable<RemoteEvent>;
27
29
  }>;
28
30
  abort(): Promise<{
@@ -40,6 +40,10 @@ export type RemoteEvent = {
40
40
  id: string;
41
41
  message: string;
42
42
  detail?: string;
43
+ } | {
44
+ t: 'busy';
45
+ busy: boolean;
46
+ why?: string[];
43
47
  } | {
44
48
  t: 'end';
45
49
  reason: EndReason;
@@ -14,6 +14,7 @@ export type RemoteOpened = {
14
14
  delivered: Delivered | null;
15
15
  head: string | null;
16
16
  gap: boolean;
17
+ busy?: boolean;
17
18
  } | {
18
19
  ok: false;
19
20
  code: string;
@@ -35,6 +36,7 @@ export interface AgentRemote {
35
36
  agentId: string | null;
36
37
  since: string | null;
37
38
  thinking: boolean;
39
+ busy?: boolean;
38
40
  }, context: Context): Promise<RemoteOpened>;
39
41
  next(stream: string, waitMs: number, context: Context): Promise<RemoteBatch>;
40
42
  close(stream: string, context: Context): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-agent",
3
- "version": "6.14.2",
3
+ "version": "6.15.0",
4
4
  "description": "SLICC's agent core on pi durable: an agent worker, session storage, remoting and pi extensions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -68,7 +68,7 @@
68
68
  "@ai-ecoverse/slicc-kernel": "1.132.1",
69
69
  "@ai-ecoverse/slicc-scrub": "1.0.0",
70
70
  "@ai-ecoverse/slicc-shared-web": "1.25.1",
71
- "@ai-ecoverse/slicc-spectrum": "1.98.0",
71
+ "@ai-ecoverse/slicc-spectrum": "1.99.1",
72
72
  "@ai-ecoverse/wasi-fnox": "1.39.0-5",
73
73
  "@ai-ecoverse/wasm-bash": "5.3.0-21",
74
74
  "@ai-ecoverse/wasm-coreutils": "9.12.0-10",
@@ -86,7 +86,7 @@
86
86
  },
87
87
  "dependencies": {
88
88
  "@ai-ecoverse/slicc-browsers": "1.9.0",
89
- "@ai-ecoverse/slicc-vault": "1.13.0",
89
+ "@ai-ecoverse/slicc-vault": "1.14.0",
90
90
  "@aws-sdk/middleware-websocket": "^3.972.54",
91
91
  "@earendil-works/chord": "1.3.0",
92
92
  "@earendil-works/pi-ai": "1.3.0",
@@ -98,7 +98,7 @@
98
98
  },
99
99
  "peerDependencies": {
100
100
  "@ai-ecoverse/slicc-kernel": ">=1.110.0 <2",
101
- "@ai-ecoverse/slicc-spectrum": "^1.96.0"
101
+ "@ai-ecoverse/slicc-spectrum": "^1.98.0"
102
102
  },
103
103
  "peerDependenciesMeta": {
104
104
  "@ai-ecoverse/slicc-spectrum": {