loop-sdk 0.1.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 +591 -0
- package/dist/agent.d.ts +31 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +48 -0
- package/dist/agent.js.map +1 -0
- package/dist/checkpoint.d.ts +16 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +20 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/claude-cli.d.ts +35 -0
- package/dist/claude-cli.d.ts.map +1 -0
- package/dist/claude-cli.js +135 -0
- package/dist/claude-cli.js.map +1 -0
- package/dist/context.d.ts +53 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +95 -0
- package/dist/context.js.map +1 -0
- package/dist/events.d.ts +111 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +53 -0
- package/dist/events.js.map +1 -0
- package/dist/flow.d.ts +36 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +62 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +37 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +64 -0
- package/dist/logger.js.map +1 -0
- package/dist/loop.d.ts +199 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +403 -0
- package/dist/loop.js.map +1 -0
- package/dist/loopfile.d.ts +82 -0
- package/dist/loopfile.d.ts.map +1 -0
- package/dist/loopfile.js +235 -0
- package/dist/loopfile.js.map +1 -0
- package/dist/mcp/server.d.ts +26 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +160 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/notify.d.ts +43 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +68 -0
- package/dist/notify.js.map +1 -0
- package/dist/plugins/retry.d.ts +41 -0
- package/dist/plugins/retry.d.ts.map +1 -0
- package/dist/plugins/retry.js +53 -0
- package/dist/plugins/retry.js.map +1 -0
- package/dist/providers/playwright.d.ts +38 -0
- package/dist/providers/playwright.d.ts.map +1 -0
- package/dist/providers/playwright.js +155 -0
- package/dist/providers/playwright.js.map +1 -0
- package/dist/session.d.ts +43 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +26 -0
- package/dist/session.js.map +1 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# loop-sdk
|
|
2
|
+
|
|
3
|
+
Framework for building long-running agentic loops. Steps can be browser actions, AI agent calls, data operations, or anything else — composed into named, resumable sequences with structured logging, checkpointing, events, and a plugin system.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install loop-sdk
|
|
9
|
+
npm install @ai-sdk/anthropic # or @ai-sdk/openai, @ai-sdk/google, etc.
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { Loop } from 'loop-sdk'
|
|
16
|
+
import { claudeCli } from 'loop-sdk'
|
|
17
|
+
|
|
18
|
+
const loop = new Loop('research')
|
|
19
|
+
|
|
20
|
+
loop.step('gather', async (ctx) => {
|
|
21
|
+
const result = await claudeCli(ctx, 'List the top 5 JS frameworks in 2026.')
|
|
22
|
+
ctx.set('frameworks', result.text)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
loop.step('summarize', async (ctx) => {
|
|
26
|
+
const list = ctx.get('frameworks')
|
|
27
|
+
await claudeCli(ctx, `Summarize this list in one sentence: ${list}`)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
await loop.run({ session: null })
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Concepts
|
|
36
|
+
|
|
37
|
+
### Loop
|
|
38
|
+
|
|
39
|
+
A named sequence of steps. Steps are plain async functions that receive a `ctx` (Context).
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const loop = new Loop('my-loop')
|
|
43
|
+
|
|
44
|
+
loop.step('step-name', async (ctx) => {
|
|
45
|
+
// do work
|
|
46
|
+
}, {
|
|
47
|
+
retries: 3, // retry this step up to 3 times on failure
|
|
48
|
+
retryDelay: 1000, // wait 1s between retries
|
|
49
|
+
retryBackoff: 'exponential', // 'flat' | 'linear' | 'exponential'
|
|
50
|
+
skipOnError: false, // skip this step instead of failing the loop
|
|
51
|
+
onError: async (err, ctx) => {
|
|
52
|
+
// fallback: return a value to use as the step result, or re-throw to fail
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
await loop.run({ session })
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**`loop.run(opts)`** options:
|
|
60
|
+
|
|
61
|
+
| Option | Type | Description |
|
|
62
|
+
|--------|------|-------------|
|
|
63
|
+
| `session` | `Session \| null` | The session to run against. |
|
|
64
|
+
| `vars` | `object` | Initial variables available as `ctx.vars`. |
|
|
65
|
+
| `logDir` | `string` | Directory to write a JSON run log. |
|
|
66
|
+
| `startAt` | `number` | Skip steps before this 1-based index. |
|
|
67
|
+
| `stopAt` | `number` | Stop after this 1-based index. |
|
|
68
|
+
| `signal` | `AbortSignal` | External abort signal to cancel the run. |
|
|
69
|
+
| `checkpointFile` | `string` | Path to write checkpoints after each step. |
|
|
70
|
+
| `resumeFrom` | `string` | Checkpoint file to resume from (skips completed steps). |
|
|
71
|
+
| `keepCheckpointOnSuccess` | `boolean` | Keep the checkpoint file after a successful run (default: `false`). |
|
|
72
|
+
| `onError` | `(err, ctx, failedStep) => Promise<void>` | Called when the loop fails. |
|
|
73
|
+
|
|
74
|
+
Returns a run log object: `{ loop, session, status, steps, startedAt, finishedAt }`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### Parallel steps
|
|
79
|
+
|
|
80
|
+
Run multiple step functions concurrently within a single named step:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
loop.parallel('fetch-all', [
|
|
84
|
+
async (ctx) => { ctx.set('a', await fetch('https://a.com')) },
|
|
85
|
+
async (ctx) => { ctx.set('b', await fetch('https://b.com')) },
|
|
86
|
+
async (ctx) => { ctx.set('c', await fetch('https://c.com')) },
|
|
87
|
+
])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
All functions in a parallel group share the same context and run via `Promise.all`.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### Background execution
|
|
95
|
+
|
|
96
|
+
`runBackground()` starts the loop in the background and immediately returns a `RunHandle`.
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const handle = loop.runBackground({ session })
|
|
100
|
+
|
|
101
|
+
console.log(handle.id) // unique run ID
|
|
102
|
+
console.log(handle.status) // 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'
|
|
103
|
+
|
|
104
|
+
const log = await handle.wait() // resolves when the loop finishes
|
|
105
|
+
handle.cancel() // cancel the loop
|
|
106
|
+
handle.pause() // pause after the current step
|
|
107
|
+
const newHandle = handle.resume() // resume from where it paused
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Checkpoints are auto-written under `.loop/<id>.checkpoint.json` during background runs. Pause and resume use the checkpoint file automatically.
|
|
111
|
+
|
|
112
|
+
**Run multiple loops in parallel:**
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
const handles = await Loop.runAll([
|
|
116
|
+
{ loop: loopA, opts: { session } },
|
|
117
|
+
{ loop: loopB, opts: { session } },
|
|
118
|
+
{ loop: loopC, opts: { session } },
|
|
119
|
+
])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### Checkpointing
|
|
125
|
+
|
|
126
|
+
Checkpoints save progress after each step so a loop can be resumed after a crash or pause.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
await loop.run({
|
|
130
|
+
session,
|
|
131
|
+
checkpointFile: '.loop/my-run.checkpoint.json',
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Later, resume from where it left off:
|
|
135
|
+
await loop.run({
|
|
136
|
+
session,
|
|
137
|
+
resumeFrom: '.loop/my-run.checkpoint.json',
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Helpers:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { checkpointExists, deleteCheckpoint } from 'loop-sdk'
|
|
145
|
+
|
|
146
|
+
if (await checkpointExists('.loop/my-run.checkpoint.json')) {
|
|
147
|
+
// resume
|
|
148
|
+
}
|
|
149
|
+
await deleteCheckpoint('.loop/my-run.checkpoint.json')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Checkpoint files are auto-deleted on successful completion unless `keepCheckpointOnSuccess: true`.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### Events
|
|
157
|
+
|
|
158
|
+
Loops emit typed events throughout their lifecycle. Listen with `loop.on()` / `loop.off()`.
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
loop.on('loop:start', ({ totalSteps }) => {
|
|
162
|
+
console.log(`Starting with ${totalSteps} steps`)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
loop.on('loop:complete', ({ status, durationMs, stepsCompleted }) => {
|
|
166
|
+
console.log(`Finished: ${status} in ${durationMs}ms`)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
loop.on('step:start', ({ step, index }) => { })
|
|
170
|
+
loop.on('step:complete', ({ step, durationMs }) => { })
|
|
171
|
+
loop.on('step:error', ({ step, error, attempt }) => { })
|
|
172
|
+
loop.on('step:skip', ({ step, reason }) => { }) // reason: 'checkpoint' | 'range' | 'error'
|
|
173
|
+
loop.on('step:retry', ({ step, attempt, delay }) => { })
|
|
174
|
+
loop.on('checkpoint:saved', ({ file, completedSteps }) => { })
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Custom events** — emit from inside a step and listen anywhere:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
loop.step('draft', async (ctx) => {
|
|
181
|
+
ctx.set('draft', '...')
|
|
182
|
+
ctx.emit('draft:ready', { preview: ctx.get('draft') })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
loop.on('draft:ready', async ({ preview }) => {
|
|
186
|
+
// approve, notify, update UI, etc.
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Context
|
|
193
|
+
|
|
194
|
+
Passed to every step. Carries shared state, per-iteration variables, and browser shortcuts.
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
// State — shared across all steps in a run
|
|
198
|
+
ctx.set('key', value)
|
|
199
|
+
ctx.get('key')
|
|
200
|
+
ctx.has('key')
|
|
201
|
+
ctx.snapshot() // plain object of all state
|
|
202
|
+
|
|
203
|
+
// Variables — injected per-iteration by each()
|
|
204
|
+
ctx.vars.item
|
|
205
|
+
ctx.vars.subtype
|
|
206
|
+
|
|
207
|
+
// Emit a custom event
|
|
208
|
+
ctx.emit('my:event', { data: 123 })
|
|
209
|
+
|
|
210
|
+
// Logging
|
|
211
|
+
ctx.log('message', { optional: 'data' })
|
|
212
|
+
|
|
213
|
+
// Browser shortcuts (delegate to ctx.session)
|
|
214
|
+
ctx.navigate(url)
|
|
215
|
+
ctx.click({ selector, text, x, y })
|
|
216
|
+
ctx.type(text)
|
|
217
|
+
ctx.key('Enter')
|
|
218
|
+
ctx.scroll({ deltaY: 300 })
|
|
219
|
+
ctx.screenshot() // returns Buffer
|
|
220
|
+
|
|
221
|
+
// Direct session access
|
|
222
|
+
ctx.session.mcp('browser_evaluate', { function: '() => document.title' })
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### Session
|
|
228
|
+
|
|
229
|
+
Abstract interface that all browser providers implement. Extend it to add your own:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
import { Session } from 'loop-sdk'
|
|
233
|
+
|
|
234
|
+
export class MySession extends Session {
|
|
235
|
+
async navigate(url) { /* ... */ }
|
|
236
|
+
async click(opts) { /* ... */ }
|
|
237
|
+
async type(text) { /* ... */ }
|
|
238
|
+
async key(key) { /* ... */ }
|
|
239
|
+
async scroll(opts) { /* ... */ }
|
|
240
|
+
async screenshot() { /* returns Buffer */ }
|
|
241
|
+
async destroy() { /* ... */ }
|
|
242
|
+
|
|
243
|
+
// Optional: expose an MCP URL so agent() / claudeCli() gets browser tool access
|
|
244
|
+
get mcpUrl() { return `http://my-daemon/sessions/${this.id}/mcp` }
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### agent()
|
|
251
|
+
|
|
252
|
+
Run any AI model as a loop step. Uses the [Vercel AI SDK](https://sdk.vercel.ai) so every `@ai-sdk/*` provider works.
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
import { agent } from 'loop-sdk'
|
|
256
|
+
import { anthropic } from '@ai-sdk/anthropic'
|
|
257
|
+
|
|
258
|
+
loop.step('summarize', async (ctx) => {
|
|
259
|
+
const result = await agent(ctx, 'Summarize the main content of this page.', {
|
|
260
|
+
model: anthropic('claude-opus-4-8'),
|
|
261
|
+
screenshot: true, // attach a screenshot before calling the model
|
|
262
|
+
maxSteps: 50,
|
|
263
|
+
system: 'You are a research assistant.',
|
|
264
|
+
})
|
|
265
|
+
ctx.set('summary', result.text)
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
If `ctx.session.mcpUrl` is set, the model automatically gets the session's browser tools via MCP.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### claudeCli()
|
|
274
|
+
|
|
275
|
+
Spawn the `claude` CLI subprocess (`claude -p`) as a loop step. Use this when you want Claude Code's built-in tool-use loop, retry logic, and MCP permission model.
|
|
276
|
+
|
|
277
|
+
Requires `claude` to be installed and on PATH.
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
import { claudeCli } from 'loop-sdk'
|
|
281
|
+
|
|
282
|
+
loop.step('fill', async (ctx) => {
|
|
283
|
+
const result = await claudeCli(ctx, 'Fill out the visible form fields with test data.', {
|
|
284
|
+
screenshot: true,
|
|
285
|
+
model: 'claude-opus-4-8',
|
|
286
|
+
timeout: 240_000,
|
|
287
|
+
})
|
|
288
|
+
ctx.set('output', result.text)
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**`agent()` vs `claudeCli()`:**
|
|
293
|
+
|
|
294
|
+
| | `agent()` | `claudeCli()` |
|
|
295
|
+
|-|-----------|---------------|
|
|
296
|
+
| Provider | Any `@ai-sdk/*` model | Claude CLI only |
|
|
297
|
+
| Tool-use loop | Managed by Vercel AI SDK | Managed by Claude Code CLI |
|
|
298
|
+
| Best for | Multi-provider, programmatic control | Delegating full control to Claude |
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### each()
|
|
303
|
+
|
|
304
|
+
Iterate over a list of items. Each item gets its own forked context with per-item vars.
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
import { each } from 'loop-sdk'
|
|
308
|
+
|
|
309
|
+
loop.step('process-pages', async (ctx) => {
|
|
310
|
+
const pages = ['https://a.com', 'https://b.com', 'https://c.com']
|
|
311
|
+
|
|
312
|
+
await each(ctx, pages, async (ctx) => {
|
|
313
|
+
await ctx.navigate(ctx.vars.item)
|
|
314
|
+
await claudeCli(ctx, 'Summarize this page.')
|
|
315
|
+
}, { continueOnError: true })
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Items can carry subtypes:
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
const items = [
|
|
323
|
+
{ type: 'Category A', subtypes: ['Sub 1', 'Sub 2'] },
|
|
324
|
+
'Category B',
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
await each(ctx, items, async (ctx) => {
|
|
328
|
+
console.log(ctx.vars.item, ctx.vars.subtype)
|
|
329
|
+
// → 'Category A', 'Sub 1'
|
|
330
|
+
// → 'Category A', 'Sub 2'
|
|
331
|
+
// → 'Category B', undefined
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### sub()
|
|
338
|
+
|
|
339
|
+
Run a `Loop` as a sub-step, sharing the current session and state.
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
import { sub } from 'loop-sdk'
|
|
343
|
+
|
|
344
|
+
const loginLoop = new Loop('login')
|
|
345
|
+
loginLoop.step('authenticate', async (ctx) => {
|
|
346
|
+
await claudeCli(ctx, `Log in as ${ctx.vars.username}.`)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
loop.step('login', async (ctx) => {
|
|
350
|
+
await sub(ctx, loginLoop, { username: 'admin' })
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
State written inside the sub-loop is visible in the parent loop (shared `ctx` state Map).
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
### Plugins
|
|
359
|
+
|
|
360
|
+
Plugins hook into the loop lifecycle. Return `true` from `onStepError` to retry the failed step.
|
|
361
|
+
|
|
362
|
+
```js
|
|
363
|
+
const crashRecovery = {
|
|
364
|
+
name: 'crash-recovery',
|
|
365
|
+
hooks: {
|
|
366
|
+
onStepError: async (err, step, ctx) => {
|
|
367
|
+
if (!err.message.includes('context was lost')) return false
|
|
368
|
+
await ctx.session.recreate()
|
|
369
|
+
return true // retry the step
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
loop.use(crashRecovery)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### RetryPlugin
|
|
378
|
+
|
|
379
|
+
Built-in plugin for automatic step retries with backoff:
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
import { RetryPlugin } from 'loop-sdk'
|
|
383
|
+
|
|
384
|
+
loop.use(RetryPlugin({
|
|
385
|
+
attempts: 3,
|
|
386
|
+
delay: 500,
|
|
387
|
+
backoff: 'exponential', // 'flat' | 'linear' | 'exponential'
|
|
388
|
+
retryIf: (err) => !err.message.includes('auth'), // optional filter
|
|
389
|
+
}))
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### macOS Notifications
|
|
395
|
+
|
|
396
|
+
Send native macOS notifications from a loop:
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
import { notify, notifyOn } from 'loop-sdk'
|
|
400
|
+
|
|
401
|
+
// One-off notification
|
|
402
|
+
notify('Step completed!', { title: 'My Loop', sound: true })
|
|
403
|
+
|
|
404
|
+
// Auto-wire lifecycle notifications to a loop
|
|
405
|
+
notifyOn(loop, {
|
|
406
|
+
onStart: true,
|
|
407
|
+
onComplete: true,
|
|
408
|
+
onError: true,
|
|
409
|
+
onStepError: false,
|
|
410
|
+
title: 'My Loop',
|
|
411
|
+
sound: true,
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
No-op on non-macOS platforms.
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## .loop files
|
|
420
|
+
|
|
421
|
+
`.loop` files let you define loops in a simple text format. An AI agent can generate them via MCP; your app parses and runs them.
|
|
422
|
+
|
|
423
|
+
**Format:**
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
---
|
|
427
|
+
name: my-loop
|
|
428
|
+
description: Does something useful
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## step-one
|
|
432
|
+
|
|
433
|
+
action: claudeCli
|
|
434
|
+
prompt: Write a haiku about the ocean.
|
|
435
|
+
|
|
436
|
+
## step-two
|
|
437
|
+
|
|
438
|
+
action: log
|
|
439
|
+
message: Done! Output was {{step-one}}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Supported actions:**
|
|
443
|
+
|
|
444
|
+
| Action | Description |
|
|
445
|
+
|--------|-------------|
|
|
446
|
+
| `claudeCli` | Run `claude -p` with `prompt`. |
|
|
447
|
+
| `navigate` | Navigate to `url`. |
|
|
448
|
+
| `screenshot` | Take a screenshot. |
|
|
449
|
+
| `log` | Print `message` to stdout. |
|
|
450
|
+
| `parallel` | Run multiple steps concurrently (nested `steps` list). |
|
|
451
|
+
| `sub` | Run another `.loop` file (`file` path). |
|
|
452
|
+
| `each` | Iterate over `items` (array, context key, or `claudeCli` prompt) and run `steps` or `file` per item. |
|
|
453
|
+
|
|
454
|
+
**`{{step-name}}` interpolation** — reference the output of any previous step by name.
|
|
455
|
+
|
|
456
|
+
**Running a `.loop` file:**
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
import { loadLoop, runFile, runFileBackground } from 'loop-sdk'
|
|
460
|
+
|
|
461
|
+
// Run synchronously (returns run log)
|
|
462
|
+
await runFile('./my-loop.loop', { session: null })
|
|
463
|
+
|
|
464
|
+
// Run in the background (returns RunHandle)
|
|
465
|
+
const handle = await runFileBackground('./my-loop.loop', { session: null })
|
|
466
|
+
await handle.wait()
|
|
467
|
+
|
|
468
|
+
// Parse and build a Loop instance manually
|
|
469
|
+
const loop = await loadLoop('./my-loop.loop')
|
|
470
|
+
await loop.run({ session: null })
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## MCP server
|
|
476
|
+
|
|
477
|
+
The `loop-mcp` server lets AI agents create and manage `.loop` files via MCP tools.
|
|
478
|
+
|
|
479
|
+
**Start it:**
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
npx loop-mcp
|
|
483
|
+
# or
|
|
484
|
+
npm run mcp
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Available tools:**
|
|
488
|
+
|
|
489
|
+
| Tool | Description |
|
|
490
|
+
|------|-------------|
|
|
491
|
+
| `write_loop` | Write (or overwrite) a `.loop` file. Validates before writing. |
|
|
492
|
+
| `read_loop` | Read the contents of a `.loop` file. |
|
|
493
|
+
| `list_loops` | List all `.loop` files with step summaries. |
|
|
494
|
+
| `validate_loop` | Validate a `.loop` file without writing it. |
|
|
495
|
+
|
|
496
|
+
Loop files are stored in the directory set by `LOOP_DIR` (default: `.loop`).
|
|
497
|
+
|
|
498
|
+
**Claude Desktop config:**
|
|
499
|
+
|
|
500
|
+
```json
|
|
501
|
+
{
|
|
502
|
+
"mcpServers": {
|
|
503
|
+
"loop": {
|
|
504
|
+
"command": "npx",
|
|
505
|
+
"args": ["loop-mcp"]
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## PlaywrightSession
|
|
514
|
+
|
|
515
|
+
The built-in browser provider. Wraps the aria-playwright daemon.
|
|
516
|
+
|
|
517
|
+
```js
|
|
518
|
+
import { PlaywrightSession } from 'loop-sdk/playwright'
|
|
519
|
+
|
|
520
|
+
const session = new PlaywrightSession('session-id', {
|
|
521
|
+
daemon: 'http://localhost:4848', // default
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
await session.ensure() // create if it doesn't exist
|
|
525
|
+
await session.recreate() // destroy + re-create (after a crash)
|
|
526
|
+
await session.destroy() // tear down
|
|
527
|
+
|
|
528
|
+
// Extra methods:
|
|
529
|
+
await session.evaluate('() => document.title')
|
|
530
|
+
await session.currentUrl()
|
|
531
|
+
await session.mcp('browser_snapshot', {})
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Run log
|
|
537
|
+
|
|
538
|
+
When `logDir` is passed to `loop.run()`, a JSON log is written incrementally:
|
|
539
|
+
|
|
540
|
+
```json
|
|
541
|
+
{
|
|
542
|
+
"loop": "my-loop",
|
|
543
|
+
"session": "my-session",
|
|
544
|
+
"status": "completed",
|
|
545
|
+
"startedAt": "2026-06-28T10:00:00.000Z",
|
|
546
|
+
"finishedAt": "2026-06-28T10:01:23.456Z",
|
|
547
|
+
"steps": [
|
|
548
|
+
{ "name": "gather", "status": "ok" },
|
|
549
|
+
{ "name": "summarize", "status": "ok" }
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Possible statuses: `completed`, `failed`, `running` (if the process was killed mid-run), `cancelled`.
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## Writing a custom provider
|
|
559
|
+
|
|
560
|
+
Any class that extends `Session` works as a provider:
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
import { Session } from 'loop-sdk'
|
|
564
|
+
import puppeteer from 'puppeteer'
|
|
565
|
+
|
|
566
|
+
export class PuppeteerSession extends Session {
|
|
567
|
+
constructor(id) {
|
|
568
|
+
super(id)
|
|
569
|
+
this._browser = null
|
|
570
|
+
this._page = null
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async ensure() {
|
|
574
|
+
this._browser = await puppeteer.launch()
|
|
575
|
+
this._page = await this._browser.newPage()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async navigate(url) { await this._page.goto(url) }
|
|
579
|
+
async click({ selector }) { await this._page.click(selector) }
|
|
580
|
+
async type(text) { await this._page.keyboard.type(text) }
|
|
581
|
+
async key(key) { await this._page.keyboard.press(key) }
|
|
582
|
+
async scroll({ deltaY }) { await this._page.evaluate(dy => window.scrollBy(0, dy), deltaY) }
|
|
583
|
+
async screenshot() { return this._page.screenshot({ type: 'jpeg' }) }
|
|
584
|
+
async destroy() { await this._browser?.close() }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const session = new PuppeteerSession('puppeteer-session')
|
|
588
|
+
await session.ensure()
|
|
589
|
+
await loop.run({ session })
|
|
590
|
+
await session.destroy()
|
|
591
|
+
```
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LanguageModel } from 'ai';
|
|
2
|
+
import type { Context } from './context.js';
|
|
3
|
+
export interface AgentOptions {
|
|
4
|
+
model: LanguageModel;
|
|
5
|
+
system?: string;
|
|
6
|
+
maxSteps?: number;
|
|
7
|
+
/** Attach a screenshot of the current browser state before calling the model. */
|
|
8
|
+
screenshot?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface AgentResult {
|
|
11
|
+
ok: true;
|
|
12
|
+
text: string;
|
|
13
|
+
usage?: {
|
|
14
|
+
totalTokens?: number;
|
|
15
|
+
promptTokens?: number;
|
|
16
|
+
completionTokens?: number;
|
|
17
|
+
};
|
|
18
|
+
steps?: unknown[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* agent — run any AI model as a loop step via the Vercel AI SDK.
|
|
22
|
+
*
|
|
23
|
+
* Pass any @ai-sdk/* model — provider switching is one argument change.
|
|
24
|
+
* If ctx.session.mcpUrl is set, the model gets live browser tool access via MCP.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { anthropic } from '@ai-sdk/anthropic'
|
|
28
|
+
* await agent(ctx, 'Summarize the page.', { model: anthropic('claude-opus-4-8') })
|
|
29
|
+
*/
|
|
30
|
+
export declare function agent(ctx: Context, prompt: string, opts: AgentOptions): Promise<AgentResult>;
|
|
31
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,aAAa,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAA;IACR,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAClF,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA0ClG"}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { generateText, experimental_createMCPClient } from 'ai';
|
|
2
|
+
/**
|
|
3
|
+
* agent — run any AI model as a loop step via the Vercel AI SDK.
|
|
4
|
+
*
|
|
5
|
+
* Pass any @ai-sdk/* model — provider switching is one argument change.
|
|
6
|
+
* If ctx.session.mcpUrl is set, the model gets live browser tool access via MCP.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { anthropic } from '@ai-sdk/anthropic'
|
|
10
|
+
* await agent(ctx, 'Summarize the page.', { model: anthropic('claude-opus-4-8') })
|
|
11
|
+
*/
|
|
12
|
+
export async function agent(ctx, prompt, opts) {
|
|
13
|
+
const { model, system, maxSteps = 50, screenshot = false } = opts;
|
|
14
|
+
let userContent = prompt;
|
|
15
|
+
if (screenshot && ctx.session?.screenshot) {
|
|
16
|
+
const imgBytes = await ctx.session.screenshot();
|
|
17
|
+
userContent = [
|
|
18
|
+
{ type: 'text', text: prompt },
|
|
19
|
+
{ type: 'image', image: imgBytes, mimeType: 'image/jpeg' },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
let tools;
|
|
23
|
+
let mcpClient = null;
|
|
24
|
+
if (ctx.session?.mcpUrl) {
|
|
25
|
+
mcpClient = await experimental_createMCPClient({
|
|
26
|
+
transport: { type: 'sse', url: ctx.session.mcpUrl },
|
|
27
|
+
});
|
|
28
|
+
tools = await mcpClient.tools();
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const result = await generateText({
|
|
32
|
+
model,
|
|
33
|
+
system,
|
|
34
|
+
tools,
|
|
35
|
+
maxSteps,
|
|
36
|
+
messages: [{ role: 'user', content: userContent }],
|
|
37
|
+
});
|
|
38
|
+
ctx.log(`agent: ${result.usage?.totalTokens ?? '?'} tokens`, {
|
|
39
|
+
steps: result.steps?.length,
|
|
40
|
+
finishReason: result.finishReason,
|
|
41
|
+
});
|
|
42
|
+
return { ok: true, text: result.text, usage: result.usage, steps: result.steps };
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await mcpClient?.close().catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,4BAA4B,EAAE,MAAM,IAAI,CAAA;AAmB/D;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,GAAY,EAAE,MAAc,EAAE,IAAkB;IAC1E,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAG,EAAE,EAAE,UAAU,GAAG,KAAK,EAAE,GAAG,IAAI,CAAA;IAGjE,IAAI,WAAW,GAAmB,MAAM,CAAA;IAExC,IAAI,UAAU,IAAI,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;QAC1C,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,CAAA;QAC/C,WAAW,GAAG;YACZ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;YAC9B,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE;SAC3D,CAAA;IACH,CAAC;IAED,IAAI,KAAyG,CAAA;IAC7G,IAAI,SAAS,GAAoE,IAAI,CAAA;IAErF,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QACxB,SAAS,GAAG,MAAM,4BAA4B,CAAC;YAC7C,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE;SACpD,CAAC,CAAA;QACF,KAAK,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;IACjC,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;YAChC,KAAK;YACL,MAAM;YACN,KAAK;YACL,QAAQ;YACR,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;SACnD,CAAC,CAAA;QAEF,GAAG,CAAC,GAAG,CAAC,UAAU,MAAM,CAAC,KAAK,EAAE,WAAW,IAAI,GAAG,SAAS,EAAE;YAC3D,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM;YAC3B,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,CAAC,CAAA;QAEF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;IAClF,CAAC;YAAS,CAAC;QACT,MAAM,SAAS,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAC1C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Checkpoint {
|
|
2
|
+
loop: string;
|
|
3
|
+
session: string;
|
|
4
|
+
savedAt: string;
|
|
5
|
+
/** Names of every step that completed successfully before this checkpoint. */
|
|
6
|
+
completedSteps: string[];
|
|
7
|
+
/** Index of the last completed step (0-based). -1 if no steps completed yet. */
|
|
8
|
+
lastCompletedIndex: number;
|
|
9
|
+
/** Full ctx.snapshot() at the time of the checkpoint. */
|
|
10
|
+
state: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export declare function writeCheckpoint(file: string, data: Checkpoint): void;
|
|
13
|
+
export declare function readCheckpoint(file: string): Checkpoint;
|
|
14
|
+
export declare function checkpointExists(file: string): boolean;
|
|
15
|
+
export declare function deleteCheckpoint(file: string): void;
|
|
16
|
+
//# sourceMappingURL=checkpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint.d.ts","sourceRoot":"","sources":["../src/checkpoint.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,gFAAgF;IAChF,kBAAkB,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAGpE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAGvD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEnD"}
|