@ai-ecoverse/slicc-agent 3.2.0 → 3.4.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 +45 -1
- package/dist/control.js +44 -1
- package/dist/control.js.map +2 -2
- package/dist/host.js +5 -1
- package/dist/host.js.map +2 -2
- package/dist/kernel/activity.js +39 -0
- package/dist/kernel/activity.js.map +7 -0
- package/dist/kernel/env.js +16 -8
- package/dist/kernel/env.js.map +2 -2
- package/dist/licks/boot.js +66 -0
- package/dist/licks/boot.js.map +7 -0
- package/dist/licks/config.js +126 -0
- package/dist/licks/config.js.map +7 -0
- package/dist/licks/cron.js +120 -0
- package/dist/licks/cron.js.map +7 -0
- package/dist/licks/extension.js +123 -0
- package/dist/licks/extension.js.map +7 -0
- package/dist/licks/fswatch.js +149 -0
- package/dist/licks/fswatch.js.map +7 -0
- package/dist/licks/glob.js +47 -0
- package/dist/licks/glob.js.map +7 -0
- package/dist/licks/index.js +36 -0
- package/dist/licks/index.js.map +7 -0
- package/dist/licks/lick.js +102 -0
- package/dist/licks/lick.js.map +7 -0
- package/dist/licks/licks.js +181 -0
- package/dist/licks/licks.js.map +7 -0
- package/dist/licks/schedules.js +50 -0
- package/dist/licks/schedules.js.map +7 -0
- package/dist/licks/sources.js +138 -0
- package/dist/licks/sources.js.map +7 -0
- package/dist/licks/state.js +56 -0
- package/dist/licks/state.js.map +7 -0
- package/dist/main.js +23 -4
- package/dist/main.js.map +2 -2
- package/dist/services.js.map +2 -2
- package/dist/spectrum/agent-port.js +6 -1
- package/dist/spectrum/agent-port.js.map +2 -2
- package/dist/spectrum/messages.js +94 -6
- package/dist/spectrum/messages.js.map +2 -2
- package/dist/system.js +16 -0
- package/dist/system.js.map +2 -2
- package/dist/types/control.d.ts +6 -1
- package/dist/types/host.d.ts +2 -0
- package/dist/types/kernel/activity.d.ts +10 -0
- package/dist/types/kernel/env.d.ts +2 -0
- package/dist/types/licks/boot.d.ts +9 -0
- package/dist/types/licks/config.d.ts +35 -0
- package/dist/types/licks/cron.d.ts +16 -0
- package/dist/types/licks/extension.d.ts +22 -0
- package/dist/types/licks/fswatch.d.ts +20 -0
- package/dist/types/licks/glob.d.ts +1 -0
- package/dist/types/licks/index.d.ts +24 -0
- package/dist/types/licks/lick.d.ts +21 -0
- package/dist/types/licks/licks.d.ts +56 -0
- package/dist/types/licks/schedules.d.ts +25 -0
- package/dist/types/licks/sources.d.ts +38 -0
- package/dist/types/licks/state.d.ts +14 -0
- package/dist/types/services.d.ts +18 -0
- package/dist/types/spectrum/agent-port.d.ts +1 -1
- package/dist/types/spectrum/messages.d.ts +5 -1
- package/dist/types/system.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -106,7 +106,51 @@ app.model = { ...createKernelModel({ kernel, root }), ...createAgentModel(agent,
|
|
|
106
106
|
|
|
107
107
|
The adapter keeps the replicated transcript and maps it on every update. User, assistant and compaction entries become messages; tool results fill in the tool calls they answer; the generation in flight shows as a streaming message; retries and running compactions show as system messages. A failed generation ends in an error card whose action fits the error: credential errors (an invalid or missing key, 401/403, not authorized) open settings, a model the account can't use offers the model picker, and anything else offers Retry. A content-filter stop is different: every later request resends the content that tripped the filter, so Retry, and any new message, would be stopped again. Content-filter stops are Bedrock's `content_filtered` and `guardrail_intervened`, Anthropic's `sensitive` and refusals, and OpenAI's `content_filter`. Their card leads with "The model's content filter stopped this reply." and offers **Drop the last turn** (spectrum ≥ 1.12.0). `AgentControl.rewind(messageId)` finds the user turn before that message and forks the cone's conversation at the entry before it (`Conversation.fork`, so `pi.agent` comes back as it was there). It points the cone at the fork and returns the turn's prompt, which spectrum puts back into the composer. Forking the very first turn creates a fresh conversation with the same model and thinking level. The filtered branch stays in storage, off the active path. The fork starts with a `slicc.rewound` entry that shows as a "Rewound 1 turn" notice, and clicking the card of an earlier filtered turn rewinds that turn.
|
|
108
108
|
|
|
109
|
-
The cone's active conversation is the session document `slicc.cone` (the root until the first rewind). The host's transcript follows it across forks. Rewinding is refused while the cone is running or has queued input, because a fork starts with an empty inbox. A background compaction still running on the old branch finishes there. A fork also starts with fresh `pi.usage`. Scoops don't exist yet. When they do, a fork won't carry tasks or their documents along, so rewinding past a scoop's creation will need its own rule. Credential and model errors lead with a plain sentence ("Bedrock rejected the API key.", "This model isn't available with the current account.") and keep the provider's raw error as `detail`, which spectrum (≥ 1.11.0) shows underneath. `busy()` and `queue()` read durable's live and inbox documents. Steering is the default (#4): a send or a steer goes out with `whenBusy: 'steer'`, so while a run is going it joins after the current tool round, and a `queue` send is a follow-up. User messages carry `delivered` from the host's deliveries; entries the host didn't record (from before a reload) count as steered when they follow a tool result. There is one cone so far; scoops, the freezer
|
|
109
|
+
The cone's active conversation is the session document `slicc.cone` (the root until the first rewind). The host's transcript follows it across forks. Rewinding is refused while the cone is running or has queued input, because a fork starts with an empty inbox. A background compaction still running on the old branch finishes there. A fork also starts with fresh `pi.usage`. Scoops don't exist yet. When they do, a fork won't carry tasks or their documents along, so rewinding past a scoop's creation will need its own rule. Credential and model errors lead with a plain sentence ("Bedrock rejected the API key.", "This model isn't available with the current account.") and keep the provider's raw error as `detail`, which spectrum (≥ 1.11.0) shows underneath. `busy()` and `queue()` read durable's live and inbox documents. Steering is the default (#4): a send or a steer goes out with `whenBusy: 'steer'`, so while a run is going it joins after the current tool round, and a `queue` send is a follow-up. User messages carry `delivered` from the host's deliveries; entries the host didn't record (from before a reload) count as steered when they follow a tool result. There is one cone so far; scoops, the freezer and questions come later. Licks show as spectrum's lick cards (below). slicc-spectrum is an optional peer dependency (≥ 1.13.0), used for types only, so package managers don't install it next to the agent.
|
|
110
|
+
|
|
111
|
+
Each tool call shows its input in the way that suits it:
|
|
112
|
+
- `bash`: its command, with a timeout as the card's `meta`;
|
|
113
|
+
- `read`: the path, with its line range as `meta`;
|
|
114
|
+
- `write`: the path;
|
|
115
|
+
- `edit`: the path, and its change as a `diff` (spectrum ≥ 1.13.0). While the edit runs, the diff comes from its `oldText`/`newText` pairs. Once done, it comes from the hunks of the unified `patch` that pi's edit tool returns in its result details, with context lines;
|
|
116
|
+
- other tools: their arguments as pretty-printed JSON.
|
|
117
|
+
|
|
118
|
+
### Licks
|
|
119
|
+
|
|
120
|
+
A lick is an event from outside the conversation (a file change, a schedule, a webhook, a reload, an upgrade) that reaches the agent as a steering input. pi durable's `submit({ type: 'input', whenBusy: 'steer' })` does the delivery: an idle cone starts a run, and a busy one gets the lick at its next boundary, after the current tool round, like a steer the user types. So the agent sees events during a long run instead of after it. Durable has no custom message role, so the input's text carries the lick:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
<lick id="lk-…" channel="fswatch" source="notes" title="notes: /home/notes/**/*.md" count="2" at="2026-10-08T12:00:00.000Z">
|
|
124
|
+
2 paths changed under /home/notes matching **/*.md
|
|
125
|
+
changed /home/notes/today.md
|
|
126
|
+
gone /home/notes/old.md
|
|
127
|
+
</lick>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`&`, `<` and `>` are escaped everywhere inside it, and `"` and line breaks in attributes too, so no payload (a webhook body, a file name, a crontab message) can close the tag, open another one or add an attribute. A `licks` prompt section tells the model that lick text comes from events, not from the user, and that webhook payloads may be hostile. The adapter maps these inputs to spectrum's `LickMessage` (channel, title, text, body, `count` when more than one). A message the user starts with `<lick ` the same way also shows as a card.
|
|
131
|
+
|
|
132
|
+
**Coalescing.** Licks with the same target, channel and source share an outbox (the session document `slicc.licks`). At most one of them is undelivered (queued in `pi.inbox`) at a time. Events that arrive while it waits are merged into the next one: the count adds up and the paths are unioned (capped at 50 shown). Once the queued lick is placed, the merged one is submitted. Nothing is ever withdrawn, so a lick that already reached the model is never touched, and each lick's `count` is exactly the events it carries. Webhook requests are never merged. A lick's `requestId` is `lick:<id>`, and events with an id (cron fires, webhook deliveries, boot facts) are remembered per source, so a crash between admitting a lick and recording it doesn't deliver it twice.
|
|
133
|
+
|
|
134
|
+
**Stop.** pi's `Conversation.abort()` withdraws every queued steer and follow-up, so a lick that wasn't placed yet is withdrawn with them, and the events merged behind it are dropped too: Stop leaves the cone quiet. Events that arrive after Stop are delivered as usual, and an idle cone starts a run for them.
|
|
135
|
+
|
|
136
|
+
**Configuration** lives in files under `~/.slicc` (`/home/.slicc`), which the user and the agent edit with any tool. The worker watches the folder and reconciles on every change. Each malformed line or file becomes one lick naming the file, the line and the problem, once per distinct problem; the entry is ignored until it's fixed. Names are 1–64 of `a-z 0-9 . _ -`, starting with a letter or digit. A target is `cone` or `scoop:<name>` (scoops go to the cone until they exist). Files starting with `.` or ending in `~` are skipped; any other file that isn't `<name>.json` in `watches/` or `webhooks/` is an error.
|
|
137
|
+
|
|
138
|
+
- `~/.slicc/crontab`, one job per line: `<schedule> <name> [<target>] [<message>]`. The schedule is five fields (minute, hour, day of month, month, day of week, with `*`, ranges, steps, lists and `jan`–`dec`/`sun`–`sat` names; 7 is Sunday, and a job whose day of month and day of week are both set runs on either) or `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`. Times are the browser's local time. Blank lines and `#` comments are ignored.
|
|
139
|
+
```
|
|
140
|
+
*/15 9-17 * * mon-fri standup Summarize what changed in ~/notes since the last standup
|
|
141
|
+
@daily backup scoop:ops
|
|
142
|
+
```
|
|
143
|
+
- `~/.slicc/watches/<name>.json`: `{ "path": "~/notes", "glob": "**/*.md", "target": "cone", "message": "…", "debounce": 500 }`. `path` is absolute or starts with `~/`; `glob` (default `**`) is matched against paths relative to it, with `*`, `**`, `?`, `[…]` and `{a,b}`. `debounce` is 100–60000 ms. `node_modules` and `.git` are never scanned.
|
|
144
|
+
- `~/.slicc/webhooks/<name>.json`: `{ "target": "cone", "message": "…" }`. The URL comes from the tray hub (milestone E); until then, `AgentControl.webhook(name, { id, headers, body })` is how a delivery arrives.
|
|
145
|
+
|
|
146
|
+
**Sources.**
|
|
147
|
+
- `fswatch`: the kernel environment's `watch()` reports changed paths; those matching the glob are collected for `debounce` ms and delivered as one lick with `changed` or `gone` per path. A watch that overflows or stops says so in a lick, and saving its file restarts it. Changes made while no worker runs aren't reported. **The agent's own changes are dropped**, so it never licks itself: the kernel environment records every path its file tools write, edit, move or remove, and the time its `bash` commands run; a change to such a path within 2 s, or any change while an agent command runs or within 2 s after it, counts as the agent's. A change the user makes while an agent command runs is therefore dropped too. Background processes the agent started (`&`) aren't tracked, so their changes do lick.
|
|
148
|
+
- `cron`: each line is a background durable task owned by the root conversation (`slicc.cron`), so Stop and rewinds don't affect it and it survives reloads. It sleeps until the next due time on the Harness clock, then delivers to the target the cone pointer names at that moment. Each due time is delivered exactly once, also across a worker restart: the fire's id is `<name>@<due time>`. **Missed fires** (seven was closed or the tab asleep) are delivered once, as one lick whose `count` is 1 plus the missed fires and whose text says how many were missed (counted exactly up to 100,000, then "at least 100,000"); the next fire is then computed from now. Editing a line replaces its task; removing it aborts the task.
|
|
149
|
+
- `webhook`: each request is its own lick, with the headers and the body; a delivery `id` makes retries idempotent.
|
|
150
|
+
- `upgrade`: when the worker starts with a different slicc-agent version than the session recorded, with the release notes link. Every change of version is its own lick, also back and forth.
|
|
151
|
+
- `session-reload`: when the kernel's boot time (`btime` in `/proc/stat`) differs from the last worker start and the cone ran `bash` since then. The page reloaded, so the kernel's processes are gone; the lick lists background commands (`&`, `nohup`, `setsid`, `disown`) that are no longer running. Durable itself resumes an interrupted run.
|
|
152
|
+
|
|
153
|
+
**Decisions.** A lick channel with a handler (`licks.handle(channel, { confirm, dismiss })`) gets `actions="confirm dismiss"` (or one of them), and its card is pending until decided. The agent decides with the tools `lick_confirm` and `lick_dismiss` (`{ lick_id, reason? }`); the user decides from the card (`AgentPort.resolveLick` → `AgentControl.resolveLick`), which runs the handler and writes a `slicc.lick-state` entry that tells the agent. The state comes from these entries in the transcript, so rewinds and forks keep it consistent. Deciding twice, an unknown id, or an action the channel doesn't have is an error result. No channel in this release has a handler yet; scoop permission requests, upskill links and mount recovery will. The two tools' cards are folded into the lick card.
|
|
110
154
|
|
|
111
155
|
## Patched dependencies
|
|
112
156
|
|
package/dist/control.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ConversationBusy
|
|
4
4
|
} from "@earendil-works/pi-durable";
|
|
5
5
|
import { answerText } from "./agent.js";
|
|
6
|
+
import { LICK_STATE_KIND } from "./licks/state.js";
|
|
6
7
|
function submissionId(value) {
|
|
7
8
|
const id = Number(value);
|
|
8
9
|
return /^[1-9][0-9]*$/.test(value) && Number.isSafeInteger(id) ? id : void 0;
|
|
@@ -43,7 +44,7 @@ function busy(view) {
|
|
|
43
44
|
const inbox = view.docs["pi.inbox"];
|
|
44
45
|
return Boolean(live?.run) || Boolean(inbox?.items?.length);
|
|
45
46
|
}
|
|
46
|
-
function createAgentControl(harness, target, deliveries) {
|
|
47
|
+
function createAgentControl(harness, target, deliveries, licks) {
|
|
47
48
|
const cone = cones(target);
|
|
48
49
|
const current = () => cone.cone();
|
|
49
50
|
let lock = Promise.resolve();
|
|
@@ -123,6 +124,48 @@ function createAgentControl(harness, target, deliveries) {
|
|
|
123
124
|
abort: (context) => current().abort(context),
|
|
124
125
|
compact: (instructions, context) => accepted(() => current().compact(instructions ?? void 0, context)),
|
|
125
126
|
reset: (handoff, context) => current().reset(handoff ?? void 0, context),
|
|
127
|
+
resolveLick(lickId, state, context) {
|
|
128
|
+
return serial(async () => {
|
|
129
|
+
if (!licks) return { done: false, text: null, error: "this agent has no licks" };
|
|
130
|
+
const conversation = current();
|
|
131
|
+
const action = state === "confirmed" ? "confirm" : "dismiss";
|
|
132
|
+
try {
|
|
133
|
+
const text = await licks.licks.decide(conversation, lickId, action, void 0, context);
|
|
134
|
+
await conversation.submit(
|
|
135
|
+
{
|
|
136
|
+
type: "write",
|
|
137
|
+
entry: {
|
|
138
|
+
kind: LICK_STATE_KIND,
|
|
139
|
+
data: { lick: lickId, state, by: "user" },
|
|
140
|
+
model: [
|
|
141
|
+
{
|
|
142
|
+
role: "user",
|
|
143
|
+
content: `The user ${state} lick ${lickId}: ${text}`,
|
|
144
|
+
timestamp: Date.now()
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
context
|
|
150
|
+
);
|
|
151
|
+
return { done: true, text, error: null };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return { done: false, text: null, error: error.message };
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
async webhook(name, delivery, context) {
|
|
158
|
+
const delivered = licks ? await licks.sources.webhook(
|
|
159
|
+
name,
|
|
160
|
+
{
|
|
161
|
+
...delivery.id ? { id: delivery.id } : {},
|
|
162
|
+
headers: delivery.headers,
|
|
163
|
+
body: delivery.body
|
|
164
|
+
},
|
|
165
|
+
context
|
|
166
|
+
) : false;
|
|
167
|
+
return { delivered };
|
|
168
|
+
},
|
|
126
169
|
configure: (change, context) => current().configure(
|
|
127
170
|
{
|
|
128
171
|
...change.model ? { model: change.model } : {},
|
package/dist/control.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/control.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type AgentChange,\n AssistantEntry,\n type Conversation,\n ConversationBusy,\n type ConversationView,\n type EntryId,\n type EntryRecord,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport { answerText } from './agent.ts';\nimport type { Cones } from './cone.ts';\nimport type { Deliveries } from './deliveries.ts';\nimport type { AgentControl, OperationError, SendResponse } from './services.ts';\n\nexport function submissionId(value: string): SubmissionId | undefined {\n const id = Number(value);\n return /^[1-9][0-9]*$/.test(value) && Number.isSafeInteger(id) ? (id as SubmissionId) : undefined;\n}\n\nfunction operationError(error: unknown): OperationError {\n if (error instanceof ConversationBusy) return { code: 'busy', message: error.message };\n return { code: 'failed', message: error instanceof Error ? error.message : String(error) };\n}\n\nasync function accepted(operation: () => Promise<{ id: number } | number>): Promise<SendResponse> {\n try {\n const result = await operation();\n const id = typeof result === 'number' ? result : result.id;\n return { accepted: true, submissionId: String(id), error: null };\n } catch (error) {\n return { accepted: false, submissionId: null, error: operationError(error) };\n }\n}\n\nfunction cones(target: Conversation | Cones): Cones {\n if ('cone' in target) return target;\n return {\n cone: () => target,\n switchCone: async () => {\n throw new Error('this conversation has no cone to rewind');\n },\n };\n}\n\ninterface Turn {\n text: string;\n at: EntryId | null;\n}\n\nfunction lastTurn(view: ConversationView, messageId: string | null): Turn | null {\n const { entries } = view;\n const target = entries.findIndex((entry) => `e${entry.id}` === messageId);\n if (target < 0) return null;\n const turn = entries.slice(0, target).findLastIndex((entry) => entry.kind === 'pi.user');\n if (turn < 0) return null;\n const user = entries[turn] as EntryRecord;\n return { text: answerText(user.model?.[0]?.content), at: entries[turn - 1]?.id ?? null };\n}\n\nfunction busy(view: ConversationView): boolean {\n const live = view.docs['pi.live'] as { run?: unknown } | undefined;\n const inbox = view.docs['pi.inbox'] as { items?: unknown[] } | undefined;\n return Boolean(live?.run) || Boolean(inbox?.items?.length);\n}\n\nexport function createAgentControl(\n harness: Harness,\n target: Conversation | Cones,\n deliveries?: Deliveries\n): AgentControl {\n const cone = cones(target);\n const current = () => cone.cone();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n return {\n send(request, context) {\n return serial(async () => {\n const response = await accepted(() =>\n current().submit(\n {\n type: 'input',\n content: request.text,\n whenBusy: request.whenBusy,\n ...(request.requestId ? { requestId: request.requestId } : {}),\n },\n context\n )\n );\n const id = response.submissionId === null ? undefined : submissionId(response.submissionId);\n if (id !== undefined) deliveries?.expect(id, request.whenBusy);\n return response;\n });\n },\n rewind(messageId, context) {\n return serial(async () => {\n const conversation = current();\n const attached = await conversation.viewState(context);\n const view = attached.value;\n attached.dispose();\n const pending = (await harness.inspect(context)).submissions.some(\n (submission) => submission.conversationId === conversation.id\n );\n if (pending || busy(view)) return { done: false, text: null, reason: 'busy' };\n const turn = lastTurn(view, messageId);\n if (!turn) return { done: false, text: null, reason: 'no-turn' };\n const agent = view.docs['pi.agent'] as AgentChange | undefined;\n const ownership = { kind: 'ownerless' } as const;\n const fork =\n turn.at === null\n ? await harness.createConversation(\n {\n ownership,\n agent: {\n model: agent?.model ?? null,\n thinkingLevel: agent?.thinkingLevel ?? null,\n },\n },\n context\n )\n : await conversation.fork(turn.at, { ownership }, context);\n await fork.submit(\n { type: 'write', entry: { kind: 'slicc.rewound', data: { turns: 1 } } },\n context\n );\n await cone.switchCone(fork, context);\n return { done: true, text: turn.text, reason: null };\n });\n },\n\n async wait(id, context) {\n const parsed = submissionId(id);\n const submission =\n parsed === undefined ? undefined : await harness.submission(parsed, context);\n if (!submission) throw new Error(`unknown submission ${id}`);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) return { status: 'unanswered', text: null, reason: String(reason) };\n const entry = await harness.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return { status: 'done', text: answerText(entry?.model?.[0]?.content), reason: null };\n },\n async withdraw(id, context) {\n const parsed = submissionId(id);\n if (parsed === undefined) return { outcome: 'not_found' };\n const result = await harness.abortSubmission(parsed, context, current().id);\n if (result === 'aborted') return { outcome: 'withdrawn' };\n return { outcome: result === 'not_found' ? 'not_found' : 'already_placed' };\n },\n abort: (context) => current().abort(context),\n compact: (instructions, context) =>\n accepted(() => current().compact(instructions ?? undefined, context)),\n reset: (handoff, context) => current().reset(handoff ?? undefined, context),\n configure: (change, context) =>\n current().configure(\n {\n ...(change.model ? { model: change.model } : {}),\n ...(change.thinkingLevel ? { thinkingLevel: change.thinkingLevel } : {}),\n },\n context\n ),\n };\n}\n"],
|
|
5
|
-
"mappings": "AAAA;AAAA,EAEE;AAAA,EAEA;AAAA,OAMK;AACP,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import {\n type AgentChange,\n AssistantEntry,\n type Conversation,\n ConversationBusy,\n type ConversationView,\n type EntryId,\n type EntryRecord,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport { answerText } from './agent.ts';\nimport type { Cones } from './cone.ts';\nimport type { Deliveries } from './deliveries.ts';\nimport type { LickSources, Licks } from './licks/index.ts';\nimport { LICK_STATE_KIND } from './licks/state.ts';\nimport type { AgentControl, OperationError, SendResponse } from './services.ts';\n\nexport interface HostLicks {\n licks: Licks;\n sources: LickSources;\n}\n\nexport function submissionId(value: string): SubmissionId | undefined {\n const id = Number(value);\n return /^[1-9][0-9]*$/.test(value) && Number.isSafeInteger(id) ? (id as SubmissionId) : undefined;\n}\n\nfunction operationError(error: unknown): OperationError {\n if (error instanceof ConversationBusy) return { code: 'busy', message: error.message };\n return { code: 'failed', message: error instanceof Error ? error.message : String(error) };\n}\n\nasync function accepted(operation: () => Promise<{ id: number } | number>): Promise<SendResponse> {\n try {\n const result = await operation();\n const id = typeof result === 'number' ? result : result.id;\n return { accepted: true, submissionId: String(id), error: null };\n } catch (error) {\n return { accepted: false, submissionId: null, error: operationError(error) };\n }\n}\n\nfunction cones(target: Conversation | Cones): Cones {\n if ('cone' in target) return target;\n return {\n cone: () => target,\n switchCone: async () => {\n throw new Error('this conversation has no cone to rewind');\n },\n };\n}\n\ninterface Turn {\n text: string;\n at: EntryId | null;\n}\n\nfunction lastTurn(view: ConversationView, messageId: string | null): Turn | null {\n const { entries } = view;\n const target = entries.findIndex((entry) => `e${entry.id}` === messageId);\n if (target < 0) return null;\n const turn = entries.slice(0, target).findLastIndex((entry) => entry.kind === 'pi.user');\n if (turn < 0) return null;\n const user = entries[turn] as EntryRecord;\n return { text: answerText(user.model?.[0]?.content), at: entries[turn - 1]?.id ?? null };\n}\n\nfunction busy(view: ConversationView): boolean {\n const live = view.docs['pi.live'] as { run?: unknown } | undefined;\n const inbox = view.docs['pi.inbox'] as { items?: unknown[] } | undefined;\n return Boolean(live?.run) || Boolean(inbox?.items?.length);\n}\n\nexport function createAgentControl(\n harness: Harness,\n target: Conversation | Cones,\n deliveries?: Deliveries,\n licks?: HostLicks\n): AgentControl {\n const cone = cones(target);\n const current = () => cone.cone();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n return {\n send(request, context) {\n return serial(async () => {\n const response = await accepted(() =>\n current().submit(\n {\n type: 'input',\n content: request.text,\n whenBusy: request.whenBusy,\n ...(request.requestId ? { requestId: request.requestId } : {}),\n },\n context\n )\n );\n const id = response.submissionId === null ? undefined : submissionId(response.submissionId);\n if (id !== undefined) deliveries?.expect(id, request.whenBusy);\n return response;\n });\n },\n rewind(messageId, context) {\n return serial(async () => {\n const conversation = current();\n const attached = await conversation.viewState(context);\n const view = attached.value;\n attached.dispose();\n const pending = (await harness.inspect(context)).submissions.some(\n (submission) => submission.conversationId === conversation.id\n );\n if (pending || busy(view)) return { done: false, text: null, reason: 'busy' };\n const turn = lastTurn(view, messageId);\n if (!turn) return { done: false, text: null, reason: 'no-turn' };\n const agent = view.docs['pi.agent'] as AgentChange | undefined;\n const ownership = { kind: 'ownerless' } as const;\n const fork =\n turn.at === null\n ? await harness.createConversation(\n {\n ownership,\n agent: {\n model: agent?.model ?? null,\n thinkingLevel: agent?.thinkingLevel ?? null,\n },\n },\n context\n )\n : await conversation.fork(turn.at, { ownership }, context);\n await fork.submit(\n { type: 'write', entry: { kind: 'slicc.rewound', data: { turns: 1 } } },\n context\n );\n await cone.switchCone(fork, context);\n return { done: true, text: turn.text, reason: null };\n });\n },\n\n async wait(id, context) {\n const parsed = submissionId(id);\n const submission =\n parsed === undefined ? undefined : await harness.submission(parsed, context);\n if (!submission) throw new Error(`unknown submission ${id}`);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) return { status: 'unanswered', text: null, reason: String(reason) };\n const entry = await harness.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return { status: 'done', text: answerText(entry?.model?.[0]?.content), reason: null };\n },\n async withdraw(id, context) {\n const parsed = submissionId(id);\n if (parsed === undefined) return { outcome: 'not_found' };\n const result = await harness.abortSubmission(parsed, context, current().id);\n if (result === 'aborted') return { outcome: 'withdrawn' };\n return { outcome: result === 'not_found' ? 'not_found' : 'already_placed' };\n },\n abort: (context) => current().abort(context),\n compact: (instructions, context) =>\n accepted(() => current().compact(instructions ?? undefined, context)),\n reset: (handoff, context) => current().reset(handoff ?? undefined, context),\n resolveLick(lickId, state, context) {\n return serial(async () => {\n if (!licks) return { done: false, text: null, error: 'this agent has no licks' };\n const conversation = current();\n const action = state === 'confirmed' ? 'confirm' : 'dismiss';\n try {\n const text = await licks.licks.decide(conversation, lickId, action, undefined, context);\n await conversation.submit(\n {\n type: 'write',\n entry: {\n kind: LICK_STATE_KIND,\n data: { lick: lickId, state, by: 'user' },\n model: [\n {\n role: 'user',\n content: `The user ${state} lick ${lickId}: ${text}`,\n timestamp: Date.now(),\n },\n ],\n },\n },\n context\n );\n return { done: true, text, error: null };\n } catch (error) {\n return { done: false, text: null, error: (error as Error).message };\n }\n });\n },\n async webhook(name, delivery, context) {\n const delivered = licks\n ? await licks.sources.webhook(\n name,\n {\n ...(delivery.id ? { id: delivery.id } : {}),\n headers: delivery.headers,\n body: delivery.body,\n },\n context\n )\n : false;\n return { delivered };\n },\n configure: (change, context) =>\n current().configure(\n {\n ...(change.model ? { model: change.model } : {}),\n ...(change.thinkingLevel ? { thinkingLevel: change.thinkingLevel } : {}),\n },\n context\n ),\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/host.js
CHANGED
|
@@ -93,7 +93,10 @@ async function hostAgent(agent, options = {}) {
|
|
|
93
93
|
{ service: AgentSettings, mode: "singleton" }
|
|
94
94
|
]);
|
|
95
95
|
const deliveries = trackDeliveries(agent.harness, state, context);
|
|
96
|
-
provider.provide(
|
|
96
|
+
provider.provide(
|
|
97
|
+
AgentControl,
|
|
98
|
+
createAgentControl(agent.harness, agent, deliveries, options.licks)
|
|
99
|
+
);
|
|
97
100
|
provider.provide(AgentTranscript, { state, deliveries: deliveries.state });
|
|
98
101
|
provider.provide(AgentSettings, options.settings ?? noSettings);
|
|
99
102
|
const host = {
|
|
@@ -116,6 +119,7 @@ async function hostAgent(agent, options = {}) {
|
|
|
116
119
|
provider.dispose();
|
|
117
120
|
deliveries.dispose();
|
|
118
121
|
transcript.dispose();
|
|
122
|
+
await options.licks?.sources.close(context);
|
|
119
123
|
await agent.close();
|
|
120
124
|
}
|
|
121
125
|
};
|
package/dist/host.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/host.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type Context,\n createRemoteServiceEndpoint,\n RemoteServiceProvider,\n replicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport {\n type RoutedServerServiceHost,\n type RoutedSessionAttachment,\n type RoutedSessionHandle,\n Server,\n type ServerHost,\n SessionNotFoundError,\n} from '@earendil-works/pi-server';\nimport type { Agent } from './agent.ts';\nimport { createAgentControl } from './control.ts';\nimport { trackDeliveries } from './deliveries.ts';\nimport { PortListener } from './listener.ts';\nimport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n AgentSettings,\n AgentTranscript,\n} from './services.ts';\nimport type { PortEndpoint } from './wire.ts';\n\nexport interface AgentHost {\n readonly serverId: string;\n readonly agent: Agent;\n connect(endpoint: PortEndpoint): void;\n close(): Promise<void>;\n}\n\nfunction endpointFor(provider: RemoteServiceProvider): RoutedSessionAttachment {\n const endpoint = createRemoteServiceEndpoint(provider);\n return {\n invokeService: (call, publish, context) =>\n endpoint.invoke(call, (id, update) => publish(id, update, context), context),\n release: () => endpoint.dispose(),\n };\n}\n\nfunction sessionServices(provider: RemoteServiceProvider): RoutedSessionHandle {\n return {\n attachClient: () => endpointFor(provider),\n close: async () => {},\n };\n}\n\nfunction serverServices(): RoutedServerServiceHost {\n return {\n attachClient(presentation) {\n const provider = new RemoteServiceProvider([{ service: AgentSessions, mode: 'singleton' }]);\n provider.provide(AgentSessions, {\n attach: (sessionId, context) => presentation.attachSession(sessionId, context),\n detach: (context) => presentation.detachSession(context),\n });\n const attachment = endpointFor(provider);\n return {\n invokeService: attachment.invokeService,\n release(context) {\n attachment.release(context);\n provider.dispose();\n },\n };\n },\n };\n}\n\nconst noSettings: AgentSettings = {\n state: replicatedState({ models: [], accounts: [] }),\n connect: async () => {\n throw new Error('this agent has no accounts');\n },\n disconnect: async () => {},\n signIn: async () => null,\n};\n\nasync function follow(agent: Agent, context: Context) {\n let attached = await agent.cone().viewState(context);\n const state = replicatedState<ConversationView>(attached.value);\n const mirror = (value: ConversationView, from: Context) => state.replace(from, value);\n let off = attached.subscribe(mirror);\n const stop = agent.onCone(async (conversation) => {\n const next = await conversation.viewState(context);\n off();\n attached.dispose();\n attached = next;\n state.replace(context, next.value);\n off = next.subscribe(mirror);\n });\n return {\n state,\n dispose() {\n stop();\n off();\n attached.dispose();\n },\n };\n}\n\nexport async function hostAgent(\n agent: Agent,\n options: {
|
|
5
|
-
"mappings": "AAAA;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0BAA0B;AAEnC;AAAA,EAIE;AAAA,EAEA;AAAA,OACK;AAEP,SAAS,
|
|
4
|
+
"sourcesContent": ["import {\n type Context,\n createRemoteServiceEndpoint,\n RemoteServiceProvider,\n replicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport {\n type RoutedServerServiceHost,\n type RoutedSessionAttachment,\n type RoutedSessionHandle,\n Server,\n type ServerHost,\n SessionNotFoundError,\n} from '@earendil-works/pi-server';\nimport type { Agent } from './agent.ts';\nimport { createAgentControl, type HostLicks } from './control.ts';\nimport { trackDeliveries } from './deliveries.ts';\nimport { PortListener } from './listener.ts';\nimport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n AgentSettings,\n AgentTranscript,\n} from './services.ts';\nimport type { PortEndpoint } from './wire.ts';\n\nexport interface AgentHost {\n readonly serverId: string;\n readonly agent: Agent;\n connect(endpoint: PortEndpoint): void;\n close(): Promise<void>;\n}\n\nfunction endpointFor(provider: RemoteServiceProvider): RoutedSessionAttachment {\n const endpoint = createRemoteServiceEndpoint(provider);\n return {\n invokeService: (call, publish, context) =>\n endpoint.invoke(call, (id, update) => publish(id, update, context), context),\n release: () => endpoint.dispose(),\n };\n}\n\nfunction sessionServices(provider: RemoteServiceProvider): RoutedSessionHandle {\n return {\n attachClient: () => endpointFor(provider),\n close: async () => {},\n };\n}\n\nfunction serverServices(): RoutedServerServiceHost {\n return {\n attachClient(presentation) {\n const provider = new RemoteServiceProvider([{ service: AgentSessions, mode: 'singleton' }]);\n provider.provide(AgentSessions, {\n attach: (sessionId, context) => presentation.attachSession(sessionId, context),\n detach: (context) => presentation.detachSession(context),\n });\n const attachment = endpointFor(provider);\n return {\n invokeService: attachment.invokeService,\n release(context) {\n attachment.release(context);\n provider.dispose();\n },\n };\n },\n };\n}\n\nconst noSettings: AgentSettings = {\n state: replicatedState({ models: [], accounts: [] }),\n connect: async () => {\n throw new Error('this agent has no accounts');\n },\n disconnect: async () => {},\n signIn: async () => null,\n};\n\nasync function follow(agent: Agent, context: Context) {\n let attached = await agent.cone().viewState(context);\n const state = replicatedState<ConversationView>(attached.value);\n const mirror = (value: ConversationView, from: Context) => state.replace(from, value);\n let off = attached.subscribe(mirror);\n const stop = agent.onCone(async (conversation) => {\n const next = await conversation.viewState(context);\n off();\n attached.dispose();\n attached = next;\n state.replace(context, next.value);\n off = next.subscribe(mirror);\n });\n return {\n state,\n dispose() {\n stop();\n off();\n attached.dispose();\n },\n };\n}\n\nexport async function hostAgent(\n agent: Agent,\n options: {\n serverId?: string;\n context?: Context;\n settings?: AgentSettings;\n licks?: HostLicks;\n } = {}\n): Promise<AgentHost> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const serverId = options.serverId ?? crypto.randomUUID();\n const transcript = await follow(agent, context);\n const { state } = transcript;\n const provider = new RemoteServiceProvider([\n { service: AgentControl, mode: 'singleton' },\n { service: AgentTranscript, mode: 'singleton' },\n { service: AgentSettings, mode: 'singleton' },\n ]);\n const deliveries = trackDeliveries(agent.harness, state, context);\n provider.provide(\n AgentControl,\n createAgentControl(agent.harness, agent, deliveries, options.licks)\n );\n provider.provide(AgentTranscript, { state, deliveries: deliveries.state });\n provider.provide(AgentSettings, options.settings ?? noSettings);\n const host: ServerHost = {\n serverServices: serverServices(),\n async resolveSession(sessionId) {\n if (sessionId !== AGENT_SESSION)\n throw new SessionNotFoundError(`unknown session ${sessionId}`);\n return { id: sessionId };\n },\n openSession: async () => sessionServices(provider),\n };\n const listener = new PortListener(serverId);\n const server = await new Server(host, { serverId, listeners: [listener] }).start();\n return {\n serverId,\n agent,\n connect: (endpoint) => listener.connect(endpoint),\n async close() {\n await server.close();\n provider.dispose();\n deliveries.dispose();\n transcript.dispose();\n await options.licks?.sources.close(context);\n await agent.close();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0BAA0B;AAEnC;AAAA,EAIE;AAAA,EAEA;AAAA,OACK;AAEP,SAAS,0BAA0C;AACnD,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAUP,SAAS,YAAY,UAA0D;AAC7E,QAAM,WAAW,4BAA4B,QAAQ;AACrD,SAAO;AAAA,IACL,eAAe,CAAC,MAAM,SAAS,YAC7B,SAAS,OAAO,MAAM,CAAC,IAAI,WAAW,QAAQ,IAAI,QAAQ,OAAO,GAAG,OAAO;AAAA,IAC7E,SAAS,MAAM,SAAS,QAAQ;AAAA,EAClC;AACF;AAEA,SAAS,gBAAgB,UAAsD;AAC7E,SAAO;AAAA,IACL,cAAc,MAAM,YAAY,QAAQ;AAAA,IACxC,OAAO,YAAY;AAAA,IAAC;AAAA,EACtB;AACF;AAEA,SAAS,iBAA0C;AACjD,SAAO;AAAA,IACL,aAAa,cAAc;AACzB,YAAM,WAAW,IAAI,sBAAsB,CAAC,EAAE,SAAS,eAAe,MAAM,YAAY,CAAC,CAAC;AAC1F,eAAS,QAAQ,eAAe;AAAA,QAC9B,QAAQ,CAAC,WAAW,YAAY,aAAa,cAAc,WAAW,OAAO;AAAA,QAC7E,QAAQ,CAAC,YAAY,aAAa,cAAc,OAAO;AAAA,MACzD,CAAC;AACD,YAAM,aAAa,YAAY,QAAQ;AACvC,aAAO;AAAA,QACL,eAAe,WAAW;AAAA,QAC1B,QAAQ,SAAS;AACf,qBAAW,QAAQ,OAAO;AAC1B,mBAAS,QAAQ;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,aAA4B;AAAA,EAChC,OAAO,gBAAgB,EAAE,QAAQ,CAAC,GAAG,UAAU,CAAC,EAAE,CAAC;AAAA,EACnD,SAAS,YAAY;AACnB,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAAA,EACA,YAAY,YAAY;AAAA,EAAC;AAAA,EACzB,QAAQ,YAAY;AACtB;AAEA,eAAe,OAAO,OAAc,SAAkB;AACpD,MAAI,WAAW,MAAM,MAAM,KAAK,EAAE,UAAU,OAAO;AACnD,QAAM,QAAQ,gBAAkC,SAAS,KAAK;AAC9D,QAAM,SAAS,CAAC,OAAyB,SAAkB,MAAM,QAAQ,MAAM,KAAK;AACpF,MAAI,MAAM,SAAS,UAAU,MAAM;AACnC,QAAM,OAAO,MAAM,OAAO,OAAO,iBAAiB;AAChD,UAAM,OAAO,MAAM,aAAa,UAAU,OAAO;AACjD,QAAI;AACJ,aAAS,QAAQ;AACjB,eAAW;AACX,UAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,UAAM,KAAK,UAAU,MAAM;AAAA,EAC7B,CAAC;AACD,SAAO;AAAA,IACL;AAAA,IACA,UAAU;AACR,WAAK;AACL,UAAI;AACJ,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;AAEA,eAAsB,UACpB,OACA,UAKI,CAAC,GACe;AACpB,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,WAAW,QAAQ,YAAY,OAAO,WAAW;AACvD,QAAM,aAAa,MAAM,OAAO,OAAO,OAAO;AAC9C,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,WAAW,IAAI,sBAAsB;AAAA,IACzC,EAAE,SAAS,cAAc,MAAM,YAAY;AAAA,IAC3C,EAAE,SAAS,iBAAiB,MAAM,YAAY;AAAA,IAC9C,EAAE,SAAS,eAAe,MAAM,YAAY;AAAA,EAC9C,CAAC;AACD,QAAM,aAAa,gBAAgB,MAAM,SAAS,OAAO,OAAO;AAChE,WAAS;AAAA,IACP;AAAA,IACA,mBAAmB,MAAM,SAAS,OAAO,YAAY,QAAQ,KAAK;AAAA,EACpE;AACA,WAAS,QAAQ,iBAAiB,EAAE,OAAO,YAAY,WAAW,MAAM,CAAC;AACzE,WAAS,QAAQ,eAAe,QAAQ,YAAY,UAAU;AAC9D,QAAM,OAAmB;AAAA,IACvB,gBAAgB,eAAe;AAAA,IAC/B,MAAM,eAAe,WAAW;AAC9B,UAAI,cAAc;AAChB,cAAM,IAAI,qBAAqB,mBAAmB,SAAS,EAAE;AAC/D,aAAO,EAAE,IAAI,UAAU;AAAA,IACzB;AAAA,IACA,aAAa,YAAY,gBAAgB,QAAQ;AAAA,EACnD;AACA,QAAM,WAAW,IAAI,aAAa,QAAQ;AAC1C,QAAM,SAAS,MAAM,IAAI,OAAO,MAAM,EAAE,UAAU,WAAW,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM;AACjF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,SAAS,CAAC,aAAa,SAAS,QAAQ,QAAQ;AAAA,IAChD,MAAM,QAAQ;AACZ,YAAM,OAAO,MAAM;AACnB,eAAS,QAAQ;AACjB,iBAAW,QAAQ;AACnB,iBAAW,QAAQ;AACnB,YAAM,QAAQ,OAAO,QAAQ,MAAM,OAAO;AAC1C,YAAM,MAAM,MAAM;AAAA,IACpB;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function related(changed, written) {
|
|
2
|
+
return changed === written || changed.startsWith(`${written}/`) || written.startsWith(`${changed}/`);
|
|
3
|
+
}
|
|
4
|
+
function createActivity(options = {}) {
|
|
5
|
+
const windowMs = options.windowMs ?? 2e3;
|
|
6
|
+
const now = options.now ?? Date.now;
|
|
7
|
+
const writes = /* @__PURE__ */ new Map();
|
|
8
|
+
let running = 0;
|
|
9
|
+
let lastExec = Number.NEGATIVE_INFINITY;
|
|
10
|
+
const recent = (at) => now() - at <= windowMs;
|
|
11
|
+
return {
|
|
12
|
+
wrote(path) {
|
|
13
|
+
writes.set(path, now());
|
|
14
|
+
},
|
|
15
|
+
began() {
|
|
16
|
+
running++;
|
|
17
|
+
let ended = false;
|
|
18
|
+
return () => {
|
|
19
|
+
if (ended) return;
|
|
20
|
+
ended = true;
|
|
21
|
+
running--;
|
|
22
|
+
lastExec = now();
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
agentMade(path) {
|
|
26
|
+
if (running > 0 || recent(lastExec)) return true;
|
|
27
|
+
let found = false;
|
|
28
|
+
for (const [written, at] of writes) {
|
|
29
|
+
if (!recent(at)) writes.delete(written);
|
|
30
|
+
else if (related(path, written)) found = true;
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
createActivity
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/kernel/activity.ts"],
|
|
4
|
+
"sourcesContent": ["export interface Activity {\n wrote(path: string): void;\n began(): () => void;\n agentMade(path: string): boolean;\n}\n\nexport interface ActivityOptions {\n windowMs?: number;\n now?: () => number;\n}\n\nfunction related(changed: string, written: string): boolean {\n return (\n changed === written || changed.startsWith(`${written}/`) || written.startsWith(`${changed}/`)\n );\n}\n\nexport function createActivity(options: ActivityOptions = {}): Activity {\n const windowMs = options.windowMs ?? 2000;\n const now = options.now ?? Date.now;\n const writes = new Map<string, number>();\n let running = 0;\n let lastExec = Number.NEGATIVE_INFINITY;\n const recent = (at: number) => now() - at <= windowMs;\n return {\n wrote(path) {\n writes.set(path, now());\n },\n began() {\n running++;\n let ended = false;\n return () => {\n if (ended) return;\n ended = true;\n running--;\n lastExec = now();\n };\n },\n agentMade(path) {\n if (running > 0 || recent(lastExec)) return true;\n let found = false;\n for (const [written, at] of writes) {\n if (!recent(at)) writes.delete(written);\n else if (related(path, written)) found = true;\n }\n return found;\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AAWA,SAAS,QAAQ,SAAiB,SAA0B;AAC1D,SACE,YAAY,WAAW,QAAQ,WAAW,GAAG,OAAO,GAAG,KAAK,QAAQ,WAAW,GAAG,OAAO,GAAG;AAEhG;AAEO,SAAS,eAAe,UAA2B,CAAC,GAAa;AACtE,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,MAAM,QAAQ,OAAO,KAAK;AAChC,QAAM,SAAS,oBAAI,IAAoB;AACvC,MAAI,UAAU;AACd,MAAI,WAAW,OAAO;AACtB,QAAM,SAAS,CAAC,OAAe,IAAI,IAAI,MAAM;AAC7C,SAAO;AAAA,IACL,MAAM,MAAM;AACV,aAAO,IAAI,MAAM,IAAI,CAAC;AAAA,IACxB;AAAA,IACA,QAAQ;AACN;AACA,UAAI,QAAQ;AACZ,aAAO,MAAM;AACX,YAAI,MAAO;AACX,gBAAQ;AACR;AACA,mBAAW,IAAI;AAAA,MACjB;AAAA,IACF;AAAA,IACA,UAAU,MAAM;AACd,UAAI,UAAU,KAAK,OAAO,QAAQ,EAAG,QAAO;AAC5C,UAAI,QAAQ;AACZ,iBAAW,CAAC,SAAS,EAAE,KAAK,QAAQ;AAClC,YAAI,CAAC,OAAO,EAAE,EAAG,QAAO,OAAO,OAAO;AAAA,iBAC7B,QAAQ,MAAM,OAAO,EAAG,SAAQ;AAAA,MAC3C;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/kernel/env.js
CHANGED
|
@@ -13,15 +13,22 @@ class SliccKernelEnv {
|
|
|
13
13
|
#client;
|
|
14
14
|
#watchers = /* @__PURE__ */ new Set();
|
|
15
15
|
#poll;
|
|
16
|
+
#activity;
|
|
16
17
|
constructor(client, options) {
|
|
17
18
|
this.#client = client;
|
|
18
19
|
this.cwd = options.cwd;
|
|
19
20
|
this.id = options.id ?? "slicc-kernel";
|
|
20
21
|
this.#poll = options.watch ?? {};
|
|
22
|
+
this.#activity = options.activity;
|
|
21
23
|
}
|
|
22
24
|
#path(path) {
|
|
23
25
|
return resolve(this.cwd, path);
|
|
24
26
|
}
|
|
27
|
+
#written(path) {
|
|
28
|
+
const resolved = this.#path(path);
|
|
29
|
+
this.#activity?.wrote(resolved);
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
25
32
|
get #fs() {
|
|
26
33
|
return this.#client.fs;
|
|
27
34
|
}
|
|
@@ -68,14 +75,14 @@ class SliccKernelEnv {
|
|
|
68
75
|
});
|
|
69
76
|
}
|
|
70
77
|
writeFile(path, content, context) {
|
|
71
|
-
const resolved = this.#
|
|
78
|
+
const resolved = this.#written(path);
|
|
72
79
|
return attempt(context, resolved, async () => {
|
|
73
80
|
await this.#fs.mkdir(dirname(resolved));
|
|
74
81
|
await this.#fs.writeFile(resolved, content);
|
|
75
82
|
});
|
|
76
83
|
}
|
|
77
84
|
appendFile(path, content, context) {
|
|
78
|
-
const resolved = this.#
|
|
85
|
+
const resolved = this.#written(path);
|
|
79
86
|
return attempt(context, resolved, async () => {
|
|
80
87
|
const before = await this.#fs.exists(resolved) ? await this.#fs.readFile(resolved) : new Uint8Array();
|
|
81
88
|
const added = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
@@ -87,7 +94,7 @@ class SliccKernelEnv {
|
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
96
|
truncateFile(path, size, context) {
|
|
90
|
-
const resolved = this.#
|
|
97
|
+
const resolved = this.#written(path);
|
|
91
98
|
if (!Number.isSafeInteger(size) || size < 0) {
|
|
92
99
|
return Promise.resolve(
|
|
93
100
|
err(new FileError("invalid", "File size must be a non-negative integer", resolved))
|
|
@@ -108,8 +115,8 @@ class SliccKernelEnv {
|
|
|
108
115
|
});
|
|
109
116
|
}
|
|
110
117
|
renameFile(sourcePath, destinationPath, context) {
|
|
111
|
-
const source = this.#
|
|
112
|
-
return attempt(context, source, () => this.#fs.rename(source, this.#
|
|
118
|
+
const source = this.#written(sourcePath);
|
|
119
|
+
return attempt(context, source, () => this.#fs.rename(source, this.#written(destinationPath)));
|
|
113
120
|
}
|
|
114
121
|
fileInfo(path, context) {
|
|
115
122
|
const resolved = this.#path(path);
|
|
@@ -152,11 +159,11 @@ class SliccKernelEnv {
|
|
|
152
159
|
return found.error.code === "not_found" ? ok(false) : found;
|
|
153
160
|
}
|
|
154
161
|
createDir(path, _options, context) {
|
|
155
|
-
const resolved = this.#
|
|
162
|
+
const resolved = this.#written(path);
|
|
156
163
|
return attempt(context, resolved, () => this.#fs.mkdir(resolved));
|
|
157
164
|
}
|
|
158
165
|
remove(path, options, context) {
|
|
159
|
-
const resolved = this.#
|
|
166
|
+
const resolved = this.#written(path);
|
|
160
167
|
return attempt(context, resolved, async () => {
|
|
161
168
|
if (!options?.recursive) {
|
|
162
169
|
const stat = await this.#fs.lstat(resolved).catch((error) => {
|
|
@@ -185,7 +192,8 @@ class SliccKernelEnv {
|
|
|
185
192
|
});
|
|
186
193
|
}
|
|
187
194
|
exec(command, options, context) {
|
|
188
|
-
|
|
195
|
+
const ended = this.#activity?.began();
|
|
196
|
+
return execute(this.#client, this.cwd, command, options, context).finally(ended);
|
|
189
197
|
}
|
|
190
198
|
async cleanup(context) {
|
|
191
199
|
for (const watcher of [...this.#watchers]) await watcher.close(context);
|
package/dist/kernel/env.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/kernel/env.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport type { KernelClient } from './client.ts';\nimport { execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { type PollOptions, pollWatch } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#path(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#path(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n const watcher = await pollWatch(this.#fs, this.cwd, targets, onChange, this.#poll);\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n return execute(this.#client, this.cwd, command, options, context);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> = {}\n): (target: { cwd?: string }) => SliccKernelEnv {\n return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });\n}\n"],
|
|
5
|
-
"mappings": "AACA;AAAA,EAKE;AAAA,EACA;AAAA,EAGA;AAAA,OAOK;
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport type { Activity } from './activity.ts';\nimport type { KernelClient } from './client.ts';\nimport { execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { type PollOptions, pollWatch } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n activity?: Activity;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n readonly #activity: Activity | undefined;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n this.#activity = options.activity;\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n #written(path: string): string {\n const resolved = this.#path(path);\n this.#activity?.wrote(resolved);\n return resolved;\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#written(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#written(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n const watcher = await pollWatch(this.#fs, this.cwd, targets, onChange, this.#poll);\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n const ended = this.#activity?.began();\n return execute(this.#client, this.cwd, command, options, context).finally(ended);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> = {}\n): (target: { cwd?: string }) => SliccKernelEnv {\n return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { LicksHostDoc } from "./schedules.js";
|
|
2
|
+
const BACKGROUND = /(?:&\s*$|&\s*disown\b|\bnohup\b|\bsetsid\b)/m;
|
|
3
|
+
function bashCommands(entries, after) {
|
|
4
|
+
const commands = [];
|
|
5
|
+
for (const entry of entries) {
|
|
6
|
+
if (entry.kind !== "pi.assistant" || Number(entry.id) <= after) continue;
|
|
7
|
+
const content = entry.model?.[0]?.content;
|
|
8
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
9
|
+
const command = block.arguments?.command;
|
|
10
|
+
if (block.type === "toolCall" && block.name === "bash" && typeof command === "string")
|
|
11
|
+
commands.push(command);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return commands;
|
|
15
|
+
}
|
|
16
|
+
function upgraded(from, to, upgrades) {
|
|
17
|
+
return {
|
|
18
|
+
channel: "upgrade",
|
|
19
|
+
source: "slicc-agent",
|
|
20
|
+
title: `slicc-agent ${from} \u2192 ${to}`,
|
|
21
|
+
text: `The agent was updated from ${from} to ${to}.`,
|
|
22
|
+
body: `Release notes: https://github.com/ai-ecoverse/slicc-agent/releases/tag/v${to}`,
|
|
23
|
+
target: "cone",
|
|
24
|
+
eventId: `${from}->${to}#${upgrades}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function restarted(boot, commands) {
|
|
28
|
+
const background = commands.filter((command) => BACKGROUND.test(command)).slice(-5);
|
|
29
|
+
return {
|
|
30
|
+
channel: "session-reload",
|
|
31
|
+
source: "kernel",
|
|
32
|
+
title: "The kernel restarted",
|
|
33
|
+
text: "The page reloaded, so the kernel started fresh: processes from before the reload are gone.",
|
|
34
|
+
body: background.length ? `Background commands that are no longer running:
|
|
35
|
+
${background.join("\n")}` : "Files are unchanged. Start again whatever you still need running.",
|
|
36
|
+
target: "cone",
|
|
37
|
+
eventId: `boot:${boot}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function bootLicks(harness, cone, deliver, facts, context) {
|
|
41
|
+
const state = await harness.snapshot(LicksHostDoc, context);
|
|
42
|
+
const previous = state?.agent ?? null;
|
|
43
|
+
if (previous !== null && previous !== facts.version)
|
|
44
|
+
await deliver(upgraded(previous, facts.version, state?.upgrades ?? 0), context);
|
|
45
|
+
const { entries } = await cone.context(context);
|
|
46
|
+
if (facts.boot !== null && state?.boot && facts.boot !== state.boot) {
|
|
47
|
+
const commands = bashCommands(entries, state.mark);
|
|
48
|
+
if (commands.length) await deliver(restarted(facts.boot, commands), context);
|
|
49
|
+
}
|
|
50
|
+
const { entries: after } = await cone.context(context);
|
|
51
|
+
const mark = Number(after.at(-1)?.id ?? 0);
|
|
52
|
+
await harness.commit(async (tx) => {
|
|
53
|
+
const doc = await tx.doc(LicksHostDoc);
|
|
54
|
+
if (doc.agent !== null && doc.agent !== facts.version) doc.upgrades += 1;
|
|
55
|
+
doc.agent = facts.version;
|
|
56
|
+
if (facts.boot !== null && doc.boot !== facts.boot) {
|
|
57
|
+
doc.boot = facts.boot;
|
|
58
|
+
doc.mark = mark;
|
|
59
|
+
}
|
|
60
|
+
}, context);
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
bashCommands,
|
|
64
|
+
bootLicks
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=boot.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/boot.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, EntryRecord, Harness } from '@earendil-works/pi-durable';\nimport type { Deliver } from './fswatch.ts';\nimport { LicksHostDoc } from './schedules.ts';\n\nexport interface BootFacts {\n version: string;\n boot: string | null;\n}\n\nconst BACKGROUND = /(?:&\\s*$|&\\s*disown\\b|\\bnohup\\b|\\bsetsid\\b)/m;\n\ninterface Block {\n type?: string;\n name?: string;\n arguments?: { command?: unknown };\n}\n\nexport function bashCommands(entries: readonly EntryRecord[], after: number): string[] {\n const commands: string[] = [];\n for (const entry of entries) {\n if (entry.kind !== 'pi.assistant' || Number(entry.id) <= after) continue;\n const content = (entry.model?.[0] as { content?: unknown } | undefined)?.content;\n for (const block of Array.isArray(content) ? (content as Block[]) : []) {\n const command = block.arguments?.command;\n if (block.type === 'toolCall' && block.name === 'bash' && typeof command === 'string')\n commands.push(command);\n }\n }\n return commands;\n}\n\nfunction upgraded(from: string, to: string, upgrades: number) {\n return {\n channel: 'upgrade',\n source: 'slicc-agent',\n title: `slicc-agent ${from} \u2192 ${to}`,\n text: `The agent was updated from ${from} to ${to}.`,\n body: `Release notes: https://github.com/ai-ecoverse/slicc-agent/releases/tag/v${to}`,\n target: 'cone',\n eventId: `${from}->${to}#${upgrades}`,\n } as const;\n}\n\nfunction restarted(boot: string, commands: readonly string[]) {\n const background = commands.filter((command) => BACKGROUND.test(command)).slice(-5);\n return {\n channel: 'session-reload',\n source: 'kernel',\n title: 'The kernel restarted',\n text: 'The page reloaded, so the kernel started fresh: processes from before the reload are gone.',\n body: background.length\n ? `Background commands that are no longer running:\\n${background.join('\\n')}`\n : 'Files are unchanged. Start again whatever you still need running.',\n target: 'cone',\n eventId: `boot:${boot}`,\n } as const;\n}\n\nexport async function bootLicks(\n harness: Harness,\n cone: Conversation,\n deliver: Deliver,\n facts: BootFacts,\n context: Context\n): Promise<void> {\n const state = await harness.snapshot(LicksHostDoc, context);\n const previous = state?.agent ?? null;\n if (previous !== null && previous !== facts.version)\n await deliver(upgraded(previous, facts.version, state?.upgrades ?? 0), context);\n const { entries } = await cone.context(context);\n if (facts.boot !== null && state?.boot && facts.boot !== state.boot) {\n const commands = bashCommands(entries, state.mark);\n if (commands.length) await deliver(restarted(facts.boot, commands), context);\n }\n const { entries: after } = await cone.context(context);\n const mark = Number(after.at(-1)?.id ?? 0);\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksHostDoc);\n if (doc.agent !== null && doc.agent !== facts.version) doc.upgrades += 1;\n doc.agent = facts.version;\n if (facts.boot !== null && doc.boot !== facts.boot) {\n doc.boot = facts.boot;\n doc.mark = mark;\n }\n }, context);\n}\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,oBAAoB;AAO7B,MAAM,aAAa;AAQZ,SAAS,aAAa,SAAiC,OAAyB;AACrF,QAAM,WAAqB,CAAC;AAC5B,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,SAAS,kBAAkB,OAAO,MAAM,EAAE,KAAK,MAAO;AAChE,UAAM,UAAW,MAAM,QAAQ,CAAC,GAAyC;AACzE,eAAW,SAAS,MAAM,QAAQ,OAAO,IAAK,UAAsB,CAAC,GAAG;AACtE,YAAM,UAAU,MAAM,WAAW;AACjC,UAAI,MAAM,SAAS,cAAc,MAAM,SAAS,UAAU,OAAO,YAAY;AAC3E,iBAAS,KAAK,OAAO;AAAA,IACzB;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,MAAc,IAAY,UAAkB;AAC5D,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO,eAAe,IAAI,WAAM,EAAE;AAAA,IAClC,MAAM,8BAA8B,IAAI,OAAO,EAAE;AAAA,IACjD,MAAM,2EAA2E,EAAE;AAAA,IACnF,QAAQ;AAAA,IACR,SAAS,GAAG,IAAI,KAAK,EAAE,IAAI,QAAQ;AAAA,EACrC;AACF;AAEA,SAAS,UAAU,MAAc,UAA6B;AAC5D,QAAM,aAAa,SAAS,OAAO,CAAC,YAAY,WAAW,KAAK,OAAO,CAAC,EAAE,MAAM,EAAE;AAClF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM,WAAW,SACb;AAAA,EAAoD,WAAW,KAAK,IAAI,CAAC,KACzE;AAAA,IACJ,QAAQ;AAAA,IACR,SAAS,QAAQ,IAAI;AAAA,EACvB;AACF;AAEA,eAAsB,UACpB,SACA,MACA,SACA,OACA,SACe;AACf,QAAM,QAAQ,MAAM,QAAQ,SAAS,cAAc,OAAO;AAC1D,QAAM,WAAW,OAAO,SAAS;AACjC,MAAI,aAAa,QAAQ,aAAa,MAAM;AAC1C,UAAM,QAAQ,SAAS,UAAU,MAAM,SAAS,OAAO,YAAY,CAAC,GAAG,OAAO;AAChF,QAAM,EAAE,QAAQ,IAAI,MAAM,KAAK,QAAQ,OAAO;AAC9C,MAAI,MAAM,SAAS,QAAQ,OAAO,QAAQ,MAAM,SAAS,MAAM,MAAM;AACnE,UAAM,WAAW,aAAa,SAAS,MAAM,IAAI;AACjD,QAAI,SAAS,OAAQ,OAAM,QAAQ,UAAU,MAAM,MAAM,QAAQ,GAAG,OAAO;AAAA,EAC7E;AACA,QAAM,EAAE,SAAS,MAAM,IAAI,MAAM,KAAK,QAAQ,OAAO;AACrD,QAAM,OAAO,OAAO,MAAM,GAAG,EAAE,GAAG,MAAM,CAAC;AACzC,QAAM,QAAQ,OAAO,OAAO,OAAO;AACjC,UAAM,MAAM,MAAM,GAAG,IAAI,YAAY;AACrC,QAAI,IAAI,UAAU,QAAQ,IAAI,UAAU,MAAM,QAAS,KAAI,YAAY;AACvE,QAAI,QAAQ,MAAM;AAClB,QAAI,MAAM,SAAS,QAAQ,IAAI,SAAS,MAAM,MAAM;AAClD,UAAI,OAAO,MAAM;AACjB,UAAI,OAAO;AAAA,IACb;AAAA,EACF,GAAG,OAAO;AACZ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|