@personaai/runtime 0.5.1 → 0.5.2
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 +421 -416
- package/dist/index.cjs +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +71 -71
package/README.md
CHANGED
|
@@ -1,416 +1,421 @@
|
|
|
1
|
-
# @personaai/runtime
|
|
2
|
-
|
|
3
|
-
Framework-agnostic runtime engine for [Persona](https://persona.hasanraiyan.me). This is the
|
|
4
|
-
shared engine every framework adapter (`@personaai/express`, `@personaai/nextjs`, ...) is meant
|
|
5
|
-
to be a thin translation layer over — see
|
|
6
|
-
[the SDK Ecosystem plan](https://github.com/hasanraiyan/agent-marketplace/blob/feat/ai/product-research/11-sdk-new/package-ecosystem.md).
|
|
7
|
-
|
|
8
|
-
**v0.5.**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
| `POST` | `/
|
|
86
|
-
| `GET` | `/
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `GET` | `/
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `GET` | `/
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `DELETE` | `/
|
|
100
|
-
| `GET` | `/
|
|
101
|
-
| `GET` | `/
|
|
102
|
-
| `
|
|
103
|
-
| `DELETE` | `/
|
|
104
|
-
| `
|
|
105
|
-
| `GET` | `/
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `POST` | `/mcps
|
|
131
|
-
| `GET` | `/mcps/:id
|
|
132
|
-
| `POST` | `/mcps
|
|
133
|
-
| `GET
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `POST` | `/
|
|
137
|
-
| `GET` | `/providers
|
|
138
|
-
| `GET` | `/providers/:id
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `GET` | `/
|
|
143
|
-
| `GET`/`POST` | `/
|
|
144
|
-
| `GET`/`PATCH`/`DELETE` | `/
|
|
145
|
-
| `POST` | `/
|
|
146
|
-
| `GET` | `/
|
|
147
|
-
| `POST` | `/knowledge
|
|
148
|
-
| `GET` | `/knowledge/:id
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `GET
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `GET` | `/
|
|
156
|
-
| `
|
|
157
|
-
| `GET` | `/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
- **
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
1
|
+
# @personaai/runtime
|
|
2
|
+
|
|
3
|
+
Framework-agnostic runtime engine for [Persona](https://persona.hasanraiyan.me). This is the
|
|
4
|
+
shared engine every framework adapter (`@personaai/express`, `@personaai/nextjs`, ...) is meant
|
|
5
|
+
to be a thin translation layer over — see
|
|
6
|
+
[the SDK Ecosystem plan](https://github.com/hasanraiyan/agent-marketplace/blob/feat/ai/product-research/11-sdk-new/package-ecosystem.md).
|
|
7
|
+
|
|
8
|
+
**v0.5.1.** The first framework adapter has shipped: [`@personaai/express` v0.1.0](https://persona.hasanraiyan.me/guides/express/quickstart)
|
|
9
|
+
(Wave 3 of the
|
|
10
|
+
[SDK Ecosystem plan](https://github.com/hasanraiyan/agent-marketplace/blob/feat/ai/product-research/11-sdk-new/package-ecosystem.md))
|
|
11
|
+
is published and mounts this runtime as an Express Router. For non-Express hosts, see
|
|
12
|
+
[Quickstart](#quickstart) for how to run it directly against raw Node `http`, and
|
|
13
|
+
[Not yet implemented](#not-yet-implemented) for what's missing before it's a complete Level 2
|
|
14
|
+
runtime.
|
|
15
|
+
|
|
16
|
+
**Server-side only.** The credential this runtime holds is a server-side secret — never bundle
|
|
17
|
+
this into a browser app.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npm install @personaai/runtime
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The user resolver contract
|
|
26
|
+
|
|
27
|
+
Persona never authenticates users. The runtime receives a request and asks *you* who it's from:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createRuntime } from '@personaai/runtime';
|
|
31
|
+
|
|
32
|
+
const runtime = createRuntime({
|
|
33
|
+
baseUrl: process.env.PERSONA_BASE_URL!,
|
|
34
|
+
credential: process.env.PERSONA_CREDENTIAL!,
|
|
35
|
+
resolveUser: async (request) => {
|
|
36
|
+
// Your auth, your rules — Clerk, a JWT, a session cookie, whatever you
|
|
37
|
+
// already use. Return the resolved external user id, or null/throw if
|
|
38
|
+
// the request isn't authenticated (the runtime responds 401 either way).
|
|
39
|
+
return getUserIdFromSession(request.headers['cookie']);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`resolveUser` is the single point of contact between your auth world and Persona's runtime
|
|
45
|
+
world — see `RunContext`/the design notes below for why this boundary is absolute.
|
|
46
|
+
|
|
47
|
+
## Quickstart (raw Node `http`)
|
|
48
|
+
|
|
49
|
+
Using Express? Skip this section and use the published [`@personaai/express` adapter](https://persona.hasanraiyan.me/guides/express/quickstart).
|
|
50
|
+
For every other host there's no `@personaai/node` adapter package yet, so this release ships a
|
|
51
|
+
small, tested bridge at [`examples/node-handler.ts`](./examples/node-handler.ts) in this repo
|
|
52
|
+
for running the runtime directly against Node's `http` module, just enough to demo/smoke-test
|
|
53
|
+
the runtime end to end until `@personaai/node` ships. It parses multipart file uploads too, via
|
|
54
|
+
Node's native `Request`/`FormData` (undici) — no extra dependency.
|
|
55
|
+
|
|
56
|
+
**This file is not published to npm and has no `exports` entry** (`package.json`'s `files` only
|
|
57
|
+
ships `dist/`, and `exports` only maps `.`) — `import ... from '@personaai/runtime/examples/...'`
|
|
58
|
+
fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` after a real `npm install`. Copy the file into your own
|
|
59
|
+
project instead (it's plain TypeScript with no runtime-internal dependencies):
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createServer } from 'node:http';
|
|
63
|
+
import { createRuntime } from '@personaai/runtime';
|
|
64
|
+
import { toNodeHandler } from './node-handler.js'; // copied from this repo's examples/ — not importable from the published package, see note below
|
|
65
|
+
|
|
66
|
+
const runtime = createRuntime({
|
|
67
|
+
baseUrl: process.env.PERSONA_BASE_URL!,
|
|
68
|
+
credential: process.env.PERSONA_CREDENTIAL!,
|
|
69
|
+
resolveUser: (request) => request.headers['x-demo-user-id'] ?? null,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
createServer(toNodeHandler(runtime)).listen(3210);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Routes
|
|
76
|
+
|
|
77
|
+
All routes are relative to whatever `mountPath` you configure (default: none, i.e. the runtime
|
|
78
|
+
expects `request.path` already stripped). Every route except `/health` requires an authenticated
|
|
79
|
+
user; `resolveUser` returning `null` or throwing responds `401`.
|
|
80
|
+
|
|
81
|
+
### Always on — end-user-scoped, no capability flag needed
|
|
82
|
+
|
|
83
|
+
| Method | Path | Proxies to |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `POST` | `/chat` | `client.chat.stream(agentId, {messages, threadId, resume, contextOverride})`, streamed out as SSE. `agentId`/`messages` required in the body. Response carries an `x-persona-run-id` header — see [Reconnect and resume](#reconnect-and-resume). |
|
|
86
|
+
| `GET` | `/chat/:runId/resume` | Reattaches to the run started by the matching `POST /chat`. See [Reconnect and resume](#reconnect-and-resume). |
|
|
87
|
+
| `GET` | `/threads` | `client.threads.list({page, limit})` |
|
|
88
|
+
| `POST` | `/threads` | `client.threads.create({agentId})` |
|
|
89
|
+
| `POST` | `/threads/bulk-delete` | `client.threads.bulkDelete(ids)` |
|
|
90
|
+
| `GET` | `/threads/:id` | `client.threads.get(id)` |
|
|
91
|
+
| `PATCH` | `/threads/:id` | `client.threads.update(id, {title?, isArchived?})` |
|
|
92
|
+
| `DELETE` | `/threads/:id` | `client.threads.delete(id)` → `204` |
|
|
93
|
+
| `GET` | `/threads/:id/messages` | `client.threads.getMessages(id)` — full history + graph state, the same data `chat.stream()` resumes from; load a past conversation on page reopen. |
|
|
94
|
+
| `GET` | `/agents` | `client.agents.list({page, limit, search, category, scope})` — read-only discovery, e.g. "let the user pick an agent." |
|
|
95
|
+
| `GET` | `/files` | `client.files.list({page, limit})` |
|
|
96
|
+
| `POST` | `/files` | `client.files.upload({filename, content, contentType?, agentId?, threadId?})` — multipart, `file` part required. `201` |
|
|
97
|
+
| `POST` | `/files/bulk-delete` | `client.files.bulkDelete(ids)` |
|
|
98
|
+
| `GET` | `/files/:id` | `client.files.download(id)` — raw bytes, streamed through as `kind: 'binary'` |
|
|
99
|
+
| `DELETE` | `/files/:id` | `client.files.delete(id)` → `204` |
|
|
100
|
+
| `GET` | `/memory` | `client.memory.list()` |
|
|
101
|
+
| `GET` | `/memory/file` | `client.memory.getFile({path, scope?, agentId?})` — `path` query param required |
|
|
102
|
+
| `PUT` | `/memory/file` | `client.memory.writeFile({path, content, scope?, agentId?})` — creates or overwrites |
|
|
103
|
+
| `DELETE` | `/memory/file` | `client.memory.deleteFile({path, scope?, agentId?})` → `204` |
|
|
104
|
+
| `GET` | `/mcps/:id/oauth/owner/authorize` | `client.mcps.oauth.getOwnerAuthorizeUrl(id)` → `{url}` to redirect the Project owner to |
|
|
105
|
+
| `GET` | `/mcps/:id/oauth/user/authorize` | `client.mcps.oauth.getUserAuthorizeUrl(id, returnTo?)` → `{url}` to redirect the end user to |
|
|
106
|
+
| `GET` | `/mcps/:id/oauth/user/status` | `client.mcps.oauth.getUserConnectionStatus(id)` |
|
|
107
|
+
| `DELETE` | `/mcps/:id/oauth/user/connection` | `client.mcps.oauth.disconnectUserConnection(id)` → `204` |
|
|
108
|
+
| `DELETE` | `/mcps/:id/oauth/owner/connection` | `client.mcps.oauth.disconnectOwnerConnection(id)` → `204` |
|
|
109
|
+
| `GET` | `/health` | `client.whoami()` → `{status, version, capabilities}`. Does **not** require `resolveUser` — it's a liveness/capability probe, not a user-scoped call. |
|
|
110
|
+
|
|
111
|
+
`scope` for memory routes is `'user'` (default) or `'agent'` (`agentId` then required).
|
|
112
|
+
|
|
113
|
+
For `POST /files`, a framework adapter must parse the incoming multipart body and populate
|
|
114
|
+
`RuntimeRequest.file` (`{filename, content: Uint8Array, contentType?}`) plus put any other form
|
|
115
|
+
fields (`agentId`, `threadId`) on `RuntimeRequest.body` — the runtime itself never touches raw
|
|
116
|
+
bytes or a specific multipart parser. See `examples/node-handler.ts`'s `readMultipartBody` for
|
|
117
|
+
the reference approach (it also handles `POST /knowledge/:id/documents`'s multi-file `files`
|
|
118
|
+
field, below).
|
|
119
|
+
|
|
120
|
+
### Opt-in — Project-level admin surface, `capabilities.*` gated
|
|
121
|
+
|
|
122
|
+
See [Capabilities](#capabilities--admin-surface) for what these are, why they default off, and
|
|
123
|
+
how to enable them safely.
|
|
124
|
+
|
|
125
|
+
| Method | Path | Capability | Proxies to |
|
|
126
|
+
| --- | --- | --- | --- |
|
|
127
|
+
| `POST` | `/agents` | `agentsWrite` | `client.agents.create(input)` |
|
|
128
|
+
| `GET`/`PATCH`/`DELETE` | `/agents/:id` | `agentsWrite` | `client.agents.get/update/delete(id)` |
|
|
129
|
+
| `POST` | `/agents/bulk-delete` | `agentsWrite` | `client.agents.bulkDelete(ids)` |
|
|
130
|
+
| `GET`/`POST` | `/mcps` | `mcps` | `client.mcps.list/create` |
|
|
131
|
+
| `GET`/`PATCH`/`DELETE` | `/mcps/:id` | `mcps` | `client.mcps.get/update/delete(id)` |
|
|
132
|
+
| `POST` | `/mcps/bulk-delete` | `mcps` | `client.mcps.bulkDelete(ids)` |
|
|
133
|
+
| `GET` | `/mcps/:id/usage` | `mcps` | `client.mcps.getUsage(id)` |
|
|
134
|
+
| `POST` | `/mcps/:id/test` | `mcps` | `client.mcps.testConnection(id)` |
|
|
135
|
+
| `GET` | `/mcps/:id/resource?uri=` | `mcps` | `client.mcps.readResource(id, uri)` |
|
|
136
|
+
| `POST` | `/mcps/:id/call-tool` | `mcps` | `client.mcps.callTool(id, name, arguments)` |
|
|
137
|
+
| `GET`/`POST` | `/providers` | `providers` | `client.providers.list/create` — **holds API keys** |
|
|
138
|
+
| `GET`/`PATCH`/`DELETE` | `/providers/:id` | `providers` | `client.providers.get/update/delete(id)` |
|
|
139
|
+
| `POST` | `/providers/bulk-delete` | `providers` | `client.providers.bulkDelete(ids)` |
|
|
140
|
+
| `POST` | `/providers/:id/test` | `providers` | `client.providers.testConnection(id)` |
|
|
141
|
+
| `GET` | `/providers/:id/models` | `providers` | `client.providers.getModels(id)` |
|
|
142
|
+
| `GET` | `/providers/:id/usage` | `providers` | `client.providers.getUsage(id)` |
|
|
143
|
+
| `GET`/`POST` | `/skills` | `skills` | `client.skills.list/create` |
|
|
144
|
+
| `GET`/`PATCH`/`DELETE` | `/skills/:id` | `skills` | `client.skills.get/update/delete(id)` |
|
|
145
|
+
| `POST` | `/skills/bulk-delete` | `skills` | `client.skills.bulkDelete(ids)` |
|
|
146
|
+
| `GET` | `/skills/:id/usage` | `skills` | `client.skills.getUsage(id)` |
|
|
147
|
+
| `GET`/`POST` | `/knowledge` | `knowledge` | `client.knowledge.list/create` |
|
|
148
|
+
| `GET`/`PATCH`/`DELETE` | `/knowledge/:id` | `knowledge` | `client.knowledge.get/update/delete(id)` |
|
|
149
|
+
| `POST` | `/knowledge/bulk-delete` | `knowledge` | `client.knowledge.bulkDelete(ids)` |
|
|
150
|
+
| `GET` | `/knowledge/:id/usage` | `knowledge` | `client.knowledge.getUsage(id)` |
|
|
151
|
+
| `POST` | `/knowledge/:id/documents` | `knowledge` | `client.knowledge.uploadDocuments(id, files)` — multipart, one or more `files` parts required. `201` |
|
|
152
|
+
| `GET` | `/knowledge/:id/documents` | `knowledge` | `client.knowledge.listDocuments(id)` |
|
|
153
|
+
| `DELETE` | `/knowledge/:id/documents/:sourceName` | `knowledge` | `client.knowledge.deleteDocument(id, sourceName)` |
|
|
154
|
+
| `POST` | `/knowledge/:id/search` | `knowledge` | `client.knowledge.search(id, query, {topK?})` |
|
|
155
|
+
| `GET`/`POST` | `/stores` | `stores` | `client.stores.list/create` |
|
|
156
|
+
| `GET`/`PATCH`/`DELETE` | `/stores/:id` | `stores` | `client.stores.get/update/delete(id)` |
|
|
157
|
+
| `GET` | `/stores/:id/files` | `stores` | `client.stores.listFiles(id)` |
|
|
158
|
+
| `GET`/`PUT`/`DELETE` | `/stores/:id/file` | `stores` | `client.stores.getFile/writeFile/deleteFile(id, {path, content?})` |
|
|
159
|
+
| `GET` | `/audit-logs` | `auditLogs` | `client.auditLogs.list({page, limit, eventType})` |
|
|
160
|
+
| `POST` | `/architect` | `architect` | `client.architect.stream({messages, resume})`, streamed out as SSE, same `x-persona-run-id`/reconnect mechanics as `/chat`. No `agentId` — the Architect builds/edits the caller's own Agents. |
|
|
161
|
+
| `GET` | `/architect/:runId/resume` | `architect` | Reattaches to the matching `POST /architect` run. |
|
|
162
|
+
|
|
163
|
+
A disabled capability's routes are simply absent from the route table — a request to one 404s
|
|
164
|
+
(or, where an always-on route shares the same path with a different method, e.g. `POST /agents`
|
|
165
|
+
while only `GET /agents` is always-on, `405`) rather than 403, so a disabled capability leaks no
|
|
166
|
+
information about what it would have done.
|
|
167
|
+
|
|
168
|
+
## Capabilities — admin surface
|
|
169
|
+
|
|
170
|
+
The routes above are split into two trust tiers, and this is a deliberate design choice, not an
|
|
171
|
+
oversight:
|
|
172
|
+
|
|
173
|
+
- **Always on**: things an end user does in their own chat session — send messages, manage their
|
|
174
|
+
own conversations/files/memory, connect their own MCP account. Scoped entirely to whichever
|
|
175
|
+
user `resolveUser` returns.
|
|
176
|
+
- **Opt-in via `capabilities`**: Project-level configuration — LLM provider credentials, skill
|
|
177
|
+
authoring, knowledge base and vector store management, security audit logs, an agent-building
|
|
178
|
+
co-pilot, and full Agent/MCP-server CRUD. **Every one of these defaults to `false`.** Upgrading
|
|
179
|
+
this package never silently exposes new surface to whoever `resolveUser` accepts.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
createRuntime({
|
|
183
|
+
// ...
|
|
184
|
+
capabilities: {
|
|
185
|
+
agentsWrite: false, // default
|
|
186
|
+
mcps: false, // default
|
|
187
|
+
providers: false, // default — holds API keys, think hard before enabling
|
|
188
|
+
skills: false, // default
|
|
189
|
+
knowledge: false, // default
|
|
190
|
+
stores: false, // default
|
|
191
|
+
auditLogs: false, // default
|
|
192
|
+
architect: false, // default
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Most hosts should never turn any of these on**, and should instead call `@personaai/sdk`
|
|
198
|
+
directly from their own admin backend/CLI/setup script for Project configuration — that's what
|
|
199
|
+
"belongs to the host application" means in practice.
|
|
200
|
+
|
|
201
|
+
If you *do* want an admin surface reachable over HTTP (e.g. building your own internal admin
|
|
202
|
+
tool on top of this runtime), the right pattern is **two separate `createRuntime()` calls
|
|
203
|
+
mounted at two different paths**, each with its own `resolveUser`:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const appRuntime = createRuntime({
|
|
207
|
+
baseUrl, credential,
|
|
208
|
+
resolveUser: resolveEndUser, // your normal app auth — any logged-in user
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const adminRuntime = createRuntime({
|
|
212
|
+
baseUrl, credential,
|
|
213
|
+
resolveUser: resolveAdminUser, // a stricter check — only your team
|
|
214
|
+
capabilities: { providers: true, skills: true, knowledge: true, stores: true, auditLogs: true, architect: true, mcps: true, agentsWrite: true },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// mount appRuntime at /api/persona, adminRuntime at /api/admin/persona,
|
|
218
|
+
// each behind whatever auth middleware your framework adapter wires up
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This is coarse-grained by design: a capability is either fully on or fully off for whoever
|
|
222
|
+
`resolveUser` accepts on that mount — there's no per-user or per-action permission model inside
|
|
223
|
+
the runtime itself (e.g. "this user can update Agents but not delete them" isn't expressible).
|
|
224
|
+
If you need that, enforce it in `resolveUser` (reject the request before it reaches the route) or
|
|
225
|
+
in a hook, not by asking this runtime for finer granularity than "on this mount, for this
|
|
226
|
+
resolved identity, is the capability on."
|
|
227
|
+
|
|
228
|
+
## Lifecycle hooks
|
|
229
|
+
|
|
230
|
+
Plain async event listeners, not middleware — the runtime proceeds with sensible defaults when a
|
|
231
|
+
hook is omitted, and a hook that wants to reject a run just throws (the throw is caught and
|
|
232
|
+
routed through the same sanitized error response as any other failure). **All eight are wired.**
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
createRuntime({
|
|
236
|
+
// ...
|
|
237
|
+
hooks: {
|
|
238
|
+
beforeRun(ctx) {
|
|
239
|
+
// ctx: { userId, kind: 'chat' | 'architect', agentId?, threadId?, messages }
|
|
240
|
+
// Fires before POST /chat's or POST /architect's stream starts.
|
|
241
|
+
// agentId is only set for kind: 'chat' — the Architect has none of its own.
|
|
242
|
+
},
|
|
243
|
+
afterRun(ctx, result) {
|
|
244
|
+
// result: { text, eventCount, interrupted, erroredInBand }
|
|
245
|
+
// erroredInBand is true when the stream's last event was RUN_ERROR —
|
|
246
|
+
// that's a normal completed-run outcome, not a thrown exception, so
|
|
247
|
+
// afterRun still fires (onError does not).
|
|
248
|
+
},
|
|
249
|
+
onError(ctx, error) {
|
|
250
|
+
// ctx.phase: 'auth' | 'chat' | 'architect'
|
|
251
|
+
// Fires on a thrown exception only: the initial request failing
|
|
252
|
+
// (auth/validation/network) or the stream dying mid-read. Not on an
|
|
253
|
+
// in-band RUN_ERROR event — see afterRun above.
|
|
254
|
+
},
|
|
255
|
+
beforeToolCall(ctx) {
|
|
256
|
+
// ctx: { userId, agentId, threadId?, toolName, toolCallId }
|
|
257
|
+
// Fires on each TOOL_CALL_START event inside a chat stream.
|
|
258
|
+
},
|
|
259
|
+
afterToolCall(ctx, result) {
|
|
260
|
+
// Fires on the matching TOOL_CALL_RESULT event; `result` is the raw
|
|
261
|
+
// (string or already-JSON) tool output.
|
|
262
|
+
},
|
|
263
|
+
onFileUpload(ctx) {
|
|
264
|
+
// ctx: { userId, fileName, mimeType? } — fires after POST /files succeeds.
|
|
265
|
+
},
|
|
266
|
+
onThreadCreate(ctx) {
|
|
267
|
+
// ctx: { userId, agentId, threadId } — fires on an explicit POST
|
|
268
|
+
// /threads, AND when POST /chat's RUN_STARTED event reports a
|
|
269
|
+
// threadId that wasn't supplied on the way in (Persona created one
|
|
270
|
+
// implicitly for that turn).
|
|
271
|
+
},
|
|
272
|
+
onMemoryWrite(ctx) {
|
|
273
|
+
// ctx: { userId, agentId?, path } — fires after PUT /memory/file succeeds.
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Reconnect and resume
|
|
280
|
+
|
|
281
|
+
If a client's connection to `/chat` drops mid-stream, it can pick up exactly where it left off:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
const res = await fetch('/chat', { method: 'POST', body: JSON.stringify({ agentId, messages }) });
|
|
285
|
+
const runId = res.headers.get('x-persona-run-id')!;
|
|
286
|
+
// ... connection drops after receiving N frames ...
|
|
287
|
+
const resumed = await fetch(`/chat/${runId}/resume?since=${lastSeqSeen}`);
|
|
288
|
+
// streams every frame after `lastSeqSeen`, then continues live until the run finishes
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
This works because a chat run is never tied to the HTTP response that started it. `POST /chat`
|
|
292
|
+
constructs an internal `RunDriver` that starts pumping `chat.stream()` the moment the run begins
|
|
293
|
+
and keeps running independently of whether anyone is still listening — buffering every formatted
|
|
294
|
+
SSE frame with a sequence number and broadcasting to live subscribers. `GET /chat/:runId/resume`
|
|
295
|
+
just attaches a new subscriber to that same driver: it replays whatever's already buffered after
|
|
296
|
+
`since`, then streams new frames live until the run finishes. Lifecycle hooks (`afterRun`,
|
|
297
|
+
`onError`, etc.) fire exactly once per run regardless of how many times a client reconnects —
|
|
298
|
+
they belong to the driver, not to any one HTTP response.
|
|
299
|
+
|
|
300
|
+
Finished runs stay resumable for 5 minutes by default before an internal eviction sweep (running
|
|
301
|
+
every 60s) removes them; the registry also caps out at 1000 tracked runs by default, evicting the
|
|
302
|
+
oldest-finished ones first if a host's traffic pattern leaves many runs unclaimed. Both are
|
|
303
|
+
configurable:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
createRuntime({
|
|
307
|
+
// ...
|
|
308
|
+
runGraceMs: 5 * 60 * 1000, // default
|
|
309
|
+
maxTrackedRuns: 1000, // default
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
A resume request for an evicted, unknown, or someone-else's run returns `404 RUN_NOT_FOUND`
|
|
314
|
+
(never `403` — a `404` doesn't confirm whether the id ever existed).
|
|
315
|
+
|
|
316
|
+
**Honest limitation: this is single-process and in-memory only.** A `RunDriver` holds a live
|
|
317
|
+
upstream connection and a JS closure over its subscribers — it cannot be represented in Redis or
|
|
318
|
+
shared across separate runtime instances. This closes the reconnect gap for the common
|
|
319
|
+
single-instance deployment (the client dropped and came back, same server process still running).
|
|
320
|
+
If you run more than one instance behind a load balancer — multiple Kubernetes pods, a PM2
|
|
321
|
+
cluster, etc. — a reconnect that lands on a *different* instance than the one running the
|
|
322
|
+
original pump won't find the run (`404 RUN_NOT_FOUND`) even though it's still live elsewhere.
|
|
323
|
+
|
|
324
|
+
**Today's mitigation (deployment-level, no code change):** configure your load balancer for
|
|
325
|
+
session affinity / sticky sessions (route a given client to the same instance) so reconnects
|
|
326
|
+
land back where the run actually lives. This doesn't survive that instance crashing or being
|
|
327
|
+
redeployed, and doesn't apply to serverless (no persistent instance to stick to), but covers the
|
|
328
|
+
common case for free.
|
|
329
|
+
|
|
330
|
+
**Planned (not yet built):** a pluggable `RunBroker` interface — publish/subscribe/claim-ownership
|
|
331
|
+
for run frames — so a host can back it with Redis (or anything else) and get resume working
|
|
332
|
+
across instances, including a resume request landing on an instance that never touched the
|
|
333
|
+
original pump. The in-memory behavior above would remain the zero-config default; a Redis (or
|
|
334
|
+
similar) implementation would be opt-in, not a dependency this package forces on everyone.
|
|
335
|
+
Deliberately deferred rather than built speculatively — track
|
|
336
|
+
[issue #229](https://github.com/hasanraiyan/agent-marketplace/issues/229) or open a new one if you
|
|
337
|
+
need this now.
|
|
338
|
+
|
|
339
|
+
`createRuntime()` returns a `close()` method that stops the eviction timer; the timer is also
|
|
340
|
+
`unref`'d so it won't itself keep a Node process alive, but call `close()` if you construct
|
|
341
|
+
runtimes repeatedly in a long-lived process (e.g. per-test-suite setup) to avoid accumulating
|
|
342
|
+
timers.
|
|
343
|
+
|
|
344
|
+
## Heartbeats and backpressure
|
|
345
|
+
|
|
346
|
+
`POST /chat` and `GET /chat/:runId/resume` both send an SSE comment-line heartbeat
|
|
347
|
+
(`: heartbeat\n\n`) during any gap between real AG-UI events — e.g. a long-running tool call with
|
|
348
|
+
no token output — so intermediary proxies and load balancers with an idle-connection timeout
|
|
349
|
+
don't kill the stream. Comment lines are invisible to any `data:`-only SSE parser (including
|
|
350
|
+
`@personaai/sdk`'s own `parseAguiEventStream`), so a consumer never sees them as part of the
|
|
351
|
+
event sequence.
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
createRuntime({
|
|
355
|
+
// ...
|
|
356
|
+
heartbeatIntervalMs: 15000, // default; lower it for faster proxy timeouts, or raise it to reduce chatter
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Heartbeats only cover gaps *after* the first event of a run — headers can't be sent until the
|
|
361
|
+
runtime has already peeked that first event to decide whether the run started successfully
|
|
362
|
+
(a 401/400/500 has to be a normal buffered response, not a stream), so there's no way to keep a
|
|
363
|
+
connection alive with heartbeats before that point. In practice this matters little: the gap
|
|
364
|
+
heartbeats exist for is a stalled *middle* of a run (a slow tool call), not the initial
|
|
365
|
+
time-to-first-token.
|
|
366
|
+
|
|
367
|
+
**Backpressure has a real, deliberate tradeoff as of reconnect support.** Before reconnect
|
|
368
|
+
existed, a slow or disconnected consumer propagated backpressure all the way back to Persona's
|
|
369
|
+
server — the runtime never pulled a frame it hadn't been asked for. That's no longer true: a
|
|
370
|
+
`RunDriver`'s pump starts draining `chat.stream()` the moment the run begins and keeps going
|
|
371
|
+
regardless of subscriber speed, because resumability requires buffering whatever a reconnecting
|
|
372
|
+
client might ask to replay. You cannot have both "backpressure all the way to the source" and "a
|
|
373
|
+
disconnected client can come back and get what it missed" — they're in direct tension, and this
|
|
374
|
+
runtime chose resumability. What's still true and tested (`test/runDriver.test.ts`): the pump
|
|
375
|
+
drains the upstream generator exactly once, strictly in order, with no duplicate or skipped
|
|
376
|
+
`next()` calls, no matter how many subscribers attach or how slowly they read. Per-run buffers
|
|
377
|
+
are bounded by that one run's event count (not indefinite) and released after the grace period
|
|
378
|
+
described above. The Node bridge in `examples/` still layers transport-level backpressure via
|
|
379
|
+
`res.write()`'s return value and the `drain` event — that protects against one slow subscriber
|
|
380
|
+
blocking the Node process's memory, but it no longer protects against the *runtime itself*
|
|
381
|
+
buffering an in-progress run that nobody is currently reading.
|
|
382
|
+
|
|
383
|
+
## Errors
|
|
384
|
+
|
|
385
|
+
Every error response is `{"error": {"code": "...", "message": "...", "detail"?: ...}}`. Two
|
|
386
|
+
modes (`mode: 'development' | 'production'`, default `'production'` unless
|
|
387
|
+
`NODE_ENV === 'development'`):
|
|
388
|
+
|
|
389
|
+
- Errors already curated into a developer-facing message — `RuntimeHttpError` (routing/validation
|
|
390
|
+
errors this runtime raises itself) and `PersonaApiError`/`PersonaAuthError`/`PersonaValidationError`
|
|
391
|
+
from `@personaai/sdk` — pass through as-is; `detail` (the upstream response envelope) is only
|
|
392
|
+
attached in development mode.
|
|
393
|
+
- Anything else (a bug in your own hook code, a raw network error, ...) is treated as untrusted:
|
|
394
|
+
always `500`/`INTERNAL_ERROR`, with a fixed generic message in production and the real
|
|
395
|
+
message/stack under `detail` in development. This is what actually prevents internal
|
|
396
|
+
implementation details (LangGraph, Qdrant, ...) from ever reaching a caller of this runtime.
|
|
397
|
+
|
|
398
|
+
## Not yet implemented
|
|
399
|
+
|
|
400
|
+
This is v0.5.1. Every SDK resource now has a route (see [Routes](#routes)); what's left is either a
|
|
401
|
+
genuine unclosed gap or an intentional package boundary, not an oversight:
|
|
402
|
+
|
|
403
|
+
- **Multi-instance reconnect/resume** — see
|
|
404
|
+
[Reconnect and resume](#reconnect-and-resume) above. Single-process resume is implemented and
|
|
405
|
+
tested; sharing a live run across separate runtime instances would need a message-broker
|
|
406
|
+
architecture this package doesn't provide. This is the one real gap.
|
|
407
|
+
- **Fine-grained (per-user, per-action) permissions within an enabled capability** — **by
|
|
408
|
+
design**, not a gap: see [Capabilities](#capabilities--admin-surface). A capability is on or off
|
|
409
|
+
per mount; anything finer belongs in `resolveUser` or a hook, not the runtime.
|
|
410
|
+
- Framework adapters beyond Express (`@personaai/nextjs`, `@personaai/node`, `@personaai/fastify`,
|
|
411
|
+
`@personaai/hono`, `@personaai/nestjs`) — **by design**, not a gap: this package is the
|
|
412
|
+
foundation they're meant to wrap, not a replacement for them. `@personaai/express` (Wave 3)
|
|
413
|
+
has shipped as the first.
|
|
414
|
+
|
|
415
|
+
## Roadmap
|
|
416
|
+
|
|
417
|
+
- A pluggable `RunBroker` interface for multi-instance reconnect/resume (Redis or similar,
|
|
418
|
+
opt-in) — see [Reconnect and resume](#reconnect-and-resume).
|
|
419
|
+
- Framework adapters (Wave 2–3 of the ecosystem plan) are the natural next step once this runtime
|
|
420
|
+
is battle-tested — each should be a thin translation layer, proving the framework-neutral
|
|
421
|
+
contract here is actually sufficient.
|