@alvera-ai/platform-sdk 0.13.0 → 0.15.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/.agent/account_management.md +31 -0
- package/.agent/action_status_updaters.md +273 -25
- package/.agent/ai_sandbox.md +4 -2
- package/.agent/connected_apps.md +7 -2
- package/.agent/cookbook/action-status-updaters.md +73 -14
- package/.agent/cookbook/ai-agent-invoke.md +36 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +32 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +35 -1
- package/.agent/cookbook/bulk-ingest.md +48 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +36 -1
- package/.agent/cookbook/dunning-sms-for-delinquent.md +33 -1
- package/.agent/cookbook/generic-tables.md +40 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +35 -1
- package/.agent/cookbook/marketing-campaign-send.md +35 -0
- package/.agent/cookbook/paginated-restapi-poller.md +383 -0
- package/.agent/cookbook/rest-fetch.md +27 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +35 -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 +33 -1
- package/.agent/cookbook/welcome-sms-for-customers.md +33 -1
- package/.agent/datalakes.md +68 -3
- package/.agent/interoperability_contracts.md +29 -0
- 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 +189 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -182,7 +182,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
182
182
|
decision_key: DECISION_KEY,
|
|
183
183
|
position: 0,
|
|
184
184
|
trigger_template: 'now',
|
|
185
|
-
idempotency_template: '{{ customer_id }}-{{ decision_key }}
|
|
185
|
+
idempotency_template: '{{ customer_id }}-{{ decision_key }}',
|
|
186
186
|
connected_app_id: connectedAppId,
|
|
187
187
|
connected_app_route: '/portal/welcome',
|
|
188
188
|
connected_app_metadata_template: '{"customer_id":"{{ mdm_output.customer.id }}"}',
|
|
@@ -520,6 +520,38 @@ if (!tracked.message?.opened_at || !tracked.message?.form_submitted_at) {
|
|
|
520
520
|
}
|
|
521
521
|
```
|
|
522
522
|
|
|
523
|
+
## 014 — write the integration test
|
|
524
|
+
|
|
525
|
+
End the build with a test you keep: re-read the workflow and prove the
|
|
526
|
+
pipeline still executes — without a side effect. `mode: 'dry_run'` with a
|
|
527
|
+
never-matching selection runs the FULL pipeline (selection → filter →
|
|
528
|
+
decision) and intercepts only the final action call, so no message
|
|
529
|
+
leaves, yet the acknowledgement proves the workflow is runnable. This
|
|
530
|
+
block runs live under `make validate-cookbook`.
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
// Re-GET — the workflow must still be live, or nothing will run.
|
|
534
|
+
const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
|
|
535
|
+
if (wfRow.status !== 'live') {
|
|
536
|
+
throw new Error(`workflow regressed from live: ${wfRow.status}`)
|
|
537
|
+
}
|
|
538
|
+
// Behavioural probe — a dry run against a selection no row can match:
|
|
539
|
+
// the pipeline executes end-to-end, the final action call is
|
|
540
|
+
// intercepted, and the acknowledgement carries the run-log id.
|
|
541
|
+
const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
|
|
542
|
+
sql_where_clause: "ra.batch_id = 'test-never-matching-batch'",
|
|
543
|
+
mode: 'dry_run',
|
|
544
|
+
manual_override: false,
|
|
545
|
+
})
|
|
546
|
+
if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
|
|
547
|
+
throw new Error('dry-run probe returned no workflow_run_log_id')
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
If the probe fails in production, escalate with the run response as
|
|
552
|
+
evidence — don't flip the workflow's status or rewrite its configs to
|
|
553
|
+
chase the error.
|
|
554
|
+
|
|
523
555
|
# Branches
|
|
524
556
|
|
|
525
557
|
- **The no-phone customer is filtered, not failed** — §011
|
package/.agent/datalakes.md
CHANGED
|
@@ -240,7 +240,7 @@ a hostname pattern; values like `"invalid_host!"` (with
|
|
|
240
240
|
non-hostname punctuation) are rejected.
|
|
241
241
|
|
|
242
242
|
This is the only field-format constraint surfaced before the
|
|
243
|
-
synchronous reachability probe runs (see §
|
|
243
|
+
synchronous reachability probe runs (see §8 Gotcha 9) — a
|
|
244
244
|
malformed identifier never gets as far as a connection attempt.
|
|
245
245
|
|
|
246
246
|
### Schema names must be unique within a database
|
|
@@ -576,7 +576,7 @@ agent-facing markdown, or the whole-domain catalog can be fetched
|
|
|
576
576
|
in one shot via `api.datasets.metadata(tenantSlug, datalakeSlug)`. Pair `.systemDatasets()` with a
|
|
577
577
|
`page_size: 1` probe per name as a post-migration smoke (proves
|
|
578
578
|
each industry-built table is queryable; the post-ready probe
|
|
579
|
-
code is in §
|
|
579
|
+
code is in §8 Gotcha 3).
|
|
580
580
|
|
|
581
581
|
```typescript
|
|
582
582
|
const { data: catalog } = await api.datalakes.systemDatasets(
|
|
@@ -641,7 +641,48 @@ const { data: csv } = await api.datalakes.executeSql(
|
|
|
641
641
|
) // csv is a string, not the JSON envelope
|
|
642
642
|
```
|
|
643
643
|
|
|
644
|
-
## 6.
|
|
644
|
+
## 6. Logical name → physical table
|
|
645
|
+
|
|
646
|
+
The name you author is not the name you query. Datasets are addressed
|
|
647
|
+
**logically** everywhere in a manifest or SDK call, and **physically** in SQL:
|
|
648
|
+
|
|
649
|
+
| You author | You query in SQL | Schema |
|
|
650
|
+
|---|---|---|
|
|
651
|
+
| `message` | `regulated_messages` | `app_regulated` |
|
|
652
|
+
| `action_log` | `action_logs` | `app_unregulated` |
|
|
653
|
+
| a generic table `clients` | `regulated_alvera_custom_clients` | `app_regulated` |
|
|
654
|
+
|
|
655
|
+
Custom (generic) tables carry an `alvera_custom_` prefix, and the regulated side
|
|
656
|
+
adds a further `regulated_` prefix on top. The schema follows `execute-sql`'s
|
|
657
|
+
`--mode` (`regulated` → `app_regulated`, `unregulated` → `app_unregulated`).
|
|
658
|
+
|
|
659
|
+
**Don't derive these by hand — read them.** Guessing column and table names is
|
|
660
|
+
how six turns get spent on `column "error_message" does not exist` (the real
|
|
661
|
+
column is `failure_reason`):
|
|
662
|
+
|
|
663
|
+
- `alvera get-metadata dataset --only <type>` prints the **physical dataset
|
|
664
|
+
name** plus every column, from the Ecto struct itself.
|
|
665
|
+
- `alvera get-metadata generic-table --only <slug>` prints the exact **primary
|
|
666
|
+
table alias** to use in a `WHERE` clause, with a worked `SELECT`.
|
|
667
|
+
|
|
668
|
+
An `execute-sql` failure on an undefined column, relation, table, or schema now
|
|
669
|
+
names these verbs in its error, so the correction path is one command away.
|
|
670
|
+
|
|
671
|
+
## 7. The roster owns the dedupe uri — don't inject it per source
|
|
672
|
+
|
|
673
|
+
MDM matches on the full identification triple, so a `source_uri` injected
|
|
674
|
+
per data source participates in matching. Two sources describing the same
|
|
675
|
+
subject then produce two different triples, and the same subject is written
|
|
676
|
+
**twice** instead of being reconciled onto one entity.
|
|
677
|
+
|
|
678
|
+
Derive the dedupe uri **once, at the roster**, and let every data source inherit
|
|
679
|
+
it. Never compute a per-source `source_uri` and expect MDM to see through it.
|
|
680
|
+
|
|
681
|
+
This divergence is **not visible through `get-metadata`**: the roster value and
|
|
682
|
+
the per-source values each look well-formed in isolation, and only the duplicate
|
|
683
|
+
rows downstream reveal the mismatch. Check the derivation, not the metadata.
|
|
684
|
+
|
|
685
|
+
## 8. Gotchas
|
|
645
686
|
|
|
646
687
|
1. **`create()` does NOT auto-enqueue migration.** The create response
|
|
647
688
|
returns immediately with `status: 'new'`; Datalake-DB-resident
|
|
@@ -780,3 +821,27 @@ const { data: csv } = await api.datalakes.executeSql(
|
|
|
780
821
|
`meta.columns[i]`); never assume `row.someColumn`. The `?format=csv`
|
|
781
822
|
branch returns a CSV **string** with no `meta` envelope — for the
|
|
782
823
|
structural metadata, use the JSON call.
|
|
824
|
+
|
|
825
|
+
14. **On a FRESH create, never query the lake before `migrate` has
|
|
826
|
+
run.** The schema does not exist between `create()` and a completed
|
|
827
|
+
migration, so any check that queries the lake in that window fails
|
|
828
|
+
with `relation "…" does not exist` — and if that failing check
|
|
829
|
+
*halts your provisioning flow* (a CLI contract hook's
|
|
830
|
+
`afterCreateOrUpdate` runs exactly there), the halt lands BEFORE
|
|
831
|
+
migrate ever fires. The lake is then created-but-unmigrated, and a
|
|
832
|
+
reconcile-style flow reads it `unchanged` forever after —
|
|
833
|
+
unrecoverable without a manual migrate. Sequence query-the-lake
|
|
834
|
+
checks strictly after migrate + `ready` (in a CLI contract, that
|
|
835
|
+
means `afterMigrate`/`assertReady`, never `afterCreateOrUpdate`);
|
|
836
|
+
the create-then-migrate window is for platform-DB work only (§5's
|
|
837
|
+
storage-location split).
|
|
838
|
+
|
|
839
|
+
15. **A pooled reader endpoint can serve a stale schema on a
|
|
840
|
+
just-migrated lake.** If a reader profile points at a connection
|
|
841
|
+
pooler (e.g. Neon's `-pooler` host), a per-datalake DB session
|
|
842
|
+
started against the unmigrated schema can keep resolving the old,
|
|
843
|
+
empty `search_path` even after the migration lands — writes then
|
|
844
|
+
fail with `undefined_table` while `executeSql` reads the same table
|
|
845
|
+
fine. Prefer the direct (non-pooler) endpoint for the writer
|
|
846
|
+
profiles; if ingest 202s but nothing lands on a fresh lake, this is
|
|
847
|
+
the first thing to check. (Platform tracking: platform#784.)
|
|
@@ -236,6 +236,35 @@ match the consumer's expectations. If `filter_result === 'skip'`
|
|
|
236
236
|
(`stage === 'filtered'`), the row was filtered out before rendering
|
|
237
237
|
— useful for verifying the filter semantics on a known-skip example.
|
|
238
238
|
|
|
239
|
+
#### `.run` takes your payload VERBATIM — the pipeline adds two fields
|
|
240
|
+
|
|
241
|
+
The sandbox renders exactly what you send it. A real ingest does
|
|
242
|
+
not: before the templates see a row, the pipeline injects two
|
|
243
|
+
fields the sandbox has no way to know.
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
source_uri the URI of the DATA SOURCE behind the bound data
|
|
247
|
+
activation client — not anything in the row. A datalake's
|
|
248
|
+
default manual-upload data source is created with
|
|
249
|
+
`<datalake_id>.alvera.ai`, so that is what a row ingested
|
|
250
|
+
through it carries.
|
|
251
|
+
timezone the datalake's timezone
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Neither is bound during `.run` — there is no client in scope — so
|
|
255
|
+
`{{ p.source_uri }}` renders whatever your sample row happens to
|
|
256
|
+
carry (commonly nothing, or a placeholder like `test://crm/clients`).
|
|
257
|
+
Both templates are faithful; the INPUT differs.
|
|
258
|
+
|
|
259
|
+
The consequence bites when the identification `uri` is derived from
|
|
260
|
+
`source_uri`: MDM stores the rendered `uri` verbatim, so the sandbox
|
|
261
|
+
shows your placeholder and the persisted
|
|
262
|
+
`legal_entity_identifications` row shows `<datalake_id>.alvera.ai`.
|
|
263
|
+
**Do not assert on a `uri` that comes from `source_uri` in `.run`
|
|
264
|
+
output** — assert on `id_type` / `id_number`, which are row-derived
|
|
265
|
+
and survive persistence unchanged. To verify the client-derived
|
|
266
|
+
half, ingest through the DAC and query the persisted row.
|
|
267
|
+
|
|
239
268
|
### Generic-table contracts: auto-created identity contracts
|
|
240
269
|
|
|
241
270
|
When you create a generic table (see `generic_tables.md`), the
|
|
@@ -17,6 +17,7 @@ Fetches data via REST API with pagination support.
|
|
|
17
17
|
- **body** (TemplateConfig, nullable) — Request body, rendered as Liquid template
|
|
18
18
|
- **params** (TemplateConfig, nullable) — Query parameters, rendered as Liquid template
|
|
19
19
|
- **pagination_context_template** (TemplateConfig) — Pagination control template, must render to JSON with `has_next` boolean field
|
|
20
|
+
- **events_template** (TemplateConfig, nullable) — Extracts the raw event array from a response page; required when this config drives an action status updater REST poll (the poll fails with `missing_events_template` otherwise), unused by data-activation fetches
|
|
20
21
|
|
|
21
22
|
<!-- alvera:section kind="tool-call-config" atom="sql_query" module="Platform.Tools.ToolCallConfig.SQLQuery" -->
|
|
22
23
|
|
|
@@ -79,6 +80,16 @@ Sends an SMS message via the configured SMS provider.
|
|
|
79
80
|
- **body** (TemplateConfig) — Message body, rendered as Liquid template
|
|
80
81
|
- **sms_type** (enum: transactional, promotional) — Message classification; defaults to `transactional`
|
|
81
82
|
|
|
83
|
+
<!-- alvera:section kind="tool-call-config" atom="mms_request" module="Platform.Tools.ToolCallConfig.MMSRequest" -->
|
|
84
|
+
|
|
85
|
+
## Tool Call Configuration (mms_request)
|
|
86
|
+
|
|
87
|
+
Sends an MMS (media message) via AWS End User Messaging.
|
|
88
|
+
|
|
89
|
+
- **to** (TemplateConfig) — Recipient phone number in E.164 format, rendered as Liquid template
|
|
90
|
+
- **body** (TemplateConfig) — Message body (caption), rendered as Liquid template
|
|
91
|
+
- **media_url** (string) — Public http(s) URL of the media to attach; verified reachable (GET 2xx + image/*) at authoring, fetched and re-staged into the tool's S3 media bucket at send
|
|
92
|
+
|
|
82
93
|
<!-- alvera:section kind="tool-call-config" atom="email_request" module="Platform.Tools.ToolCallConfig.EmailRequest" -->
|
|
83
94
|
|
|
84
95
|
## Tool Call Configuration (email_request)
|
package/.agent/tools.md
CHANGED
|
@@ -36,6 +36,7 @@ Intent enum (typed):
|
|
|
36
36
|
|
|
37
37
|
ToolIntent.DATA_EXCHANGE // fetch / push payloads
|
|
38
38
|
ToolIntent.SMS // outbound text messages
|
|
39
|
+
ToolIntent.MMS // outbound MMS (media) messages
|
|
39
40
|
ToolIntent.EMAIL // outbound email
|
|
40
41
|
ToolIntent.VOICE // outbound voice / voicemail
|
|
41
42
|
ToolIntent.EXPORT // report / extract generation
|
|
@@ -53,11 +54,11 @@ Intent enum (typed):
|
|
|
53
54
|
// with AI agents (see ai_agents.md).
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
`ToolIntent` has exactly these
|
|
57
|
-
`sms`, `email`, `export`, `voice`, `data_exchange`,
|
|
58
|
-
`llm_enrichment` (`Ecto.Enum` in
|
|
59
|
-
line
|
|
60
|
-
`packages/sdk/src/generated/types.gen.ts` line
|
|
57
|
+
`ToolIntent` has exactly these eight members — the wire values are
|
|
58
|
+
`sms`, `mms`, `email`, `export`, `voice`, `data_exchange`,
|
|
59
|
+
`status_poller`, `llm_enrichment` (`Ecto.Enum` in
|
|
60
|
+
`platform/lib/platform/tools/tool.ex` line 241; `enum ToolIntent` in
|
|
61
|
+
`packages/sdk/src/generated/types.gen.ts` line 881). There is no
|
|
61
62
|
`chat_completion` or `context_extraction` intent.
|
|
62
63
|
|
|
63
64
|
Polymorphic body dispatch — the `body.tool_body_type` field is
|
|
@@ -73,6 +74,10 @@ the discriminator. Each branch is its own TypeScript type:
|
|
|
73
74
|
tool_body_type = 'rest_api' → generic REST endpoint
|
|
74
75
|
tool_body_type = 'sns' → AWS SNS topic (used for
|
|
75
76
|
SMS / push notifications)
|
|
77
|
+
tool_body_type = 'end_user_messaging'
|
|
78
|
+
→ AWS End User Messaging (used
|
|
79
|
+
for MMS / media messages via
|
|
80
|
+
the SendMediaMessage API)
|
|
76
81
|
tool_body_type = 'sharepoint' → Microsoft Graph SharePoint
|
|
77
82
|
tool_body_type = 'aws_lambda' → invoke a Lambda function
|
|
78
83
|
tool_body_type = 'sqs' → AWS SQS queue
|
|
@@ -95,9 +100,9 @@ discriminator is a string-literal on each branch of the generated
|
|
|
95
100
|
Consult the branch literals in
|
|
96
101
|
`packages/sdk/src/generated/types.gen.ts` for the complete set,
|
|
97
102
|
since new body types land independently of doc revisions. The
|
|
98
|
-
platform declares the same
|
|
103
|
+
platform declares the same twelve variants in
|
|
99
104
|
`platform/lib/platform/tools/tool.ex` (`polymorphic_embeds_one(:body,
|
|
100
|
-
…)`, lines
|
|
105
|
+
…)`, lines 255–269).
|
|
101
106
|
|
|
102
107
|
## 2. Rules the type cannot encode
|
|
103
108
|
|
|
@@ -111,6 +116,7 @@ create time:
|
|
|
111
116
|
data_exchange → s3, sftp, sql_database, rest_api,
|
|
112
117
|
manual_upload, aws_lambda, sharepoint, sqs
|
|
113
118
|
sms → sns, rest_api (provider-specific)
|
|
119
|
+
mms → end_user_messaging
|
|
114
120
|
email → email (dedicated body), rest_api,
|
|
115
121
|
aws_lambda (custom dispatchers)
|
|
116
122
|
voice → rest_api (Twilio Voice, etc.)
|
|
@@ -148,6 +154,21 @@ Each branch has its own required-list. Common patterns:
|
|
|
148
154
|
+ optional endpoint_url)
|
|
149
155
|
OR (iam_role)
|
|
150
156
|
|
|
157
|
+
end_user_messaging body region, auth_method (same three-way
|
|
158
|
+
access_key / iam_role / assume_role
|
|
159
|
+
shape as sns — gotcha 3), phone_number
|
|
160
|
+
(E.164 origination number), media_bucket,
|
|
161
|
+
configuration_set_name, plus EITHER
|
|
162
|
+
(access_key_id + secret_access_key)
|
|
163
|
+
OR (iam_role)
|
|
164
|
+
OR (assume_role_arn +
|
|
165
|
+
assume_role_external_id);
|
|
166
|
+
optional endpoint_url (overrides the End
|
|
167
|
+
User Messaging API endpoint) and
|
|
168
|
+
media_endpoint_url (overrides the S3
|
|
169
|
+
media-staging endpoint; blank ⇒ S3
|
|
170
|
+
derived from the tool's region)
|
|
171
|
+
|
|
151
172
|
sql_database body db_type, db_host, db_port, db_name,
|
|
152
173
|
auth_method, db_username,
|
|
153
174
|
db_password (or iam_role)
|
|
@@ -383,7 +404,7 @@ Common rejections:
|
|
|
383
404
|
───────────────────────────── ─────────────────────────────
|
|
384
405
|
"is invalid" on intent value not in ToolIntent enum
|
|
385
406
|
"is invalid" on tool_body_type value not one of the
|
|
386
|
-
|
|
407
|
+
twelve body-branch
|
|
387
408
|
discriminator literals
|
|
388
409
|
"is invalid" on body intent ↔ body_type pairing
|
|
389
410
|
rejected at platform level
|
|
@@ -430,22 +451,32 @@ await api.tools.testInvocation(
|
|
|
430
451
|
inlines the value verbatim; `type: 'system'` references a
|
|
431
452
|
platform-shipped template (see `templates.md`).
|
|
432
453
|
|
|
433
|
-
**Test-invocation accepts exactly
|
|
454
|
+
**Test-invocation accepts exactly FIVE `tool_call_type` variants** —
|
|
434
455
|
the interactive ones. Its request casts into a `ManualToolInvocation`
|
|
435
456
|
embed (`on_type_not_found: :raise`) whose polymorphic set is
|
|
436
|
-
`sms_request`, `email_request`, `restapi_request`,
|
|
457
|
+
`sms_request`, `mms_request`, `email_request`, `restapi_request`,
|
|
437
458
|
`aws_lambda_request`. A tool whose body is SFTP / S3 / SQL /
|
|
438
459
|
SharePoint has **no** test-invocation variant and cannot be exercised
|
|
439
460
|
through this endpoint (same shape as the `status_poller` case below).
|
|
440
461
|
Those config shapes still exist — but on the broader **workflow-action
|
|
441
462
|
`ToolCallConfig`** surface (see `tool-call-configs.md`), not here. The
|
|
442
|
-
per-branch required-list for the
|
|
463
|
+
per-branch required-list for the five test-invocable variants:
|
|
443
464
|
|
|
444
465
|
```
|
|
445
466
|
tool_call_type: 'sms_request'
|
|
446
467
|
required: to, body, sms_type
|
|
447
468
|
enums: sms_type ∈ { 'transactional' (default), 'promotional' }
|
|
448
469
|
|
|
470
|
+
tool_call_type: 'mms_request'
|
|
471
|
+
required: to, body, media_url
|
|
472
|
+
notes: body is the message caption; media_url is a PLAIN
|
|
473
|
+
STRING — a public http(s) URL, no Liquid. The server
|
|
474
|
+
verifies it at authoring (GET must answer 2xx with an
|
|
475
|
+
image/* content-type; malformed/non-http 422s without
|
|
476
|
+
fetching), then re-stages it into the tool's
|
|
477
|
+
media_bucket before send — targets a tool whose body
|
|
478
|
+
is end_user_messaging
|
|
479
|
+
|
|
449
480
|
tool_call_type: 'email_request'
|
|
450
481
|
required: to, subject, body
|
|
451
482
|
embeds: TemplateConfig variants accepted on each ({ type:
|
|
@@ -472,10 +503,10 @@ the workflow-action `ToolCallConfig` catalog in `tool-call-configs.md`;
|
|
|
472
503
|
they are **not** part of test-invocation.
|
|
473
504
|
|
|
474
505
|
Liquid template bodies (`query`, `payload`, body/params on
|
|
475
|
-
`restapi_request`, `sms_request` body, `
|
|
476
|
-
parsed at create time — invalid Liquid
|
|
477
|
-
|
|
478
|
-
outer request.
|
|
506
|
+
`restapi_request`, `sms_request` body, `mms_request` body,
|
|
507
|
+
`email_request` body) are parsed at create time — invalid Liquid
|
|
508
|
+
syntax surfaces as a field rejection on the corresponding
|
|
509
|
+
TemplateConfig path, not on the outer request.
|
|
479
510
|
|
|
480
511
|
The response shape:
|
|
481
512
|
|
|
@@ -495,27 +526,63 @@ Only platform-internal failures (DB unreachable, etc.) produce
|
|
|
495
526
|
5xx. Treat 5xx as infra noise; treat `200 + status:"error"` as a
|
|
496
527
|
real (and handleable) configuration problem.
|
|
497
528
|
|
|
498
|
-
### `status_poller`
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
529
|
+
### Proving a `status_poller` tool — it depends on the BODY, not the intent
|
|
530
|
+
|
|
531
|
+
Invocability is keyed on the tool's **body type**, not its intent.
|
|
532
|
+
`restapi_request` is the call type for ANY intent with a `rest_api`
|
|
533
|
+
body, so a `status_poller` with a `rest_api` body — the shape a
|
|
534
|
+
provider events-API poller takes — **can** be test-invoked, and
|
|
535
|
+
that is the cheapest way to prove its credentials:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
await api.tools.testInvocation(tenantSlug, datalakeSlug, pollerId, {
|
|
539
|
+
tool_call: {
|
|
540
|
+
tool_call_type: 'restapi_request',
|
|
541
|
+
method: 'get',
|
|
542
|
+
path: { type: 'custom', body: '/v3/YOUR-DOMAIN/events' },
|
|
543
|
+
params: { type: 'custom', body: '{"limit": 1}' }, // keep it read-only
|
|
544
|
+
},
|
|
545
|
+
})
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
A `cloud_watch_log_group` body is the one that has no call type —
|
|
549
|
+
nothing in the closed `tool_call_type` set fits it. **That** poller's
|
|
550
|
+
run surface is the action status updater that fetches through it:
|
|
551
|
+
its `last_run_at` / `last_run_status` / `last_run_error` /
|
|
552
|
+
`last_run_events_found` fields (excluded from the drift checksum, so
|
|
553
|
+
a poll never reads as config drift) plus the `Last run` section of
|
|
554
|
+
the ASU's AI metadata. A failed poll lands its structured reason in
|
|
555
|
+
`last_run_error`.
|
|
556
|
+
|
|
557
|
+
You do **not** have to wait for the cron. `actionStatusUpdaters.refresh`
|
|
558
|
+
fires one poll cycle on demand (CLI: `alvera refresh-action-status-updater
|
|
559
|
+
--id <uuid>`), and reading `last_run_*` correctly is what makes it
|
|
560
|
+
usable as a proof — three things to get right:
|
|
561
|
+
|
|
562
|
+
- **`refresh` answers `202` with the updater row AS-IS.** The poll runs
|
|
563
|
+
asynchronously, so the response carries the PREVIOUS run's stamps.
|
|
564
|
+
Capture `last_run_at` as a baseline BEFORE calling it, then poll the
|
|
565
|
+
row until `last_run_at` advances past that baseline.
|
|
566
|
+
- **`last_run_status` has three values.** `'ok'` means the run completed
|
|
567
|
+
and read its whole time window; `'partial'` means the provider fetch was
|
|
568
|
+
truncated, so the newest events may be missing and `last_run_error`
|
|
569
|
+
carries the detail; `'error'` means the run failed. Only
|
|
570
|
+
`last_run_status === 'ok'` proves a complete reconciliation — a
|
|
571
|
+
`!== 'error'` check silently accepts a truncated `partial`.
|
|
572
|
+
`last_run_events_found` counts the current run's events, and because
|
|
573
|
+
`refresh` is async an early read can show an in-progress figure.
|
|
574
|
+
- **`status` is the halt flag.** It is `'active'` normally and the
|
|
575
|
+
server sets `'cycle_detected'` when a run re-reads events it has
|
|
576
|
+
already handled — every later job then fails without calling the
|
|
577
|
+
provider, and only setting it back to `'active'` resumes polling. It
|
|
578
|
+
is also the fastest deterministic failure to assert on: a
|
|
579
|
+
non-advancing pagination config trips it within seconds, no provider
|
|
580
|
+
credential rejection required.
|
|
581
|
+
|
|
582
|
+
See `action_status_updaters.md` §7 for the full refresh + `last_run_*`
|
|
583
|
+
contract. A born-red contract for a CloudWatch poller should assert the
|
|
584
|
+
tool's own config truths and prove the polling behaviour through the
|
|
585
|
+
owning ASU on those terms.
|
|
519
586
|
|
|
520
587
|
### Update
|
|
521
588
|
|
package/.agent/type_naming.md
CHANGED
|
@@ -105,6 +105,10 @@ await api.tools.get(tenantSlug, datalakeSlug, toolId)
|
|
|
105
105
|
// 2b. downstream resources reference the tool by id
|
|
106
106
|
await api.actionStatusUpdaters.create(tenantSlug, datalakeSlug, {
|
|
107
107
|
updater_tool_id: toolId,
|
|
108
|
+
action_log_config: {
|
|
109
|
+
type: 'custom',
|
|
110
|
+
body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
|
|
111
|
+
},
|
|
108
112
|
// …
|
|
109
113
|
})
|
|
110
114
|
```
|