acrylblends 1.0.2 → 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.
Files changed (49) hide show
  1. package/dist/index.js +7 -5
  2. package/dist/index.js.map +1 -1
  3. package/docs/framework/README.md +1 -0
  4. package/docs/framework/agents/index.md +1 -1
  5. package/docs/framework/guides/data.md +12 -0
  6. package/docs/framework/workshop/README.md +381 -0
  7. package/docs/framework/workshop/facilitator.md +108 -0
  8. package/lib/{audit-BXnYsLge.d.ts → audit-D5z9uwOP.d.ts} +14 -1
  9. package/lib/cordis/db-sqlite.js +10 -2
  10. package/lib/cordis/door.d.ts +8 -2
  11. package/lib/cordis/door.js +88 -12
  12. package/lib/cordis/inspect.js +1 -1
  13. package/lib/cordis/kv-json.js +10 -2
  14. package/lib/cordis/node-server.js +1 -1
  15. package/lib/cordis/router-http.d.ts +1 -1
  16. package/lib/cordis/router-http.js +1 -1
  17. package/lib/cordis/router.d.ts +1 -1
  18. package/lib/cordis/router.js +1 -1
  19. package/lib/cordis/state.js +1 -1
  20. package/lib/door/host.d.ts +2 -2
  21. package/lib/door/host.js +1 -1
  22. package/lib/door/page.d.ts +12 -1
  23. package/lib/door/page.js +21 -0
  24. package/lib/door/protocol.d.ts +2 -2
  25. package/lib/door/protocol.js +19 -3
  26. package/lib/{index-D4nVR3wu.d.ts → index-DMOtA-NJ.d.ts} +7 -1
  27. package/lib/node-server.js +1 -1
  28. package/lib/{protocol-BizVt4J6.d.ts → protocol-C37gPePD.d.ts} +9 -2
  29. package/lib/{snapshot-text-DmE0YfeZ.js → snapshot-text-BLz8nlSc.js} +49 -1
  30. package/lib/{src-2Z4mJg0x.js → src-dy3Fk9tA.js} +16 -0
  31. package/lib/{test-websocket-ZXHNHQSU.js → test-websocket-D8WWqE5G.js} +1 -2
  32. package/package.json +1 -1
  33. package/plugins/acryl-db-sqlite/plugin.json +1 -1
  34. package/plugins/acryl-db-sqlite/src/index.ts +8 -1
  35. package/plugins/acryl-door/src/cli/acryl.ts +5 -3
  36. package/plugins/acryl-door/src/host/channel.ts +36 -0
  37. package/plugins/acryl-door/src/host/door.ts +26 -4
  38. package/plugins/acryl-door/src/host/shim.ts +23 -0
  39. package/plugins/acryl-door/src/host/verbs.ts +4 -3
  40. package/plugins/acryl-door/src/index.ts +26 -3
  41. package/plugins/acryl-door/src/page/driver/driver.ts +22 -1
  42. package/plugins/acryl-door/src/page/protocol.ts +13 -2
  43. package/plugins/acryl-kv-json/plugin.json +1 -1
  44. package/plugins/acryl-kv-json/src/index.ts +8 -1
  45. package/plugins/acryl-node-server/src/adapter/request.ts +3 -2
  46. package/plugins/acryl-router/src/service.ts +19 -1
  47. package/plugins/agent-panel/client/main.ts +63 -34
  48. package/plugins/agent-panel/index.ts +115 -25
  49. 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 (running) return c.json({ error: 'a run is already going; wait for it to end' }, 409)
125
- running = true
126
- return streamSSE(c, async (stream) => {
127
- const send = (event: string, data: unknown): Promise<void> =>
128
- stream.writeSSE({ event, data: JSON.stringify(data) })
129
- let changed = false
130
- const off = ctx.on('extensions/changed', (extension: string, action: string) => {
131
- changed = true
132
- void send('change', { extension, action })
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
- audit({ ask: 'start', driver: driverName, characters: message.length })
135
- // Stop in the page closes the stream; that cancels the run (the model call, the tool or the program in flight is dropped).
136
- const cancel = new AbortController()
137
- stream.onAbort(() => cancel.abort())
138
- try {
139
- for await (const { event, data } of driver.start({ message, signal: cancel.signal }))
140
- await send(event, data)
141
- await send('done', { changed })
142
- } catch (error) {
143
- await send('error', { message: error instanceof Error ? error.message : String(error) })
144
- } finally {
145
- off()
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
  }
@@ -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.