@alvera-ai/platform-sdk 0.13.0 → 0.14.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 +174 -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 +34 -0
- package/.agent/cookbook/bulk-ingest.md +48 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +35 -0
- package/.agent/cookbook/dunning-sms-for-delinquent.md +32 -0
- package/.agent/cookbook/generic-tables.md +40 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +34 -0
- package/.agent/cookbook/marketing-campaign-send.md +35 -0
- package/.agent/cookbook/paginated-restapi-poller.md +329 -0
- package/.agent/cookbook/rest-fetch.md +27 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +34 -0
- package/.agent/cookbook/score-leads-with-llm-categorization.md +35 -0
- 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 +32 -0
- package/.agent/cookbook/welcome-sms-for-customers.md +32 -0
- package/.agent/datalakes.md +24 -0
- 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
|
@@ -547,6 +547,41 @@ for (const wel of ourWels) {
|
|
|
547
547
|
}
|
|
548
548
|
```
|
|
549
549
|
|
|
550
|
+
## 011 — write the integration test
|
|
551
|
+
|
|
552
|
+
End the build with a test you keep: re-read the workflow and prove the
|
|
553
|
+
pipeline still executes — without a side effect. `mode: 'dry_run'` with a
|
|
554
|
+
never-matching selection runs the FULL pipeline (selection → filter →
|
|
555
|
+
decision) and intercepts only the final action call, so no message
|
|
556
|
+
leaves, yet the acknowledgement proves the workflow is runnable. This
|
|
557
|
+
block runs live under `make validate-cookbook`.
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// Re-GET — the workflow must still be live, or nothing will run.
|
|
561
|
+
const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
|
|
562
|
+
if (wfRow.status !== 'live') {
|
|
563
|
+
throw new Error(`workflow regressed from live: ${wfRow.status}`)
|
|
564
|
+
}
|
|
565
|
+
// Behavioural probe — a dry run against a selection no row can match:
|
|
566
|
+
// the pipeline executes end-to-end, the final action call is
|
|
567
|
+
// intercepted, and the acknowledgement carries the run-log id. The
|
|
568
|
+
// clause speaks this workflow's selection dialect: a GENERIC-TABLE
|
|
569
|
+
// dataset is addressed by its own columns (no `ra.` dataset alias —
|
|
570
|
+
// that alias exists only for system-dataset selections).
|
|
571
|
+
const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
|
|
572
|
+
sql_where_clause: "submission_id = 'test-never-matching-submission'",
|
|
573
|
+
mode: 'dry_run',
|
|
574
|
+
manual_override: false,
|
|
575
|
+
})
|
|
576
|
+
if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
|
|
577
|
+
throw new Error('dry-run probe returned no workflow_run_log_id')
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
If the probe fails in production, escalate with the run response as
|
|
582
|
+
evidence — don't flip the workflow's status or rewrite its configs to
|
|
583
|
+
chase the error.
|
|
584
|
+
|
|
550
585
|
# Branches
|
|
551
586
|
|
|
552
587
|
- **The filter is permissive** — `filter_config.body: 'true'` passes
|
|
@@ -103,6 +103,42 @@ if (typeof detail !== 'string' || detail.length === 0) {
|
|
|
103
103
|
}
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
## 004 — write the integration test
|
|
107
|
+
|
|
108
|
+
End the walk with a test you keep: anything that references a system
|
|
109
|
+
template by path (`template_config: { type: 'system', path: … }`) depends
|
|
110
|
+
on that path staying in this datalake's scope — so the durable test
|
|
111
|
+
re-asserts the anchor is listed, the domain filter holds, and the detail
|
|
112
|
+
surface renders. This block runs live under `make validate-cookbook`.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const { data: tplList } = await api.templates.systemTemplates(tenantSlug, datalakeSlug)
|
|
116
|
+
const tplPaths = (tplList.data ?? []).map((t) => t.path)
|
|
117
|
+
const anchor = 'data_activation/interoperability/subscription/stripe/customers_subscription_customer'
|
|
118
|
+
if (!tplPaths.some((p) => p.includes(anchor))) {
|
|
119
|
+
throw new Error(`anchor template ${anchor} vanished from the domain-scoped list`)
|
|
120
|
+
}
|
|
121
|
+
const foreign = ['healthcare', 'payments', 'foundation', 'core_banking', 'service_commerce', 'trading']
|
|
122
|
+
const leakedPaths = tplPaths.filter((p) => foreign.some((d) => p.split('/').includes(d)))
|
|
123
|
+
if (leakedPaths.length > 0) {
|
|
124
|
+
throw new Error(`cross-domain templates leaked: ${leakedPaths.join(', ')}`)
|
|
125
|
+
}
|
|
126
|
+
// Behavioural probe — the detail surface must render for the anchor.
|
|
127
|
+
const { data: anchorDetail } = await api.templates.metadataDetails(
|
|
128
|
+
tenantSlug,
|
|
129
|
+
datalakeSlug,
|
|
130
|
+
'customers_subscription_customer',
|
|
131
|
+
'data_activation_interoperability',
|
|
132
|
+
)
|
|
133
|
+
if (typeof anchorDetail !== 'string' || anchorDetail.length === 0) {
|
|
134
|
+
throw new Error('anchor template metadataDetails came back empty')
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If the anchor disappears in production, escalate with the listing
|
|
139
|
+
evidence — a `type: system` reference that stops resolving is a platform
|
|
140
|
+
regression, not something to paper over by inlining the template body.
|
|
141
|
+
|
|
106
142
|
# Gotchas
|
|
107
143
|
|
|
108
144
|
- **The list is domain-scoped, not global.** `systemTemplates` only returns
|
|
@@ -103,6 +103,45 @@ if (!/\bn\b/.test(csv) || !csv.includes('1')) {
|
|
|
103
103
|
}
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
## 004 — write the integration test
|
|
107
|
+
|
|
108
|
+
End the walk with a test you keep: both surfaces are read-only, so the
|
|
109
|
+
durable test is the pair of deterministic boundary probes — the
|
|
110
|
+
round-trip works, and the read-only wall holds. Neither depends on an
|
|
111
|
+
LLM provider or seeded data, which is what makes the test durable. This
|
|
112
|
+
block runs live under `make validate-cookbook`.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Round-trip probe — deterministic, data-independent.
|
|
116
|
+
const probe = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
117
|
+
sql: 'SELECT 1 AS ok',
|
|
118
|
+
mode: 'unregulated',
|
|
119
|
+
})
|
|
120
|
+
if (typeof probe.data === 'string' || probe.data.data[0]?.[0] !== 1) {
|
|
121
|
+
throw new Error('executeSql round-trip probe failed')
|
|
122
|
+
}
|
|
123
|
+
// Boundary probe — write SQL must be rejected with a 422; the
|
|
124
|
+
// read-only wall is the security property this capability rests on.
|
|
125
|
+
let walled = false
|
|
126
|
+
try {
|
|
127
|
+
await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
|
|
128
|
+
sql: "INSERT INTO legal_entities (legal_name) VALUES ('test-should-never-land')",
|
|
129
|
+
mode: 'unregulated',
|
|
130
|
+
})
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const status = (err as { _httpStatus?: number })._httpStatus
|
|
133
|
+
if (status !== 422) throw err
|
|
134
|
+
walled = true
|
|
135
|
+
}
|
|
136
|
+
if (!walled) {
|
|
137
|
+
throw new Error('executeSql accepted a write statement — the read-only wall is down')
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If the write probe ever lands in production, stop and escalate
|
|
142
|
+
immediately with the accepted statement as evidence — a read-only
|
|
143
|
+
boundary failure is a security regression, never a config problem.
|
|
144
|
+
|
|
106
145
|
# Branches
|
|
107
146
|
|
|
108
147
|
- **Generation failure.** If every configured LLM provider fails, `textToSql`
|
|
@@ -480,6 +480,38 @@ for (const wel of ourWels) {
|
|
|
480
480
|
}
|
|
481
481
|
```
|
|
482
482
|
|
|
483
|
+
## 013 — write the integration test
|
|
484
|
+
|
|
485
|
+
End the build with a test you keep: re-read the workflow and prove the
|
|
486
|
+
pipeline still executes — without a side effect. `mode: 'dry_run'` with a
|
|
487
|
+
never-matching selection runs the FULL pipeline (selection → filter →
|
|
488
|
+
decision) and intercepts only the final action call, so no message
|
|
489
|
+
leaves, yet the acknowledgement proves the workflow is runnable. This
|
|
490
|
+
block runs live under `make validate-cookbook`.
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// Re-GET — the workflow must still be live, or nothing will run.
|
|
494
|
+
const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
|
|
495
|
+
if (wfRow.status !== 'live') {
|
|
496
|
+
throw new Error(`workflow regressed from live: ${wfRow.status}`)
|
|
497
|
+
}
|
|
498
|
+
// Behavioural probe — a dry run against a selection no row can match:
|
|
499
|
+
// the pipeline executes end-to-end, the final action call is
|
|
500
|
+
// intercepted, and the acknowledgement carries the run-log id.
|
|
501
|
+
const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
|
|
502
|
+
sql_where_clause: "ra.batch_id = 'test-never-matching-batch'",
|
|
503
|
+
mode: 'dry_run',
|
|
504
|
+
manual_override: false,
|
|
505
|
+
})
|
|
506
|
+
if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
|
|
507
|
+
throw new Error('dry-run probe returned no workflow_run_log_id')
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
If the probe fails in production, escalate with the run response as
|
|
512
|
+
evidence — don't flip the workflow's status or rewrite its configs to
|
|
513
|
+
chase the error.
|
|
514
|
+
|
|
483
515
|
# Branches
|
|
484
516
|
|
|
485
517
|
- **Agent emits an out-of-enum band** — the response schema's
|
|
@@ -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
|
@@ -780,3 +780,27 @@ const { data: csv } = await api.datalakes.executeSql(
|
|
|
780
780
|
`meta.columns[i]`); never assume `row.someColumn`. The `?format=csv`
|
|
781
781
|
branch returns a CSV **string** with no `meta` envelope — for the
|
|
782
782
|
structural metadata, use the JSON call.
|
|
783
|
+
|
|
784
|
+
14. **On a FRESH create, never query the lake before `migrate` has
|
|
785
|
+
run.** The schema does not exist between `create()` and a completed
|
|
786
|
+
migration, so any check that queries the lake in that window fails
|
|
787
|
+
with `relation "…" does not exist` — and if that failing check
|
|
788
|
+
*halts your provisioning flow* (a CLI contract hook's
|
|
789
|
+
`afterCreateOrUpdate` runs exactly there), the halt lands BEFORE
|
|
790
|
+
migrate ever fires. The lake is then created-but-unmigrated, and a
|
|
791
|
+
reconcile-style flow reads it `unchanged` forever after —
|
|
792
|
+
unrecoverable without a manual migrate. Sequence query-the-lake
|
|
793
|
+
checks strictly after migrate + `ready` (in a CLI contract, that
|
|
794
|
+
means `afterMigrate`/`assertReady`, never `afterCreateOrUpdate`);
|
|
795
|
+
the create-then-migrate window is for platform-DB work only (§5's
|
|
796
|
+
storage-location split).
|
|
797
|
+
|
|
798
|
+
15. **A pooled reader endpoint can serve a stale schema on a
|
|
799
|
+
just-migrated lake.** If a reader profile points at a connection
|
|
800
|
+
pooler (e.g. Neon's `-pooler` host), a per-datalake DB session
|
|
801
|
+
started against the unmigrated schema can keep resolving the old,
|
|
802
|
+
empty `search_path` even after the migration lands — writes then
|
|
803
|
+
fail with `undefined_table` while `executeSql` reads the same table
|
|
804
|
+
fine. Prefer the direct (non-pooler) endpoint for the writer
|
|
805
|
+
profiles; if ingest 202s but nothing lands on a fresh lake, this is
|
|
806
|
+
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
|
```
|