@personaai/runtime 0.9.4 → 0.10.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 CHANGED
@@ -1,14 +1,11 @@
1
1
  # @personaai/runtime
2
2
 
3
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
4
+ shared engine every framework adapter (`@personaai/adapters/express`, `@personaai/adapters/nextjs/server`, ...) is meant
5
5
  to be a thin translation layer over — see
6
6
  [the SDK Ecosystem plan](https://github.com/hasanraiyan/agent-marketplace/blob/feat/ai/product-research/11-sdk-new/package-ecosystem.md).
7
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
8
+ **v0.10.0.** Added full Workflows engine support (`POST /workflows/:id/stream`, `GET /workflows`, `POST /workflows/runs/:runId/cancel`, `GET /workflows/runs/:runId/resume`), `workflowsWrite` gated authoring capability, and `beforeWorkflowNode`/`afterWorkflowNode` lifecycle hooks. Unified adapter [`@personaai/adapters`](https://persona.hasanraiyan.me/guides/express/quickstart) provides `express`/`nextjs`/`nestjs` bindings. For non-covered hosts, see
12
9
  [Quickstart](#quickstart) for how to run it directly against raw Node `http`, and
13
10
  [Not yet implemented](#not-yet-implemented) for what's missing before it's a complete Level 2
14
11
  runtime.
@@ -46,7 +43,7 @@ world — see `RunContext`/the design notes below for why this boundary is absol
46
43
 
47
44
  ## Quickstart (raw Node `http`)
48
45
 
49
- Using Express? Skip this section and use the published [`@personaai/express` adapter](https://persona.hasanraiyan.me/guides/express/quickstart).
46
+ Using Express, Next.js, or NestJS? Skip this section and use the published [`@personaai/adapters` adapters](https://persona.hasanraiyan.me/guides/express/quickstart) — `import { toExpressRouter } from '@personaai/adapters/express'`, `import { createPersonaHandler } from '@personaai/adapters/nextjs/server'`, or `import { PersonaModule } from '@personaai/adapters/nestjs'`.
50
47
  For every other host there's no `@personaai/node` adapter package yet, so this release ships a
51
48
  small, tested bridge at [`examples/node-handler.ts`](./examples/node-handler.ts) in this repo
52
49
  for running the runtime directly against Node's `http` module, just enough to demo/smoke-test
@@ -108,6 +105,10 @@ user; `resolveUser` returning `null` or throwing responds `401`.
108
105
  | `GET` | `/mcps/:id/oauth/user/status` | `client.mcps.oauth.getUserConnectionStatus(id)` |
109
106
  | `DELETE` | `/mcps/:id/oauth/user/connection` | `client.mcps.oauth.disconnectUserConnection(id)` → `204` |
110
107
  | `DELETE` | `/mcps/:id/oauth/owner/connection` | `client.mcps.oauth.disconnectOwnerConnection(id)` → `204` |
108
+ | `GET` | `/workflows` | `client.workflows.list({page, limit, search, scope, visibility, isEnabled})` — read-only discovery of available workflows. |
109
+ | `POST` | `/workflows/:id/stream` | `client.workflows.stream(id, {input, dryRun, version})` — multi-step workflow execution with node-level AG-UI event streaming. Response carries `x-persona-run-id`. |
110
+ | `GET` | `/workflows/runs/:runId/resume` | Reattaches to in-flight or recent workflow execution with frame replay (`?since=<seq>`). |
111
+ | `POST` | `/workflows/runs/:runId/cancel` | `client.workflows.cancel(runId)` → aborts active execution. |
111
112
  | `GET` | `/health` | `client.whoami()` → `{status, version, capabilities}`. Does **not** require `resolveUser` — it's a liveness/capability probe, not a user-scoped call. |
112
113
 
113
114
  `scope` for memory routes is `'user'` (default) or `'agent'` (`agentId` then required).
@@ -161,10 +162,19 @@ how to enable them safely.
161
162
  | `GET` | `/audit-logs` | `auditLogs` | `client.auditLogs.list({page, limit, eventType})` |
162
163
  | `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. |
163
164
  | `GET` | `/architect/:runId/resume` | `architect` | Reattaches to the matching `POST /architect` run. |
165
+ | `POST` | `/workflows` | `workflowsWrite` | `client.workflows.create(input)` |
166
+ | `GET`/`PATCH`/`DELETE` | `/workflows/:id` | `workflowsWrite` | `client.workflows.get/update/delete(id)` |
167
+ | `PUT` | `/workflows/:id/draft` | `workflowsWrite` | `client.workflows.saveDraft(id, draft)` |
168
+ | `POST` | `/workflows/:id/publish` | `workflowsWrite` | `client.workflows.publish(id)` |
169
+ | `GET` | `/workflows/:id/versions` | `workflowsWrite` | `client.workflows.listVersions(id)` |
170
+ | `GET` | `/workflows/:id/versions/:version` | `workflowsWrite` | `client.workflows.getVersion(id, version)` |
171
+ | `GET` | `/workflows/:id/mermaid` | `workflowsWrite` | `client.workflows.getMermaid(id)` → `{mermaid}` |
172
+ | `GET` | `/workflows/:id/runs` | `workflowsWrite` | `client.workflows.listRuns(id, params)` |
173
+ | `GET` | `/workflows/runs/:runId` | `workflowsWrite` | `client.workflows.getRun(runId)` |
164
174
 
165
175
  A disabled capability's routes are simply absent from the route table — a request to one 404s
166
176
  (or, where an always-on route shares the same path with a different method, e.g. `POST /agents`
167
- while only `GET /agents` is always-on, `405`) rather than 403, so a disabled capability leaks no
177
+ while only `GET /agents` is always-on, or `POST /workflows` while `GET /workflows` is always-on, `405`) rather than 403, so a disabled capability leaks no
168
178
  information about what it would have done.
169
179
 
170
180
  ## Capabilities — admin surface
@@ -173,25 +183,26 @@ The routes above are split into two trust tiers, and this is a deliberate design
173
183
  oversight:
174
184
 
175
185
  - **Always on**: things an end user does in their own chat session — send messages, manage their
176
- own conversations/files/memory, connect their own MCP account. Scoped entirely to whichever
186
+ own conversations/files/memory, connect their own MCP account, run workflows. Scoped entirely to whichever
177
187
  user `resolveUser` returns.
178
188
  - **Opt-in via `capabilities`**: Project-level configuration — LLM provider credentials, skill
179
189
  authoring, knowledge base and vector store management, security audit logs, an agent-building
180
- co-pilot, and full Agent/MCP-server CRUD. **Every one of these defaults to `false`.** Upgrading
190
+ co-pilot, workflow canvas builder/publishing, and full Agent/MCP-server CRUD. **Every one of these defaults to `false`.** Upgrading
181
191
  this package never silently exposes new surface to whoever `resolveUser` accepts.
182
192
 
183
193
  ```ts
184
194
  createRuntime({
185
195
  // ...
186
196
  capabilities: {
187
- agentsWrite: false, // default
188
- mcps: false, // default
189
- providers: false, // default — holds API keys, think hard before enabling
190
- skills: false, // default
191
- knowledge: false, // default
192
- stores: false, // default
193
- auditLogs: false, // default
194
- architect: false, // default
197
+ agentsWrite: false, // default
198
+ mcps: false, // default
199
+ providers: false, // default — holds API keys, think hard before enabling
200
+ skills: false, // default
201
+ knowledge: false, // default
202
+ stores: false, // default
203
+ auditLogs: false, // default
204
+ architect: false, // default
205
+ workflowsWrite: false, // default — authoring and version snapshots
195
206
  },
196
207
  });
197
208
  ```
@@ -415,9 +426,8 @@ genuine unclosed gap or an intentional package boundary, not an oversight:
415
426
  design**, not a gap: see [Capabilities](#capabilities--admin-surface). A capability is on or off
416
427
  per mount; anything finer belongs in `resolveUser` or a hook, not the runtime.
417
428
  - Framework adapters beyond the shipped ones (`@personaai/node`, `@personaai/fastify`,
418
- `@personaai/hono`) — **by design**, not a gap: this package is the foundation they're meant to
419
- wrap, not a replacement for them. `@personaai/nextjs` (Wave 2), `@personaai/express` and
420
- `@personaai/nestjs` (Wave 3) have shipped.
429
+ `@personaai/hono`) — **by design**, not a gap: this package is the foundation they're meant to
430
+ wrap, not a replacement for them. `@personaai/adapters` (unified `express`/`nextjs`/`nestjs` via subpaths, Wave 3) has shipped — legacy names `@personaai/nextjs`/`@personaai/express`/`@personaai/nestjs` are deprecated shims.
421
431
 
422
432
  ## Roadmap
423
433
 
package/dist/index.cjs CHANGED
@@ -159,7 +159,7 @@ function evictStaleRuns(runs, now, graceMs = DEFAULT_RUN_GRACE_MS, maxTracked =
159
159
  }
160
160
 
161
161
  // src/version.ts
162
- var RUNTIME_VERSION = "0.9.4";
162
+ var RUNTIME_VERSION = "0.10.0";
163
163
 
164
164
  // src/routes/health.ts
165
165
  var alwaysOnCapabilities = {
@@ -168,7 +168,8 @@ var alwaysOnCapabilities = {
168
168
  agents: true,
169
169
  files: true,
170
170
  memory: true,
171
- mcpOAuth: true
171
+ mcpOAuth: true,
172
+ workflows: true
172
173
  };
173
174
  var healthRoute = async (_request, ctx) => {
174
175
  try {
@@ -322,7 +323,8 @@ function newAccumulator() {
322
323
  interrupted: false,
323
324
  erroredInBand: false,
324
325
  threadCreateFired: false,
325
- toolNames: /* @__PURE__ */ new Map()
326
+ toolNames: /* @__PURE__ */ new Map(),
327
+ nodeMeta: /* @__PURE__ */ new Map()
326
328
  };
327
329
  }
328
330
  async function processEvent(event, acc, runCtx, hooks) {
@@ -342,7 +344,45 @@ async function processEvent(event, acc, runCtx, hooks) {
342
344
  break;
343
345
  }
344
346
  case import_sdk3.EventType.CUSTOM: {
345
- if (e.name === "hitl_request" || e.name === "clarification_request") acc.interrupted = true;
347
+ if (e.name === "hitl_request" || e.name === "clarification_request") {
348
+ acc.interrupted = true;
349
+ } else if (e.name === "workflow_node_started") {
350
+ const val = e.value ?? {};
351
+ const nodeId = String(val.nodeId ?? "");
352
+ const nodeType = String(val.nodeType ?? "");
353
+ const nodeLabel = String(val.nodeLabel ?? "");
354
+ if (nodeId) acc.nodeMeta.set(nodeId, { nodeType, nodeLabel });
355
+ if (runCtx.workflowId) {
356
+ await hooks?.beforeWorkflowNode?.({
357
+ userId: runCtx.userId,
358
+ workflowId: runCtx.workflowId,
359
+ nodeId,
360
+ nodeType,
361
+ nodeLabel,
362
+ input: val.input
363
+ });
364
+ }
365
+ } else if (e.name === "workflow_node_completed") {
366
+ const val = e.value ?? {};
367
+ const nodeId = String(val.nodeId ?? "");
368
+ const meta = acc.nodeMeta.get(nodeId);
369
+ const nodeType = String(val.nodeType ?? meta?.nodeType ?? "");
370
+ const nodeLabel = String(val.nodeLabel ?? meta?.nodeLabel ?? "");
371
+ if (runCtx.workflowId) {
372
+ await hooks?.afterWorkflowNode?.(
373
+ {
374
+ userId: runCtx.userId,
375
+ workflowId: runCtx.workflowId,
376
+ nodeId,
377
+ nodeType,
378
+ nodeLabel
379
+ },
380
+ val.output ?? val
381
+ );
382
+ }
383
+ } else if (e.name === "workflow_node_failed") {
384
+ acc.erroredInBand = true;
385
+ }
346
386
  break;
347
387
  }
348
388
  case import_sdk3.EventType.RUN_ERROR: {
@@ -449,6 +489,7 @@ var RunDriver = class {
449
489
  userId: this.runCtx.userId,
450
490
  phase: this.runCtx.kind,
451
491
  agentId: this.runCtx.agentId,
492
+ workflowId: this.runCtx.workflowId,
452
493
  threadId: this.runCtx.threadId
453
494
  },
454
495
  err
@@ -865,6 +906,7 @@ var createAgent = async (request, ctx) => {
865
906
  socialLinks: body.socialLinks ?? void 0,
866
907
  modelName: typeof body.modelName === "string" ? body.modelName : void 0,
867
908
  webSearchEnabled: typeof body.webSearchEnabled === "boolean" ? body.webSearchEnabled : void 0,
909
+ sandboxEnabled: typeof body.sandboxEnabled === "boolean" ? body.sandboxEnabled : void 0,
868
910
  visibility: body.visibility,
869
911
  category: body.category,
870
912
  skills: Array.isArray(body.skills) ? body.skills : void 0,
@@ -1346,6 +1388,158 @@ var listAuditLogs = async (request, ctx) => {
1346
1388
  return json(200, items);
1347
1389
  };
1348
1390
 
1391
+ // src/routes/workflows.ts
1392
+ var listWorkflows = async (request, ctx) => {
1393
+ const items = await ctx.client.workflows.list({
1394
+ page: toInt(request.query.page),
1395
+ limit: toInt(request.query.limit),
1396
+ search: request.query.search,
1397
+ scope: request.query.scope,
1398
+ visibility: request.query.visibility,
1399
+ isEnabled: request.query.isEnabled !== void 0 ? request.query.isEnabled === "true" : void 0
1400
+ });
1401
+ return json(200, items);
1402
+ };
1403
+ var createWorkflow = async (request, ctx) => {
1404
+ const body = requireBodyObject(request.body);
1405
+ const input = {
1406
+ name: requireStringField(body, "name"),
1407
+ description: typeof body.description === "string" ? body.description : void 0,
1408
+ visibility: body.visibility,
1409
+ isEnabled: typeof body.isEnabled === "boolean" ? body.isEnabled : void 0,
1410
+ draft: body.draft ?? void 0
1411
+ };
1412
+ const idempotencyKey = request.headers["idempotency-key"];
1413
+ const workflow = await ctx.client.workflows.create(input, idempotencyKey);
1414
+ return json(201, workflow);
1415
+ };
1416
+ var getWorkflow = async (_request, ctx) => {
1417
+ const workflow = await ctx.client.workflows.get(requireParam(ctx.params, "id"));
1418
+ return json(200, workflow);
1419
+ };
1420
+ var updateWorkflow = async (request, ctx) => {
1421
+ const body = requireBodyObject(request.body);
1422
+ const input = {
1423
+ name: typeof body.name === "string" ? body.name : void 0,
1424
+ description: typeof body.description === "string" ? body.description : void 0,
1425
+ visibility: body.visibility,
1426
+ isEnabled: typeof body.isEnabled === "boolean" ? body.isEnabled : void 0,
1427
+ draft: body.draft ?? void 0
1428
+ };
1429
+ const updated = await ctx.client.workflows.update(
1430
+ requireParam(ctx.params, "id"),
1431
+ input
1432
+ );
1433
+ return json(200, updated);
1434
+ };
1435
+ var deleteWorkflow = async (_request, ctx) => {
1436
+ await ctx.client.workflows.delete(requireParam(ctx.params, "id"));
1437
+ return noContent();
1438
+ };
1439
+ var saveWorkflowDraft = async (request, ctx) => {
1440
+ const body = requireBodyObject(request.body);
1441
+ const draft = body;
1442
+ const updated = await ctx.client.workflows.saveDraft(
1443
+ requireParam(ctx.params, "id"),
1444
+ draft
1445
+ );
1446
+ return json(200, updated);
1447
+ };
1448
+ var publishWorkflow = async (_request, ctx) => {
1449
+ const version = await ctx.client.workflows.publish(requireParam(ctx.params, "id"));
1450
+ return json(201, version);
1451
+ };
1452
+ var listWorkflowVersions = async (request, ctx) => {
1453
+ const items = await ctx.client.workflows.listVersions(
1454
+ requireParam(ctx.params, "id"),
1455
+ {
1456
+ page: toInt(request.query.page),
1457
+ limit: toInt(request.query.limit)
1458
+ }
1459
+ );
1460
+ return json(200, items);
1461
+ };
1462
+ var getWorkflowVersion = async (_request, ctx) => {
1463
+ const versionNum = toInt(ctx.params.version);
1464
+ const version = await ctx.client.workflows.getVersion(
1465
+ requireParam(ctx.params, "id"),
1466
+ versionNum ?? 1
1467
+ );
1468
+ return json(200, version);
1469
+ };
1470
+ var getWorkflowMermaid = async (_request, ctx) => {
1471
+ const result = await ctx.client.workflows.getMermaid(requireParam(ctx.params, "id"));
1472
+ return json(200, result);
1473
+ };
1474
+ var listWorkflowRuns = async (request, ctx) => {
1475
+ const items = await ctx.client.workflows.listRuns(requireParam(ctx.params, "id"), {
1476
+ page: toInt(request.query.page),
1477
+ limit: toInt(request.query.limit),
1478
+ status: request.query.status,
1479
+ isDryRun: request.query.isDryRun !== void 0 ? request.query.isDryRun === "true" : void 0
1480
+ });
1481
+ return json(200, items);
1482
+ };
1483
+ var getWorkflowRun = async (_request, ctx) => {
1484
+ const run = await ctx.client.workflows.getRun(requireParam(ctx.params, "runId"));
1485
+ return json(200, run);
1486
+ };
1487
+ var cancelWorkflowRun = async (_request, ctx) => {
1488
+ const result = await ctx.client.workflows.cancel(requireParam(ctx.params, "runId"));
1489
+ return json(200, result);
1490
+ };
1491
+
1492
+ // src/routes/workflowStream.ts
1493
+ function parseWorkflowStreamBody(body) {
1494
+ if (body === void 0 || body === null) return {};
1495
+ if (typeof body !== "object") {
1496
+ throw new RuntimeHttpError(400, "INVALID_REQUEST", "Request body must be a JSON object.");
1497
+ }
1498
+ const b = body;
1499
+ return {
1500
+ input: b.input,
1501
+ dryRun: typeof b.dryRun === "boolean" ? b.dryRun : void 0,
1502
+ version: typeof b.version === "number" ? b.version : void 0
1503
+ };
1504
+ }
1505
+ var workflowStreamRoute = async (request, ctx) => {
1506
+ const workflowId = requireParam(ctx.params, "id");
1507
+ const body = parseWorkflowStreamBody(request.body);
1508
+ const userId = request.userId;
1509
+ const runCtx = {
1510
+ userId,
1511
+ kind: "workflow",
1512
+ workflowId,
1513
+ input: body.input
1514
+ };
1515
+ await ctx.hooks?.beforeRun?.(runCtx);
1516
+ const stream = ctx.client.workflows.stream(workflowId, {
1517
+ input: body.input,
1518
+ dryRun: body.dryRun,
1519
+ version: body.version
1520
+ });
1521
+ const runId = crypto.randomUUID();
1522
+ const driver = new RunDriver(runId, runCtx, stream, ctx.hooks, ctx.mode);
1523
+ ctx.runs.set(runId, driver);
1524
+ try {
1525
+ await driver.waitForFirstFrame();
1526
+ } catch (err) {
1527
+ ctx.runs.delete(runId);
1528
+ throw err;
1529
+ }
1530
+ return {
1531
+ kind: "stream",
1532
+ status: 200,
1533
+ headers: {
1534
+ "content-type": "text/event-stream",
1535
+ "cache-control": "no-cache",
1536
+ connection: "keep-alive",
1537
+ "x-persona-run-id": runId
1538
+ },
1539
+ body: withHeartbeats(driver.subscribe(-1), ctx.heartbeatIntervalMs)
1540
+ };
1541
+ };
1542
+
1349
1543
  // src/runtime.ts
1350
1544
  function resolveCapabilities(capabilities) {
1351
1545
  return {
@@ -1356,7 +1550,8 @@ function resolveCapabilities(capabilities) {
1356
1550
  knowledge: capabilities?.knowledge ?? false,
1357
1551
  stores: capabilities?.stores ?? false,
1358
1552
  auditLogs: capabilities?.auditLogs ?? false,
1359
- architect: capabilities?.architect ?? false
1553
+ architect: capabilities?.architect ?? false,
1554
+ workflowsWrite: capabilities?.workflowsWrite ?? false
1360
1555
  };
1361
1556
  }
1362
1557
  function buildRoutes(capabilities) {
@@ -1432,6 +1627,19 @@ function buildRoutes(capabilities) {
1432
1627
  method: "POST",
1433
1628
  pattern: ["mcps", ":id", "call-tool"],
1434
1629
  handler: callMcpTool
1630
+ },
1631
+ // Workflows — read-only discovery & execution always on; authoring ops behind workflowsWrite.
1632
+ { method: "GET", pattern: ["workflows"], handler: listWorkflows },
1633
+ { method: "POST", pattern: ["workflows", ":id", "stream"], handler: workflowStreamRoute },
1634
+ {
1635
+ method: "GET",
1636
+ pattern: ["workflows", "runs", ":runId", "resume"],
1637
+ handler: createResumeRoute("workflow")
1638
+ },
1639
+ {
1640
+ method: "POST",
1641
+ pattern: ["workflows", "runs", ":runId", "cancel"],
1642
+ handler: cancelWorkflowRun
1435
1643
  }
1436
1644
  ];
1437
1645
  if (capabilities.agentsWrite) {
@@ -1532,6 +1740,21 @@ function buildRoutes(capabilities) {
1532
1740
  }
1533
1741
  );
1534
1742
  }
1743
+ if (capabilities.workflowsWrite) {
1744
+ routes.push(
1745
+ { method: "POST", pattern: ["workflows"], handler: createWorkflow },
1746
+ { method: "GET", pattern: ["workflows", ":id"], handler: getWorkflow },
1747
+ { method: "PATCH", pattern: ["workflows", ":id"], handler: updateWorkflow },
1748
+ { method: "DELETE", pattern: ["workflows", ":id"], handler: deleteWorkflow },
1749
+ { method: "PUT", pattern: ["workflows", ":id", "draft"], handler: saveWorkflowDraft },
1750
+ { method: "POST", pattern: ["workflows", ":id", "publish"], handler: publishWorkflow },
1751
+ { method: "GET", pattern: ["workflows", ":id", "versions"], handler: listWorkflowVersions },
1752
+ { method: "GET", pattern: ["workflows", ":id", "versions", ":version"], handler: getWorkflowVersion },
1753
+ { method: "GET", pattern: ["workflows", ":id", "mermaid"], handler: getWorkflowMermaid },
1754
+ { method: "GET", pattern: ["workflows", ":id", "runs"], handler: listWorkflowRuns },
1755
+ { method: "GET", pattern: ["workflows", "runs", ":runId"], handler: getWorkflowRun }
1756
+ );
1757
+ }
1535
1758
  return routes;
1536
1759
  }
1537
1760
  function resolveMode(options) {