@ai-ecoverse/slicc-agent 3.40.1 → 3.41.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 +2 -0
- package/dist/licks/config.js +20 -4
- package/dist/licks/config.js.map +2 -2
- package/dist/licks/fswatch.js +2 -1
- package/dist/licks/fswatch.js.map +2 -2
- package/dist/licks/licks.js +31 -4
- package/dist/licks/licks.js.map +3 -3
- package/dist/licks/sources.js +2 -1
- package/dist/licks/sources.js.map +2 -2
- package/dist/licks/triage.js +86 -0
- package/dist/licks/triage.js.map +7 -0
- package/dist/main.js +2 -0
- package/dist/main.js.map +2 -2
- package/dist/types/licks/config.d.ts +2 -0
- package/dist/types/licks/licks.d.ts +3 -0
- package/dist/types/licks/triage.d.ts +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ gone /home/notes/old.md
|
|
|
169
169
|
- `~/.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.
|
|
170
170
|
- `~/.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.
|
|
171
171
|
|
|
172
|
+
**Triage.** A watch or webhook file can add `"triage": "<which events the agent should see>"`, a sentence of 1–1000 characters such as `"pushes to main that touch src/"`. Then each of its events (a debounced batch of changes, or a webhook request) first goes to Clef Flash (`adobe/clef-flash`, see Classifiers) with the channel, source, title, text, body (cut at 4000 characters) and up to 50 paths. One `choice` question asks whether the event matches that sentence: `act` or `ignore`. When the answer is `ignore` with a probability of at least 0.75 (`TRIAGE_SKIP`), the lick isn't delivered and starts no turn. Instead, a passive `slicc.lick-triage` entry goes into the target's transcript. It has no model message, so neither the model nor the chat sees it, and it records the would-be lick with the triage sentence, the decision, Clef's confidence and probabilities, the model and the cost. An event whose delivery id was already seen isn't triaged again. A skipped one is remembered only after its record is written, under a `requestId` taken from the delivery id, so a crash in between neither loses the record nor writes it twice. Triage fails open: without an Adobe sign-in, or when the proxy errors, times out (15 s) or returns any other answer, the lick is delivered as usual. Watch notices (stopped, overflow), config errors, frozen notices, crontab jobs and boot licks are never triaged. A small webhook event is about 220 input tokens, which is $0.00002 against the weekly budget; a 4000-character body is about $0.0001.
|
|
173
|
+
|
|
172
174
|
**Sources.**
|
|
173
175
|
- `fswatch`: the kernel environment's `watch()` reports changed paths; those matching the glob are collected for `debounce` ms and delivered as one lick with `changed` or `gone` per path. A watch that overflows or stops says so in a lick (a stopped one with `severity="error"`, an overflow with `severity="warn"`), and saving its file restarts it. Changes made while no worker runs aren't reported. **The agent's own changes are dropped**, so it never licks itself: the kernel environment records every path its file tools write, edit, move or remove, and the time its `bash` commands run; a change to such a path within 2 s, or any change while an agent command runs or within 2 s after it, counts as the agent's. A change the user makes while an agent command runs is therefore dropped too. Background processes the agent started (`&`) aren't tracked, so their changes do lick.
|
|
174
176
|
- `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.
|
package/dist/licks/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseSchedule } from "./cron.js";
|
|
|
2
2
|
const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
3
3
|
const TARGET = /^(?:cone|scoop:[a-z0-9][a-z0-9._-]{0,63})$/;
|
|
4
4
|
const DEBOUNCE = { min: 100, max: 6e4, default: 500 };
|
|
5
|
+
const TRIAGE_MAX = 1e3;
|
|
5
6
|
function isName(value) {
|
|
6
7
|
return NAME.test(value);
|
|
7
8
|
}
|
|
@@ -95,9 +96,18 @@ function debounce(value) {
|
|
|
95
96
|
);
|
|
96
97
|
return value;
|
|
97
98
|
}
|
|
99
|
+
function triage(value) {
|
|
100
|
+
if (value === void 0) return {};
|
|
101
|
+
const sentence = typeof value === "string" ? value.trim() : "";
|
|
102
|
+
if (!sentence || sentence.length > TRIAGE_MAX)
|
|
103
|
+
throw new Error(
|
|
104
|
+
`"triage" must be a sentence of 1\u2013${TRIAGE_MAX} characters saying which events the agent should see`
|
|
105
|
+
);
|
|
106
|
+
return { triage: sentence };
|
|
107
|
+
}
|
|
98
108
|
function parseWatch(file, source, home) {
|
|
99
109
|
const name = nameOf(file);
|
|
100
|
-
const json = object(source, ["path", "glob", "target", "message", "debounce"]);
|
|
110
|
+
const json = object(source, ["path", "glob", "target", "message", "debounce", "triage"]);
|
|
101
111
|
const glob = text(json.glob, "glob", "**");
|
|
102
112
|
if (glob === "" || glob.startsWith("/"))
|
|
103
113
|
throw new Error('"glob" must be relative to "path", such as "**/*.md"');
|
|
@@ -107,13 +117,19 @@ function parseWatch(file, source, home) {
|
|
|
107
117
|
glob,
|
|
108
118
|
target: target(json.target),
|
|
109
119
|
message: text(json.message, "message", ""),
|
|
110
|
-
debounce: debounce(json.debounce)
|
|
120
|
+
debounce: debounce(json.debounce),
|
|
121
|
+
...triage(json.triage)
|
|
111
122
|
};
|
|
112
123
|
}
|
|
113
124
|
function parseWebhook(file, source) {
|
|
114
125
|
const name = nameOf(file);
|
|
115
|
-
const json = object(source, ["target", "message"]);
|
|
116
|
-
return {
|
|
126
|
+
const json = object(source, ["target", "message", "triage"]);
|
|
127
|
+
return {
|
|
128
|
+
name,
|
|
129
|
+
target: target(json.target),
|
|
130
|
+
message: text(json.message, "message", ""),
|
|
131
|
+
...triage(json.triage)
|
|
132
|
+
};
|
|
117
133
|
}
|
|
118
134
|
export {
|
|
119
135
|
isName,
|
package/dist/licks/config.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/licks/config.ts"],
|
|
4
|
-
"sourcesContent": ["import { parseSchedule } from './cron.ts';\nimport type { LickTarget } from './lick.ts';\n\nexport interface CronEntry {\n name: string;\n schedule: string;\n target: LickTarget;\n message: string;\n}\n\nexport interface WatchEntry {\n name: string;\n path: string;\n glob: string;\n target: LickTarget;\n message: string;\n debounce: number;\n}\n\nexport interface WebhookEntry {\n name: string;\n target: LickTarget;\n message: string;\n}\n\nexport interface ConfigError {\n file: string;\n line?: number;\n error: string;\n}\n\nexport interface Parsed<T> {\n entries: T[];\n errors: ConfigError[];\n}\n\nconst NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;\nconst TARGET = /^(?:cone|scoop:[a-z0-9][a-z0-9._-]{0,63})$/;\nconst DEBOUNCE = { min: 100, max: 60_000, default: 500 };\n\nexport function isName(value: string): boolean {\n return NAME.test(value);\n}\n\nexport function isTarget(value: string): value is LickTarget {\n return TARGET.test(value);\n}\n\nfunction scheduleOf(tokens: string[]): [string, string[]] {\n if (tokens[0]?.startsWith('@')) return [tokens[0], tokens.slice(1)];\n return [tokens.slice(0, 5).join(' '), tokens.slice(5)];\n}\n\nfunction cronLine(text: string): CronEntry {\n const [schedule, rest] = scheduleOf(text.trim().split(/\\s+/));\n parseSchedule(schedule);\n const [name, maybeTarget, ...words] = rest;\n if (name === undefined) throw new Error('a name must follow the schedule');\n if (!isName(name))\n throw new Error(\n `name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n const target = maybeTarget !== undefined && isTarget(maybeTarget) ? maybeTarget : 'cone';\n const message = (target === maybeTarget ? words : [maybeTarget, ...words])\n .filter(Boolean)\n .join(' ');\n return { name, schedule, target, message };\n}\n\nexport function parseCrontab(file: string, text: string): Parsed<CronEntry> {\n const entries: CronEntry[] = [];\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n text.split('\\n').forEach((raw, index) => {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) return;\n try {\n const entry = cronLine(line);\n if (seen.has(entry.name)) throw new Error(`name \"${entry.name}\" is used twice`);\n seen.add(entry.name);\n entries.push(entry);\n } catch (error) {\n errors.push({ file, line: index + 1, error: (error as Error).message });\n }\n });\n return { entries, errors };\n}\n\ntype Json = Record<string, unknown>;\n\nfunction object(text: string, keys: readonly string[]): Json {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n throw new Error(`not valid JSON: ${(error as Error).message}`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value))\n throw new Error('must be a JSON object');\n const unknown = Object.keys(value).filter((key) => !keys.includes(key));\n if (unknown.length)\n throw new Error(\n `unknown field${unknown.length > 1 ? 's' : ''} ${unknown.join(', ')} (allowed: ${keys.join(', ')})`\n );\n return value as Json;\n}\n\nfunction text(value: unknown, field: string, fallback?: string): string {\n if (value === undefined && fallback !== undefined) return fallback;\n if (typeof value !== 'string') throw new Error(`\"${field}\" must be a string`);\n return value;\n}\n\nfunction target(value: unknown): LickTarget {\n const chosen = text(value, 'target', 'cone');\n if (!isTarget(chosen))\n throw new Error(`\"target\" must be \"cone\" or \"scoop:<name>\", not \"${chosen}\"`);\n return chosen;\n}\n\nexport function nameOf(file: string): string {\n const base = file.slice(file.lastIndexOf('/') + 1);\n if (!base.endsWith('.json')) throw new Error('must end in .json');\n const name = base.slice(0, -'.json'.length);\n if (!isName(name))\n throw new Error(\n `file name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n return name;\n}\n\nfunction absolute(path: string, home: string): string {\n if (path === '~') return home;\n if (path.startsWith('~/')) return `${home}${path.slice(1)}`;\n if (!path.startsWith('/'))\n throw new Error(`\"path\" must be absolute or start with ~/, not \"${path}\"`);\n return path;\n}\n\nfunction debounce(value: unknown): number {\n if (value === undefined) return DEBOUNCE.default;\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < DEBOUNCE.min ||\n value > DEBOUNCE.max\n )\n throw new Error(\n `\"debounce\" must be a whole number of milliseconds from ${DEBOUNCE.min} to ${DEBOUNCE.max}`\n );\n return value;\n}\n\nexport function parseWatch(file: string, source: string, home: string): WatchEntry {\n const name = nameOf(file);\n const json = object(source, ['path', 'glob', 'target', 'message', 'debounce']);\n const glob = text(json.glob, 'glob', '**');\n if (glob === '' || glob.startsWith('/'))\n throw new Error('\"glob\" must be relative to \"path\", such as \"**/*.md\"');\n return {\n name,\n path: absolute(text(json.path, 'path'), home).replace(/\\/+$/, '') || '/',\n glob,\n target: target(json.target),\n message: text(json.message, 'message', ''),\n debounce: debounce(json.debounce),\n };\n}\n\nexport function parseWebhook(file: string, source: string): WebhookEntry {\n const name = nameOf(file);\n const json = object(source, ['target', 'message']);\n return {
|
|
5
|
-
"mappings": "AAAA,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { parseSchedule } from './cron.ts';\nimport type { LickTarget } from './lick.ts';\n\nexport interface CronEntry {\n name: string;\n schedule: string;\n target: LickTarget;\n message: string;\n}\n\nexport interface WatchEntry {\n name: string;\n path: string;\n glob: string;\n target: LickTarget;\n message: string;\n debounce: number;\n triage?: string;\n}\n\nexport interface WebhookEntry {\n name: string;\n target: LickTarget;\n message: string;\n triage?: string;\n}\n\nexport interface ConfigError {\n file: string;\n line?: number;\n error: string;\n}\n\nexport interface Parsed<T> {\n entries: T[];\n errors: ConfigError[];\n}\n\nconst NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;\nconst TARGET = /^(?:cone|scoop:[a-z0-9][a-z0-9._-]{0,63})$/;\nconst DEBOUNCE = { min: 100, max: 60_000, default: 500 };\nconst TRIAGE_MAX = 1000;\n\nexport function isName(value: string): boolean {\n return NAME.test(value);\n}\n\nexport function isTarget(value: string): value is LickTarget {\n return TARGET.test(value);\n}\n\nfunction scheduleOf(tokens: string[]): [string, string[]] {\n if (tokens[0]?.startsWith('@')) return [tokens[0], tokens.slice(1)];\n return [tokens.slice(0, 5).join(' '), tokens.slice(5)];\n}\n\nfunction cronLine(text: string): CronEntry {\n const [schedule, rest] = scheduleOf(text.trim().split(/\\s+/));\n parseSchedule(schedule);\n const [name, maybeTarget, ...words] = rest;\n if (name === undefined) throw new Error('a name must follow the schedule');\n if (!isName(name))\n throw new Error(\n `name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n const target = maybeTarget !== undefined && isTarget(maybeTarget) ? maybeTarget : 'cone';\n const message = (target === maybeTarget ? words : [maybeTarget, ...words])\n .filter(Boolean)\n .join(' ');\n return { name, schedule, target, message };\n}\n\nexport function parseCrontab(file: string, text: string): Parsed<CronEntry> {\n const entries: CronEntry[] = [];\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n text.split('\\n').forEach((raw, index) => {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) return;\n try {\n const entry = cronLine(line);\n if (seen.has(entry.name)) throw new Error(`name \"${entry.name}\" is used twice`);\n seen.add(entry.name);\n entries.push(entry);\n } catch (error) {\n errors.push({ file, line: index + 1, error: (error as Error).message });\n }\n });\n return { entries, errors };\n}\n\ntype Json = Record<string, unknown>;\n\nfunction object(text: string, keys: readonly string[]): Json {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n throw new Error(`not valid JSON: ${(error as Error).message}`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value))\n throw new Error('must be a JSON object');\n const unknown = Object.keys(value).filter((key) => !keys.includes(key));\n if (unknown.length)\n throw new Error(\n `unknown field${unknown.length > 1 ? 's' : ''} ${unknown.join(', ')} (allowed: ${keys.join(', ')})`\n );\n return value as Json;\n}\n\nfunction text(value: unknown, field: string, fallback?: string): string {\n if (value === undefined && fallback !== undefined) return fallback;\n if (typeof value !== 'string') throw new Error(`\"${field}\" must be a string`);\n return value;\n}\n\nfunction target(value: unknown): LickTarget {\n const chosen = text(value, 'target', 'cone');\n if (!isTarget(chosen))\n throw new Error(`\"target\" must be \"cone\" or \"scoop:<name>\", not \"${chosen}\"`);\n return chosen;\n}\n\nexport function nameOf(file: string): string {\n const base = file.slice(file.lastIndexOf('/') + 1);\n if (!base.endsWith('.json')) throw new Error('must end in .json');\n const name = base.slice(0, -'.json'.length);\n if (!isName(name))\n throw new Error(\n `file name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n return name;\n}\n\nfunction absolute(path: string, home: string): string {\n if (path === '~') return home;\n if (path.startsWith('~/')) return `${home}${path.slice(1)}`;\n if (!path.startsWith('/'))\n throw new Error(`\"path\" must be absolute or start with ~/, not \"${path}\"`);\n return path;\n}\n\nfunction debounce(value: unknown): number {\n if (value === undefined) return DEBOUNCE.default;\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < DEBOUNCE.min ||\n value > DEBOUNCE.max\n )\n throw new Error(\n `\"debounce\" must be a whole number of milliseconds from ${DEBOUNCE.min} to ${DEBOUNCE.max}`\n );\n return value;\n}\n\nfunction triage(value: unknown): { triage?: string } {\n if (value === undefined) return {};\n const sentence = typeof value === 'string' ? value.trim() : '';\n if (!sentence || sentence.length > TRIAGE_MAX)\n throw new Error(\n `\"triage\" must be a sentence of 1\u2013${TRIAGE_MAX} characters saying which events the agent should see`\n );\n return { triage: sentence };\n}\n\nexport function parseWatch(file: string, source: string, home: string): WatchEntry {\n const name = nameOf(file);\n const json = object(source, ['path', 'glob', 'target', 'message', 'debounce', 'triage']);\n const glob = text(json.glob, 'glob', '**');\n if (glob === '' || glob.startsWith('/'))\n throw new Error('\"glob\" must be relative to \"path\", such as \"**/*.md\"');\n return {\n name,\n path: absolute(text(json.path, 'path'), home).replace(/\\/+$/, '') || '/',\n glob,\n target: target(json.target),\n message: text(json.message, 'message', ''),\n debounce: debounce(json.debounce),\n ...triage(json.triage),\n };\n}\n\nexport function parseWebhook(file: string, source: string): WebhookEntry {\n const name = nameOf(file);\n const json = object(source, ['target', 'message', 'triage']);\n return {\n name,\n target: target(json.target),\n message: text(json.message, 'message', ''),\n ...triage(json.triage),\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["text", "target"]
|
|
7
7
|
}
|
package/dist/licks/fswatch.js
CHANGED
|
@@ -60,7 +60,8 @@ class Watches {
|
|
|
60
60
|
text: `${paths.length} path${many ? "s" : ""} changed under ${entry.path} matching ${entry.glob}`,
|
|
61
61
|
...entry.message ? { body: entry.message } : {},
|
|
62
62
|
items,
|
|
63
|
-
target: entry.target
|
|
63
|
+
target: entry.target,
|
|
64
|
+
...entry.triage ? { triage: entry.triage } : {}
|
|
64
65
|
},
|
|
65
66
|
context
|
|
66
67
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/licks/fswatch.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv, FileWatcher, WatchChange } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type ConfigError, parseWatch, type WatchEntry } from './config.ts';\nimport { glob } from './glob.ts';\nimport type { LickEvent } from './licks.ts';\n\nconst IGNORED = ['node_modules', '.git'];\n\ninterface Watching {\n json: string;\n entry: WatchEntry;\n match: (path: string) => boolean;\n watcher?: FileWatcher;\n timer?: ReturnType<typeof setTimeout>;\n paths: Set<string>;\n}\n\nexport interface ConfigFiles {\n read(path: string, context: Context): Promise<string | undefined>;\n list(path: string, context: Context): Promise<string[]>;\n}\n\nexport type Deliver = (event: LickEvent, context: Context) => Promise<void>;\n\nexport function relativeTo(base: string, path: string): string | undefined {\n if (path === base) return '';\n const prefix = base === '/' ? '/' : `${base}/`;\n return path.startsWith(prefix) ? path.slice(prefix.length) : undefined;\n}\n\nexport function configFiles(env: ExecutionEnv): ConfigFiles {\n return {\n async read(path, context) {\n const result = await env.readTextFile(path, context);\n return result.ok ? result.value : undefined;\n },\n async list(path, context) {\n const result = await env.listDir(path, context);\n if (!result.ok) return [];\n return result.value\n .filter(\n (info) => info.kind === 'file' && !info.name.startsWith('.') && !info.name.endsWith('~')\n )\n .map((info) => info.name)\n .sort();\n },\n };\n}\n\nfunction title(entry: WatchEntry): string {\n return `${entry.name}: ${entry.path}/${entry.glob}`;\n}\n\nexport class Watches {\n readonly #env: ExecutionEnv;\n readonly #deliver: Deliver;\n readonly #files: ConfigFiles;\n readonly #home: string;\n readonly #watching = new Map<string, Watching>();\n readonly #activity: Activity | undefined;\n\n constructor(env: ExecutionEnv, deliver: Deliver, home: string, activity?: Activity) {\n this.#env = env;\n this.#deliver = deliver;\n this.#files = configFiles(env);\n this.#home = home;\n this.#activity = activity;\n }\n\n get names(): string[] {\n return [...this.#watching.keys()];\n }\n\n async #fire(watch: Watching, context: Context): Promise<void> {\n const paths = [...watch.paths].sort();\n watch.paths.clear();\n if (!paths.length) return;\n const items: string[] = [];\n for (const path of paths) {\n const exists = await this.#env.exists(path, context);\n items.push(`${exists.ok && !exists.value ? 'gone' : 'changed'} ${path}`);\n }\n const { entry } = watch;\n const many = paths.length > 1;\n await this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text: `${paths.length} path${many ? 's' : ''} changed under ${entry.path} matching ${entry.glob}`,\n ...(entry.message ? { body: entry.message } : {}),\n items,\n target: entry.target,\n },\n context\n );\n }\n\n #paths(watch: Watching, paths: readonly string[], context: Context): void {\n for (const path of paths) {\n const relative = relativeTo(watch.entry.path, path);\n if (relative && watch.match(relative) && !this.#activity?.agentMade(path))\n watch.paths.add(path);\n }\n if (!watch.paths.size) return;\n clearTimeout(watch.timer);\n watch.timer = setTimeout(() => {\n void this.#fire(watch, context).catch(() => undefined);\n }, watch.entry.debounce);\n }\n\n #changed(watch: Watching, change: WatchChange, context: Context): void {\n if ('paths' in change) {\n this.#paths(watch, change.paths, context);\n return;\n }\n const stopped = 'error' in change;\n const text = stopped\n ? `The watch stopped: ${change.error.message}. Save its file again to restart it.`\n : 'Changes may have been missed for a while; look at the watched files again.';\n const { entry } = watch;\n if (stopped && this.#watching.get(entry.name) === watch) {\n this.#watching.delete(entry.name);\n clearTimeout(watch.timer);\n void watch.watcher?.close(context).catch(() => undefined);\n }\n void this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text,\n target: entry.target,\n severity: stopped ? 'error' : 'warn',\n },\n context\n ).catch(() => undefined);\n }\n\n async #start(entry: WatchEntry, json: string, file: string, context: Context) {\n const watch: Watching = { json, entry, match: glob(entry.glob), paths: new Set() };\n const result = await this.#env.watch(\n [{ path: entry.path, recursive: true, exclude: { names: IGNORED } }],\n (change) => this.#changed(watch, change, context),\n context\n );\n if (!result.ok) return { file, error: `can't watch ${entry.path}: ${result.error.message}` };\n watch.watcher = result.value;\n this.#watching.set(entry.name, watch);\n return undefined;\n }\n\n async stop(name: string, context: Context): Promise<void> {\n const watch = this.#watching.get(name);\n if (!watch) return;\n this.#watching.delete(name);\n clearTimeout(watch.timer);\n await watch.watcher?.close(context);\n }\n\n async reconcile(dir: string, context: Context): Promise<ConfigError[]> {\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n for (const name of await this.#files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await this.#files.read(file, context);\n if (json === undefined) continue;\n let entry: WatchEntry;\n try {\n entry = parseWatch(file, json, this.#home);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n continue;\n }\n seen.add(entry.name);\n if (this.#watching.get(entry.name)?.json === json) continue;\n await this.stop(entry.name, context);\n const failed = await this.#start(entry, json, file, context);\n if (failed) errors.push(failed);\n }\n for (const name of this.names) if (!seen.has(name)) await this.stop(name, context);\n return errors;\n }\n\n async close(context: Context): Promise<void> {\n for (const name of this.names) await this.stop(name, context);\n }\n}\n"],
|
|
5
|
-
"mappings": "AAGA,SAA2B,kBAAmC;AAC9D,SAAS,YAAY;AAGrB,MAAM,UAAU,CAAC,gBAAgB,MAAM;AAkBhC,SAAS,WAAW,MAAc,MAAkC;AACzE,MAAI,SAAS,KAAM,QAAO;AAC1B,QAAM,SAAS,SAAS,MAAM,MAAM,GAAG,IAAI;AAC3C,SAAO,KAAK,WAAW,MAAM,IAAI,KAAK,MAAM,OAAO,MAAM,IAAI;AAC/D;AAEO,SAAS,YAAY,KAAgC;AAC1D,SAAO;AAAA,IACL,MAAM,KAAK,MAAM,SAAS;AACxB,YAAM,SAAS,MAAM,IAAI,aAAa,MAAM,OAAO;AACnD,aAAO,OAAO,KAAK,OAAO,QAAQ;AAAA,IACpC;AAAA,IACA,MAAM,KAAK,MAAM,SAAS;AACxB,YAAM,SAAS,MAAM,IAAI,QAAQ,MAAM,OAAO;AAC9C,UAAI,CAAC,OAAO,GAAI,QAAO,CAAC;AACxB,aAAO,OAAO,MACX;AAAA,QACC,CAAC,SAAS,KAAK,SAAS,UAAU,CAAC,KAAK,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,KAAK,SAAS,GAAG;AAAA,MACzF,EACC,IAAI,CAAC,SAAS,KAAK,IAAI,EACvB,KAAK;AAAA,IACV;AAAA,EACF;AACF;AAEA,SAAS,MAAM,OAA2B;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,MAAM,IAAI,IAAI,MAAM,IAAI;AACnD;AAEO,MAAM,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY,oBAAI,IAAsB;AAAA,EACtC;AAAA,EAET,YAAY,KAAmB,SAAkB,MAAc,UAAqB;AAClF,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,SAAS,YAAY,GAAG;AAC7B,SAAK,QAAQ;AACb,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,IAAI,QAAkB;AACpB,WAAO,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,EAClC;AAAA,EAEA,MAAM,MAAM,OAAiB,SAAiC;AAC5D,UAAM,QAAQ,CAAC,GAAG,MAAM,KAAK,EAAE,KAAK;AACpC,UAAM,MAAM,MAAM;AAClB,QAAI,CAAC,MAAM,OAAQ;AACnB,UAAM,QAAkB,CAAC;AACzB,eAAW,QAAQ,OAAO;AACxB,YAAM,SAAS,MAAM,KAAK,KAAK,OAAO,MAAM,OAAO;AACnD,YAAM,KAAK,GAAG,OAAO,MAAM,CAAC,OAAO,QAAQ,SAAS,SAAS,IAAI,IAAI,EAAE;AAAA,IACzE;AACA,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,OAAO,MAAM,SAAS;AAC5B,UAAM,KAAK;AAAA,MACT;AAAA,QACE,SAAS;AAAA,QACT,QAAQ,MAAM;AAAA,QACd,OAAO,MAAM,KAAK;AAAA,QAClB,MAAM,GAAG,MAAM,MAAM,QAAQ,OAAO,MAAM,EAAE,kBAAkB,MAAM,IAAI,aAAa,MAAM,IAAI;AAAA,QAC/F,GAAI,MAAM,UAAU,EAAE,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,QAC/C;AAAA,QACA,QAAQ,MAAM;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv, FileWatcher, WatchChange } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type ConfigError, parseWatch, type WatchEntry } from './config.ts';\nimport { glob } from './glob.ts';\nimport type { LickEvent } from './licks.ts';\n\nconst IGNORED = ['node_modules', '.git'];\n\ninterface Watching {\n json: string;\n entry: WatchEntry;\n match: (path: string) => boolean;\n watcher?: FileWatcher;\n timer?: ReturnType<typeof setTimeout>;\n paths: Set<string>;\n}\n\nexport interface ConfigFiles {\n read(path: string, context: Context): Promise<string | undefined>;\n list(path: string, context: Context): Promise<string[]>;\n}\n\nexport type Deliver = (event: LickEvent, context: Context) => Promise<void>;\n\nexport function relativeTo(base: string, path: string): string | undefined {\n if (path === base) return '';\n const prefix = base === '/' ? '/' : `${base}/`;\n return path.startsWith(prefix) ? path.slice(prefix.length) : undefined;\n}\n\nexport function configFiles(env: ExecutionEnv): ConfigFiles {\n return {\n async read(path, context) {\n const result = await env.readTextFile(path, context);\n return result.ok ? result.value : undefined;\n },\n async list(path, context) {\n const result = await env.listDir(path, context);\n if (!result.ok) return [];\n return result.value\n .filter(\n (info) => info.kind === 'file' && !info.name.startsWith('.') && !info.name.endsWith('~')\n )\n .map((info) => info.name)\n .sort();\n },\n };\n}\n\nfunction title(entry: WatchEntry): string {\n return `${entry.name}: ${entry.path}/${entry.glob}`;\n}\n\nexport class Watches {\n readonly #env: ExecutionEnv;\n readonly #deliver: Deliver;\n readonly #files: ConfigFiles;\n readonly #home: string;\n readonly #watching = new Map<string, Watching>();\n readonly #activity: Activity | undefined;\n\n constructor(env: ExecutionEnv, deliver: Deliver, home: string, activity?: Activity) {\n this.#env = env;\n this.#deliver = deliver;\n this.#files = configFiles(env);\n this.#home = home;\n this.#activity = activity;\n }\n\n get names(): string[] {\n return [...this.#watching.keys()];\n }\n\n async #fire(watch: Watching, context: Context): Promise<void> {\n const paths = [...watch.paths].sort();\n watch.paths.clear();\n if (!paths.length) return;\n const items: string[] = [];\n for (const path of paths) {\n const exists = await this.#env.exists(path, context);\n items.push(`${exists.ok && !exists.value ? 'gone' : 'changed'} ${path}`);\n }\n const { entry } = watch;\n const many = paths.length > 1;\n await this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text: `${paths.length} path${many ? 's' : ''} changed under ${entry.path} matching ${entry.glob}`,\n ...(entry.message ? { body: entry.message } : {}),\n items,\n target: entry.target,\n ...(entry.triage ? { triage: entry.triage } : {}),\n },\n context\n );\n }\n\n #paths(watch: Watching, paths: readonly string[], context: Context): void {\n for (const path of paths) {\n const relative = relativeTo(watch.entry.path, path);\n if (relative && watch.match(relative) && !this.#activity?.agentMade(path))\n watch.paths.add(path);\n }\n if (!watch.paths.size) return;\n clearTimeout(watch.timer);\n watch.timer = setTimeout(() => {\n void this.#fire(watch, context).catch(() => undefined);\n }, watch.entry.debounce);\n }\n\n #changed(watch: Watching, change: WatchChange, context: Context): void {\n if ('paths' in change) {\n this.#paths(watch, change.paths, context);\n return;\n }\n const stopped = 'error' in change;\n const text = stopped\n ? `The watch stopped: ${change.error.message}. Save its file again to restart it.`\n : 'Changes may have been missed for a while; look at the watched files again.';\n const { entry } = watch;\n if (stopped && this.#watching.get(entry.name) === watch) {\n this.#watching.delete(entry.name);\n clearTimeout(watch.timer);\n void watch.watcher?.close(context).catch(() => undefined);\n }\n void this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text,\n target: entry.target,\n severity: stopped ? 'error' : 'warn',\n },\n context\n ).catch(() => undefined);\n }\n\n async #start(entry: WatchEntry, json: string, file: string, context: Context) {\n const watch: Watching = { json, entry, match: glob(entry.glob), paths: new Set() };\n const result = await this.#env.watch(\n [{ path: entry.path, recursive: true, exclude: { names: IGNORED } }],\n (change) => this.#changed(watch, change, context),\n context\n );\n if (!result.ok) return { file, error: `can't watch ${entry.path}: ${result.error.message}` };\n watch.watcher = result.value;\n this.#watching.set(entry.name, watch);\n return undefined;\n }\n\n async stop(name: string, context: Context): Promise<void> {\n const watch = this.#watching.get(name);\n if (!watch) return;\n this.#watching.delete(name);\n clearTimeout(watch.timer);\n await watch.watcher?.close(context);\n }\n\n async reconcile(dir: string, context: Context): Promise<ConfigError[]> {\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n for (const name of await this.#files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await this.#files.read(file, context);\n if (json === undefined) continue;\n let entry: WatchEntry;\n try {\n entry = parseWatch(file, json, this.#home);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n continue;\n }\n seen.add(entry.name);\n if (this.#watching.get(entry.name)?.json === json) continue;\n await this.stop(entry.name, context);\n const failed = await this.#start(entry, json, file, context);\n if (failed) errors.push(failed);\n }\n for (const name of this.names) if (!seen.has(name)) await this.stop(name, context);\n return errors;\n }\n\n async close(context: Context): Promise<void> {\n for (const name of this.names) await this.stop(name, context);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/licks/licks.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
lickId
|
|
7
7
|
} from "./lick.js";
|
|
8
8
|
import { entryLick, LICK_STATE_KIND, lickDecisions } from "./state.js";
|
|
9
|
+
import { skipEntry, skips } from "./triage.js";
|
|
9
10
|
const LicksOutbox = defineDoc({
|
|
10
11
|
kind: "slicc.licks",
|
|
11
12
|
version: 1,
|
|
@@ -100,9 +101,9 @@ function createLicks(host) {
|
|
|
100
101
|
async function previous(harness, id, context) {
|
|
101
102
|
if (id === null) return "gone";
|
|
102
103
|
const submission = await harness.submission(id, context);
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
return
|
|
104
|
+
const record2 = await submission?.status(context);
|
|
105
|
+
if (record2?.status === "queued") return "queued";
|
|
106
|
+
return record2?.status === "unanswered" && record2.reason === "aborted" ? "withdrawn" : "gone";
|
|
106
107
|
}
|
|
107
108
|
async function settled(key, context) {
|
|
108
109
|
const { harness } = await host;
|
|
@@ -149,20 +150,45 @@ function createLicks(host) {
|
|
|
149
150
|
if (!lick) return void 0;
|
|
150
151
|
return { lick, decision: lickDecisions(entries).get(id) };
|
|
151
152
|
}
|
|
153
|
+
async function judged(event, context) {
|
|
154
|
+
if (!judge || !event.triage) return void 0;
|
|
155
|
+
return judge(event, context).catch(() => void 0);
|
|
156
|
+
}
|
|
157
|
+
async function seenBefore(event, context) {
|
|
158
|
+
if (event.eventId === void 0) return false;
|
|
159
|
+
const { harness } = await host;
|
|
160
|
+
const events = (await harness.snapshot(LicksOutbox, context))?.events ?? {};
|
|
161
|
+
return (events[`${event.channel}|${event.source}`] ?? []).includes(event.eventId);
|
|
162
|
+
}
|
|
163
|
+
async function record(event, verdict, now, context) {
|
|
164
|
+
const conversation = await (await host).resolve(event.target, event.channel, context);
|
|
165
|
+
await conversation.submit(skipEntry(event, verdict, now), context);
|
|
166
|
+
}
|
|
152
167
|
let check;
|
|
168
|
+
let judge;
|
|
153
169
|
return {
|
|
154
170
|
guard(next) {
|
|
155
171
|
check = next;
|
|
156
172
|
},
|
|
157
|
-
|
|
173
|
+
triage(next) {
|
|
174
|
+
judge = next;
|
|
175
|
+
},
|
|
176
|
+
async deliver(incoming, context) {
|
|
158
177
|
const blocked = check?.(incoming.target);
|
|
159
178
|
const event = blocked ? frozenNotice(incoming, blocked) : incoming;
|
|
179
|
+
if (judge && event.triage && await seenBefore(event, context)) return;
|
|
180
|
+
const verdict = await judged(event, context);
|
|
181
|
+
const skipped = skips(verdict);
|
|
160
182
|
return serial(async () => {
|
|
161
183
|
const { harness } = await host;
|
|
162
184
|
const key = keyOf(event);
|
|
163
185
|
await settled(key, context);
|
|
164
186
|
const stream = `${event.channel}|${event.source}`;
|
|
165
187
|
const now = event.at ?? Date.now();
|
|
188
|
+
if (skipped) {
|
|
189
|
+
if (await seenBefore(event, context)) return;
|
|
190
|
+
await record(event, verdict, now, context);
|
|
191
|
+
}
|
|
166
192
|
const accepted = await harness.commit(async (tx) => {
|
|
167
193
|
const doc = await tx.doc(LicksOutbox);
|
|
168
194
|
const seen = doc.events[stream] ?? [];
|
|
@@ -170,6 +196,7 @@ function createLicks(host) {
|
|
|
170
196
|
if (seen.includes(event.eventId)) return false;
|
|
171
197
|
doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);
|
|
172
198
|
}
|
|
199
|
+
if (skipped) return false;
|
|
173
200
|
const queue = doc.queues[key] ?? { submission: null, pending: null };
|
|
174
201
|
queue.pending = queue.pending ? merge(queue.pending, event, now) : fresh(event, now, actions(event.channel));
|
|
175
202
|
doc.queues[key] = queue;
|
package/dist/licks/licks.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/licks/licks.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport {\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickSeverity,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n severity?: LickSeverity;\n eventId?: string;\n at?: number;\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n};\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;\n guard(check: (target: LickTarget) => Blocked | undefined): void;\n}\n\nexport type Blocked = { id: string; name: string; at: number };\n\nexport function frozenNotice(event: LickEvent, blocked: Blocked): LickEvent {\n return {\n channel: event.channel,\n source: `frozen:${event.source}`,\n title: `${blocked.name} is frozen`,\n text: `${event.title} was skipped`,\n body: `${event.target} belongs to ${blocked.name}, which is frozen (${blocked.id}). Thaw it in the Freezer to get its licks again.`,\n target: 'cone',\n severity: 'warn',\n eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}`,\n };\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, channel: LickChannel, context: Context): Promise<Conversation>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nconst RANK: Record<LickSeverity, number> = { warn: 1, error: 2 };\n\nexport function strongest(\n a: LickSeverity | undefined,\n b: LickSeverity | undefined\n): LickSeverity | undefined {\n if (!a) return b;\n if (!b) return a;\n return RANK[b] > RANK[a] ? b : a;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n const { severity: _, ...lick } = pending.lick;\n const severity = strongest(pending.lick.severity, event.severity);\n return {\n lick: {\n ...lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n ...(severity ? { severity } : {}),\n },\n items,\n body: event.body ?? pending.body,\n };\n}\n\nfunction fresh(event: LickEvent, now: number, listed: LickAction[] | undefined): Pending {\n return {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n ...(event.severity ? { severity: event.severity } : {}),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\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\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, queue.pending.lick.channel, context);\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n let check: ((target: LickTarget) => Blocked | undefined) | undefined;\n return {\n guard(next) {\n check = next;\n },\n deliver(incoming, context) {\n const blocked = check?.(incoming.target);\n const event = blocked ? frozenNotice(incoming, blocked) : incoming;\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : fresh(event, now, actions(event.channel));\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n forget(target, channel, source, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = `${target}|${channel}|${source}`;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const queue = doc.queues[key];\n if (queue?.pending) queue.pending = null;\n }, context);\n });\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
|
|
5
|
-
"mappings": "AACA;AAAA,EAEE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EAMA;AAAA,OACK;AACP,SAAS,WAAW,iBAAoC,qBAAqB;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport {\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickSeverity,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\nimport { skipEntry, skips, type Triage, type Verdict } from './triage.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n severity?: LickSeverity;\n eventId?: string;\n at?: number;\n triage?: string;\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n};\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;\n guard(check: (target: LickTarget) => Blocked | undefined): void;\n triage(judge: Triage): void;\n}\n\nexport type Blocked = { id: string; name: string; at: number };\n\nexport function frozenNotice(event: LickEvent, blocked: Blocked): LickEvent {\n return {\n channel: event.channel,\n source: `frozen:${event.source}`,\n title: `${blocked.name} is frozen`,\n text: `${event.title} was skipped`,\n body: `${event.target} belongs to ${blocked.name}, which is frozen (${blocked.id}). Thaw it in the Freezer to get its licks again.`,\n target: 'cone',\n severity: 'warn',\n eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}`,\n };\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, channel: LickChannel, context: Context): Promise<Conversation>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nconst RANK: Record<LickSeverity, number> = { warn: 1, error: 2 };\n\nexport function strongest(\n a: LickSeverity | undefined,\n b: LickSeverity | undefined\n): LickSeverity | undefined {\n if (!a) return b;\n if (!b) return a;\n return RANK[b] > RANK[a] ? b : a;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n const { severity: _, ...lick } = pending.lick;\n const severity = strongest(pending.lick.severity, event.severity);\n return {\n lick: {\n ...lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n ...(severity ? { severity } : {}),\n },\n items,\n body: event.body ?? pending.body,\n };\n}\n\nfunction fresh(event: LickEvent, now: number, listed: LickAction[] | undefined): Pending {\n return {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n ...(event.severity ? { severity: event.severity } : {}),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\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\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, queue.pending.lick.channel, context);\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n async function judged(event: LickEvent, context: Context): Promise<Verdict | undefined> {\n if (!judge || !event.triage) return undefined;\n return judge(event, context).catch(() => undefined);\n }\n\n async function seenBefore(event: LickEvent, context: Context): Promise<boolean> {\n if (event.eventId === undefined) return false;\n const { harness } = await host;\n const events = (await harness.snapshot(LicksOutbox, context))?.events ?? {};\n return (events[`${event.channel}|${event.source}`] ?? []).includes(event.eventId);\n }\n\n async function record(event: LickEvent, verdict: Verdict, now: number, context: Context) {\n const conversation = await (await host).resolve(event.target, event.channel, context);\n await conversation.submit(skipEntry(event, verdict, now), context);\n }\n\n let check: ((target: LickTarget) => Blocked | undefined) | undefined;\n let judge: Triage | undefined;\n return {\n guard(next) {\n check = next;\n },\n triage(next) {\n judge = next;\n },\n async deliver(incoming, context) {\n const blocked = check?.(incoming.target);\n const event = blocked ? frozenNotice(incoming, blocked) : incoming;\n if (judge && event.triage && (await seenBefore(event, context))) return;\n const verdict = await judged(event, context);\n const skipped = skips(verdict);\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n if (skipped) {\n if (await seenBefore(event, context)) return;\n await record(event, verdict, now, context);\n }\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n if (skipped) return false;\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : fresh(event, now, actions(event.channel));\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n forget(target, channel, source, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = `${target}|${channel}|${source}`;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const queue = doc.queues[key];\n if (queue?.pending) queue.pending = null;\n }, context);\n });\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["record"]
|
|
7
7
|
}
|
package/dist/licks/sources.js
CHANGED
|
@@ -38,7 +38,8 @@ function webhookLick(name, entry, delivery) {
|
|
|
38
38
|
body: [headers, payload].filter(Boolean).join("\n\n"),
|
|
39
39
|
target: entry.target,
|
|
40
40
|
coalesce: false,
|
|
41
|
-
...delivery.id ? { eventId: delivery.id } : {}
|
|
41
|
+
...delivery.id ? { eventId: delivery.id } : {},
|
|
42
|
+
...entry.triage ? { triage: entry.triage } : {}
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
async function webhooksIn(files, dir, context) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/licks/sources.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type BootFacts, bootLicks } from './boot.ts';\nimport { type ConfigError, parseCrontab, parseWebhook, type WebhookEntry } from './config.ts';\nimport type { cronTask } from './extension.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\nimport { reconcileCron } from './schedules.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n cron: ReturnType<typeof cronTask>;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(error: ConfigError, home: string): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: 'cone',\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [headers, payload].filter(Boolean).join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const dir = `${home}/${SLICC_DIR}`;\n const now = options.now ?? Date.now;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const watches = new Watches(env, deliver, home, options.activity);\n let webhooks = new Map<string, WebhookEntry>();\n let control: FileWatcher | undefined;\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\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\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const crontabFile = `${dir}/crontab`;\n const crontab = parseCrontab(crontabFile, (await files.read(crontabFile, context)) ?? '');\n await reconcileCron({ ...options, now }, crontab.entries, context);\n const watchErrors = await watches.reconcile(`${dir}/watches`, context);\n const found = await webhooksIn(files, `${dir}/webhooks`, context);\n webhooks = found.entries;\n const errors = [...crontab.errors, ...watchErrors, ...found.errors];\n for (const error of errors) await deliver(configLick(error, home), context);\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await reconcile(context);\n const result = await env.watch(\n [{ path: dir, recursive: true }],\n () => scheduleReconcile(context),\n context\n );\n if (result.ok) control = result.value;\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(webhookLick(name, entry, delivery), context);\n return true;\n },\n async close(context) {\n closed = true;\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n await watches.close(context);\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "AAIA,SAAyB,iBAAiB;AAC1C,SAA2B,cAAc,oBAAuC;AAEhF,SAA2B,aAAa,eAAe;AAGvD,SAAS,qBAAqB;AAEvB,MAAM,YAAY;AA6BlB,SAAS,KAAK,MAAsB;AACzC,MAAI,QAAQ;AACZ,WAAS,KAAK,GAAG,KAAK,KAAK,QAAQ,KAAM,SAAQ,KAAK,KAAK,QAAQ,KAAK,WAAW,EAAE,GAAG,QAAQ;AAChG,UAAQ,UAAU,GAAG,SAAS,EAAE;AAClC;AAEO,SAAS,UAAU,MAAc,MAAsB;AAC5D,SAAO,SAAS,QAAQ,KAAK,WAAW,GAAG,IAAI,GAAG,IAAI,IAAI,KAAK,MAAM,KAAK,MAAM,CAAC,KAAK;AACxF;AAEO,SAAS,WAAW,OAAoB,MAAyB;AACtE,QAAM,OAAO,UAAU,MAAM,MAAM,IAAI;AACvC,QAAM,UAAuB,MAAM,KAAK,SAAS,WAAW,IACxD,YACA,MAAM,KAAK,SAAS,YAAY,IAC9B,YACA;AACN,QAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,MAAM,IAAI,OAAO,EAAE,GAAG,MAAM,KAAK;AACtE,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,UAAU,IAAI;AAAA,IACtB,OAAO,WAAW,IAAI;AAAA,IACtB;AAAA,IACA,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS,KAAK,GAAG,IAAI;AAAA,EAAK,IAAI,EAAE;AAAA,EAClC;AACF;AAEO,SAAS,YACd,MACA,OACA,UACW;AACX,QAAM,UACJ,OAAO,SAAS,SAAS,WAAW,SAAS,OAAO,KAAK,UAAU,SAAS,MAAM,MAAM,CAAC;AAC3F,QAAM,UAAU,OAAO,QAAQ,SAAS,WAAW,CAAC,CAAC,EAClD,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,KAAK,KAAK,EAAE,EACxC,KAAK,IAAI;AACZ,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,MAAM,MAAM,WAAW,WAAW,IAAI;AAAA,IACtC,MAAM,CAAC,SAAS,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,MAAM;AAAA,IACpD,QAAQ,MAAM;AAAA,IACd,UAAU;AAAA,IACV,GAAI,SAAS,KAAK,EAAE,SAAS,SAAS,GAAG,IAAI,CAAC;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type BootFacts, bootLicks } from './boot.ts';\nimport { type ConfigError, parseCrontab, parseWebhook, type WebhookEntry } from './config.ts';\nimport type { cronTask } from './extension.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\nimport { reconcileCron } from './schedules.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n cron: ReturnType<typeof cronTask>;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(error: ConfigError, home: string): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: 'cone',\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [headers, payload].filter(Boolean).join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n ...(entry.triage ? { triage: entry.triage } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const dir = `${home}/${SLICC_DIR}`;\n const now = options.now ?? Date.now;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const watches = new Watches(env, deliver, home, options.activity);\n let webhooks = new Map<string, WebhookEntry>();\n let control: FileWatcher | undefined;\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\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\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const crontabFile = `${dir}/crontab`;\n const crontab = parseCrontab(crontabFile, (await files.read(crontabFile, context)) ?? '');\n await reconcileCron({ ...options, now }, crontab.entries, context);\n const watchErrors = await watches.reconcile(`${dir}/watches`, context);\n const found = await webhooksIn(files, `${dir}/webhooks`, context);\n webhooks = found.entries;\n const errors = [...crontab.errors, ...watchErrors, ...found.errors];\n for (const error of errors) await deliver(configLick(error, home), context);\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await reconcile(context);\n const result = await env.watch(\n [{ path: dir, recursive: true }],\n () => scheduleReconcile(context),\n context\n );\n if (result.ok) control = result.value;\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(webhookLick(name, entry, delivery), context);\n return true;\n },\n async close(context) {\n closed = true;\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n await watches.close(context);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ADOBE } from "../adobe.js";
|
|
2
|
+
import { lickId } from "./lick.js";
|
|
3
|
+
const TRIAGE_MODEL = "clef-flash";
|
|
4
|
+
const TRIAGE_SKIP = 0.75;
|
|
5
|
+
const TRIAGE_KIND = "slicc.lick-triage";
|
|
6
|
+
const TRIAGE_LIMIT = 4e3;
|
|
7
|
+
const TRIAGE_TIMEOUT = 15e3;
|
|
8
|
+
function clip(text) {
|
|
9
|
+
return text.length > TRIAGE_LIMIT ? `${text.slice(0, TRIAGE_LIMIT)}\u2026` : text;
|
|
10
|
+
}
|
|
11
|
+
function stateOf(event) {
|
|
12
|
+
return {
|
|
13
|
+
channel: event.channel,
|
|
14
|
+
source: event.source,
|
|
15
|
+
title: event.title,
|
|
16
|
+
text: event.text,
|
|
17
|
+
...event.body ? { body: clip(event.body) } : {},
|
|
18
|
+
...event.items?.length ? { items: event.items.slice(0, 50) } : {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function skips(verdict) {
|
|
22
|
+
return verdict?.decision === "ignore" && (verdict.probabilities.ignore ?? 0) >= TRIAGE_SKIP;
|
|
23
|
+
}
|
|
24
|
+
function lickTriage(models) {
|
|
25
|
+
return async (event) => {
|
|
26
|
+
const model = models.getModelOfType("classifier", ADOBE, TRIAGE_MODEL);
|
|
27
|
+
if (!model || !event.triage) return void 0;
|
|
28
|
+
const result = await models.classify(
|
|
29
|
+
model,
|
|
30
|
+
{
|
|
31
|
+
state: stateOf(event),
|
|
32
|
+
questions: {
|
|
33
|
+
triage: {
|
|
34
|
+
type: "choice",
|
|
35
|
+
instructions: `The state is an event from a ${event.channel} source the user configured. The user wants the agent woken only for events that match this: ${event.triage}`,
|
|
36
|
+
criteria: {
|
|
37
|
+
act: "the event matches what the user wants the agent to act on",
|
|
38
|
+
ignore: "the event does not match it, so the agent need not see it"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{ timeoutMs: TRIAGE_TIMEOUT, maxRetries: 1 }
|
|
44
|
+
);
|
|
45
|
+
const answer = result.answers.triage;
|
|
46
|
+
if (result.stopReason !== "stop" || answer?.type !== "choice") return void 0;
|
|
47
|
+
if (answer.choice !== "act" && answer.choice !== "ignore") return void 0;
|
|
48
|
+
return {
|
|
49
|
+
decision: answer.choice,
|
|
50
|
+
confidence: answer.confidence,
|
|
51
|
+
probabilities: answer.probabilities,
|
|
52
|
+
model: `${ADOBE}/${TRIAGE_MODEL}`,
|
|
53
|
+
cost: result.usage?.cost.total ?? null
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function skipEntry(event, verdict, now) {
|
|
58
|
+
const id = lickId();
|
|
59
|
+
const delivery = event.eventId ? `${event.channel}|${event.source}|${event.eventId}` : id;
|
|
60
|
+
return {
|
|
61
|
+
type: "write",
|
|
62
|
+
requestId: `lick-triage:${delivery}`,
|
|
63
|
+
entry: {
|
|
64
|
+
kind: TRIAGE_KIND,
|
|
65
|
+
data: {
|
|
66
|
+
lick: id,
|
|
67
|
+
...stateOf(event),
|
|
68
|
+
count: event.count ?? 1,
|
|
69
|
+
at: now,
|
|
70
|
+
triage: event.triage ?? "",
|
|
71
|
+
...verdict
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
TRIAGE_KIND,
|
|
78
|
+
TRIAGE_LIMIT,
|
|
79
|
+
TRIAGE_MODEL,
|
|
80
|
+
TRIAGE_SKIP,
|
|
81
|
+
clip,
|
|
82
|
+
lickTriage,
|
|
83
|
+
skipEntry,
|
|
84
|
+
skips
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=triage.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/triage.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport { ADOBE } from '../adobe.ts';\nimport { lickId } from './lick.ts';\nimport type { LickEvent } from './licks.ts';\n\nexport const TRIAGE_MODEL = 'clef-flash';\nexport const TRIAGE_SKIP = 0.75;\nexport const TRIAGE_KIND = 'slicc.lick-triage';\nexport const TRIAGE_LIMIT = 4000;\nconst TRIAGE_TIMEOUT = 15_000;\n\nexport interface Verdict {\n decision: 'act' | 'ignore';\n confidence: number;\n probabilities: Record<string, number>;\n model: string;\n cost: number | null;\n}\n\nexport type Triage = (event: LickEvent, context: Context) => Promise<Verdict | undefined>;\n\nexport function clip(text: string): string {\n return text.length > TRIAGE_LIMIT ? `${text.slice(0, TRIAGE_LIMIT)}\u2026` : text;\n}\n\nfunction stateOf(event: LickEvent) {\n return {\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n ...(event.body ? { body: clip(event.body) } : {}),\n ...(event.items?.length ? { items: event.items.slice(0, 50) } : {}),\n };\n}\n\nexport function skips(verdict: Verdict | undefined): verdict is Verdict {\n return verdict?.decision === 'ignore' && (verdict.probabilities.ignore ?? 0) >= TRIAGE_SKIP;\n}\n\nexport function lickTriage(models: Pick<Models, 'getModelOfType' | 'classify'>): Triage {\n return async (event) => {\n const model = models.getModelOfType('classifier', ADOBE, TRIAGE_MODEL);\n if (!model || !event.triage) return undefined;\n const result = await models.classify(\n model,\n {\n state: stateOf(event),\n questions: {\n triage: {\n type: 'choice',\n instructions: `The state is an event from a ${event.channel} source the user configured. The user wants the agent woken only for events that match this: ${event.triage}`,\n criteria: {\n act: 'the event matches what the user wants the agent to act on',\n ignore: 'the event does not match it, so the agent need not see it',\n },\n },\n },\n },\n { timeoutMs: TRIAGE_TIMEOUT, maxRetries: 1 }\n );\n const answer = result.answers.triage;\n if (result.stopReason !== 'stop' || answer?.type !== 'choice') return undefined;\n if (answer.choice !== 'act' && answer.choice !== 'ignore') return undefined;\n return {\n decision: answer.choice,\n confidence: answer.confidence,\n probabilities: answer.probabilities,\n model: `${ADOBE}/${TRIAGE_MODEL}`,\n cost: result.usage?.cost.total ?? null,\n };\n };\n}\n\nexport function skipEntry(event: LickEvent, verdict: Verdict, now: number) {\n const id = lickId();\n const delivery = event.eventId ? `${event.channel}|${event.source}|${event.eventId}` : id;\n return {\n type: 'write' as const,\n requestId: `lick-triage:${delivery}`,\n entry: {\n kind: TRIAGE_KIND,\n data: {\n lick: id,\n ...stateOf(event),\n count: event.count ?? 1,\n at: now,\n triage: event.triage ?? '',\n ...verdict,\n },\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,aAAa;AACtB,SAAS,cAAc;AAGhB,MAAM,eAAe;AACrB,MAAM,cAAc;AACpB,MAAM,cAAc;AACpB,MAAM,eAAe;AAC5B,MAAM,iBAAiB;AAYhB,SAAS,KAAK,MAAsB;AACzC,SAAO,KAAK,SAAS,eAAe,GAAG,KAAK,MAAM,GAAG,YAAY,CAAC,WAAM;AAC1E;AAEA,SAAS,QAAQ,OAAkB;AACjC,SAAO;AAAA,IACL,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,MAAM,MAAM;AAAA,IACZ,GAAI,MAAM,OAAO,EAAE,MAAM,KAAK,MAAM,IAAI,EAAE,IAAI,CAAC;AAAA,IAC/C,GAAI,MAAM,OAAO,SAAS,EAAE,OAAO,MAAM,MAAM,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC;AAAA,EACnE;AACF;AAEO,SAAS,MAAM,SAAkD;AACtE,SAAO,SAAS,aAAa,aAAa,QAAQ,cAAc,UAAU,MAAM;AAClF;AAEO,SAAS,WAAW,QAA6D;AACtF,SAAO,OAAO,UAAU;AACtB,UAAM,QAAQ,OAAO,eAAe,cAAc,OAAO,YAAY;AACrE,QAAI,CAAC,SAAS,CAAC,MAAM,OAAQ,QAAO;AACpC,UAAM,SAAS,MAAM,OAAO;AAAA,MAC1B;AAAA,MACA;AAAA,QACE,OAAO,QAAQ,KAAK;AAAA,QACpB,WAAW;AAAA,UACT,QAAQ;AAAA,YACN,MAAM;AAAA,YACN,cAAc,gCAAgC,MAAM,OAAO,gGAAgG,MAAM,MAAM;AAAA,YACvK,UAAU;AAAA,cACR,KAAK;AAAA,cACL,QAAQ;AAAA,YACV;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,WAAW,gBAAgB,YAAY,EAAE;AAAA,IAC7C;AACA,UAAM,SAAS,OAAO,QAAQ;AAC9B,QAAI,OAAO,eAAe,UAAU,QAAQ,SAAS,SAAU,QAAO;AACtE,QAAI,OAAO,WAAW,SAAS,OAAO,WAAW,SAAU,QAAO;AAClE,WAAO;AAAA,MACL,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,MACnB,eAAe,OAAO;AAAA,MACtB,OAAO,GAAG,KAAK,IAAI,YAAY;AAAA,MAC/B,MAAM,OAAO,OAAO,KAAK,SAAS;AAAA,IACpC;AAAA,EACF;AACF;AAEO,SAAS,UAAU,OAAkB,SAAkB,KAAa;AACzE,QAAM,KAAK,OAAO;AAClB,QAAM,WAAW,MAAM,UAAU,GAAG,MAAM,OAAO,IAAI,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AACvF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,WAAW,eAAe,QAAQ;AAAA,IAClC,OAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,GAAG,QAAQ,KAAK;AAAA,QAChB,OAAO,MAAM,SAAS;AAAA,QACtB,IAAI;AAAA,QACJ,QAAQ,MAAM,UAAU;AAAA,QACxB,GAAG;AAAA,MACL;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -24,6 +24,7 @@ import { HOME, kernelEnvironment, SliccKernelEnv } from "./kernel/env.js";
|
|
|
24
24
|
import { processGroups } from "./kernel/groups.js";
|
|
25
25
|
import { adminOver, userClients } from "./kernel/users.js";
|
|
26
26
|
import { setupLicks } from "./licks/index.js";
|
|
27
|
+
import { lickTriage } from "./licks/triage.js";
|
|
27
28
|
import { setupMcp } from "./mcp/index.js";
|
|
28
29
|
import { setupMemory } from "./memory/index.js";
|
|
29
30
|
import { modelsCommand } from "./models/index.js";
|
|
@@ -110,6 +111,7 @@ async function start(scope, ports, options) {
|
|
|
110
111
|
const codemode = await codemodeTools(client, options, native, mcp.codemode);
|
|
111
112
|
registry.install(codemodeExtension(codemode));
|
|
112
113
|
const licks = setupLicks(registry);
|
|
114
|
+
licks.licks.triage(lickTriage(models));
|
|
113
115
|
const scoops = setupScoops(registry, licks.licks, [...coding, ...codemode, ...memory.tools]);
|
|
114
116
|
const skills = setupSkills(registry, licks.licks);
|
|
115
117
|
const activity = createActivity();
|
package/dist/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import { attachKernel } from '@ai-ecoverse/slicc-kernel';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { CredentialStore, Provider } from '@earendil-works/pi-ai';\nimport {\n createRegistry,\n type ModelRef,\n type Storage,\n type ToolRegistration,\n} from '@earendil-works/pi-durable';\nimport { CodingTools } from '@earendil-works/pi-durable/tools';\nimport { type Agent, openAgent } from './agent.ts';\nimport { attachChanges, CHANGES_MS } from './changes/index.ts';\nimport {\n codemodeExtension,\n codemodeTool,\n disabledBySettings,\n locateWasm,\n sandboxFactory,\n} from './codemode/index.ts';\nimport { EncryptedCredentialStore } from './credentials.ts';\nimport { attachFreezer } from './freezer/index.ts';\nimport { settingsText, titler } from './freezer/title.ts';\nimport { lateGelatiere } from './gelatiere/index.ts';\nimport { type AgentHost, hostAgent } from './host.ts';\nimport { imageReads } from './images/index.ts';\nimport { createActivity } from './kernel/activity.ts';\nimport type { KernelClient } from './kernel/client.ts';\nimport { HOME, kernelEnvironment, SliccKernelEnv } from './kernel/env.ts';\nimport { processGroups } from './kernel/groups.ts';\nimport { adminOver, type UserClient, type UserClients, userClients } from './kernel/users.ts';\nimport { setupLicks } from './licks/index.ts';\nimport { setupMcp } from './mcp/index.ts';\nimport { setupMemory } from './memory/index.ts';\nimport { modelsCommand } from './models/index.ts';\nimport { type Transport, transportFetch } from './net.ts';\nimport { sliccPrompt } from './prompt.ts';\nimport { identity } from './scoops/identity.ts';\nimport { type Assets, packageAssets, type ScoopsRuntime, setupScoops } from './scoops/index.ts';\nimport {\n createAgentSettings,\n createSliccModels,\n DEFAULT_MODEL,\n sliccProviders,\n} from './settings.ts';\nimport { RELOAD_MS, setupSkills } from './skills/index.ts';\nimport { setupSprinkles } from './sprinkles/index.ts';\nimport { openOpfsSqliteStorage } from './sqlite.ts';\nimport {\n agentVersion,\n browserConnected,\n commandsOnPath,\n kernelBoot,\n transportName,\n} from './system.ts';\nimport { type KernelPorts, kernelPorts, serveConnections, type WorkerScope } from './worker.ts';\n\nexport interface AgentWorkerScope extends WorkerScope {\n fetch: typeof fetch;\n location: { origin: string };\n}\n\nexport type KernelAttach = (port: MessagePort) => Promise<KernelClient & { transport: Transport }>;\n\nexport interface AgentWorkerOptions {\n model?: ModelRef;\n providers?: Provider[];\n attach?: KernelAttach;\n credentials?: () => Promise<CredentialStore>;\n storage?: () => Promise<Storage>;\n assets?: Assets;\n codemodeWasm?: () => Promise<WebAssembly.Module | undefined>;\n codemodeWorker?: string | URL;\n catalog?: string;\n users?: boolean;\n mcpRedirect?: string;\n}\n\nexport function usersOf(\n admin: MessagePort | undefined,\n wanted: boolean | undefined,\n attach: KernelAttach\n): UserClients | undefined {\n if (!admin) return undefined;\n if (wanted === false) {\n admin.close();\n return undefined;\n }\n return userClients(adminOver(admin), attach as (port: MessagePort) => Promise<UserClient>);\n}\n\nexport const STORE_DIR = '/.slicc';\n\nasync function attached(ports: Promise<KernelPorts>, options: AgentWorkerOptions) {\n const attach: KernelAttach = options.attach ?? attachKernel;\n const { kernel, admin } = await ports;\n const client = await attach(kernel);\n return { client, users: usersOf(admin, options.users, attach) };\n}\n\nfunction privateStore(env: SliccKernelEnv): Promise<unknown> {\n return env.exec(`mkdir -p ${STORE_DIR} && chmod 700 ${STORE_DIR}`, undefined, BACKGROUND_CONTEXT);\n}\n\nasync function codemodeTools(\n client: KernelClient,\n options: AgentWorkerOptions,\n native: typeof fetch,\n extra: Parameters<typeof codemodeTool>[0]['extra']\n) {\n const settings = await client.fs\n .readFile(`${HOME}/.pi/agent/settings.json`)\n .catch(() => undefined);\n if (disabledBySettings(settings === undefined ? undefined : new TextDecoder().decode(settings)))\n return [];\n const sandbox = sandboxFactory(\n options.codemodeWasm ?? (() => locateWasm(native, new URL(import.meta.url))),\n options.codemodeWorker\n );\n return [\n codemodeTool({ declared: CodingTools.tools as readonly ToolRegistration[], sandbox, extra }),\n ];\n}\n\nexport const MCP_CALLBACK = '/oauth/mcp-callback';\n\nasync function start(\n scope: AgentWorkerScope,\n ports: Promise<KernelPorts>,\n options: AgentWorkerOptions\n): Promise<AgentHost> {\n const { client, users } = await attached(ports, options);\n const home = new SliccKernelEnv(client, { cwd: HOME });\n await privateStore(home);\n const native = scope.fetch.bind(scope);\n scope.fetch = transportFetch(client.transport, scope.location.origin, native);\n const facts = {\n version: await agentVersion((url) => native(url)),\n commands: await commandsOnPath(client),\n transport: transportName(client.transport),\n browser: await browserConnected(client),\n };\n const credentials = await (options.credentials ?? (() => EncryptedCredentialStore.open()))();\n const providers = options.providers ?? sliccProviders();\n const models = createSliccModels(credentials, providers);\n const registry = createRegistry();\n registry.install(CodingTools);\n registry.install(sliccPrompt(facts));\n registry.install(\n imageReads(\n (CodingTools.tools as readonly ToolRegistration[]).find(\n (tool) => tool.name === 'read'\n ) as ToolRegistration\n )\n );\n const memory = setupMemory(registry);\n const coding = CodingTools.tools as readonly ToolRegistration[];\n const mcp = setupMcp(registry);\n const codemode = await codemodeTools(client, options, native, mcp.codemode);\n registry.install(codemodeExtension(codemode));\n const licks = setupLicks(registry);\n const scoops = setupScoops(registry, licks.licks, [...coding, ...codemode, ...memory.tools]);\n const skills = setupSkills(registry, licks.licks);\n const activity = createActivity();\n const groups = processGroups(client);\n const model = options.model ?? DEFAULT_MODEL;\n let opened: Agent | undefined;\n const environment = kernelEnvironment(client, {\n activity,\n groups,\n identify: identity(() => opened?.agents, model),\n });\n const agent = await openAgent({\n models,\n model,\n storage: await (options.storage ?? (() => openOpfsSqliteStorage()))(),\n registry,\n env: environment,\n });\n opened = agent;\n const sources = licks.attach(agent, { env: home, home: HOME, activity });\n await sources.start(BACKGROUND_CONTEXT);\n await sources.boot(\n { version: facts.version, boot: await kernelBoot(client) },\n BACKGROUND_CONTEXT\n );\n const skillsRuntime = await skills.attach(\n { env: home, home: HOME, assets: options.assets ?? packageAssets(), reloadMs: RELOAD_MS },\n BACKGROUND_CONTEXT\n );\n const mcpRuntime = await mcp.attach(\n {\n env: home,\n home: HOME,\n credentials,\n licks: licks.licks,\n fetch: scope.fetch,\n version: facts.version,\n reloadMs: RELOAD_MS,\n codemode: codemode.length > 0,\n cors: client.transport.traits?.crossOrigin === 'cors',\n redirect: options.mcpRedirect ?? new URL(MCP_CALLBACK, scope.location.origin).href,\n },\n BACKGROUND_CONTEXT\n );\n const gelatiere = lateGelatiere();\n const sprinkles = await setupSprinkles().attach(\n {\n intercept: gelatiere.intercept,\n harness: agent.harness,\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n assets: options.assets ?? packageAssets(),\n reloadMs: RELOAD_MS,\n },\n BACKGROUND_CONTEXT\n );\n let roles: ScoopsRuntime | undefined;\n const memoryRuntime = await memory.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n home: HOME,\n reloadMs: RELOAD_MS,\n roles: async (using) => roles?.roles(using),\n },\n BACKGROUND_CONTEXT\n );\n const freezer = attachFreezer({\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n extract: memory.extract,\n title: titler(models, settingsText(home, HOME)),\n });\n licks.licks.guard(freezer.frozenTarget);\n const settings = await createAgentSettings(models, credentials, providers, { mcp: mcpRuntime });\n const runtime = await scoops.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n groups,\n env: home,\n home: HOME,\n alive: alive(client),\n reads: skillsRuntime.dirs,\n sprinkle: sprinkles.command,\n memory: memoryRuntime.command,\n gelatiere: gelatiere.command,\n freezer: freezer.command,\n models: modelsCommand({ agents: agent.agents, settings, catalog: models }),\n mcp: mcp.direct,\n ...(options.assets ? { assets: options.assets } : {}),\n },\n BACKGROUND_CONTEXT\n );\n roles = runtime;\n gelatiere.attach({\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n home: HOME,\n scoops: scoops.scoops,\n roles: runtime.roles,\n assets: options.assets ?? packageAssets(),\n fetch: scope.fetch,\n skills: skillsRuntime.skills,\n ...(options.catalog ? { catalog: options.catalog } : {}),\n });\n await memoryRuntime.reload(BACKGROUND_CONTEXT).catch(() => undefined);\n const changes = attachChanges({ agents: agent.agents, env: home, reloadMs: CHANGES_MS });\n return hostAgent(agent, {\n settings,\n licks: { licks: licks.licks, sources },\n scoops: { scoops: scoops.scoops, runtime },\n skills: skillsRuntime,\n sprinkles,\n memory: memoryRuntime,\n freezer,\n changes,\n mcp: mcpRuntime,\n ...(users ? { users } : {}),\n monitor: {\n kernel: client,\n groups,\n ...(users ? { users: () => users.users.list() } : {}),\n secrets: () => mcpRuntime.secrets(),\n },\n });\n}\n\nexport function alive(client: KernelClient) {\n return async (pid: number): Promise<boolean> => {\n if (!client.ps) return true;\n return (await client.ps()).some((process) => process.pid === pid);\n };\n}\n\nexport function runAgentWorker(\n scope: AgentWorkerScope,\n options: AgentWorkerOptions = {}\n): Promise<AgentHost> {\n const host = start(scope, kernelPorts(scope), options);\n host.catch(() => undefined);\n serveConnections(scope, host);\n return host;\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,OAIK;AACP,SAAS,mBAAmB;AAC5B,SAAqB,iBAAiB;AACtC,SAAS,eAAe,kBAAkB;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,cAAc,cAAc;AACrC,SAAS,qBAAqB;AAC9B,SAAyB,iBAAiB;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAE/B,SAAS,MAAM,mBAAmB,sBAAsB;AACxD,SAAS,qBAAqB;AAC9B,SAAS,WAA8C,mBAAmB;AAC1E,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAyB,sBAAsB;AAC/C,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAsB,eAAmC,mBAAmB;AAC5E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAAW,mBAAmB;AACvC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAA2B,aAAa,wBAA0C;AAuB3E,SAAS,QACd,OACA,QACA,QACyB;AACzB,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,WAAW,OAAO;AACpB,UAAM,MAAM;AACZ,WAAO;AAAA,EACT;AACA,SAAO,YAAY,UAAU,KAAK,GAAG,MAAoD;AAC3F;AAEO,MAAM,YAAY;AAEzB,eAAe,SAAS,OAA6B,SAA6B;AAChF,QAAM,SAAuB,QAAQ,UAAU;AAC/C,QAAM,EAAE,QAAQ,MAAM,IAAI,MAAM;AAChC,QAAM,SAAS,MAAM,OAAO,MAAM;AAClC,SAAO,EAAE,QAAQ,OAAO,QAAQ,OAAO,QAAQ,OAAO,MAAM,EAAE;AAChE;AAEA,SAAS,aAAa,KAAuC;AAC3D,SAAO,IAAI,KAAK,YAAY,SAAS,iBAAiB,SAAS,IAAI,QAAW,kBAAkB;AAClG;AAEA,eAAe,cACb,QACA,SACA,QACA,OACA;AACA,QAAM,WAAW,MAAM,OAAO,GAC3B,SAAS,GAAG,IAAI,0BAA0B,EAC1C,MAAM,MAAM,MAAS;AACxB,MAAI,mBAAmB,aAAa,SAAY,SAAY,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;AAC5F,WAAO,CAAC;AACV,QAAM,UAAU;AAAA,IACd,QAAQ,iBAAiB,MAAM,WAAW,QAAQ,IAAI,IAAI,YAAY,GAAG,CAAC;AAAA,IAC1E,QAAQ;AAAA,EACV;AACA,SAAO;AAAA,IACL,aAAa,EAAE,UAAU,YAAY,OAAsC,SAAS,MAAM,CAAC;AAAA,EAC7F;AACF;AAEO,MAAM,eAAe;AAE5B,eAAe,MACb,OACA,OACA,SACoB;AACpB,QAAM,EAAE,QAAQ,MAAM,IAAI,MAAM,SAAS,OAAO,OAAO;AACvD,QAAM,OAAO,IAAI,eAAe,QAAQ,EAAE,KAAK,KAAK,CAAC;AACrD,QAAM,aAAa,IAAI;AACvB,QAAM,SAAS,MAAM,MAAM,KAAK,KAAK;AACrC,QAAM,QAAQ,eAAe,OAAO,WAAW,MAAM,SAAS,QAAQ,MAAM;AAC5E,QAAM,QAAQ;AAAA,IACZ,SAAS,MAAM,aAAa,CAAC,QAAQ,OAAO,GAAG,CAAC;AAAA,IAChD,UAAU,MAAM,eAAe,MAAM;AAAA,IACrC,WAAW,cAAc,OAAO,SAAS;AAAA,IACzC,SAAS,MAAM,iBAAiB,MAAM;AAAA,EACxC;AACA,QAAM,cAAc,OAAO,QAAQ,gBAAgB,MAAM,yBAAyB,KAAK,IAAI;AAC3F,QAAM,YAAY,QAAQ,aAAa,eAAe;AACtD,QAAM,SAAS,kBAAkB,aAAa,SAAS;AACvD,QAAM,WAAW,eAAe;AAChC,WAAS,QAAQ,WAAW;AAC5B,WAAS,QAAQ,YAAY,KAAK,CAAC;AACnC,WAAS;AAAA,IACP;AAAA,MACG,YAAY,MAAsC;AAAA,QACjD,CAAC,SAAS,KAAK,SAAS;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AACA,QAAM,SAAS,YAAY,QAAQ;AACnC,QAAM,SAAS,YAAY;AAC3B,QAAM,MAAM,SAAS,QAAQ;AAC7B,QAAM,WAAW,MAAM,cAAc,QAAQ,SAAS,QAAQ,IAAI,QAAQ;AAC1E,WAAS,QAAQ,kBAAkB,QAAQ,CAAC;AAC5C,QAAM,QAAQ,WAAW,QAAQ;AACjC,QAAM,SAAS,YAAY,UAAU,MAAM,OAAO,CAAC,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,KAAK,CAAC;AAC3F,QAAM,SAAS,YAAY,UAAU,MAAM,KAAK;AAChD,QAAM,WAAW,eAAe;AAChC,QAAM,SAAS,cAAc,MAAM;AACnC,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI;AACJ,QAAM,cAAc,kBAAkB,QAAQ;AAAA,IAC5C;AAAA,IACA;AAAA,IACA,UAAU,SAAS,MAAM,QAAQ,QAAQ,KAAK;AAAA,EAChD,CAAC;AACD,QAAM,QAAQ,MAAM,UAAU;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,SAAS,OAAO,QAAQ,YAAY,MAAM,sBAAsB,IAAI;AAAA,IACpE;AAAA,IACA,KAAK;AAAA,EACP,CAAC;AACD,WAAS;AACT,QAAM,UAAU,MAAM,OAAO,OAAO,EAAE,KAAK,MAAM,MAAM,MAAM,SAAS,CAAC;AACvE,QAAM,QAAQ,MAAM,kBAAkB;AACtC,QAAM,QAAQ;AAAA,IACZ,EAAE,SAAS,MAAM,SAAS,MAAM,MAAM,WAAW,MAAM,EAAE;AAAA,IACzD;AAAA,EACF;AACA,QAAM,gBAAgB,MAAM,OAAO;AAAA,IACjC,EAAE,KAAK,MAAM,MAAM,MAAM,QAAQ,QAAQ,UAAU,cAAc,GAAG,UAAU,UAAU;AAAA,IACxF;AAAA,EACF;AACA,QAAM,aAAa,MAAM,IAAI;AAAA,IAC3B;AAAA,MACE,KAAK;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,MACf,UAAU;AAAA,MACV,UAAU,SAAS,SAAS;AAAA,MAC5B,MAAM,OAAO,UAAU,QAAQ,gBAAgB;AAAA,MAC/C,UAAU,QAAQ,eAAe,IAAI,IAAI,cAAc,MAAM,SAAS,MAAM,EAAE;AAAA,IAChF;AAAA,IACA;AAAA,EACF;AACA,QAAM,YAAY,cAAc;AAChC,QAAM,YAAY,MAAM,eAAe,EAAE;AAAA,IACvC;AAAA,MACE,WAAW,UAAU;AAAA,MACrB,SAAS,MAAM;AAAA,MACf,QAAQ,MAAM;AAAA,MACd,OAAO,MAAM;AAAA,MACb,KAAK;AAAA,MACL,QAAQ,QAAQ,UAAU,cAAc;AAAA,MACxC,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,EACF;AACA,MAAI;AACJ,QAAM,gBAAgB,MAAM,OAAO;AAAA,IACjC;AAAA,MACE,SAAS,MAAM;AAAA,MACf,QAAQ,MAAM;AAAA,MACd,KAAK;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO,OAAO,UAAU,OAAO,MAAM,KAAK;AAAA,IAC5C;AAAA,IACA;AAAA,EACF;AACA,QAAM,UAAU,cAAc;AAAA,IAC5B,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,KAAK;AAAA,IACL,SAAS,OAAO;AAAA,IAChB,OAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,CAAC;AAAA,EAChD,CAAC;AACD,QAAM,MAAM,MAAM,QAAQ,YAAY;AACtC,QAAM,WAAW,MAAM,oBAAoB,QAAQ,aAAa,WAAW,EAAE,KAAK,WAAW,CAAC;AAC9F,QAAM,UAAU,MAAM,OAAO;AAAA,IAC3B;AAAA,MACE,SAAS,MAAM;AAAA,MACf,QAAQ,MAAM;AAAA,MACd;AAAA,MACA,KAAK;AAAA,MACL,MAAM;AAAA,MACN,OAAO,MAAM,MAAM;AAAA,MACnB,OAAO,cAAc;AAAA,MACrB,UAAU,UAAU;AAAA,MACpB,QAAQ,cAAc;AAAA,MACtB,WAAW,UAAU;AAAA,MACrB,SAAS,QAAQ;AAAA,MACjB,QAAQ,cAAc,EAAE,QAAQ,MAAM,QAAQ,UAAU,SAAS,OAAO,CAAC;AAAA,MACzE,KAAK,IAAI;AAAA,MACT,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,IACrD;AAAA,IACA;AAAA,EACF;AACA,UAAQ;AACR,YAAU,OAAO;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,OAAO;AAAA,IACf,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ,UAAU,cAAc;AAAA,IACxC,OAAO,MAAM;AAAA,IACb,QAAQ,cAAc;AAAA,IACtB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,EACxD,CAAC;AACD,QAAM,cAAc,OAAO,kBAAkB,EAAE,MAAM,MAAM,MAAS;AACpE,QAAM,UAAU,cAAc,EAAE,QAAQ,MAAM,QAAQ,KAAK,MAAM,UAAU,WAAW,CAAC;AACvF,SAAO,UAAU,OAAO;AAAA,IACtB;AAAA,IACA,OAAO,EAAE,OAAO,MAAM,OAAO,QAAQ;AAAA,IACrC,QAAQ,EAAE,QAAQ,OAAO,QAAQ,QAAQ;AAAA,IACzC,QAAQ;AAAA,IACR;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,KAAK;AAAA,IACL,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,SAAS;AAAA,MACP,QAAQ;AAAA,MACR;AAAA,MACA,GAAI,QAAQ,EAAE,OAAO,MAAM,MAAM,MAAM,KAAK,EAAE,IAAI,CAAC;AAAA,MACnD,SAAS,MAAM,WAAW,QAAQ;AAAA,IACpC;AAAA,EACF,CAAC;AACH;AAEO,SAAS,MAAM,QAAsB;AAC1C,SAAO,OAAO,QAAkC;AAC9C,QAAI,CAAC,OAAO,GAAI,QAAO;AACvB,YAAQ,MAAM,OAAO,GAAG,GAAG,KAAK,CAAC,YAAY,QAAQ,QAAQ,GAAG;AAAA,EAClE;AACF;AAEO,SAAS,eACd,OACA,UAA8B,CAAC,GACX;AACpB,QAAM,OAAO,MAAM,OAAO,YAAY,KAAK,GAAG,OAAO;AACrD,OAAK,MAAM,MAAM,MAAS;AAC1B,mBAAiB,OAAO,IAAI;AAC5B,SAAO;AACT;",
|
|
4
|
+
"sourcesContent": ["import { attachKernel } from '@ai-ecoverse/slicc-kernel';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { CredentialStore, Provider } from '@earendil-works/pi-ai';\nimport {\n createRegistry,\n type ModelRef,\n type Storage,\n type ToolRegistration,\n} from '@earendil-works/pi-durable';\nimport { CodingTools } from '@earendil-works/pi-durable/tools';\nimport { type Agent, openAgent } from './agent.ts';\nimport { attachChanges, CHANGES_MS } from './changes/index.ts';\nimport {\n codemodeExtension,\n codemodeTool,\n disabledBySettings,\n locateWasm,\n sandboxFactory,\n} from './codemode/index.ts';\nimport { EncryptedCredentialStore } from './credentials.ts';\nimport { attachFreezer } from './freezer/index.ts';\nimport { settingsText, titler } from './freezer/title.ts';\nimport { lateGelatiere } from './gelatiere/index.ts';\nimport { type AgentHost, hostAgent } from './host.ts';\nimport { imageReads } from './images/index.ts';\nimport { createActivity } from './kernel/activity.ts';\nimport type { KernelClient } from './kernel/client.ts';\nimport { HOME, kernelEnvironment, SliccKernelEnv } from './kernel/env.ts';\nimport { processGroups } from './kernel/groups.ts';\nimport { adminOver, type UserClient, type UserClients, userClients } from './kernel/users.ts';\nimport { setupLicks } from './licks/index.ts';\nimport { lickTriage } from './licks/triage.ts';\nimport { setupMcp } from './mcp/index.ts';\nimport { setupMemory } from './memory/index.ts';\nimport { modelsCommand } from './models/index.ts';\nimport { type Transport, transportFetch } from './net.ts';\nimport { sliccPrompt } from './prompt.ts';\nimport { identity } from './scoops/identity.ts';\nimport { type Assets, packageAssets, type ScoopsRuntime, setupScoops } from './scoops/index.ts';\nimport {\n createAgentSettings,\n createSliccModels,\n DEFAULT_MODEL,\n sliccProviders,\n} from './settings.ts';\nimport { RELOAD_MS, setupSkills } from './skills/index.ts';\nimport { setupSprinkles } from './sprinkles/index.ts';\nimport { openOpfsSqliteStorage } from './sqlite.ts';\nimport {\n agentVersion,\n browserConnected,\n commandsOnPath,\n kernelBoot,\n transportName,\n} from './system.ts';\nimport { type KernelPorts, kernelPorts, serveConnections, type WorkerScope } from './worker.ts';\n\nexport interface AgentWorkerScope extends WorkerScope {\n fetch: typeof fetch;\n location: { origin: string };\n}\n\nexport type KernelAttach = (port: MessagePort) => Promise<KernelClient & { transport: Transport }>;\n\nexport interface AgentWorkerOptions {\n model?: ModelRef;\n providers?: Provider[];\n attach?: KernelAttach;\n credentials?: () => Promise<CredentialStore>;\n storage?: () => Promise<Storage>;\n assets?: Assets;\n codemodeWasm?: () => Promise<WebAssembly.Module | undefined>;\n codemodeWorker?: string | URL;\n catalog?: string;\n users?: boolean;\n mcpRedirect?: string;\n}\n\nexport function usersOf(\n admin: MessagePort | undefined,\n wanted: boolean | undefined,\n attach: KernelAttach\n): UserClients | undefined {\n if (!admin) return undefined;\n if (wanted === false) {\n admin.close();\n return undefined;\n }\n return userClients(adminOver(admin), attach as (port: MessagePort) => Promise<UserClient>);\n}\n\nexport const STORE_DIR = '/.slicc';\n\nasync function attached(ports: Promise<KernelPorts>, options: AgentWorkerOptions) {\n const attach: KernelAttach = options.attach ?? attachKernel;\n const { kernel, admin } = await ports;\n const client = await attach(kernel);\n return { client, users: usersOf(admin, options.users, attach) };\n}\n\nfunction privateStore(env: SliccKernelEnv): Promise<unknown> {\n return env.exec(`mkdir -p ${STORE_DIR} && chmod 700 ${STORE_DIR}`, undefined, BACKGROUND_CONTEXT);\n}\n\nasync function codemodeTools(\n client: KernelClient,\n options: AgentWorkerOptions,\n native: typeof fetch,\n extra: Parameters<typeof codemodeTool>[0]['extra']\n) {\n const settings = await client.fs\n .readFile(`${HOME}/.pi/agent/settings.json`)\n .catch(() => undefined);\n if (disabledBySettings(settings === undefined ? undefined : new TextDecoder().decode(settings)))\n return [];\n const sandbox = sandboxFactory(\n options.codemodeWasm ?? (() => locateWasm(native, new URL(import.meta.url))),\n options.codemodeWorker\n );\n return [\n codemodeTool({ declared: CodingTools.tools as readonly ToolRegistration[], sandbox, extra }),\n ];\n}\n\nexport const MCP_CALLBACK = '/oauth/mcp-callback';\n\nasync function start(\n scope: AgentWorkerScope,\n ports: Promise<KernelPorts>,\n options: AgentWorkerOptions\n): Promise<AgentHost> {\n const { client, users } = await attached(ports, options);\n const home = new SliccKernelEnv(client, { cwd: HOME });\n await privateStore(home);\n const native = scope.fetch.bind(scope);\n scope.fetch = transportFetch(client.transport, scope.location.origin, native);\n const facts = {\n version: await agentVersion((url) => native(url)),\n commands: await commandsOnPath(client),\n transport: transportName(client.transport),\n browser: await browserConnected(client),\n };\n const credentials = await (options.credentials ?? (() => EncryptedCredentialStore.open()))();\n const providers = options.providers ?? sliccProviders();\n const models = createSliccModels(credentials, providers);\n const registry = createRegistry();\n registry.install(CodingTools);\n registry.install(sliccPrompt(facts));\n registry.install(\n imageReads(\n (CodingTools.tools as readonly ToolRegistration[]).find(\n (tool) => tool.name === 'read'\n ) as ToolRegistration\n )\n );\n const memory = setupMemory(registry);\n const coding = CodingTools.tools as readonly ToolRegistration[];\n const mcp = setupMcp(registry);\n const codemode = await codemodeTools(client, options, native, mcp.codemode);\n registry.install(codemodeExtension(codemode));\n const licks = setupLicks(registry);\n licks.licks.triage(lickTriage(models));\n const scoops = setupScoops(registry, licks.licks, [...coding, ...codemode, ...memory.tools]);\n const skills = setupSkills(registry, licks.licks);\n const activity = createActivity();\n const groups = processGroups(client);\n const model = options.model ?? DEFAULT_MODEL;\n let opened: Agent | undefined;\n const environment = kernelEnvironment(client, {\n activity,\n groups,\n identify: identity(() => opened?.agents, model),\n });\n const agent = await openAgent({\n models,\n model,\n storage: await (options.storage ?? (() => openOpfsSqliteStorage()))(),\n registry,\n env: environment,\n });\n opened = agent;\n const sources = licks.attach(agent, { env: home, home: HOME, activity });\n await sources.start(BACKGROUND_CONTEXT);\n await sources.boot(\n { version: facts.version, boot: await kernelBoot(client) },\n BACKGROUND_CONTEXT\n );\n const skillsRuntime = await skills.attach(\n { env: home, home: HOME, assets: options.assets ?? packageAssets(), reloadMs: RELOAD_MS },\n BACKGROUND_CONTEXT\n );\n const mcpRuntime = await mcp.attach(\n {\n env: home,\n home: HOME,\n credentials,\n licks: licks.licks,\n fetch: scope.fetch,\n version: facts.version,\n reloadMs: RELOAD_MS,\n codemode: codemode.length > 0,\n cors: client.transport.traits?.crossOrigin === 'cors',\n redirect: options.mcpRedirect ?? new URL(MCP_CALLBACK, scope.location.origin).href,\n },\n BACKGROUND_CONTEXT\n );\n const gelatiere = lateGelatiere();\n const sprinkles = await setupSprinkles().attach(\n {\n intercept: gelatiere.intercept,\n harness: agent.harness,\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n assets: options.assets ?? packageAssets(),\n reloadMs: RELOAD_MS,\n },\n BACKGROUND_CONTEXT\n );\n let roles: ScoopsRuntime | undefined;\n const memoryRuntime = await memory.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n home: HOME,\n reloadMs: RELOAD_MS,\n roles: async (using) => roles?.roles(using),\n },\n BACKGROUND_CONTEXT\n );\n const freezer = attachFreezer({\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n extract: memory.extract,\n title: titler(models, settingsText(home, HOME)),\n });\n licks.licks.guard(freezer.frozenTarget);\n const settings = await createAgentSettings(models, credentials, providers, { mcp: mcpRuntime });\n const runtime = await scoops.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n groups,\n env: home,\n home: HOME,\n alive: alive(client),\n reads: skillsRuntime.dirs,\n sprinkle: sprinkles.command,\n memory: memoryRuntime.command,\n gelatiere: gelatiere.command,\n freezer: freezer.command,\n models: modelsCommand({ agents: agent.agents, settings, catalog: models }),\n mcp: mcp.direct,\n ...(options.assets ? { assets: options.assets } : {}),\n },\n BACKGROUND_CONTEXT\n );\n roles = runtime;\n gelatiere.attach({\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n home: HOME,\n scoops: scoops.scoops,\n roles: runtime.roles,\n assets: options.assets ?? packageAssets(),\n fetch: scope.fetch,\n skills: skillsRuntime.skills,\n ...(options.catalog ? { catalog: options.catalog } : {}),\n });\n await memoryRuntime.reload(BACKGROUND_CONTEXT).catch(() => undefined);\n const changes = attachChanges({ agents: agent.agents, env: home, reloadMs: CHANGES_MS });\n return hostAgent(agent, {\n settings,\n licks: { licks: licks.licks, sources },\n scoops: { scoops: scoops.scoops, runtime },\n skills: skillsRuntime,\n sprinkles,\n memory: memoryRuntime,\n freezer,\n changes,\n mcp: mcpRuntime,\n ...(users ? { users } : {}),\n monitor: {\n kernel: client,\n groups,\n ...(users ? { users: () => users.users.list() } : {}),\n secrets: () => mcpRuntime.secrets(),\n },\n });\n}\n\nexport function alive(client: KernelClient) {\n return async (pid: number): Promise<boolean> => {\n if (!client.ps) return true;\n return (await client.ps()).some((process) => process.pid === pid);\n };\n}\n\nexport function runAgentWorker(\n scope: AgentWorkerScope,\n options: AgentWorkerOptions = {}\n): Promise<AgentHost> {\n const host = start(scope, kernelPorts(scope), options);\n host.catch(() => undefined);\n serveConnections(scope, host);\n return host;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,11 +12,13 @@ export interface WatchEntry {
|
|
|
12
12
|
target: LickTarget;
|
|
13
13
|
message: string;
|
|
14
14
|
debounce: number;
|
|
15
|
+
triage?: string;
|
|
15
16
|
}
|
|
16
17
|
export interface WebhookEntry {
|
|
17
18
|
name: string;
|
|
18
19
|
target: LickTarget;
|
|
19
20
|
message: string;
|
|
21
|
+
triage?: string;
|
|
20
22
|
}
|
|
21
23
|
export interface ConfigError {
|
|
22
24
|
file: string;
|
|
@@ -2,6 +2,7 @@ import type { Context } from '@earendil-works/chord';
|
|
|
2
2
|
import { type Conversation, type Harness } from '@earendil-works/pi-durable';
|
|
3
3
|
import { type Lick, type LickAction, type LickChannel, type LickSeverity, type LickTarget } from './lick.ts';
|
|
4
4
|
import { LICK_STATE_KIND, type LickDecision } from './state.ts';
|
|
5
|
+
import { type Triage } from './triage.ts';
|
|
5
6
|
export interface LickEvent {
|
|
6
7
|
channel: LickChannel;
|
|
7
8
|
source: string;
|
|
@@ -15,6 +16,7 @@ export interface LickEvent {
|
|
|
15
16
|
severity?: LickSeverity;
|
|
16
17
|
eventId?: string;
|
|
17
18
|
at?: number;
|
|
19
|
+
triage?: string;
|
|
18
20
|
}
|
|
19
21
|
type Pending = {
|
|
20
22
|
lick: Lick;
|
|
@@ -47,6 +49,7 @@ export interface Licks {
|
|
|
47
49
|
pending(context: Context): Promise<number>;
|
|
48
50
|
forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;
|
|
49
51
|
guard(check: (target: LickTarget) => Blocked | undefined): void;
|
|
52
|
+
triage(judge: Triage): void;
|
|
50
53
|
}
|
|
51
54
|
export type Blocked = {
|
|
52
55
|
id: string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Context } from '@earendil-works/chord';
|
|
2
|
+
import type { Models } from '@earendil-works/pi-ai/models';
|
|
3
|
+
import type { LickEvent } from './licks.ts';
|
|
4
|
+
export declare const TRIAGE_MODEL = "clef-flash";
|
|
5
|
+
export declare const TRIAGE_SKIP = 0.75;
|
|
6
|
+
export declare const TRIAGE_KIND = "slicc.lick-triage";
|
|
7
|
+
export declare const TRIAGE_LIMIT = 4000;
|
|
8
|
+
export interface Verdict {
|
|
9
|
+
decision: 'act' | 'ignore';
|
|
10
|
+
confidence: number;
|
|
11
|
+
probabilities: Record<string, number>;
|
|
12
|
+
model: string;
|
|
13
|
+
cost: number | null;
|
|
14
|
+
}
|
|
15
|
+
export type Triage = (event: LickEvent, context: Context) => Promise<Verdict | undefined>;
|
|
16
|
+
export declare function clip(text: string): string;
|
|
17
|
+
export declare function skips(verdict: Verdict | undefined): verdict is Verdict;
|
|
18
|
+
export declare function lickTriage(models: Pick<Models, 'getModelOfType' | 'classify'>): Triage;
|
|
19
|
+
export declare function skipEntry(event: LickEvent, verdict: Verdict, now: number): {
|
|
20
|
+
type: 'write';
|
|
21
|
+
requestId: string;
|
|
22
|
+
entry: {
|
|
23
|
+
kind: string;
|
|
24
|
+
data: {
|
|
25
|
+
decision: 'act' | 'ignore';
|
|
26
|
+
confidence: number;
|
|
27
|
+
probabilities: Record<string, number>;
|
|
28
|
+
model: string;
|
|
29
|
+
cost: number | null;
|
|
30
|
+
channel: "bash" | "cherry" | "cron" | "discovery" | "fswatch" | "jshd" | "navigate" | "preview" | "scoop-idle" | "scoop-notify" | "scoop-wait" | "session-reload" | "sprinkle" | "sudo-request" | "upgrade" | "webhook" | "workflow";
|
|
31
|
+
source: string;
|
|
32
|
+
title: string;
|
|
33
|
+
text: string;
|
|
34
|
+
body?: string | undefined;
|
|
35
|
+
items?: string[] | undefined;
|
|
36
|
+
lick: string;
|
|
37
|
+
count: number;
|
|
38
|
+
at: number;
|
|
39
|
+
triage: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
package/package.json
CHANGED