@alvera-ai/platform-sdk 0.13.0 → 0.15.0-next.g09659bd
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/account_management.md +31 -0
- package/.agent/action_status_updaters.md +339 -26
- package/.agent/ai_sandbox.md +4 -2
- package/.agent/connected_apps.md +7 -2
- package/.agent/cookbook/action-status-updaters.md +112 -14
- package/.agent/cookbook/ai-agent-invoke.md +36 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +41 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +44 -1
- package/.agent/cookbook/bulk-ingest.md +57 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +36 -1
- package/.agent/cookbook/dunning-sms-for-delinquent.md +42 -1
- package/.agent/cookbook/generic-tables.md +40 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +44 -1
- package/.agent/cookbook/marketing-campaign-send.md +65 -3
- package/.agent/cookbook/paginated-restapi-poller.md +400 -0
- package/.agent/cookbook/rest-fetch.md +36 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +44 -1
- package/.agent/cookbook/score-leads-with-llm-categorization.md +36 -1
- package/.agent/cookbook/system-templates.md +36 -0
- package/.agent/cookbook/talk-to-data.md +39 -0
- package/.agent/cookbook/triage-prospects-by-priority.md +42 -1
- package/.agent/cookbook/welcome-sms-for-customers.md +42 -1
- package/.agent/datalakes.md +68 -3
- package/.agent/interoperability_contracts.md +29 -0
- package/.agent/mdm.md +45 -2
- package/.agent/tool-call-configs.md +11 -0
- package/.agent/tools.md +103 -36
- package/.agent/type_naming.md +4 -0
- package/dist/index.d.mts +548 -27
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +64 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -102,6 +102,37 @@ app authenticates **tenant-scoped against exactly one tenant**,
|
|
|
102
102
|
with that tenant's publishable key; connected apps in particular
|
|
103
103
|
are single-tenant by construction (`connected_apps.md`).
|
|
104
104
|
|
|
105
|
+
### Key-only clients — machine-to-machine on the publishable key
|
|
106
|
+
|
|
107
|
+
`sessionToken` is optional on the client config. A deployed
|
|
108
|
+
connected app that authenticates purely on its tenant's publishable
|
|
109
|
+
key constructs with just the key — no session mint, no fabricated
|
|
110
|
+
empty token:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const api = createIsolatedPlatformApi({ baseUrl, apiKey })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Such a client sends `X-API-Key` and **no `Authorization` header**,
|
|
117
|
+
and reaches exactly the `public_api` (publishable-key) allowlist —
|
|
118
|
+
five calls:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
api.connectedApps.resolvePage mint/read a delivered page
|
|
122
|
+
api.connectedApps.updateMessageTracking opened_at / form_submitted_at
|
|
123
|
+
api.dataActivationClients.ingest submit rows
|
|
124
|
+
api.dataActivationClients.ingestFile submit a file
|
|
125
|
+
api.mdm.verify resolve an identity
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
(The same allowlist `connected_apps.md` documents — that guide owns
|
|
129
|
+
the runtime-call details.) Every call outside these five returns
|
|
130
|
+
401; that ceiling is the point, not an error to work around.
|
|
131
|
+
Minting a session and passing `sessionToken` unlocks the full
|
|
132
|
+
tenant-scoped API exactly as before — the two credentials are
|
|
133
|
+
complementary (the key identifies the client; the Bearer
|
|
134
|
+
authorizes), never substitutes.
|
|
135
|
+
|
|
105
136
|
### Explicit teardown — `revokeSession`
|
|
106
137
|
|
|
107
138
|
The inverse of `createSession`. Revokes the current Bearer on
|
|
@@ -40,8 +40,8 @@ const { data: created } = await api.actionStatusUpdaters.create(
|
|
|
40
40
|
updater_body: {
|
|
41
41
|
updater_body_type: 'cloud_watch_request',
|
|
42
42
|
log_group_name: 'sns/us-east-1/...',
|
|
43
|
-
start_time: '{{
|
|
44
|
-
end_time: '{{
|
|
43
|
+
start_time: '{{ now_msec | minutes_ago: 45 }}', // Liquid window
|
|
44
|
+
end_time: '{{ now_msec }}',
|
|
45
45
|
},
|
|
46
46
|
|
|
47
47
|
// Liquid, rendered ONCE PER EVENT — the event IS the assigns, so there is no
|
|
@@ -51,6 +51,14 @@ const { data: created } = await api.actionStatusUpdaters.create(
|
|
|
51
51
|
type: 'custom',
|
|
52
52
|
body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
|
|
53
53
|
},
|
|
54
|
+
|
|
55
|
+
// REQUIRED on every updater, both types, on create AND update. Same
|
|
56
|
+
// per-event rendering contract as `message_config`, but targets the
|
|
57
|
+
// action_log row instead of the message row.
|
|
58
|
+
action_log_config: {
|
|
59
|
+
type: 'custom',
|
|
60
|
+
body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
|
|
61
|
+
},
|
|
54
62
|
},
|
|
55
63
|
)
|
|
56
64
|
```
|
|
@@ -74,22 +82,69 @@ updater_body_type: 'cloud_watch_request'
|
|
|
74
82
|
follow the same `<verb>_request` naming and carry the fields
|
|
75
83
|
their target API requires.)
|
|
76
84
|
|
|
85
|
+
**Four poll-contract fields are required on EVERY updater type**, and
|
|
86
|
+
they live at the **root of the create body** — not inside
|
|
87
|
+
`updater_body`:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
events_template REQUIRED embed — { type, body } — event-list
|
|
91
|
+
extraction; maps the provider's
|
|
92
|
+
own id into `external_id`
|
|
93
|
+
pagination_context_template REQUIRED embed — { type, body } — must render JSON
|
|
94
|
+
carrying a boolean `has_next`
|
|
95
|
+
events_output_schema REQUIRED object — JSON Schema; FLOORED at an array
|
|
96
|
+
whose items list `external_id`
|
|
97
|
+
in `required`
|
|
98
|
+
pagination_context_output_schema REQUIRED object — JSON Schema; FLOORED at an object
|
|
99
|
+
listing `has_next` in `required`
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Mind the level.** These sit at the ASU root, deliberately — the
|
|
103
|
+
`restapi_request` body embed is shared with the data-activation
|
|
104
|
+
`tool_call`, and putting them there would force every DAC to supply
|
|
105
|
+
them too. A `restapi_request` body carries its own `events_template`;
|
|
106
|
+
that is a different field at a different level, and supplying it does
|
|
107
|
+
**not** satisfy the root requirement.
|
|
108
|
+
|
|
109
|
+
This was `restapi`-only while CloudWatch was a separate loop with no
|
|
110
|
+
templates of its own. It is not one any more: every source now renders
|
|
111
|
+
through `events_template` + `pagination_context_template` and
|
|
112
|
+
terminates on the same `has_next`, so a `cloud_watch` updater declares
|
|
113
|
+
the same contract a REST one does — the platform just ships the
|
|
114
|
+
templates it normally points at. An older `cloud_watch` manifest that
|
|
115
|
+
predates the change 422s on its next create.
|
|
116
|
+
|
|
117
|
+
> ⚠ **The OpenAPI schema is behind the changeset on two of these.**
|
|
118
|
+
> `events_output_schema` and `pagination_context_output_schema` are
|
|
119
|
+
> generated **optional**, but `validate_expected_output_schemas/1`
|
|
120
|
+
> requires them unconditionally. Omit them and the type-checker is
|
|
121
|
+
> happy while the server returns 422. Treat all four as required until
|
|
122
|
+
> the spec catches up.
|
|
123
|
+
|
|
124
|
+
The floors exist for a reason. `external_id` is what the apply path
|
|
125
|
+
pops off each rendered event to find the message and action-log rows to
|
|
126
|
+
update — an events contract that does not demand it accepts a poll that
|
|
127
|
+
fetches pages forever and reconciles nothing. `has_next` is the key the
|
|
128
|
+
page loop ends on.
|
|
129
|
+
|
|
77
130
|
`start_time` / `end_time` are Liquid, and they **must render to
|
|
78
|
-
unix milliseconds** — that is what the poll request takes.
|
|
79
|
-
|
|
80
|
-
|
|
131
|
+
unix milliseconds** — that is what the poll request takes. Use the
|
|
132
|
+
**`now_msec`** variable (unix ms) and pipe it through the
|
|
133
|
+
relative-time filters. **Not the bare `now`** — that is a DateTime,
|
|
134
|
+
and feeding it a millisecond filter now raises at render:
|
|
81
135
|
|
|
82
136
|
```
|
|
83
|
-
start_time: '{{
|
|
84
|
-
end_time: '{{
|
|
137
|
+
start_time: '{{ now_msec | minutes_ago: 45 }}' // 45 minutes back
|
|
138
|
+
end_time: '{{ now_msec }}' // right now
|
|
85
139
|
```
|
|
86
140
|
|
|
87
141
|
Also available: `hours_ago: N`.
|
|
88
142
|
|
|
89
|
-
**Do not use the
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
literal is not valid on
|
|
143
|
+
**Do not use the bare `now` variable or the ISO-8601 `now` filter
|
|
144
|
+
here.** The `now` variable is a DateTime and `{{ "" | now }}`
|
|
145
|
+
renders a datetime *string* — neither is a valid unix-ms window,
|
|
146
|
+
and the platform rejects both. An ISO 8601 literal is not valid on
|
|
147
|
+
these fields either.
|
|
93
148
|
|
|
94
149
|
### `message_config.body` renders ONCE PER EVENT
|
|
95
150
|
|
|
@@ -101,6 +156,10 @@ scope — do not loop.
|
|
|
101
156
|
{"external_id": "{{ notification.messageId }}", "status": "delivered"}
|
|
102
157
|
```
|
|
103
158
|
|
|
159
|
+
> **Shape only — do not copy this mapping.** A hardcoded `"delivered"`
|
|
160
|
+
> records every event as delivered, failures included. Real bodies
|
|
161
|
+
> branch on the provider's status; see the SNS and MMS sections below.
|
|
162
|
+
|
|
104
163
|
It must render a **flat** JSON object:
|
|
105
164
|
|
|
106
165
|
```
|
|
@@ -123,6 +182,174 @@ events for messages this updater doesn't own.
|
|
|
123
182
|
Custom Liquid filters (`json_escape`, `e164`, `to_json`, ...) are
|
|
124
183
|
available here, as everywhere.
|
|
125
184
|
|
|
185
|
+
### SMS-over-SNS delivery tracking — two `custom` bodies
|
|
186
|
+
|
|
187
|
+
The SNS SMS delivery receipt is **binary and single-shot**: one terminal
|
|
188
|
+
outcome, `status` of `SUCCESS` or `FAILURE`. Its event shape is
|
|
189
|
+
**disjoint from EUM's** — `status`, `notification.messageId`,
|
|
190
|
+
`notification.timestamp`, `delivery.phoneCarrier`,
|
|
191
|
+
`delivery.providerResponse` all sit at the root. Do not carry an EUM
|
|
192
|
+
template over to SNS or vice versa.
|
|
193
|
+
|
|
194
|
+
Two convenient consequences, both unlike MMS:
|
|
195
|
+
|
|
196
|
+
- `delivered_at` is `notification.timestamp` — a UTC
|
|
197
|
+
`"YYYY-MM-DD HH:MM:SS.sss"` string Ecto casts directly, so **no date
|
|
198
|
+
filter is needed** (EUM needs `from_unix_ms` on its Unix-ms field).
|
|
199
|
+
- the DLR **does** surface the US carrier as `delivery.phoneCarrier`.
|
|
200
|
+
|
|
201
|
+
**`message_config`:**
|
|
202
|
+
|
|
203
|
+
```liquid
|
|
204
|
+
{"external_id": "{{ notification.messageId }}", "status": "{% if status == 'SUCCESS' %}delivered{% else %}failed{% endif %}", "status_description": "{{ delivery.providerResponse | json_escape }}"{% if status == 'SUCCESS' and notification.timestamp %}, "delivered_at": "{{ notification.timestamp }}"{% endif %}{% unless status == 'SUCCESS' %}, "failure_reason": "{{ status | json_escape }}"{% endunless %}{% if delivery.phoneCarrier and delivery.phoneCarrier != '' %}, "sms_carrier": "{{ delivery.phoneCarrier | json_escape }}"{% endif %}}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**`action_log_config`:**
|
|
208
|
+
|
|
209
|
+
```liquid
|
|
210
|
+
{"external_id": "{{ notification.messageId }}", "status": "{% if status == 'SUCCESS' %}delivered{% else %}failed{% endif %}", "status_description": "{{ delivery.providerResponse | json_escape }}"}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`status_description` is `delivery.providerResponse` verbatim on both
|
|
214
|
+
sides — the carrier's own text, on success and failure alike. Because
|
|
215
|
+
SNS's status is binary, `failure_reason` can only ever be `"FAILURE"`;
|
|
216
|
+
the granular carrier text stays in `status_description`.
|
|
217
|
+
|
|
218
|
+
**Note how the two SNS bodies differ from each other**: same status
|
|
219
|
+
mapping, fewer emitted fields on the action log (no `delivered_at`,
|
|
220
|
+
`sms_carrier`, or `failure_reason` — those are message concerns). That
|
|
221
|
+
is a *different* reason from MMS's, where the two also disagree on the
|
|
222
|
+
mapping itself. Both channels need two bodies; they need them for
|
|
223
|
+
different reasons.
|
|
224
|
+
|
|
225
|
+
### MMS delivery tracking needs TWO different templates — author both as `custom`
|
|
226
|
+
|
|
227
|
+
MMS delivery tracking is a `cloud_watch` updater. Author **both**
|
|
228
|
+
`message_config` and `action_log_config` as `custom` bodies — the two
|
|
229
|
+
render *different* JSON from the same event, so one body cannot serve
|
|
230
|
+
both. This is the single most-missed thing about updaters.
|
|
231
|
+
|
|
232
|
+
**Why they differ.** A message's status vocabulary includes
|
|
233
|
+
`customer_rejected`; an action log's does not. A recipient or carrier
|
|
234
|
+
rejection (`SPAM` / `BLOCKED` / `CARRIER_BLOCKED`) is a message-level
|
|
235
|
+
outcome — the action *did* deliver — so the action-log render must
|
|
236
|
+
**NO-OP** on those events while the message render records
|
|
237
|
+
`customer_rejected`. The message side also carries `delivered_at`,
|
|
238
|
+
`failure_reason`, and `sms_carrier`; only `status` +
|
|
239
|
+
`status_description` are ever applied to an action log.
|
|
240
|
+
|
|
241
|
+
AWS End User Messaging (`sms-voice`) event fields: top-level
|
|
242
|
+
`messageId`, `eventType` (`MEDIA_*`), `messageStatus`,
|
|
243
|
+
`messageStatusDescription`, `eventTimestamp` (Unix ms), `isFinal`.
|
|
244
|
+
|
|
245
|
+
**`message_config`** — map every status, emit the extra fields:
|
|
246
|
+
|
|
247
|
+
```liquid
|
|
248
|
+
{% case messageStatus %}
|
|
249
|
+
{% when 'DELIVERED' %}{% assign mapped_status = 'delivered' %}
|
|
250
|
+
{% when 'SUCCESSFUL' %}{% assign mapped_status = 'sent' %}
|
|
251
|
+
{% when 'PENDING' %}{% assign mapped_status = 'sent' %}
|
|
252
|
+
{% when 'QUEUED' %}{% assign mapped_status = 'queued' %}
|
|
253
|
+
{% when 'SPAM' %}{% assign mapped_status = 'customer_rejected' %}
|
|
254
|
+
{% when 'BLOCKED' %}{% assign mapped_status = 'customer_rejected' %}
|
|
255
|
+
{% when 'CARRIER_BLOCKED' %}{% assign mapped_status = 'customer_rejected' %}
|
|
256
|
+
{% else %}{% assign mapped_status = 'failed' %}
|
|
257
|
+
{% endcase %}
|
|
258
|
+
{"external_id": "{{ messageId }}", "status": "{{ mapped_status }}", "status_description": "{{ messageStatusDescription | json_escape }}"{% if messageStatus == 'DELIVERED' and eventTimestamp %}, "delivered_at": "{{ eventTimestamp | from_unix_ms }}"{% endif %}{% if mapped_status == 'failed' or mapped_status == 'customer_rejected' %}, "failure_reason": "{{ messageStatus | json_escape }}"{% endif %}{% if carrierName and carrierName != '' %}, "sms_carrier": "{{ carrierName | json_escape }}"{% endif %}}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**`action_log_config`** — the rejection statuses fall through to a
|
|
262
|
+
bare `external_id`, which applies nothing:
|
|
263
|
+
|
|
264
|
+
```liquid
|
|
265
|
+
{% assign al_status = '' %}{% case messageStatus %}{% when 'DELIVERED' %}{% assign al_status = 'delivered' %}{% when 'SUCCESSFUL' %}{% assign al_status = 'sent' %}{% when 'PENDING' %}{% assign al_status = 'sent' %}{% when 'QUEUED' %}{% assign al_status = 'queued' %}{% when 'SPAM', 'BLOCKED', 'CARRIER_BLOCKED' %}{% else %}{% assign al_status = 'failed' %}{% endcase %}{"external_id": "{{ messageId }}"{% if al_status != '' %}, "status": "{{ al_status }}", "status_description": "{{ messageStatusDescription | json_escape }}"{% endif %}}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Note the `{% when 'SPAM', 'BLOCKED', 'CARRIER_BLOCKED' %}` arm is
|
|
269
|
+
deliberately **empty** — it leaves `al_status` blank, so the render
|
|
270
|
+
emits `external_id` alone and the action log is untouched.
|
|
271
|
+
|
|
272
|
+
**Capture every event, not just the terminal one.** 2–3 events arrive
|
|
273
|
+
per message — interim `MEDIA_QUEUED` / `MEDIA_PENDING` /
|
|
274
|
+
`MEDIA_SUCCESSFUL` (`isFinal: false`), then one terminal event
|
|
275
|
+
(`isFinal: true`), up to 72h late and out of order. Apply is
|
|
276
|
+
idempotent and a monotonic guard only ever advances `status`, so a
|
|
277
|
+
late or interim lower-rank event can never demote a message that
|
|
278
|
+
already reached a higher state. Poll a window that overlaps
|
|
279
|
+
deliberately; nothing is dropped.
|
|
280
|
+
|
|
281
|
+
`queued_at` and `sent_at` are **your outbound pipeline's** times, set
|
|
282
|
+
when the platform enqueues and dispatches — a delivery poll never
|
|
283
|
+
overwrites them. Only `delivered_at` comes from the carrier event
|
|
284
|
+
(`DELIVERED`'s own `eventTimestamp` via `from_unix_ms`), so
|
|
285
|
+
re-polling a wide window does not drift it.
|
|
286
|
+
|
|
287
|
+
### Carrier information is available for SMS, not MMS
|
|
288
|
+
|
|
289
|
+
The recipient's mobile carrier is present on SMS delivery events but
|
|
290
|
+
not MMS. This is AWS behavior — verified against real delivery
|
|
291
|
+
events — not something the platform chooses to drop:
|
|
292
|
+
|
|
293
|
+
- **SMS via Amazon SNS** — the delivery-status log
|
|
294
|
+
(`sns/<region>/<account>/DirectPublishToPhoneNumber`) nests carrier
|
|
295
|
+
under `delivery.phoneCarrier` (e.g. `"T-mobile USA Inc."`, with
|
|
296
|
+
numeric `mcc`/`mnc`). The SNS status template maps it to
|
|
297
|
+
`sms_carrier`.
|
|
298
|
+
- **MMS via AWS End User Messaging (`sms-voice`)** — the event *can*
|
|
299
|
+
carry `carrierName`, but it is DLR-populated and comes back
|
|
300
|
+
**absent for US destinations** (the US DLR does not surface it),
|
|
301
|
+
unlike the SNS path's `phoneCarrier`, whose US DLR does. So map it
|
|
302
|
+
when present — `{% if carrierName and carrierName != '' %}` — and
|
|
303
|
+
expect `sms_carrier` to stay null for US long-code traffic.
|
|
304
|
+
Verified against real AWS delivery events: the terminal
|
|
305
|
+
`MEDIA_DELIVERED` event for a US destination exposes
|
|
306
|
+
`totalCarrierFee` (a cost) and no `carrierName`.
|
|
307
|
+
|
|
308
|
+
AWS's Nov-2024 EventBridge-integration announcement states MMS
|
|
309
|
+
delivery events carry carrier information in EventBridge, but the
|
|
310
|
+
real events do not — a documented doc-vs-behavior discrepancy. For
|
|
311
|
+
per-number US carrier, use `PhoneNumberValidate` (returns `Carrier` +
|
|
312
|
+
`PhoneType`) at enrollment and join on destination/`messageId`; it is
|
|
313
|
+
not in the delivery stream.
|
|
314
|
+
|
|
315
|
+
### `action_log_config` is required — same rendering contract as `message_config`
|
|
316
|
+
|
|
317
|
+
`action_log_config` is a **required** field on every action status
|
|
318
|
+
updater — `cloud_watch` and `restapi` alike — on **both `create()` and
|
|
319
|
+
`update()`**. There is no updater for which it is optional and no way
|
|
320
|
+
to omit it on a PUT.
|
|
321
|
+
|
|
322
|
+
It follows the same per-event rendering contract as `message_config`
|
|
323
|
+
(§2 above), but targets the **action_log** row instead of the message
|
|
324
|
+
row: it must render a JSON object containing `external_id` plus at
|
|
325
|
+
least one updatable action-log field — one of `status`, `sent_at`, or
|
|
326
|
+
`metadata`. A minimal valid value:
|
|
327
|
+
|
|
328
|
+
```liquid
|
|
329
|
+
{"external_id": "{{ notification.messageId }}", "status": "delivered"}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Two distinct rejections come off this field, and they read differently:
|
|
333
|
+
|
|
334
|
+
- **Missing entirely** — omitting `action_log_config` on create or
|
|
335
|
+
update is a 422 with `Missing field: action_log_config` at
|
|
336
|
+
`source.pointer: /action_log_config`.
|
|
337
|
+
- **Present but renders nothing** — a value that passes shallow
|
|
338
|
+
validation but renders `null` or an empty result at run time (e.g.
|
|
339
|
+
a `{"type": "null"}` config) is refused with a 422 whose detail
|
|
340
|
+
begins `must render a JSON object containing external_id`.
|
|
341
|
+
|
|
342
|
+
**Response nullability is lazy, not retroactive.** Updaters created
|
|
343
|
+
before this became required still read back `action_log_config: null`
|
|
344
|
+
— the platform does not backfill a template onto old rows. That
|
|
345
|
+
`null` persists until the row's **next edit** forces a real template
|
|
346
|
+
through, and "next edit" is broader than a plain field change: it
|
|
347
|
+
also includes resuming a halted updater by setting `status` back to
|
|
348
|
+
`'active'` (§7) and issuing a `refresh()` call with a one-shot
|
|
349
|
+
`updater_body` override (§7) — both now require `action_log_config`
|
|
350
|
+
to be present too, which is what completes the migration for an old
|
|
351
|
+
row.
|
|
352
|
+
|
|
126
353
|
### `cron_expression` is a standard 5-field cron string
|
|
127
354
|
|
|
128
355
|
```
|
|
@@ -201,8 +428,17 @@ sender_tool_ids required UUID[] — tools whose messages this updater trac
|
|
|
201
428
|
datalake_id required UUID
|
|
202
429
|
updater_body required embed — { updater_body_type, ... }
|
|
203
430
|
message_config required embed — { type, body }
|
|
431
|
+
action_log_config required embed — { type, body } (required on create AND update)
|
|
432
|
+
|
|
433
|
+
events_template required embed — EVERY updater type, not just restapi
|
|
434
|
+
pagination_context_template required embed — EVERY updater type
|
|
435
|
+
events_output_schema required object — JSON Schema
|
|
436
|
+
pagination_context_output_schema required object — JSON Schema, floored at
|
|
437
|
+
`has_next` in `required`
|
|
204
438
|
```
|
|
205
439
|
|
|
440
|
+
The last four widened from `restapi`-only to universal — see §2.
|
|
441
|
+
|
|
206
442
|
**Write-only (Request-only).** None.
|
|
207
443
|
|
|
208
444
|
## 4. Error envelopes
|
|
@@ -216,6 +452,8 @@ Standard JSON:API envelopes per `errors.md`. Common rejections:
|
|
|
216
452
|
| `/sender_tool_ids/0` | tool id doesn't exist or wrong datalake |
|
|
217
453
|
| `/updater_body/updater_body_type` | value not in the polymorphic embed's type enum |
|
|
218
454
|
| `/message_config/body` | empty when `type: 'custom'` |
|
|
455
|
+
| `/action_log_config` | missing entirely — `Missing field: action_log_config` (create AND update) |
|
|
456
|
+
| (detail, not pointer) | `action_log_config` present but renders `null`/empty — detail begins `must render a JSON object containing external_id` |
|
|
219
457
|
| `/base` | `cron_management_failed` — body validated and saved but scheduler registration failed; the row is rolled back |
|
|
220
458
|
|
|
221
459
|
## 5. Lifecycle
|
|
@@ -318,7 +556,7 @@ check or retry for partial-delete states.
|
|
|
318
556
|
The poll follows pages while `has_next` is true and stops on
|
|
319
557
|
the first empty page.
|
|
320
558
|
|
|
321
|
-
## 7. Manual trigger via `refresh` —
|
|
559
|
+
## 7. Manual trigger via `refresh` — 202, and what `last_run_*` means
|
|
322
560
|
|
|
323
561
|
An action status updater CAN be fired on demand:
|
|
324
562
|
|
|
@@ -328,28 +566,103 @@ const { data } = await api.actionStatusUpdaters.refresh(
|
|
|
328
566
|
);
|
|
329
567
|
```
|
|
330
568
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
569
|
+
**`refresh` answers `202` with the updater row AS-IS — the poll is
|
|
570
|
+
ENQUEUED, not run.** The `last_run_*` fields on that response are the
|
|
571
|
+
PREVIOUS run's stamps; reading them as the outcome of the refresh you
|
|
572
|
+
just issued is the single most common mistake here. An optional body
|
|
573
|
+
carries a one-shot `updater_body` override (e.g. a widened
|
|
574
|
+
`start_time`/`end_time` window for a backfill) that does not mutate
|
|
575
|
+
the persisted updater. Apply jobs are asynchronous too — observe
|
|
576
|
+
delivery status on the message rows, not in this response.
|
|
577
|
+
(CLI: `alvera refresh-action-status-updater --id <uuid>`, alias
|
|
578
|
+
`refresh-updater`.)
|
|
335
579
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
580
|
+
The correct read is baseline-then-advance:
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
const before = await api.actionStatusUpdaters.get(tenantSlug, datalakeSlug, updaterId)
|
|
584
|
+
const baseline = before.data.last_run_at ?? null // BEFORE refreshing
|
|
585
|
+
|
|
586
|
+
await api.actionStatusUpdaters.refresh(tenantSlug, datalakeSlug, updaterId)
|
|
587
|
+
|
|
588
|
+
let row = before.data
|
|
589
|
+
const deadline = Date.now() + 60_000
|
|
590
|
+
while (Date.now() < deadline) {
|
|
591
|
+
await new Promise((r) => setTimeout(r, 2_000))
|
|
592
|
+
const { data } = await api.actionStatusUpdaters.get(tenantSlug, datalakeSlug, updaterId)
|
|
593
|
+
if (data.last_run_at && data.last_run_at !== baseline) { row = data; break }
|
|
594
|
+
}
|
|
595
|
+
// row.last_run_status / last_run_error now describe THIS run
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
The ASU now **does** have a per-run log — the sibling of a data
|
|
599
|
+
activation client's `logs` and a workflow's `batchLogs`. There are two
|
|
600
|
+
run-outcome surfaces and they answer different questions:
|
|
601
|
+
|
|
602
|
+
```
|
|
603
|
+
actionStatusUpdaters.runLogs.list(tenantSlug, datalakeSlug, updaterId, query?)
|
|
604
|
+
actionStatusUpdaters.runLogs.get(tenantSlug, datalakeSlug, updaterId, logId)
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
`.list` is Flop-paginated like every other list — same `page` /
|
|
608
|
+
`page_size` / `filters` / `order_by` vocabulary (see `async.md`).
|
|
609
|
+
|
|
610
|
+
**The summary surface — four fields on the updater row, plus the halt
|
|
611
|
+
flag:**
|
|
340
612
|
|
|
341
613
|
```
|
|
342
|
-
last_run_at when the poll worker last
|
|
614
|
+
last_run_at when the poll worker last made progress
|
|
343
615
|
last_run_status 'ok' | 'error'
|
|
344
|
-
last_run_error why the last run failed
|
|
345
|
-
last_run_events_found
|
|
616
|
+
last_run_error why the last run failed
|
|
617
|
+
last_run_events_found events fetched so far in the current run
|
|
618
|
+
status 'active' | 'cycle_detected'
|
|
346
619
|
```
|
|
347
620
|
|
|
621
|
+
**The per-run surface — one row per poll run, from `runLogs`:**
|
|
622
|
+
|
|
623
|
+
```
|
|
624
|
+
status 'ok' | 'partial' | 'error' ← the outcome of THIS page
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**`'partial'` lives on the run log, NOT on the updater — this is the
|
|
628
|
+
trap.** A truncated fetch means the window was not fully read and the
|
|
629
|
+
events it missed are the **newest** ones. The updater's own
|
|
630
|
+
`last_run_status` cannot tell you that: it carries `'ok' | 'error'`
|
|
631
|
+
only, so a truncated run reads as `'ok'` there. If completeness of
|
|
632
|
+
reconciliation matters to you, read the run log — checking
|
|
633
|
+
`last_run_status === 'ok'` on the updater is **not** sufficient, and a
|
|
634
|
+
client that treats it as sufficient silently accepts an incomplete
|
|
635
|
+
reconciliation.
|
|
636
|
+
|
|
637
|
+
- `'ok'` — the page completed and read its whole slice of the window.
|
|
638
|
+
- `'partial'` — the page completed but its provider fetch was
|
|
639
|
+
**truncated**. Only visible on the run-log row.
|
|
640
|
+
- `'error'` — the run failed; `last_run_error` on the updater says why.
|
|
641
|
+
|
|
642
|
+
`last_run_events_found` is the event count for the current run; because
|
|
643
|
+
`refresh` is asynchronous (below), a read taken before the run finishes can
|
|
644
|
+
legitimately show a smaller, in-progress figure than the final count.
|
|
645
|
+
|
|
646
|
+
**`status` is the halt flag.** The server sets `'cycle_detected'` when
|
|
647
|
+
a run re-reads events it has already handled — the runtime backstop for
|
|
648
|
+
a poll whose pagination does not advance (a provider cursor that never
|
|
649
|
+
drains, or a one-shot `updater_body` override). Every later job then
|
|
650
|
+
fails without calling the provider, and `last_run_error` carries the
|
|
651
|
+
diagnostic. **Nothing clears it but setting `status` back to
|
|
652
|
+
`'active'`.** It is also the cheapest deterministic failure to assert
|
|
653
|
+
on: a non-advancing config trips it within seconds, no provider
|
|
654
|
+
credential rejection required. Related: a `restapi` updater that
|
|
655
|
+
declares pagination but whose `path`/`params`/`body` never read
|
|
656
|
+
`msg.pagination_context` or `msg.page` is now refused at create/update
|
|
657
|
+
— see §2's template contract.
|
|
658
|
+
|
|
348
659
|
**If a message's status is not reconciling, do this — in order:**
|
|
349
660
|
|
|
350
|
-
1. Call `refresh()
|
|
351
|
-
|
|
352
|
-
|
|
661
|
+
1. Call `refresh()`, wait for `last_run_at` to advance past the
|
|
662
|
+
baseline you captured, then read `last_run_status` /
|
|
663
|
+
`last_run_error` / `status`. They name the failing stage: window
|
|
664
|
+
render, source fetch, template render, JSON parse — or a halted
|
|
665
|
+
poll cycle.
|
|
353
666
|
2. Check the config against §2 above. A config that violates the
|
|
354
667
|
template contract is rejected at create, so a *deployed* updater's
|
|
355
668
|
templates are structurally sound — the fault is more likely the
|
package/.agent/ai_sandbox.md
CHANGED
|
@@ -103,10 +103,12 @@ consumers cannot extend the set:
|
|
|
103
103
|
|
|
104
104
|
minutes_ago Unix ms N minutes before a unix-ms input. Bounds
|
|
105
105
|
the start of an ActionStatusUpdater poll window.
|
|
106
|
-
|
|
106
|
+
`now_msec` is the injected unix-ms variable (the
|
|
107
|
+
bare `now` is a DateTime — feeding it here raises).
|
|
108
|
+
Example: {{ now_msec | minutes_ago: 45 }} → 1773585000000
|
|
107
109
|
|
|
108
110
|
hours_ago Unix ms N hours before a unix-ms input.
|
|
109
|
-
Example: {{
|
|
111
|
+
Example: {{ now_msec | hours_ago: 2 }} → 1773578400000
|
|
110
112
|
|
|
111
113
|
uuid Generate a new v4 UUID.
|
|
112
114
|
Example: {{ '' | uuid }} → a1b2c3d4-…
|
package/.agent/connected_apps.md
CHANGED
|
@@ -64,7 +64,8 @@ const { data: created } = await api.connectedApps.create(
|
|
|
64
64
|
)
|
|
65
65
|
// created.id, created.slug — server-derived
|
|
66
66
|
// created.status === 'pending' (transient) → 'synced' after route fetch
|
|
67
|
-
//
|
|
67
|
+
// (an M2M key IS auto-provisioned at create (see §2), but its id is NOT
|
|
68
|
+
// echoed on the resource — no response carries an api_key_id field)
|
|
68
69
|
// created.routes — array populated from .well-known/routes.json
|
|
69
70
|
// (empty until first sync)
|
|
70
71
|
// created.last_synced_at — set when routes successfully validated
|
|
@@ -191,9 +192,13 @@ last_synced_at string — ISO 8601; set at successful route sync
|
|
|
191
192
|
error string — populated when status === 'error'
|
|
192
193
|
routes array — discovered from .well-known/routes.json
|
|
193
194
|
(each: { name, path, description? })
|
|
194
|
-
api_key_id UUID — id of the auto-provisioned M2M key
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
(The auto-provisioned M2M key's id is **not** among these — neither
|
|
198
|
+
`.get` nor `get-metadata` returns an `api_key_id`. Don't assert on it
|
|
199
|
+
in a contract hook; the key itself reaches the deployment as
|
|
200
|
+
`ALVERA_API_KEY`, §2.)
|
|
201
|
+
|
|
197
202
|
**Caller-supplied (round-trip).**
|
|
198
203
|
|
|
199
204
|
```
|