@lunnoa/client 0.0.0-development → 1.0.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/LICENSE +202 -202
- package/README.md +372 -338
- package/dist/cli.js +31 -1
- package/dist/index.cjs +31 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +703 -585
- package/dist/index.d.ts +703 -585
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +31 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +780 -719
- package/dist/react.d.ts +780 -719
- package/dist/react.js +31 -1
- package/dist/react.js.map +1 -1
- package/package.json +7 -5
- package/skills/lunnoa-client/SKILL.md +517 -0
- package/skills/lunnoa-solution-design/SKILL.md +190 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunnoa/client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "TypeScript SDK for building applications on top of Lunnoa Automate — typed client for the Public API (agents, workflows, executions, entities, knowledge, queues), SSE agent chat streaming, and deployment-specific codegen",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -20,19 +20,20 @@
|
|
|
20
20
|
"./package.json": "./package.json"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
|
-
"lunnoa-client": "
|
|
23
|
+
"lunnoa-client": "dist/cli.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"dist"
|
|
26
|
+
"dist",
|
|
27
|
+
"skills"
|
|
27
28
|
],
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
|
|
30
|
-
|
|
31
31
|
"codegen:spec": "openapi-typescript spec/openapi.public.json -o src/generated/schema.d.ts",
|
|
32
32
|
"test": "vitest run",
|
|
33
33
|
"test:watch": "vitest",
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
35
|
"check:codegen-sample": "node --experimental-strip-types scripts/check-codegen-sample.ts && tsc --noEmit -p scripts/tsconfig.codegen-sample.json",
|
|
36
|
+
"test:acceptance": "tsx scripts/acceptance-soak.ts",
|
|
36
37
|
"prepublishOnly": "pnpm run build",
|
|
37
38
|
"release": "semantic-release"
|
|
38
39
|
},
|
|
@@ -67,11 +68,12 @@
|
|
|
67
68
|
"react-dom": "^19.1.0",
|
|
68
69
|
"semantic-release": "^25.0.3",
|
|
69
70
|
"tsup": "^8.3.0",
|
|
71
|
+
"tsx": "^4.23.7",
|
|
70
72
|
"typescript": "~5.7.0",
|
|
71
73
|
"vitest": "^3.2.0"
|
|
72
74
|
},
|
|
73
75
|
"publishConfig": {
|
|
74
|
-
"access": "
|
|
76
|
+
"access": "public",
|
|
75
77
|
"registry": "https://registry.npmjs.org/"
|
|
76
78
|
},
|
|
77
79
|
"repository": {
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lunnoa-client
|
|
3
|
+
description: >-
|
|
4
|
+
Builds custom frontends and backends on top of a Lunnoa Automate deployment
|
|
5
|
+
using the @lunnoa/client TypeScript SDK. Covers the two auth patterns
|
|
6
|
+
(server-side lna_ API keys via createServerClient, browser user JWTs via
|
|
7
|
+
createBrowserClient + lunnoa.auth.login / refresh / SSO — never lna_ keys in a
|
|
8
|
+
browser), discovery of the deployment's agents, workflows, and entity
|
|
9
|
+
types, running the deployment codegen first for typed accessors, querying
|
|
10
|
+
and creating entities, triggering workflows and polling executions,
|
|
11
|
+
resuming NEEDS_INPUT executions from custom forms, streaming agent chat
|
|
12
|
+
over SSE in the AI SDK UIMessage format, and rendering execution progress
|
|
13
|
+
from the executionPath expansion. Use when asked to build a portal,
|
|
14
|
+
internal tool, dashboard, chat UI, form, or any application backed by
|
|
15
|
+
Lunnoa Automate agents, workflows, entities (Objects), queues, or
|
|
16
|
+
knowledge; or when the user mentions @lunnoa/client, lna_ API keys, the
|
|
17
|
+
Lunnoa Public API, stream-message, executionPath, or NEEDS_INPUT.
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Building custom UIs on Lunnoa Automate with @lunnoa/client
|
|
21
|
+
|
|
22
|
+
Lunnoa Automate is a self-hosted agent and workflow automation platform. Its
|
|
23
|
+
Public API (machine-authenticated, stability-promised) lets you build fully
|
|
24
|
+
custom applications on top: portals, review tools, chat UIs, dashboards. The
|
|
25
|
+
official SDK is **`@lunnoa/client`** (npm).
|
|
26
|
+
|
|
27
|
+
This skill teaches an agent how to build such an application correctly.
|
|
28
|
+
|
|
29
|
+
**If requirements are unclear, load `lunnoa-solution-design` first** (interview
|
|
30
|
+
→ build brief → then return here to implement).
|
|
31
|
+
|
|
32
|
+
**Boundary:** `@lunnoa/toolkit` is a different SDK for building integration
|
|
33
|
+
apps that run *inside* the platform (`createAction` / `createApp`). For
|
|
34
|
+
applications *on top of* the platform, always use `@lunnoa/client` —
|
|
35
|
+
including `defineAction` / `defineWorkflow` / `defineAgent`, `runs.start`,
|
|
36
|
+
and CLI deploy. Do not confuse toolkit `createAction` with client
|
|
37
|
+
`defineAction`.
|
|
38
|
+
|
|
39
|
+
**No prebuilt UI exists on purpose.** The SDK ships no components or widgets.
|
|
40
|
+
Optional **headless** React hooks live in `@lunnoa/client/react`. You build the
|
|
41
|
+
UI in the app's own framework and design system; the SDK provides typed data
|
|
42
|
+
access and protocols.
|
|
43
|
+
|
|
44
|
+
Human docs (detail): Lunnoa Docs → Develop → TypeScript client SDK.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Mandatory checklist (always)
|
|
49
|
+
|
|
50
|
+
Run these before inventing resources or shipping UI code:
|
|
51
|
+
|
|
52
|
+
1. **Auth** — Pattern A (`createServerClient` + `lna_` on the server) or
|
|
53
|
+
Pattern B (`createBrowserClient` + JWT). **Never** put `lna_` in the
|
|
54
|
+
browser (SDK throws when `window` exists).
|
|
55
|
+
2. **Codegen** — When building typed UIs, run
|
|
56
|
+
`npx @lunnoa/client codegen` against the deployment first.
|
|
57
|
+
3. **Discover before inventing** — `workflowApps.list()`, `agents.list`,
|
|
58
|
+
`workflows.list`, `entityTypes.list` (and `discovery.enabledFeatures()`).
|
|
59
|
+
Do not invent catalogue action ids.
|
|
60
|
+
4. **Run vs deploy** — Ad-hoc: `actions.run` / `runs.start` /
|
|
61
|
+
`workflows.execute`. Durable shared automation: CLI
|
|
62
|
+
`npx @lunnoa/client workflows|agents deploy` (upsert by slug).
|
|
63
|
+
5. **Toolkit boundary** — Client `defineAction` ≠ toolkit `createAction`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 1. Auth: decide the pattern first
|
|
68
|
+
|
|
69
|
+
| Pattern | Credential | Use when |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| **A. Backend holds the key** | `createServerClient({ apiKey: 'lna_...' })` | The app has its own users/login; Lunnoa never needs to know them. All Lunnoa calls go through the app's backend. |
|
|
72
|
+
| **B. End users are Lunnoa users** | `createBrowserClient({ baseUrl })` + `lunnoa.auth.login` | Per-user permissions, agent shares, and task history must apply. The browser talks to Lunnoa directly. |
|
|
73
|
+
|
|
74
|
+
Hard rules:
|
|
75
|
+
|
|
76
|
+
- **NEVER put an `lna_` key in browser code, env vars bundled into the client,
|
|
77
|
+
or anything shipped to the browser.** The SDK throws if an `lna_` key is
|
|
78
|
+
used while `window` exists. A leaked key exposes the whole workspace.
|
|
79
|
+
- For browser apps with their own user base, use the **backend-for-frontend
|
|
80
|
+
pattern**: the browser calls your backend; your backend holds the key and
|
|
81
|
+
calls Lunnoa. Next.js route handlers / server actions count as backend.
|
|
82
|
+
- Pattern B needs the deployment to allow the app's origin via
|
|
83
|
+
`CORS_ALLOWED_ORIGINS`.
|
|
84
|
+
- Keys are minted by a SuperAdmin in **Adminspace → API keys** (workspace +
|
|
85
|
+
RBAC role + optional expiry; secret shown once). Recommend a minimal custom
|
|
86
|
+
role (e.g. `entities:read`, `entities:create`, `agents:use`), not Admin.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// Pattern A (server only)
|
|
90
|
+
import { createServerClient } from '@lunnoa/client';
|
|
91
|
+
const lunnoa = createServerClient({
|
|
92
|
+
baseUrl: process.env.LUNNOA_URL!,
|
|
93
|
+
apiKey: process.env.LUNNOA_API_KEY!,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Pattern B (browser) — login + localStorage token store + auto-refresh on 401
|
|
97
|
+
import { createBrowserClient, isLoginRequires2FA } from '@lunnoa/client';
|
|
98
|
+
const lunnoa = createBrowserClient({ baseUrl });
|
|
99
|
+
const result = await lunnoa.auth.login({ email, password });
|
|
100
|
+
if (isLoginRequires2FA(result)) {
|
|
101
|
+
await lunnoa.auth.verify2faLogin({
|
|
102
|
+
sessionToken: result.sessionToken,
|
|
103
|
+
token: totpOrBackupCode,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const me = await lunnoa.auth.me();
|
|
107
|
+
// SSO: lunnoa.auth.sso.listProviders() / getLoginUrl(id), then loginWithToken
|
|
108
|
+
// Logout is local only: await lunnoa.auth.logout()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You can still pass a raw `accessToken` getter to `new LunnoaClient(...)` when
|
|
112
|
+
you already hold a JWT. Prefer `tokenStore` / `createBrowserClient` when the
|
|
113
|
+
SDK should persist and refresh tokens.
|
|
114
|
+
|
|
115
|
+
SSO browser callbacks redirect to `{CLIENT_URL}/verify-token`, not an arbitrary
|
|
116
|
+
custom origin. Password login and `loginWithToken` work from CORS-allowed apps.
|
|
117
|
+
|
|
118
|
+
**React portals:** import from `@lunnoa/client/react`:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import {
|
|
122
|
+
LunnoaAuthProvider,
|
|
123
|
+
useLunnoaAuth,
|
|
124
|
+
useLunnoaClient,
|
|
125
|
+
useExecutionProgress,
|
|
126
|
+
useNeedsInput,
|
|
127
|
+
useAgentChat,
|
|
128
|
+
useApprovalsInbox,
|
|
129
|
+
useEntityList,
|
|
130
|
+
} from '@lunnoa/client/react';
|
|
131
|
+
|
|
132
|
+
<LunnoaAuthProvider baseUrl={baseUrl}>
|
|
133
|
+
<App />
|
|
134
|
+
</LunnoaAuthProvider>
|
|
135
|
+
|
|
136
|
+
const { user, status, login, logout, verify2faLogin } = useLunnoaAuth();
|
|
137
|
+
const lunnoa = useLunnoaClient(); // same client for entities / actions / chat
|
|
138
|
+
const { progress } = useExecutionProgress(executionId);
|
|
139
|
+
const { waiting, pendingInput, submitInput } = useNeedsInput(progress);
|
|
140
|
+
const { messages, send, stop, taskId } = useAgentChat(agentId); // peer: @ai-sdk/react
|
|
141
|
+
const { items, decide } = useApprovalsInbox({ pollIntervalMs: 15_000 });
|
|
142
|
+
const { data, setPage } = useEntityList({ objectTypeSlug: 'invoice' });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Peers: `react` ≥ 18 for `/react`; `@ai-sdk/react` only for `useAgentChat`.
|
|
146
|
+
Errors are `LunnoaApiError` with `.status`, `.body`, `.isUnauthorized`,
|
|
147
|
+
`.isForbidden`, `.isRateLimited`. A 403 means the key's role or the
|
|
148
|
+
deployment's licence edition does not cover the endpoint.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 2. Run codegen before writing UI code
|
|
153
|
+
|
|
154
|
+
Entity types, workflow inputs, and agents are **per-deployment**, so the
|
|
155
|
+
generic SDK types them loosely. Always generate deployment-specific types
|
|
156
|
+
first — a UI built against generated types cannot construct an invalid form
|
|
157
|
+
or payload:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npx @lunnoa/client codegen --url <deployment-url> --api-key <lna_key> --out ./src/lunnoa
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This emits `src/lunnoa/lunnoa.generated.ts` with `createDeploymentClient`:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { createDeploymentClient } from './lunnoa/lunnoa.generated';
|
|
167
|
+
const lunnoa = createDeploymentClient(new LunnoaClient({ baseUrl, apiKey }));
|
|
168
|
+
|
|
169
|
+
await lunnoa.entities.invoice.list({ state: 'pending' }); // typed attributes + states
|
|
170
|
+
await lunnoa.workflows.processInvoice.execute({ invoiceId }); // typed trigger inputs
|
|
171
|
+
await lunnoa.agents.supportAgent.streamMessage(taskId, 'Hi'); // agents by name
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Re-run codegen when the deployment's definitions change. If codegen is not
|
|
175
|
+
possible (no key yet), fall back to the generic namespaces and discover at
|
|
176
|
+
runtime (next section).
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 3. Discover what the deployment offers
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Feature flags — adapt the UI to the deployment's edition/infrastructure
|
|
184
|
+
const features = await lunnoa.discovery.enabledFeatures();
|
|
185
|
+
|
|
186
|
+
// Agents, workflows, entity types
|
|
187
|
+
const agents = await lunnoa.agents.list({ expansion: ['description'] });
|
|
188
|
+
const workflows = await lunnoa.workflows.list({ expansion: ['description', 'isActive'] });
|
|
189
|
+
const entityTypes = await lunnoa.entityTypes.list({
|
|
190
|
+
expansion: ['attributeSchema', 'stateSchema'],
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- `attributeSchema.sections[].fields[]` defines each entity field: `id`
|
|
195
|
+
(attribute key), `label`, `type` (`text`, `number`, `currency`, `date`,
|
|
196
|
+
`dropdown` with `config.options`, `boolean`, ...), `required`. Build forms
|
|
197
|
+
and tables from this.
|
|
198
|
+
- `stateSchema.states[]` / `transitions[]` is the entity state machine.
|
|
199
|
+
`entities.getStateTransitions(id)` returns the transitions valid *now* —
|
|
200
|
+
render state buttons from that, not from the full schema.
|
|
201
|
+
- A workflow's manual-trigger input fields live on
|
|
202
|
+
`workflows.get(id, { expansion: ['triggerNode'] })` →
|
|
203
|
+
`triggerNode.value.customInputConfig` (each field: `id`, `label`,
|
|
204
|
+
`inputType`, `required`, `selectOptions`).
|
|
205
|
+
- `workflowApps.list()` resolves `executionPath` step `appId`/`actionId`
|
|
206
|
+
pairs to human-readable names and icons.
|
|
207
|
+
|
|
208
|
+
List conventions used across the API: `expansion` (comma-separated extra
|
|
209
|
+
fields — responses are minimal by default, always request what you render),
|
|
210
|
+
`filterBy` (`key:value` pairs), `includeType`.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 4. Recipe: entity table + create form
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Table: one page (default pageSize 20, max 100)
|
|
218
|
+
const { data, pagination } = await lunnoa.entities.list({
|
|
219
|
+
objectTypeSlug: 'invoice',
|
|
220
|
+
state: 'pending',
|
|
221
|
+
search: userQuery,
|
|
222
|
+
page: 1,
|
|
223
|
+
pageSize: 25,
|
|
224
|
+
expansion: ['attributes', 'createdAt'],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Everything (lazy pages) — exports, aggregations
|
|
228
|
+
for await (const entity of lunnoa.entities.iterate({ objectTypeSlug: 'invoice' })) { ... }
|
|
229
|
+
|
|
230
|
+
// Create — attributes keyed by field ID from the attributeSchema
|
|
231
|
+
await lunnoa.entities.create({
|
|
232
|
+
name: 'INV-2026-0042',
|
|
233
|
+
objectTypeId: invoiceType.id,
|
|
234
|
+
attributes: { amount: 1250.5, status: 'pending' },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// State change (validated against the type's state machine)
|
|
238
|
+
await lunnoa.entities.changeState(entityId, { newState: 'approved', reason: '...' });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
With codegen: `lunnoa.entities.invoice.create({ name, attributes })` is
|
|
242
|
+
compile-time checked.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 5. Recipe: trigger a workflow and render progress
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const { id } = await lunnoa.workflows.execute(workflowId, inputs);
|
|
250
|
+
|
|
251
|
+
// Poll with backoff until terminal (SUCCESS/FAILED/CANCELLED) or NEEDS_INPUT
|
|
252
|
+
const execution = await lunnoa.executions.waitUntilFinished(id, {
|
|
253
|
+
expansion: ['executionPath'],
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Render progress from the `executionPath` expansion — never from raw
|
|
258
|
+
`nodes`/`edges`.** Prefer the SDK helpers for custom timeline UIs:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const progress = await lunnoa.executions.getProgress(id);
|
|
262
|
+
// progress.steps: label, status, iconUrl, appLogoUrl, durationMs, activeStepIndex
|
|
263
|
+
|
|
264
|
+
for await (const snap of lunnoa.executions.watchProgress(id)) {
|
|
265
|
+
// live SSE snapshots (execution.progress) until terminal
|
|
266
|
+
renderTimeline(snap);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Or poll manually:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
const execution = await lunnoa.executions.get(id, {
|
|
274
|
+
expansion: ['status', 'executionPath'],
|
|
275
|
+
});
|
|
276
|
+
// execution.executionPath: Array<{
|
|
277
|
+
// nodeId, label, appId, actionId?, status, statusMessage?,
|
|
278
|
+
// startTime?, endTime?, iterationOf?, iterationIndex?, iterationCount?
|
|
279
|
+
// }>
|
|
280
|
+
// status per step: RUNNING | SUCCESS | FAILED | NEEDS_INPUT | SCHEDULED | RETRYING | WAITING
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Render a step list: label + status chip + duration; nest steps carrying
|
|
284
|
+
`iterationOf` under their loop parent; resolve `appId`/`actionId` to icons
|
|
285
|
+
via `workflowApps.list()` (done for you by `getProgress` / `watchProgress`).
|
|
286
|
+
`watchProgress` uses `GET /executions/:id/stream` (Bearer fetch; not EventSource).
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 5b. Recipe: run a single catalogue action (ad-hoc Execution)
|
|
291
|
+
|
|
292
|
+
`actions.run` creates a **one-step Execution** with `source: SDK`. It is not a
|
|
293
|
+
separate ActionRun product; list/get it via `executions.*` like any other run.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Discover appId / actionId / inputConfig
|
|
297
|
+
const apps = await lunnoa.workflowApps.list();
|
|
298
|
+
const http = apps.find((a) => a.id === 'http');
|
|
299
|
+
// http.actions[].id / .inputConfig
|
|
300
|
+
|
|
301
|
+
const result = await lunnoa.actions.run({
|
|
302
|
+
appId: 'http',
|
|
303
|
+
actionId: 'http_action_send-request',
|
|
304
|
+
// connectionId: '<Connection.id UUID>', // required if needsConnection + multiple
|
|
305
|
+
// projectId: '<project UUID>', // required if multiple projects
|
|
306
|
+
input: { method: 'GET', url: 'https://example.com' },
|
|
307
|
+
name: 'Ping example',
|
|
308
|
+
});
|
|
309
|
+
// result: { id, status, statusMessage?, output?, executionPath? }
|
|
310
|
+
|
|
311
|
+
const again = await lunnoa.executions.get(result.id, {
|
|
312
|
+
expansion: ['status', 'source', 'name', 'executionPath'],
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Connection rules (fail-closed):** use the Connection instance UUID
|
|
317
|
+
(`Connection.id` from Connections UI / `connections.list()`). If omitted:
|
|
318
|
+
sole usable connection for the app, else workspace default (`isDefault`),
|
|
319
|
+
else 400 with candidates. Never silently pick “oldest” / “first created”.
|
|
320
|
+
|
|
321
|
+
Built-in HTTP catalogue action (no connection): `http` /
|
|
322
|
+
`http_action_send-request`.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 5c. Recipe: define*, runs.start, and CLI deploy
|
|
327
|
+
|
|
328
|
+
Pure typed config (no network):
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import {
|
|
332
|
+
defineAction,
|
|
333
|
+
defineAgent,
|
|
334
|
+
defineWorkflow,
|
|
335
|
+
} from '@lunnoa/client';
|
|
336
|
+
|
|
337
|
+
export const ping = defineWorkflow({
|
|
338
|
+
slug: 'ping',
|
|
339
|
+
steps: [
|
|
340
|
+
{
|
|
341
|
+
use: defineAction({
|
|
342
|
+
id: 'http_action_send-request',
|
|
343
|
+
input: { method: 'GET', url: 'https://example.com' },
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
export const triage = defineAgent({
|
|
350
|
+
slug: 'support-triage',
|
|
351
|
+
model: 'gpt-4o',
|
|
352
|
+
instructions: 'Triage mail. Be concise.',
|
|
353
|
+
tools: [defineAction({ id: 'http_action_send-request' })],
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Ad-hoc multi-step run (one Execution, `source: SDK`):
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const result = await lunnoa.runs.start(ping);
|
|
361
|
+
// or lunnoa.runs.startAndWait(ping)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Deploy to Automate UI (upsert by slug; prefer CLI):
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
npx @lunnoa/client workflows deploy ./ping.ts --url … --api-key … --project-id …
|
|
368
|
+
npx @lunnoa/client agents deploy ./triage.ts --url … --api-key … --project-id …
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`defineAction` binds catalogue actions only. Toolkit `createAction` is a
|
|
372
|
+
different job (in-platform apps).
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## 6. Recipe: resume a NEEDS_INPUT execution from a custom form
|
|
377
|
+
|
|
378
|
+
Workflows can pause for human input. The waiting step appears in
|
|
379
|
+
`executionPath` with `status: 'NEEDS_INPUT'`:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
const waiting = execution.executionPath?.find((s) => s.status === 'NEEDS_INPUT');
|
|
383
|
+
if (waiting) {
|
|
384
|
+
// Render your form, then submit values keyed by field ID:
|
|
385
|
+
await lunnoa.executions.submitInput(execution.id, waiting.nodeId, {
|
|
386
|
+
approved: true,
|
|
387
|
+
comment: formValues.comment,
|
|
388
|
+
});
|
|
389
|
+
// Acknowledged immediately; resumes asynchronously:
|
|
390
|
+
const finished = await lunnoa.executions.waitUntilFinished(execution.id);
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`waitUntilFinished` stops on `NEEDS_INPUT` by default (so the form can be
|
|
395
|
+
shown); pass `stopOnNeedsInput: false` to wait through it.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## 7. Recipe: agent chat UI (SSE streaming)
|
|
400
|
+
|
|
401
|
+
The protocol is the AI SDK UIMessage chunk stream (same as the bundled UI).
|
|
402
|
+
A task = one conversation thread; client-generated UUIDs are fine — the
|
|
403
|
+
first message creates the task.
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
const taskId = crypto.randomUUID();
|
|
407
|
+
const stream = await lunnoa.agentChat.streamMessage(agentId, taskId, userText);
|
|
408
|
+
|
|
409
|
+
for await (const chunk of stream) {
|
|
410
|
+
// chunk.type: 'start' | 'text-start' | 'text-delta' | 'text-end' |
|
|
411
|
+
// 'tool-input-available' | 'tool-output-available' |
|
|
412
|
+
// 'reasoning-*' | 'finish' | 'error' | ...
|
|
413
|
+
if (chunk.type === 'text-delta') appendToUi(chunk.delta);
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
- **React apps:** prefer `useAgentChat` from `@lunnoa/client/react` (optional
|
|
418
|
+
peer `@ai-sdk/react`), which wraps AI SDK `useChat` + `DefaultChatTransport`
|
|
419
|
+
pointed at
|
|
420
|
+
`${baseUrl}/api/agents/${agentId}/tasks/${taskId}/stream-message` with an
|
|
421
|
+
`Authorization` header. You can also wire `useChat` yourself. Use
|
|
422
|
+
`@lunnoa/client` for everything non-chat.
|
|
423
|
+
- **Non-React:** iterate the `AgentChatStream` directly, or pass
|
|
424
|
+
`stream.toUIMessageStream()` to the AI SDK's `readUIMessageStream` for
|
|
425
|
+
assembled `UIMessage` snapshots.
|
|
426
|
+
- **Resume after disconnect:** `agentChat.resumeStream(agentId, taskId)`
|
|
427
|
+
replays the in-flight turn from the beginning; returns `null` (HTTP 204)
|
|
428
|
+
when nothing is running.
|
|
429
|
+
- **Stop:** `agentChat.stop(agentId, taskId, { assistantMessage })` cancels
|
|
430
|
+
the turn and can persist the partial reply.
|
|
431
|
+
- **History:** `tasks.get(taskId, { expansion: ['messages'] })` returns the
|
|
432
|
+
persisted conversation in UIMessage format.
|
|
433
|
+
- **Non-streaming:** `tasks.message(agentId, taskId, text)` waits for the
|
|
434
|
+
full turn.
|
|
435
|
+
- Proxies in front of the deployment must not buffer SSE responses.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## 8. Human-in-the-loop queues
|
|
440
|
+
|
|
441
|
+
Queues are the platform's work-item surface for review UIs. Items can
|
|
442
|
+
optionally link to an **Object** (entity) via `objectId` — the business
|
|
443
|
+
record the work is about. Creating a queue item does **not** create an Object.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
const queues = await lunnoa.queues.list();
|
|
447
|
+
const { items } = await lunnoa.queueItems.list(queueId, { limit: 50 });
|
|
448
|
+
for await (const item of lunnoa.queueItems.iterate(queueId)) { ... }
|
|
449
|
+
await lunnoa.queueItems.updateStatus(queueId, itemId, 'SUCCESS');
|
|
450
|
+
|
|
451
|
+
// Restrict which ObjectTypes a queue accepts (empty = any)
|
|
452
|
+
await lunnoa.queues.create({
|
|
453
|
+
name: 'IT triage',
|
|
454
|
+
allowedObjectTypeIds: [ticketType.id],
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Link work to an existing Object
|
|
458
|
+
await lunnoa.queueItems.create(triageQueueId, {
|
|
459
|
+
objectId: ticket.id,
|
|
460
|
+
externalId: ticket.id,
|
|
461
|
+
data: { priority: 'high' },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Work panel for that Object (across all queues)
|
|
465
|
+
const { items: work } = await lunnoa.queueItems.listByObject(ticket.id);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 9. Recipe: Approvals inbox (Request Approval nodes)
|
|
471
|
+
|
|
472
|
+
`lunnoa.approvals` mirrors Automate **Approvals** (`/approvals`). It uses the
|
|
473
|
+
workspace API (`/api/approvals/...`), not Public API v1. The inbox is filtered
|
|
474
|
+
to requests where the **current user** is eligible and has not decided yet.
|
|
475
|
+
Prefer pattern B (user JWT). Role needs `approvals:read` / `approvals:decide`.
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
const { items } = await lunnoa.approvals.inbox(); // pending + partially_approved
|
|
479
|
+
|
|
480
|
+
for (const person of items[0]?.eligibleApprovers ?? []) {
|
|
481
|
+
// person.workspaceUserId, email, name?
|
|
482
|
+
console.log(person.email);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
await lunnoa.approvals.decide(items[0]!.approvalId, {
|
|
486
|
+
decision: 'approved', // or 'rejected'
|
|
487
|
+
comment: 'Within policy limits.',
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Or when you already have executionId + nodeId:
|
|
491
|
+
await lunnoa.executions.submitApproval(executionId, nodeId, {
|
|
492
|
+
decision: 'rejected',
|
|
493
|
+
comment: 'Amount exceeds limit.',
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Do **not** use `submitInput` for Request Approval nodes; that is for generic
|
|
498
|
+
`NEEDS_INPUT` forms. Partial approvals keep the execution paused; terminal
|
|
499
|
+
decisions resume it. `eligibleApprovers` is current-stage membership with
|
|
500
|
+
emails (capped; not Azure object IDs).
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## 10. Ship checklist (after the mandatory checklist)
|
|
505
|
+
|
|
506
|
+
1. Confirm base URL + credential pattern (A or B); wire the key server-side only.
|
|
507
|
+
For Pattern B prefer `createBrowserClient` and `lunnoa.auth.login` (not raw
|
|
508
|
+
localStorage getters unless you already hold a JWT).
|
|
509
|
+
2. Run `npx @lunnoa/client codegen` against the deployment; commit the output.
|
|
510
|
+
3. Call `discovery.enabledFeatures()` and hide unsupported areas.
|
|
511
|
+
4. Build forms/tables from `attributeSchema` / `customInputConfig` (or the generated types).
|
|
512
|
+
5. Render workflow progress from `executionPath`; handle `NEEDS_INPUT` with `submitInput`.
|
|
513
|
+
6. For single-action calls, use `actions.run`; for linear multi-step, use `runs.start` / `defineWorkflow`. Deploy with `npx @lunnoa/client workflows|agents deploy`. Discover catalogue via `workflowApps.list()`.
|
|
514
|
+
7. For Request Approval nodes, use `approvals.inbox` / `approvals.decide` (or `executions.submitApproval`).
|
|
515
|
+
8. Chat: stream via SSE, handle resume (reconnects) and stop, load history from the task.
|
|
516
|
+
9. Handle `LunnoaApiError`: 401 → re-auth; 403 → hide the capability; 429 → back off.
|
|
517
|
+
10. Do not use toolkit `createAction` when you meant client `defineAction`.
|