acrylblends 1.0.3 → 1.0.4
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/docs/framework/workshop/README.md +3 -3
- package/docs/framework/workshop/facilitator.md +1 -0
- package/lib/{audit-BXnYsLge.d.ts → audit-D5z9uwOP.d.ts} +14 -1
- package/lib/cordis/door.d.ts +8 -2
- package/lib/cordis/door.js +88 -12
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/router-http.d.ts +1 -1
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.js +1 -1
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +12 -1
- package/lib/door/page.js +21 -0
- package/lib/door/protocol.d.ts +2 -2
- package/lib/door/protocol.js +19 -3
- package/lib/{index-D4nVR3wu.d.ts → index-DMOtA-NJ.d.ts} +7 -1
- package/lib/{protocol-BizVt4J6.d.ts → protocol-C37gPePD.d.ts} +9 -2
- package/lib/{snapshot-text-DmE0YfeZ.js → snapshot-text-BLz8nlSc.js} +49 -1
- package/lib/{src-2Z4mJg0x.js → src-dy3Fk9tA.js} +16 -0
- package/package.json +1 -1
- package/plugins/acryl-door/src/cli/acryl.ts +5 -3
- package/plugins/acryl-door/src/host/channel.ts +36 -0
- package/plugins/acryl-door/src/host/door.ts +26 -4
- package/plugins/acryl-door/src/host/shim.ts +23 -0
- package/plugins/acryl-door/src/host/verbs.ts +4 -3
- package/plugins/acryl-door/src/index.ts +26 -3
- package/plugins/acryl-door/src/page/driver/driver.ts +22 -1
- package/plugins/acryl-door/src/page/protocol.ts +13 -2
- package/plugins/acryl-router/src/service.ts +19 -1
- package/plugins/agent-panel/client/main.ts +63 -34
- package/plugins/agent-panel/index.ts +115 -25
- package/shared/DEFAULTS.md +4 -0
|
@@ -18,6 +18,7 @@ import { streamSSE } from '../acryl-router/src/helpers/index.ts'
|
|
|
18
18
|
import { Acryl } from '../acryl-router/src/index.ts'
|
|
19
19
|
import type {} from '../agent/index.ts'
|
|
20
20
|
import type {} from '../extensions/index.ts'
|
|
21
|
+
import type { ChatDriver } from './drivers.ts'
|
|
21
22
|
import { agentDriver, availableHeadless, type HeadlessName, headlessDriver } from './headless.ts'
|
|
22
23
|
import { provideDrivers } from './registry.ts'
|
|
23
24
|
|
|
@@ -70,7 +71,6 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
70
71
|
ctx.effect(() => registry.register(agentDriver(ctx)))
|
|
71
72
|
for (const program of availableHeadless(config.drivers))
|
|
72
73
|
ctx.effect(() => registry.register(headlessDriver(program, root)))
|
|
73
|
-
let running = false
|
|
74
74
|
|
|
75
75
|
const tag = `<script type="module" src="${PREFIX}/panel.js"></script>`
|
|
76
76
|
const app = new Acryl()
|
|
@@ -121,32 +121,122 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
121
121
|
},
|
|
122
122
|
400,
|
|
123
123
|
)
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
if (current !== undefined && !current.ended)
|
|
125
|
+
return c.json({ error: 'a run is already going; wait for it to end' }, 409)
|
|
126
|
+
const run = startRun(driverName, message, driver)
|
|
127
|
+
return follow(c, run, false)
|
|
128
|
+
})
|
|
129
|
+
// Stop: the only thing that cancels a run. A page that goes away (a reload, a move to another page, a closed tab) does not: the agent goes on, and the page picks it up again.
|
|
130
|
+
.post(`${PREFIX}/stop`, (c) => {
|
|
131
|
+
if (!fromOwnPage(c.req.raw.headers) || c.req.header('x-agent-panel-token') !== token)
|
|
132
|
+
return c.json({ error: "only the app's own page may stop a run" }, 403)
|
|
133
|
+
current?.cancel.abort()
|
|
134
|
+
return c.json({ ok: true })
|
|
135
|
+
})
|
|
136
|
+
// The latest run, for a page that just loaded: what was asked, then every event so far, then the rest as it happens (until the run ends).
|
|
137
|
+
.get(`${PREFIX}/run`, (c) => {
|
|
138
|
+
if (!fromOwnPage(c.req.raw.headers) || c.req.header('x-agent-panel-token') !== token)
|
|
139
|
+
return c.json({ error: "only the app's own page may read the run" }, 403)
|
|
140
|
+
return current === undefined ? c.json({ run: null }) : follow(c, current, true)
|
|
141
|
+
})
|
|
142
|
+
/** One run of an agent: its events as they came, and who is following. It lives in the app, not in a page. */
|
|
143
|
+
interface Run {
|
|
144
|
+
readonly id: number
|
|
145
|
+
readonly driver: string
|
|
146
|
+
readonly message: string
|
|
147
|
+
readonly events: Array<{ event: string; data: unknown }>
|
|
148
|
+
readonly followers: Set<(item: { event: string; data: unknown }) => void>
|
|
149
|
+
readonly cancel: AbortController
|
|
150
|
+
ended: boolean
|
|
151
|
+
endedAt?: number
|
|
152
|
+
}
|
|
153
|
+
let current: Run | undefined
|
|
154
|
+
let runs = 0
|
|
155
|
+
const startRun = (driverName: string, message: string, driver: ChatDriver): Run => {
|
|
156
|
+
runs += 1
|
|
157
|
+
const run: Run = {
|
|
158
|
+
id: runs,
|
|
159
|
+
driver: driverName,
|
|
160
|
+
message,
|
|
161
|
+
events: [],
|
|
162
|
+
followers: new Set(),
|
|
163
|
+
cancel: new AbortController(),
|
|
164
|
+
ended: false,
|
|
165
|
+
}
|
|
166
|
+
current = run
|
|
167
|
+
const emit = (event: string, data: unknown): void => {
|
|
168
|
+
const item = { event, data }
|
|
169
|
+
run.events.push(item)
|
|
170
|
+
for (const follower of [...run.followers]) follower(item)
|
|
171
|
+
}
|
|
172
|
+
let changed = false
|
|
173
|
+
const off = ctx.on('extensions/changed', (extension: string, action: string) => {
|
|
174
|
+
changed = true
|
|
175
|
+
emit('change', { extension, action })
|
|
176
|
+
})
|
|
177
|
+
ctx.effect(() => () => run.cancel.abort(), `agent-panel: run ${runs}`)
|
|
178
|
+
audit({ ask: 'start', driver: driverName, characters: message.length })
|
|
179
|
+
void (async () => {
|
|
180
|
+
try {
|
|
181
|
+
for await (const { event, data } of driver.start({ message, signal: run.cancel.signal }))
|
|
182
|
+
emit(event, data)
|
|
183
|
+
emit('done', { changed })
|
|
184
|
+
} catch (error) {
|
|
185
|
+
emit('error', { message: error instanceof Error ? error.message : String(error) })
|
|
186
|
+
} finally {
|
|
187
|
+
off()
|
|
188
|
+
run.ended = true
|
|
189
|
+
run.endedAt = Date.now()
|
|
190
|
+
audit({ ask: 'end', driver: driverName, changed })
|
|
191
|
+
for (const follower of [...run.followers]) follower({ event: 'end', data: {} })
|
|
192
|
+
}
|
|
193
|
+
})()
|
|
194
|
+
return run
|
|
195
|
+
}
|
|
196
|
+
/** Follow a run as server-sent events: a `run` event (what it is, whether it already ended), the events so far, then the new ones until it ends. Following does not keep it alive. */
|
|
197
|
+
const follow = (c: Parameters<typeof streamSSE>[0], run: Run, announce: boolean): Response =>
|
|
198
|
+
streamSSE(c, async (stream) => {
|
|
199
|
+
if (announce)
|
|
200
|
+
await stream.writeSSE({
|
|
201
|
+
event: 'run',
|
|
202
|
+
data: JSON.stringify({
|
|
203
|
+
id: run.id,
|
|
204
|
+
driver: run.driver,
|
|
205
|
+
message: run.message,
|
|
206
|
+
ended: run.ended,
|
|
207
|
+
endedAt: run.endedAt ?? null,
|
|
208
|
+
}),
|
|
133
209
|
})
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
running = false
|
|
147
|
-
audit({ ask: 'end', driver: driverName, changed })
|
|
148
|
-
}
|
|
210
|
+
const queue: Array<{ event: string; data: unknown }> = [...run.events]
|
|
211
|
+
let wake: (() => void) | undefined
|
|
212
|
+
const follower = (item: { event: string; data: unknown }): void => {
|
|
213
|
+
queue.push(item)
|
|
214
|
+
wake?.()
|
|
215
|
+
}
|
|
216
|
+
if (!run.ended) run.followers.add(follower)
|
|
217
|
+
let open = true
|
|
218
|
+
stream.onAbort(() => {
|
|
219
|
+
open = false
|
|
220
|
+
run.followers.delete(follower)
|
|
221
|
+
wake?.()
|
|
149
222
|
})
|
|
223
|
+
try {
|
|
224
|
+
while (open) {
|
|
225
|
+
const item = queue.shift()
|
|
226
|
+
if (item === undefined) {
|
|
227
|
+
if (run.ended) break
|
|
228
|
+
await new Promise<void>((resolve) => {
|
|
229
|
+
wake = resolve
|
|
230
|
+
})
|
|
231
|
+
wake = undefined
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (item.event === 'end') break
|
|
235
|
+
await stream.writeSSE({ event: item.event, data: JSON.stringify(item.data) })
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
run.followers.delete(follower)
|
|
239
|
+
}
|
|
150
240
|
})
|
|
151
241
|
ctx.router.route('/', app)
|
|
152
242
|
}
|
package/shared/DEFAULTS.md
CHANGED
|
@@ -34,11 +34,15 @@ The door is a command in this folder, `.acryl/bin/acryl`, that lets you look at
|
|
|
34
34
|
.acryl/bin/acryl act press --key <Enter|Escape|...>
|
|
35
35
|
.acryl/bin/acryl act scroll [--ref <r>] --direction up|down [--amount <n>]
|
|
36
36
|
.acryl/bin/acryl act wait (--text <t> | --role <role> [--name <n>]) [--gone] [--timeout-ms <ms>]
|
|
37
|
+
.acryl/bin/acryl act reload reload the page; answers once the new page is back
|
|
38
|
+
.acryl/bin/acryl act navigate --path <path> go to another path of the app, e.g. /settings (never another site)
|
|
37
39
|
.acryl/bin/acryl plugin list | add <folder> | remove <name> | reload
|
|
38
40
|
.acryl/bin/acryl tool list | call --tool <name> --args '<json>'
|
|
39
41
|
.acryl/bin/acryl verbs the verb list
|
|
40
42
|
```
|
|
41
43
|
|
|
44
|
+
- **Check your own UI work with `look` and `act`.** After you build or change something people see, open the page and use it the way a person would: look, click through the new feature, look again, and say what you saw. A plugin you add, remove or reload with `plugin add|remove|reload` reloads the open pages by itself and the verb answers once they are back; after you change any other file (`src/`, a page) run `act reload`, then `look`. A page is a path of the app: `act navigate --path /demo`.
|
|
45
|
+
- The door's page script is added to every HTML page in development, so a page you write needs no tag for `look` and `act` to see it.
|
|
42
46
|
- `look` and `act` need the app's page open in a browser (`brief` says `Page: not open` when it is not). Look again before each `act`: refs belong to the latest `look`.
|
|
43
47
|
- A password field is not shown and cannot be typed into through the door; sign in by hand, or test the sign-in with a test.
|
|
44
48
|
- Every verb takes `--json`. The exit code is 0 for done, 1 when the verb failed, 2 for a usage error and 3 when no app is running in this folder.
|