@alvera-ai/platform-sdk 0.12.1 → 0.13.0-next.gf3d2a4a
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/.agent/AGENTS.md +121 -19
- package/.agent/account_management.md +79 -38
- package/.agent/action_logs.md +144 -0
- package/.agent/action_status_updaters.md +237 -53
- package/.agent/ai_agents.md +149 -53
- package/.agent/ai_sandbox.md +11 -4
- package/.agent/async.md +16 -6
- package/.agent/connected_apps.md +116 -4
- package/.agent/cookbook/_fixtures/README.md +9 -9
- package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid → payments/_compliance_screenings_payments_compliance_screening.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_mdm.liquid → payments/_compliance_screenings_payments_mdm.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_mdm.liquid → payments/_payment_accounts_payments_mdm.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_payment_account.liquid → payments/_payment_accounts_payments_payment_account.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_customer.liquid → subscription/_customers_subscription_customer.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_mdm.liquid → subscription/_customers_subscription_mdm.liquid} +1 -1
- package/.agent/cookbook/_setup/foundation.md +24 -4
- package/.agent/cookbook/_setup/healthcare.md +24 -4
- package/.agent/cookbook/_setup/{payment_risk.md → payments.md} +51 -31
- package/.agent/cookbook/_setup/{accounts_receivable.md → subscription.md} +51 -31
- package/.agent/cookbook/action-status-updaters.md +25 -18
- package/.agent/cookbook/bulk-ingest.md +15 -15
- package/.agent/cookbook/dunning-sms-for-delinquent.md +21 -21
- package/.agent/cookbook/{custom-tables.md → generic-tables.md} +44 -41
- package/.agent/cookbook/invite-team.md +19 -13
- package/.agent/cookbook/kyc-notification-on-account-activation.md +26 -26
- package/.agent/cookbook/marketing-campaign-send.md +1003 -0
- package/.agent/cookbook/rest-fetch.md +9 -9
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +22 -22
- package/.agent/cookbook/system-templates.md +15 -15
- package/.agent/cookbook/triage-prospects-by-priority.md +17 -17
- package/.agent/cookbook/welcome-sms-for-customers.md +21 -21
- package/.agent/data_activation_clients.md +177 -42
- package/.agent/data_sources.md +55 -37
- package/.agent/datalakes.md +30 -17
- package/.agent/debugging.md +48 -14
- package/.agent/errors.md +39 -17
- package/.agent/generic_tables.md +150 -51
- package/.agent/interoperability_contracts.md +114 -15
- package/.agent/mdm.md +171 -93
- package/.agent/messages.md +252 -0
- package/.agent/templates.md +21 -4
- package/.agent/tool-call-configs.md +1 -1
- package/.agent/tools.md +150 -60
- package/.agent/type_naming.md +79 -18
- package/.agent/workflows.md +408 -62
- package/LICENSE +21 -93
- package/README.md +2 -2
- package/dist/bin/platform-sdk.mjs +53 -1
- package/dist/bin/platform-sdk.mjs.map +1 -1
- package/dist/index.d.mts +582 -66
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +654 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -6
- /package/.agent/cookbook/_fixtures/{accounts_receivable → subscription}/stripe_customers_batch1.csv +0 -0
|
@@ -44,13 +44,12 @@ const { data: created } = await api.actionStatusUpdaters.create(
|
|
|
44
44
|
end_time: '{{ now }}',
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
-
// Liquid
|
|
47
|
+
// Liquid, rendered ONCE PER EVENT — the event IS the assigns, so there is no
|
|
48
|
+
// `events` list to loop. Emits a FLAT object: the message to update, plus the
|
|
49
|
+
// fields to set at the top level.
|
|
48
50
|
message_config: {
|
|
49
51
|
type: 'custom',
|
|
50
|
-
body:
|
|
51
|
-
'{% for event in events %}' +
|
|
52
|
-
'{"external_id": "{{ event.message_id }}", "set_params": {"status": "{{ event.status }}"}}' +
|
|
53
|
-
'{% endfor %}',
|
|
52
|
+
body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
|
|
54
53
|
},
|
|
55
54
|
},
|
|
56
55
|
)
|
|
@@ -75,29 +74,54 @@ updater_body_type: 'cloud_watch_request'
|
|
|
75
74
|
follow the same `<verb>_request` naming and carry the fields
|
|
76
75
|
their target API requires.)
|
|
77
76
|
|
|
78
|
-
`start_time` / `end_time`
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
`start_time` / `end_time` are Liquid, and they **must render to
|
|
78
|
+
unix milliseconds** — that is what the poll request takes. On
|
|
79
|
+
these two fields `now` is a **variable already holding unix ms**,
|
|
80
|
+
so pipe it through the relative-time filters:
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
```
|
|
83
|
+
start_time: '{{ now | minutes_ago: 45 }}' // 45 minutes back
|
|
84
|
+
end_time: '{{ now }}' // right now
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Also available: `hours_ago: N`.
|
|
88
|
+
|
|
89
|
+
**Do not use the ISO-8601 `now` filter here.** `{{ "" | now }}` is
|
|
90
|
+
a different thing — it renders a datetime *string*, which is not a
|
|
91
|
+
valid window, and the platform rejects it at create. An ISO 8601
|
|
92
|
+
literal is not valid on these fields either.
|
|
93
|
+
|
|
94
|
+
### `message_config.body` renders ONCE PER EVENT
|
|
95
|
+
|
|
96
|
+
The template is rendered **once for each fetched event, with that
|
|
97
|
+
event as the entire assigns**. There is no `events` collection in
|
|
98
|
+
scope — do not loop.
|
|
99
|
+
|
|
100
|
+
```liquid
|
|
101
|
+
{"external_id": "{{ notification.messageId }}", "status": "delivered"}
|
|
102
|
+
```
|
|
84
103
|
|
|
85
|
-
|
|
86
|
-
response from the polling tool. Each iteration must render
|
|
87
|
-
one JSON object with two required fields:
|
|
104
|
+
It must render a **flat** JSON object:
|
|
88
105
|
|
|
89
106
|
```
|
|
90
|
-
external_id string — the message
|
|
91
|
-
the provider's id
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
(e.g. { status, delivered_at, error_code })
|
|
107
|
+
external_id string — the message to update (matches the message row's
|
|
108
|
+
external_id, i.e. the provider's id)
|
|
109
|
+
<field> any — each remaining TOP-LEVEL key is a field to set:
|
|
110
|
+
status, sent_at, delivered_at, read_at, opened_at
|
|
95
111
|
```
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
There is **no `set_params` wrapper**. Every key that is not
|
|
114
|
+
`external_id` *is* the update. Nesting the fields under
|
|
115
|
+
`set_params` renders an object the platform's field allowlist
|
|
116
|
+
drops, so the update silently applies to nothing.
|
|
117
|
+
|
|
118
|
+
The platform joins each rendered `external_id` to a message row
|
|
119
|
+
and applies the remaining fields. Events that don't match any
|
|
120
|
+
message are silently discarded — a provider poll routinely sees
|
|
121
|
+
events for messages this updater doesn't own.
|
|
122
|
+
|
|
123
|
+
Custom Liquid filters (`json_escape`, `e164`, `to_json`, ...) are
|
|
124
|
+
available here, as everywhere.
|
|
101
125
|
|
|
102
126
|
### `cron_expression` is a standard 5-field cron string
|
|
103
127
|
|
|
@@ -111,7 +135,7 @@ Sub-minute cadences are NOT supported. The minimum interval
|
|
|
111
135
|
the scheduler honors depends on platform-level configuration;
|
|
112
136
|
default minimum is 5 minutes.
|
|
113
137
|
|
|
114
|
-
### `updater_type` and `updater_body.updater_body_type`
|
|
138
|
+
### `updater_type` and `updater_body.updater_body_type` — set them to agree, but only one is load-bearing
|
|
115
139
|
|
|
116
140
|
The body carries TWO discriminator fields that name the same
|
|
117
141
|
choice from different angles:
|
|
@@ -123,25 +147,35 @@ choice from different angles:
|
|
|
123
147
|
body embed
|
|
124
148
|
```
|
|
125
149
|
|
|
126
|
-
|
|
150
|
+
The intended pairing is a fixed mapping:
|
|
127
151
|
|
|
128
152
|
```
|
|
129
153
|
updater_type: "cloud_watch" ↔ updater_body_type: "cloud_watch_request"
|
|
130
154
|
updater_type: "restapi" ↔ updater_body_type: "restapi_request"
|
|
131
155
|
```
|
|
132
156
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
**But the platform does NOT cross-validate the pair — a mismatch is
|
|
158
|
+
NOT rejected.** The two fields are cast independently; there is no
|
|
159
|
+
create-time check that `updater_type` matches
|
|
160
|
+
`updater_body_type`, so a mismatched-but-well-formed pair (e.g.
|
|
161
|
+
`updater_type: "cloud_watch"` + a `restapi_request` body) **persists
|
|
162
|
+
silently**. At runtime the platform dispatches on the polymorphic
|
|
163
|
+
**body** struct, so `updater_body_type` is the one that matters and
|
|
164
|
+
`updater_type` is effectively informational. Set them to agree
|
|
165
|
+
anyway — a future release may start enforcing it, and a lying
|
|
166
|
+
`updater_type` is a debugging trap — but don't expect a 422 to catch
|
|
167
|
+
a mismatch for you today.
|
|
139
168
|
|
|
140
169
|
### `updater_tool_id` and `sender_tool_ids` serve different roles
|
|
141
170
|
|
|
142
171
|
- `updater_tool_id` (single) — the tool the updater uses to
|
|
143
|
-
ISSUE the poll.
|
|
144
|
-
`status_poller` (e.g. a `cloud_watch_log_group` body type)
|
|
172
|
+
ISSUE the poll. By convention this is a tool whose `intent` is
|
|
173
|
+
`status_poller` (e.g. a `cloud_watch_log_group` body type) — but
|
|
174
|
+
the platform does **not** verify the intent at create time (it
|
|
175
|
+
checks only that the tool exists and belongs to the tenant). A
|
|
176
|
+
wrong-intent tool is accepted and only fails later, when the poll
|
|
177
|
+
actually runs. Wire a `status_poller` tool anyway; just don't rely
|
|
178
|
+
on a create-time 422 to enforce it.
|
|
145
179
|
- `sender_tool_ids` (array) — the tools whose sent messages
|
|
146
180
|
this updater is responsible for tracking. The match logic
|
|
147
181
|
joins each polled event back to a message row that was
|
|
@@ -178,10 +212,9 @@ Standard JSON:API envelopes per `errors.md`. Common rejections:
|
|
|
178
212
|
| `source.pointer` | Cause |
|
|
179
213
|
|-------------------------------------|----------------------------------------------------|
|
|
180
214
|
| `/cron_expression` | invalid cron syntax or below minimum interval |
|
|
181
|
-
| `/updater_tool_id` | tool
|
|
215
|
+
| `/updater_tool_id` | tool id doesn't exist or wrong datalake (intent is NOT checked — see §2) |
|
|
182
216
|
| `/sender_tool_ids/0` | tool id doesn't exist or wrong datalake |
|
|
183
|
-
| `/updater_body/updater_body_type` | enum
|
|
184
|
-
| `/updater_type` | mismatch with `updater_body.updater_body_type` |
|
|
217
|
+
| `/updater_body/updater_body_type` | value not in the polymorphic embed's type enum |
|
|
185
218
|
| `/message_config/body` | empty when `type: 'custom'` |
|
|
186
219
|
| `/base` | `cron_management_failed` — body validated and saved but scheduler registration failed; the row is rolled back |
|
|
187
220
|
|
|
@@ -204,11 +237,12 @@ branch.
|
|
|
204
237
|
### Read shapes
|
|
205
238
|
|
|
206
239
|
```
|
|
207
|
-
.list(tenantSlug, datalakeSlug)
|
|
208
|
-
|
|
240
|
+
.list(tenantSlug, datalakeSlug, query?) Paged: { data, meta }
|
|
241
|
+
(full Flop query — type_naming.md)
|
|
242
|
+
.get(tenantSlug, datalakeSlug, id) One row — UUID only
|
|
209
243
|
.metadata(tenantSlug, datalakeSlug) Markdown — catalog
|
|
210
244
|
.metadataDetails(tenantSlug, datalakeSlug, Markdown — one updater
|
|
211
|
-
|
|
245
|
+
id) (UUID only)
|
|
212
246
|
```
|
|
213
247
|
|
|
214
248
|
### Update
|
|
@@ -232,17 +266,23 @@ check or retry for partial-delete states.
|
|
|
232
266
|
|
|
233
267
|
## 6. Gotchas
|
|
234
268
|
|
|
235
|
-
1. **The polling tool
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
269
|
+
1. **The polling tool should be `status_poller` — but nothing
|
|
270
|
+
enforces it at create.** By convention `updater_tool_id` points
|
|
271
|
+
at a `status_poller`-intent tool, and the runtime poll assumes
|
|
272
|
+
that shape. The create endpoint, however, does not check the
|
|
273
|
+
tool's intent (only existence + tenant), so an `intent: 'sms'`
|
|
274
|
+
tool is accepted and fails only when the poll runs. Wire the
|
|
275
|
+
right tool; don't count on a create-time rejection to catch a
|
|
276
|
+
wrong one.
|
|
277
|
+
|
|
278
|
+
2. **`message_config.body` must render one flat JSON object per
|
|
279
|
+
event.** The two authoring mistakes that cannot work:
|
|
280
|
+
looping a `{% for event in events %}` collection (there is
|
|
281
|
+
none — the event *is* the assigns, so the loop renders
|
|
282
|
+
nothing), and nesting the fields under a `set_params` key
|
|
283
|
+
(the platform's allowlist drops them, so the update applies
|
|
284
|
+
to nothing). Both are now **rejected at create**. A render
|
|
285
|
+
that fails at run time is reported in `last_run_error`.
|
|
246
286
|
|
|
247
287
|
3. **`external_id` join is exact-match.** Events whose
|
|
248
288
|
`external_id` doesn't match a message row are discarded
|
|
@@ -251,10 +291,11 @@ check or retry for partial-delete states.
|
|
|
251
291
|
the bare id, transform inside the Liquid template (string
|
|
252
292
|
filters) so the rendered `external_id` matches.
|
|
253
293
|
|
|
254
|
-
4.
|
|
255
|
-
message row
|
|
256
|
-
|
|
257
|
-
|
|
294
|
+
4. **The fields you set must be writable columns on the
|
|
295
|
+
message row** — `status`, `sent_at`, `delivered_at`,
|
|
296
|
+
`read_at`, `opened_at`. Unknown keys are silently dropped
|
|
297
|
+
during the update. They go at the TOP LEVEL of the rendered
|
|
298
|
+
object, alongside `external_id`.
|
|
258
299
|
|
|
259
300
|
5. **Cron windows + polling-window overlap.** Choose
|
|
260
301
|
`start_time` / `end_time` to overlap consecutive cron
|
|
@@ -262,3 +303,146 @@ check or retry for partial-delete states.
|
|
|
262
303
|
transient delivery delays don't fall between two polls.
|
|
263
304
|
The defaults in the wire-shape example illustrate this
|
|
264
305
|
pattern: 30-minute cron, 45-minute window.
|
|
306
|
+
|
|
307
|
+
6. **Key `has_next` off the page being NON-EMPTY, not off the
|
|
308
|
+
cursor's presence.** Real providers (Mailgun is one) return a
|
|
309
|
+
`paging.next` URL on EVERY page — including the final, empty
|
|
310
|
+
one — so a pagination template that emits `"has_next": true`
|
|
311
|
+
whenever the cursor exists loops until the runaway cap. The
|
|
312
|
+
canonical template keys off content instead:
|
|
313
|
+
|
|
314
|
+
```liquid
|
|
315
|
+
"has_next": {% if items.size > 0 %}true{% else %}false{% endif %}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The poll follows pages while `has_next` is true and stops on
|
|
319
|
+
the first empty page.
|
|
320
|
+
|
|
321
|
+
## 7. Manual trigger via `refresh` — 202, and what `last_run_*` means
|
|
322
|
+
|
|
323
|
+
An action status updater CAN be fired on demand:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const { data } = await api.actionStatusUpdaters.refresh(
|
|
327
|
+
tenantSlug, datalakeSlug, updaterId,
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**`refresh` answers `202` with the updater row AS-IS — the poll is
|
|
332
|
+
ENQUEUED, not run.** The `last_run_*` fields on that response are the
|
|
333
|
+
PREVIOUS run's stamps; reading them as the outcome of the refresh you
|
|
334
|
+
just issued is the single most common mistake here. An optional body
|
|
335
|
+
carries a one-shot `updater_body` override (e.g. a widened
|
|
336
|
+
`start_time`/`end_time` window for a backfill) that does not mutate
|
|
337
|
+
the persisted updater. Apply jobs are asynchronous too — observe
|
|
338
|
+
delivery status on the message rows, not in this response.
|
|
339
|
+
(CLI: `alvera refresh-action-status-updater --id <uuid>`, alias
|
|
340
|
+
`refresh-updater`.)
|
|
341
|
+
|
|
342
|
+
The correct read is baseline-then-advance:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
const before = await api.actionStatusUpdaters.get(tenantSlug, datalakeSlug, updaterId)
|
|
346
|
+
const baseline = before.data.last_run_at ?? null // BEFORE refreshing
|
|
347
|
+
|
|
348
|
+
await api.actionStatusUpdaters.refresh(tenantSlug, datalakeSlug, updaterId)
|
|
349
|
+
|
|
350
|
+
let row = before.data
|
|
351
|
+
const deadline = Date.now() + 60_000
|
|
352
|
+
while (Date.now() < deadline) {
|
|
353
|
+
await new Promise((r) => setTimeout(r, 2_000))
|
|
354
|
+
const { data } = await api.actionStatusUpdaters.get(tenantSlug, datalakeSlug, updaterId)
|
|
355
|
+
if (data.last_run_at && data.last_run_at !== baseline) { row = data; break }
|
|
356
|
+
}
|
|
357
|
+
// row.last_run_status / last_run_error now describe THIS run
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
What the ASU still does NOT have is a **per-run log** (nothing like
|
|
361
|
+
a data activation client's `logs` or a workflow's `batchLogs`). The
|
|
362
|
+
run-outcome surface is four fields on the row itself, plus the halt
|
|
363
|
+
flag:
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
last_run_at when the poll worker last stamped a page
|
|
367
|
+
last_run_status 'ok' | 'error'
|
|
368
|
+
last_run_error why the last run failed (structured text)
|
|
369
|
+
last_run_events_found events fetched so far in the current run
|
|
370
|
+
status 'active' | 'cycle_detected'
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**A paginated run stamps `last_run_*` on EVERY page, not only the
|
|
374
|
+
final one.** Two consequences for anything asserting on them:
|
|
375
|
+
|
|
376
|
+
- `last_run_status: 'ok'` means *a page* succeeded — not that the run
|
|
377
|
+
completed. Do not treat it as terminal.
|
|
378
|
+
- `last_run_events_found` is a **running total** for the current run,
|
|
379
|
+
so a mid-run read is legitimately smaller than the final count.
|
|
380
|
+
|
|
381
|
+
Per-page stamping exists because the alternative hid a real incident:
|
|
382
|
+
an updater that polled continuously for 45 minutes was
|
|
383
|
+
indistinguishable from one that had never started, because only the
|
|
384
|
+
terminal page stamped and no page was ever terminal.
|
|
385
|
+
|
|
386
|
+
**`status` is the halt flag.** The server sets `'cycle_detected'` when
|
|
387
|
+
a run re-reads events it has already handled — the runtime backstop for
|
|
388
|
+
a poll whose pagination does not advance (a provider cursor that never
|
|
389
|
+
drains, or a one-shot `updater_body` override). Every later job then
|
|
390
|
+
fails without calling the provider, and `last_run_error` carries the
|
|
391
|
+
diagnostic. **Nothing clears it but setting `status` back to
|
|
392
|
+
`'active'`.** It is also the cheapest deterministic failure to assert
|
|
393
|
+
on: a non-advancing config trips it within seconds, no provider
|
|
394
|
+
credential rejection required. Related: a `restapi` updater that
|
|
395
|
+
declares pagination but whose `path`/`params`/`body` never read
|
|
396
|
+
`msg.pagination_context` or `msg.page` is now refused at create/update
|
|
397
|
+
— see §2's template contract.
|
|
398
|
+
|
|
399
|
+
**If a message's status is not reconciling, do this — in order:**
|
|
400
|
+
|
|
401
|
+
1. Call `refresh()`, wait for `last_run_at` to advance past the
|
|
402
|
+
baseline you captured, then read `last_run_status` /
|
|
403
|
+
`last_run_error` / `status`. They name the failing stage: window
|
|
404
|
+
render, source fetch, template render, JSON parse — or a halted
|
|
405
|
+
poll cycle.
|
|
406
|
+
2. Check the config against §2 above. A config that violates the
|
|
407
|
+
template contract is rejected at create, so a *deployed* updater's
|
|
408
|
+
templates are structurally sound — the fault is more likely the
|
|
409
|
+
event shape or the `external_id` join.
|
|
410
|
+
3. **If those don't explain it, ESCALATE TO A HUMAN.** Report the
|
|
411
|
+
updater id, `last_run_error`, and the event shape you expected.
|
|
412
|
+
|
|
413
|
+
A wrong config guess is worse than no change: replacing the unix-ms
|
|
414
|
+
poll window with the ISO-8601 `now` filter, for example, stops the
|
|
415
|
+
updater polling at all — verify each `refresh` outcome before
|
|
416
|
+
touching the config again.
|
|
417
|
+
|
|
418
|
+
## 8. Where delivery truth comes from (consumer view)
|
|
419
|
+
|
|
420
|
+
If you only READ the platform (an operator UI, a dashboard), this
|
|
421
|
+
is the part that matters:
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
dispatch writes: messages row, status 'sent',
|
|
425
|
+
external_id = provider's id
|
|
426
|
+
THIS updater writes: 'delivered' or 'failed' +
|
|
427
|
+
failure_reason (+ delivered_at)
|
|
428
|
+
nothing else writes those.
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
- **No updater configured → no delivery truth.** Rows stay `sent`
|
|
432
|
+
forever; a delivery-rate number computed from them is fiction.
|
|
433
|
+
Updaters are created manually (this page §1) — the platform does
|
|
434
|
+
not auto-create them per sender tool.
|
|
435
|
+
- **The join is `{external_id, set_params}`,** exact-match, silent
|
|
436
|
+
on misses (gotcha 3). "Eventually honest": a just-sent row is
|
|
437
|
+
neither delivered nor failed yet — show the in-between state.
|
|
438
|
+
- **Retry is not here.** The updater only reports. Re-sending a
|
|
439
|
+
failed message is `workflows.execute` for that subject +
|
|
440
|
+
decision (`mode: 'live'`, `manual_override: true`) — the same
|
|
441
|
+
call as any manual dispatch; there is no retry endpoint. See
|
|
442
|
+
`workflows.md` §8.
|
|
443
|
+
- **Engagement is not here either.** `opened_at` /
|
|
444
|
+
`form_submitted_at` arrive via
|
|
445
|
+
`connectedApps.updateMessageTracking` (magic-link seam,
|
|
446
|
+
`connected_apps.md` §6) and never touch `status`.
|
|
447
|
+
- Field-by-field semantics of the rows being updated:
|
|
448
|
+
`messages.md`, `action_logs.md`.
|
package/.agent/ai_agents.md
CHANGED
|
@@ -30,7 +30,7 @@ const { data: created } = await api.aiAgents.create(
|
|
|
30
30
|
|
|
31
31
|
tool_id: chatCompletionToolId, // see §2 — must be a tool with intent: 'llm_enrichment'
|
|
32
32
|
|
|
33
|
-
model: '
|
|
33
|
+
model: 'qwen3-vl:8b-instruct', // provider-specific model identifier
|
|
34
34
|
data_access: 'unregulated', // 'unregulated' | 'regulated' (compliance gate)
|
|
35
35
|
temperature: 0.0,
|
|
36
36
|
max_tokens: 2048,
|
|
@@ -139,15 +139,27 @@ data_access: 'regulated' — the LLM call receives raw values;
|
|
|
139
139
|
screening, manual review, KYC.
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
The
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
**The platform does NOT enforce `data_access` as immutable — a PUT
|
|
143
|
+
`update()` can change it today.** The field is cast by the same
|
|
144
|
+
changeset that `update()` uses and carries no immutability guard (and
|
|
145
|
+
isn't `readOnly` on the wire), so an update can flip `regulated ↔
|
|
146
|
+
unregulated`, which changes whether the agent sees raw or tokenized
|
|
147
|
+
values. **Treat it as immutable by discipline** — a data source
|
|
148
|
+
built expecting one tier shouldn't be silently re-tiered under it —
|
|
149
|
+
but know the platform won't stop a flip, so guard it in your own
|
|
150
|
+
change-control (or recreate the agent) rather than relying on a
|
|
151
|
+
rejected update.
|
|
152
|
+
|
|
153
|
+
### `enabled` gates whether a nested agent runs
|
|
154
|
+
|
|
155
|
+
At enrichment time the workflow filters its `workflow_ai_agents`
|
|
156
|
+
to the enabled ones and runs only those — a `disabled` agent is
|
|
157
|
+
silently **skipped**, not run-and-failed. There is no
|
|
158
|
+
`ai_agent_disabled` error code. `enabled: false` is the
|
|
159
|
+
operator's kill switch: it takes the agent out of the enrichment
|
|
160
|
+
set without detaching it from the parent's `*_ai_agents` array.
|
|
161
|
+
If every nested agent is disabled, the enrichment stage lands
|
|
162
|
+
`{ status: 'skipped' }` (see §7).
|
|
151
163
|
|
|
152
164
|
### Numeric clamps on `temperature` and `max_tokens`
|
|
153
165
|
|
|
@@ -166,23 +178,20 @@ value the provider doesn't support (e.g. `temperature: 1.5` on
|
|
|
166
178
|
a model that caps at 1.0) surfaces later as a provider error in
|
|
167
179
|
the workflow execution log, not at create.
|
|
168
180
|
|
|
169
|
-
### Name uniqueness is per-
|
|
181
|
+
### Name uniqueness is per-datalake
|
|
170
182
|
|
|
171
183
|
The agent's `name` (and the server-derived `slug`) must be
|
|
172
|
-
unique within the
|
|
173
|
-
|
|
174
|
-
`/name`
|
|
175
|
-
|
|
176
|
-
contracts)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
distinct names. A naming convention like `"<purpose>-<env>"` or
|
|
184
|
-
including the datalake slug in the agent name keeps the
|
|
185
|
-
namespace clean.
|
|
184
|
+
unique within the **datalake** — the uniqueness scope is
|
|
185
|
+
`[slug, datalake_id]`, and a collision is rejected with a 422 on
|
|
186
|
+
`/name` carrying "name must be unique within a datalake". This
|
|
187
|
+
matches the other datalake-scoped resources (tools, data
|
|
188
|
+
sources, interoperability contracts).
|
|
189
|
+
|
|
190
|
+
The same logical name can therefore be reused across different
|
|
191
|
+
datalakes of one tenant. A `slug` is only ever dereferenced
|
|
192
|
+
within its own datalake's workflow templates
|
|
193
|
+
(`additional_context.<slug>`), so cross-datalake reuse is safe —
|
|
194
|
+
no environment-suffix naming convention is required.
|
|
186
195
|
|
|
187
196
|
## 3. Workflow & contract nesting
|
|
188
197
|
|
|
@@ -319,7 +328,7 @@ Standard JSON:API envelopes per `errors.md`. Common rejections:
|
|
|
319
328
|
| `/data_access` | value not in enum |
|
|
320
329
|
| `/temperature` | outside the inclusive range [0.0, 2.0] |
|
|
321
330
|
| `/max_tokens` | outside the inclusive range [1, 100_000] |
|
|
322
|
-
| `/name`
|
|
331
|
+
| `/name` | name/slug collision — unique within the datalake (`[slug, datalake_id]`) |
|
|
323
332
|
|
|
324
333
|
## 6. Lifecycle
|
|
325
334
|
|
|
@@ -331,12 +340,15 @@ once `enabled: true`.
|
|
|
331
340
|
### Read shapes
|
|
332
341
|
|
|
333
342
|
```
|
|
334
|
-
.list(tenantSlug, datalakeSlug)
|
|
343
|
+
.list(tenantSlug, datalakeSlug, query?) Paged: { data, meta }
|
|
344
|
+
(full Flop query — type_naming.md)
|
|
335
345
|
.get(tenantSlug, datalakeSlug, id) One row, BY ID (UUID — not slug)
|
|
336
346
|
.metadata(tenantSlug, datalakeSlug) Markdown catalog
|
|
337
347
|
.metadataDetails(tenantSlug, datalakeSlug, Markdown for one agent;
|
|
338
|
-
id) section headings:
|
|
339
|
-
Bound tool, Prompt
|
|
348
|
+
id) section headings: Wrapper,
|
|
349
|
+
Model, Bound tool, Prompt
|
|
350
|
+
config, Input schema, LLM
|
|
351
|
+
response schema
|
|
340
352
|
```
|
|
341
353
|
|
|
342
354
|
`.get` and `.metadataDetails` accept the agent's UUID `id` as
|
|
@@ -354,10 +366,75 @@ Omitting any returns a 422.
|
|
|
354
366
|
|
|
355
367
|
### Delete
|
|
356
368
|
|
|
357
|
-
`delete()` removes the row
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
369
|
+
`delete()` removes the row — but only if nothing references it.
|
|
370
|
+
The delete is **blocked** (rejected as a 422) while the agent is
|
|
371
|
+
still nested on any workflow (`workflow_ai_agents`) or
|
|
372
|
+
interoperability contract (`interoperability_contract_ai_agents`),
|
|
373
|
+
or while it has invocation `usage_logs`. Detach it from every
|
|
374
|
+
parent first (re-supply the parent's `*_ai_agents` array without
|
|
375
|
+
it — see §3), then delete. There is no "referenced agent silently
|
|
376
|
+
deleted, workflow fails later" path: the association constraint
|
|
377
|
+
stops the delete up front, so a live workflow never loses an agent
|
|
378
|
+
out from under it.
|
|
379
|
+
|
|
380
|
+
### Invoke (direct, outside a workflow)
|
|
381
|
+
|
|
382
|
+
`invoke()` runs the agent once, synchronously, against a
|
|
383
|
+
caller-supplied input map — the direct path for reading an
|
|
384
|
+
uploaded file or doing ad-hoc extraction without nesting the
|
|
385
|
+
agent on a workflow or contract.
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
.invoke(tenantSlug, datalakeSlug, id, body) POST .../ai-agents/:id/invoke
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The body is `AiAgentInvokeRequest`:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
input required object — the prompt vars; validated against the
|
|
395
|
+
agent's input_schema BEFORE the model is
|
|
396
|
+
called (a violation is a 422)
|
|
397
|
+
files optional array — [{ key, content_type }] attachments for
|
|
398
|
+
multimodal (vision) invocation. `key` is a
|
|
399
|
+
storage key from datalakes.createUploadLink;
|
|
400
|
+
content_type ∈ application/pdf | image/png |
|
|
401
|
+
image/jpeg | image/webp. The platform
|
|
402
|
+
downloads each file from the agent's datalake,
|
|
403
|
+
Base64-encodes it, and hands it to the model
|
|
404
|
+
as a content part.
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
The response (`AiAgentInvokeResponse`):
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
output object — the parsed LLM JSON, matching llm_response_schema
|
|
411
|
+
explanation string? — the model's reasoning trace, when the tool's
|
|
412
|
+
response_extractor mapped one out; null otherwise
|
|
413
|
+
usage object — { model, input_tokens, output_tokens, total_tokens,
|
|
414
|
+
latency_ms } telemetry for the call
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Invoke passes NO `context_mapping_config` — `input` is validated
|
|
418
|
+
directly against `input_schema` (unlike the nested workflow path,
|
|
419
|
+
which remaps host context first). See the `ai-agent-invoke`
|
|
420
|
+
cookbook for the full upload → tool → agent → invoke walk.
|
|
421
|
+
|
|
422
|
+
### Drift checksum
|
|
423
|
+
|
|
424
|
+
`checksum()` computes the SHA-256 drift fingerprint a
|
|
425
|
+
create/update WOULD stamp for a given body, without persisting —
|
|
426
|
+
the stateless plan-time primitive `alvera plan` uses.
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
.checksum(tenantSlug, datalakeSlug, body) POST .../ai-agents/checksum
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Compare the result against a deployed agent's server-computed
|
|
433
|
+
`checksum` (from `get()`) to detect config drift. The fingerprint
|
|
434
|
+
covers the authored fields only — `name`, `model`, `data_access`,
|
|
435
|
+
`enabled`, `temperature`, `max_tokens`, `tool_id`, `description`,
|
|
436
|
+
`input_schema`, `llm_response_schema`, `prompt_config` — server
|
|
437
|
+
bookkeeping (`slug`, `id`, timestamps, FKs) is excluded.
|
|
361
438
|
|
|
362
439
|
## 7. Run-time failure modes
|
|
363
440
|
|
|
@@ -368,26 +445,41 @@ agent-specific codes:
|
|
|
368
445
|
|
|
369
446
|
```
|
|
370
447
|
tool_execution_failed — the bound chat-completion tool's
|
|
371
|
-
HTTP call failed (
|
|
372
|
-
timeout).
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
wired through to the provider's
|
|
381
|
-
structured-output mode
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
448
|
+
HTTP call failed (non-2xx, refused,
|
|
449
|
+
timeout). Returned as a rich
|
|
450
|
+
{code, message}: error.json's `detail`
|
|
451
|
+
carries the URL + status; the workflow
|
|
452
|
+
execution log carries a coarse
|
|
453
|
+
error_message of the form
|
|
454
|
+
"AI enrichment failed: <agent_name>"
|
|
455
|
+
json_decode_failed — the model's content didn't parse as
|
|
456
|
+
JSON (typically llm_response_schema
|
|
457
|
+
wasn't wired through to the provider's
|
|
458
|
+
structured-output mode, so the model
|
|
459
|
+
wrapped JSON in prose / code fences)
|
|
460
|
+
response_extraction_failed — the tool's response_extractor template
|
|
461
|
+
failed to render/decode the provider
|
|
462
|
+
envelope, or its output violated the
|
|
463
|
+
extractor's own output_schema
|
|
464
|
+
context_mapping_failed — the nested join's
|
|
465
|
+
context_mapping_config.body failed to
|
|
466
|
+
render or JSON-decode
|
|
467
|
+
context_mapping_not_a_map — context_mapping_config rendered valid
|
|
468
|
+
JSON that wasn't an object
|
|
469
|
+
input_schema_validation_failed — the effective prompt vars (post
|
|
470
|
+
context-mapping) didn't validate
|
|
471
|
+
against the agent's input_schema
|
|
472
|
+
tool_call_type_not_supported_for_llm_enrichment
|
|
473
|
+
— the bound tool's body can't host a model
|
|
474
|
+
call (only rest_api and aws_lambda
|
|
475
|
+
bodies can)
|
|
389
476
|
```
|
|
390
477
|
|
|
478
|
+
There is no `ai_agent_disabled` or `ai_agent_not_found` runtime
|
|
479
|
+
code: a disabled agent is silently filtered out of the enrichment
|
|
480
|
+
set and never runs (§2), and an agent still nested on a
|
|
481
|
+
workflow/contract cannot be deleted in the first place (§6, Delete).
|
|
482
|
+
|
|
391
483
|
The `error.json` shape:
|
|
392
484
|
|
|
393
485
|
```
|
|
@@ -448,8 +540,12 @@ with `{ status: 'skipped' }` and no per-agent entries.
|
|
|
448
540
|
partial body silently drops fields and returns 422 on the
|
|
449
541
|
missing required field.
|
|
450
542
|
|
|
451
|
-
6. **`data_access` is immutable
|
|
452
|
-
|
|
543
|
+
6. **`data_access` is immutable by convention only — the platform
|
|
544
|
+
won't stop a PUT from flipping it.** There's no immutability
|
|
545
|
+
guard in the changeset, so an `update()` can change the
|
|
546
|
+
compliance tier (`regulated ↔ unregulated`) and re-tier what the
|
|
547
|
+
agent sees. Enforce it in your own change-control (or delete +
|
|
548
|
+
recreate); don't rely on the platform to reject the flip (§2).
|
|
453
549
|
|
|
454
550
|
7. **`prompt_config.body` field references must resolve in
|
|
455
551
|
`input_schema`.** A `{{ field }}` that isn't declared in
|