@alvera-ai/platform-sdk 0.21.2 → 0.21.3-next.g393d024
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 +11 -18
- package/.agent/action_logs.md +2 -2
- package/.agent/action_status_updaters.md +63 -6
- package/.agent/async.md +4 -4
- package/.agent/cookbook/_fixtures/README.md +28 -32
- package/.agent/cookbook/organic-marketing.md +0 -5
- package/.agent/cookbook/payments-compliance.md +0 -10
- package/.agent/cookbook/primary-care.md +0 -5
- package/.agent/cookbook/subscription-saas.md +0 -8
- package/.agent/datalakes.md +2 -2
- package/.agent/errors.md +32 -6
- package/.agent/interoperability_contracts.md +4 -7
- package/.agent/mdm.md +165 -81
- package/.agent/tools.md +5 -1
- package/dist/index.d.mts +17 -30
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +16 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/.agent/AGENTS.md
CHANGED
|
@@ -320,8 +320,8 @@ forms are the SQL table names used by `executeSql`):
|
|
|
320
320
|
|
|
321
321
|
`.agent/cookbook/<slug>.md` files are pure-markdown scenario
|
|
322
322
|
stories — one business outcome each, told as a numbered API-call
|
|
323
|
-
sequence
|
|
324
|
-
platform
|
|
323
|
+
sequence that the cookbook validator runs, step by step, against a
|
|
324
|
+
live platform. Agents read these directly
|
|
325
325
|
from the filesystem at
|
|
326
326
|
`node_modules/@alvera-ai/platform-sdk/.agent/cookbook/`; there
|
|
327
327
|
is no SDK function and no CLI verb that returns cookbook bytes
|
|
@@ -333,24 +333,21 @@ cookbook that is missing one, repeats one, reorders them, leaves one
|
|
|
333
333
|
empty, or adds a top-level section that is not on this list:
|
|
334
334
|
|
|
335
335
|
- **Front matter** — `title`, `summary`, `use_case`, `slug`,
|
|
336
|
-
`
|
|
337
|
-
are lifted from, anchor file first), `status`. `use_case` is one
|
|
336
|
+
`status`. `use_case` is one
|
|
338
337
|
of the four surfaces — `organic-marketing`,
|
|
339
338
|
`subscription-saas`, `payments-compliance`,
|
|
340
339
|
`primary-care-feedback` — and there is exactly one cookbook per use
|
|
341
340
|
case. Three are at `cookbook/<use-case>.md`; the fourth is at
|
|
342
|
-
`cookbook/primary-care.md`, because `use_case`
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
than deriving it from the filename.
|
|
341
|
+
`cookbook/primary-care.md`, because `use_case` keeps the use case's
|
|
342
|
+
full name and the filename does not. Read `use_case` from the front
|
|
343
|
+
matter rather than deriving it from the filename.
|
|
346
344
|
- **Problem** — the business-outcome statement in domain terms,
|
|
347
|
-
sourced from the
|
|
345
|
+
sourced from what the walk's steps actually do (not from
|
|
348
346
|
customer-narrative documentation).
|
|
349
347
|
- **Composition** — table of resources provisioned, mapped to
|
|
350
348
|
the audience skill that owns each (setup / build / compose).
|
|
351
349
|
- **Walkthrough** — numbered `## NNN — <step>` subsections, each
|
|
352
|
-
carrying one fenced TypeScript block
|
|
353
|
-
ancillary vitest. State threads forward across subsections via
|
|
350
|
+
carrying one fenced TypeScript block. State threads forward across subsections via
|
|
354
351
|
plain JavaScript variables in the validator's generated
|
|
355
352
|
`describe` closure. A numbered step anywhere else in the file
|
|
356
353
|
is refused.
|
|
@@ -363,8 +360,7 @@ empty, or adds a top-level section that is not on this list:
|
|
|
363
360
|
step the cookbook does not have — and every step runs against a
|
|
364
361
|
live platform — so a gotcha cannot outlive the behaviour it warns
|
|
365
362
|
about.
|
|
366
|
-
- **See also** — links to relevant per-resource reference MDs
|
|
367
|
-
and to the anchor + ancillary vitest files.
|
|
363
|
+
- **See also** — links to relevant per-resource reference MDs.
|
|
368
364
|
|
|
369
365
|
### Every cookbook is self-reliant, and repeats what it needs
|
|
370
366
|
|
|
@@ -403,17 +399,14 @@ map inbound rows into the platform's upsert shapes. Inlining
|
|
|
403
399
|
those templates verbatim inside every cookbook markdown
|
|
404
400
|
bloats each recipe to several hundred lines of Liquid noise
|
|
405
401
|
that obscures the cookbook's lesson. The corpus ships them
|
|
406
|
-
as **vendored fixtures** instead
|
|
407
|
-
repo's own integration tests use.
|
|
402
|
+
as **vendored fixtures** instead.
|
|
408
403
|
|
|
409
404
|
- Fixtures live under
|
|
410
405
|
`.agent/cookbook/_fixtures/<industry>/`. The leading
|
|
411
406
|
underscore marks the directory as fragment infrastructure
|
|
412
407
|
(not a cookbook); the validator's discovery walk skips it.
|
|
413
408
|
Each `<industry>/` subdirectory carries a `README.md`
|
|
414
|
-
listing every template
|
|
415
|
-
production source-of-truth path it was vendored from
|
|
416
|
-
(`platform/priv/liquid_templates/…`).
|
|
409
|
+
listing every template and which cookbook consumes it.
|
|
417
410
|
- Cookbook code loads fixtures via the
|
|
418
411
|
`COOKBOOK_FIXTURES_DIR` environment variable:
|
|
419
412
|
```typescript
|
package/.agent/action_logs.md
CHANGED
|
@@ -113,8 +113,8 @@ also merges each finished batch into NDJSON artifacts in cold
|
|
|
113
113
|
storage:
|
|
114
114
|
|
|
115
115
|
```
|
|
116
|
-
<datalake>/workflows/<wf_slug
|
|
117
|
-
<datalake>/workflows/<wf_slug
|
|
116
|
+
<datalake>/workflows/<wf_slug>/merged/<batch_id>.ndjson (per-subject)
|
|
117
|
+
<datalake>/workflows/<wf_slug>/merged/<batch_id>/<decision_key>.ndjson (per-action payloads)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
The batch log row (`api.workflows.batchLogs.get`) carries the
|
|
@@ -409,6 +409,10 @@ Standard JSON:API envelopes per `errors.md`. Common rejections:
|
|
|
409
409
|
| (detail, not pointer) | `action_log_config` present but renders `null`/empty — detail begins `must render a JSON object containing external_id` |
|
|
410
410
|
| `/base` | `cron_management_failed` — body validated and saved but scheduler registration failed; the row is rolled back |
|
|
411
411
|
|
|
412
|
+
`refresh()` has two refusals of its own, both **409** with an empty
|
|
413
|
+
`source.pointer`: the updater is halted, or a poll for it is already
|
|
414
|
+
queued or running. Neither queues anything. See §7.
|
|
415
|
+
|
|
412
416
|
## 5. Lifecycle
|
|
413
417
|
|
|
414
418
|
### Create
|
|
@@ -509,7 +513,7 @@ check or retry for partial-delete states.
|
|
|
509
513
|
The poll follows pages while `has_next` is true and stops on
|
|
510
514
|
the first empty page.
|
|
511
515
|
|
|
512
|
-
## 7. Manual trigger via `refresh` — 202, and what `last_run_*` means
|
|
516
|
+
## 7. Manual trigger via `refresh` — 202 or 409, and what `last_run_*` means
|
|
513
517
|
|
|
514
518
|
An action status updater CAN be fired on demand:
|
|
515
519
|
|
|
@@ -548,16 +552,67 @@ while (Date.now() < deadline) {
|
|
|
548
552
|
// row.last_run_status / last_run_error now describe THIS run
|
|
549
553
|
```
|
|
550
554
|
|
|
555
|
+
### When `refresh` answers 409, nothing was queued
|
|
556
|
+
|
|
557
|
+
`refresh` refuses with **409 Conflict** in two cases. The body is the
|
|
558
|
+
standard `errors[]` envelope (`errors.md`), thrown with
|
|
559
|
+
`_httpStatus === 409`. Either way **no poll is queued**, and a one-shot
|
|
560
|
+
`updater_body` override sent with that call is not queued either, so
|
|
561
|
+
resend it on the retry.
|
|
562
|
+
|
|
563
|
+
| Case | `detail` | What clears it |
|
|
564
|
+
|------|----------|----------------|
|
|
565
|
+
| **Halted**: `status` is not `'active'` | `Updater is halted: status is <status>. Set status to active to resume polling.`, followed by ` Last run error: <text>` when one is recorded | Fix the cause, then `update()` with `status: 'active'` (below). Retrying `refresh` changes nothing. |
|
|
566
|
+
| **A poll is already queued or running**: a cron tick's, an earlier refresh's, or a later page of a run under way | `A poll for this updater is already queued, so this refresh was not. Retry once it finishes.` | Wait for that poll to finish (`last_run_at` advances), then call `refresh` again. |
|
|
567
|
+
|
|
568
|
+
Both carry `title: "Conflict"` and an empty `source.pointer`, so
|
|
569
|
+
`title` cannot tell them apart. Read the row instead: the halt is
|
|
570
|
+
checked first, so `status !== 'active'` means halted and anything else
|
|
571
|
+
means a poll is in flight.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
async function refreshOrExplain(updaterId: string): Promise<void> {
|
|
575
|
+
try {
|
|
576
|
+
await api.actionStatusUpdaters.refresh(tenantSlug, datalakeSlug, updaterId)
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if ((err as { _httpStatus?: number })._httpStatus !== 409) throw err
|
|
579
|
+
const { data } = await api.actionStatusUpdaters.get(tenantSlug, datalakeSlug, updaterId)
|
|
580
|
+
if (data.status !== 'active') {
|
|
581
|
+
// Halted. Fix the cause, then update() with status: 'active'.
|
|
582
|
+
throw new Error(`updater halted (${data.status}): ${data.last_run_error ?? 'no run error recorded'}`)
|
|
583
|
+
}
|
|
584
|
+
// A poll is already queued or running. Wait for last_run_at to
|
|
585
|
+
// advance past data.last_run_at, then call refresh again.
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### One run at a time per updater
|
|
591
|
+
|
|
592
|
+
A run reads every page of its window before it ends. A cron tick that
|
|
593
|
+
finds an earlier run of the same updater still going does not start a
|
|
594
|
+
second run beside it: it **skips**, and records when in
|
|
595
|
+
`last_skipped_at`. A skip is not a run, so it leaves `last_run_at`,
|
|
596
|
+
`last_run_status`, `last_run_error` and `last_run_events_found` alone.
|
|
597
|
+
The same rule is why `refresh` answers 409 while a poll is queued or
|
|
598
|
+
running.
|
|
599
|
+
|
|
600
|
+
`last_skipped_at` is `null` until a tick has skipped. A recent one
|
|
601
|
+
means a run outlasted a cron interval. If skips keep recurring, runs
|
|
602
|
+
take longer than the interval: lengthen `cron_expression` or narrow
|
|
603
|
+
the poll window.
|
|
604
|
+
|
|
551
605
|
What the ASU still does NOT have is a **per-run log** (nothing like
|
|
552
606
|
a data activation client's `logs` or a workflow's `batchLogs`). The
|
|
553
|
-
run-outcome surface is four fields on the row itself, plus the
|
|
554
|
-
flag:
|
|
607
|
+
run-outcome surface is four fields on the row itself, plus the skip
|
|
608
|
+
stamp and the halt flag:
|
|
555
609
|
|
|
556
610
|
```
|
|
557
611
|
last_run_at when the poll worker last made progress
|
|
558
612
|
last_run_status 'ok' | 'error'
|
|
559
613
|
last_run_error why the last run stopped; null when it read its whole window
|
|
560
614
|
last_run_events_found events fetched so far in the current run
|
|
615
|
+
last_skipped_at when a cron tick last skipped because a run was still going; null if none has
|
|
561
616
|
status 'active' | 'cycle_detected'
|
|
562
617
|
```
|
|
563
618
|
|
|
@@ -584,7 +639,8 @@ a poll whose pagination does not advance (a provider cursor that never
|
|
|
584
639
|
drains, or a one-shot `updater_body` override). Every later job then
|
|
585
640
|
fails without calling the provider, and `last_run_error` carries the
|
|
586
641
|
diagnostic. **Nothing clears it but setting `status` back to
|
|
587
|
-
`'active'`.**
|
|
642
|
+
`'active'`.** While it is halted, `refresh` answers 409 and queues
|
|
643
|
+
nothing (above). It is also the cheapest deterministic failure to assert
|
|
588
644
|
on: a non-advancing config trips it within seconds, no provider
|
|
589
645
|
credential rejection required. Related: a `restapi` updater that
|
|
590
646
|
declares pagination but whose `path`/`params`/`body` never read
|
|
@@ -593,8 +649,9 @@ declares pagination but whose `path`/`params`/`body` never read
|
|
|
593
649
|
|
|
594
650
|
**If a message's status is not reconciling, do this — in order:**
|
|
595
651
|
|
|
596
|
-
1. Call `refresh()
|
|
597
|
-
|
|
652
|
+
1. Call `refresh()`. A 409 says why nothing was queued: halted, or
|
|
653
|
+
a poll already in flight (above). Otherwise wait for `last_run_at`
|
|
654
|
+
to advance past the baseline you captured, then read `last_run_status` /
|
|
598
655
|
`last_run_error` / `status`. They name the failing stage: window
|
|
599
656
|
render, source fetch, template render, JSON parse — or a halted
|
|
600
657
|
poll cycle.
|
package/.agent/async.md
CHANGED
|
@@ -99,10 +99,10 @@ retry should:
|
|
|
99
99
|
that names the uniqueness key (usually `/name` or `/slug`).
|
|
100
100
|
3. Treat as "already created" and fetch the existing row.
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
This is the "idempotent retry path": use it wherever local
|
|
103
|
+
state may have diverged from server state (e.g. a prior run
|
|
104
|
+
wrote to the server but failed to capture the response
|
|
105
|
+
locally).
|
|
106
106
|
|
|
107
107
|
## Polling cost
|
|
108
108
|
|
|
@@ -52,15 +52,11 @@ the package.
|
|
|
52
52
|
|
|
53
53
|
## Provenance
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this directory extends that chain into the SDK package so the
|
|
61
|
-
cookbook corpus is self-contained at npm-install time.
|
|
62
|
-
|
|
63
|
-
When a production template changes, re-copy the file here and
|
|
55
|
+
Each template is a verbatim copy of the template the platform
|
|
56
|
+
ships for that feed, vendored here so the cookbook corpus is
|
|
57
|
+
self-contained at npm-install time.
|
|
58
|
+
|
|
59
|
+
When the platform's template changes, re-copy the file here and
|
|
64
60
|
re-run `make validate-cookbook` to confirm the cookbook still
|
|
65
61
|
passes against the new template shape.
|
|
66
62
|
|
|
@@ -68,21 +64,21 @@ passes against the new template shape.
|
|
|
68
64
|
|
|
69
65
|
### `foundation/`
|
|
70
66
|
|
|
71
|
-
| File | Consumed by cookbook(s) |
|
|
72
|
-
|
|
73
|
-
| `_lead_submissions_foundation_legal_entity.liquid` | `birthday-greeting-sms-trigger`, `score-leads-with-llm-categorization` |
|
|
74
|
-
| `_lead_submissions_foundation_generic_table.liquid` | `score-leads-with-llm-categorization` |
|
|
75
|
-
| `_lead_submissions_foundation_mdm.liquid` | `score-leads-with-llm-categorization` |
|
|
67
|
+
| File | Consumed by cookbook(s) |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `_lead_submissions_foundation_legal_entity.liquid` | `birthday-greeting-sms-trigger`, `score-leads-with-llm-categorization` |
|
|
70
|
+
| `_lead_submissions_foundation_generic_table.liquid` | `score-leads-with-llm-categorization` |
|
|
71
|
+
| `_lead_submissions_foundation_mdm.liquid` | `score-leads-with-llm-categorization` |
|
|
76
72
|
|
|
77
73
|
### `healthcare/`
|
|
78
74
|
|
|
79
|
-
| File | Consumed by cookbook(s) |
|
|
80
|
-
|
|
81
|
-
| `_cahps_appointments_healthcare_patient.liquid` | `appointment-review-sms-workflow` |
|
|
82
|
-
| `_cahps_appointments_healthcare_appointment.liquid` | `appointment-review-sms-workflow` |
|
|
83
|
-
| `_cahps_appointments_healthcare_mdm.liquid` | `appointment-review-sms-workflow` |
|
|
84
|
-
| `sample_two_page.pdf` (data) | `ai-agent-invoke` |
|
|
85
|
-
| `memorandum-of-association-01.png` (data) | `ai-agent-invoke` |
|
|
75
|
+
| File | Consumed by cookbook(s) |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `_cahps_appointments_healthcare_patient.liquid` | `appointment-review-sms-workflow` |
|
|
78
|
+
| `_cahps_appointments_healthcare_appointment.liquid` | `appointment-review-sms-workflow` |
|
|
79
|
+
| `_cahps_appointments_healthcare_mdm.liquid` | `appointment-review-sms-workflow` |
|
|
80
|
+
| `sample_two_page.pdf` (data) | `ai-agent-invoke` |
|
|
81
|
+
| `memorandum-of-association-01.png` (data) | `ai-agent-invoke` |
|
|
86
82
|
|
|
87
83
|
The `contact-us-triage-with-llm` cookbook ingests into a generic
|
|
88
84
|
table via the auto-provisioned identity contract, so it loads no
|
|
@@ -90,17 +86,17 @@ vendored template.
|
|
|
90
86
|
|
|
91
87
|
### `subscription/`
|
|
92
88
|
|
|
93
|
-
| File | Consumed by cookbook(s) |
|
|
94
|
-
|
|
95
|
-
| `_customers_subscription_customer.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` |
|
|
96
|
-
| `_customers_subscription_mdm.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` |
|
|
97
|
-
| `stripe_customers_batch1.csv` (data) | `bulk-ingest` |
|
|
89
|
+
| File | Consumed by cookbook(s) |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `_customers_subscription_customer.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` |
|
|
92
|
+
| `_customers_subscription_mdm.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` |
|
|
93
|
+
| `stripe_customers_batch1.csv` (data) | `bulk-ingest` |
|
|
98
94
|
|
|
99
95
|
### `payments/`
|
|
100
96
|
|
|
101
|
-
| File | Consumed by cookbook(s) |
|
|
102
|
-
|
|
103
|
-
| `_payment_accounts_payments_payment_account.liquid` | `kyc-notification-on-account-activation` |
|
|
104
|
-
| `_payment_accounts_payments_mdm.liquid` | `kyc-notification-on-account-activation` |
|
|
105
|
-
| `_compliance_screenings_payments_compliance_screening.liquid` | `sanctions-screening-with-agent-review` |
|
|
106
|
-
| `_compliance_screenings_payments_mdm.liquid` | `sanctions-screening-with-agent-review` |
|
|
97
|
+
| File | Consumed by cookbook(s) |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `_payment_accounts_payments_payment_account.liquid` | `kyc-notification-on-account-activation` |
|
|
100
|
+
| `_payment_accounts_payments_mdm.liquid` | `kyc-notification-on-account-activation` |
|
|
101
|
+
| `_compliance_screenings_payments_compliance_screening.liquid` | `sanctions-screening-with-agent-review` |
|
|
102
|
+
| `_compliance_screenings_payments_mdm.liquid` | `sanctions-screening-with-agent-review` |
|
|
@@ -3,11 +3,6 @@ title: "Organic marketing: score, greet, campaign — on one raw lake"
|
|
|
3
3
|
summary: The whole organic-marketing surface in one walk. Stand up a tenant and a raw datalake, then run the scenarios end to end — an LLM agent banding inbound leads into a four-way SMS fan-out, a pure-Liquid birthday trigger that schedules a greeting a year out and closes the loop through its authenticated link, and an A/B campaign that gates on suppression, splits inside the workflow and dispatches on SMS and email in one run. Ends with delivery reconciliation and a natural-language read of the lake.
|
|
4
4
|
use_case: organic-marketing
|
|
5
5
|
slug: organic-marketing
|
|
6
|
-
vitest_source:
|
|
7
|
-
- integration-tests/tests/organic-marketing/agent-driven-workflow.test.ts
|
|
8
|
-
- integration-tests/tests/organic-marketing/generic-tables.test.ts
|
|
9
|
-
- integration-tests/tests/organic-marketing/manual-upload-tool.test.ts
|
|
10
|
-
- integration-tests/tests/bootstrap/bootstrap.test.ts
|
|
11
6
|
status: draft
|
|
12
7
|
---
|
|
13
8
|
|
|
@@ -3,16 +3,6 @@ title: "Payments compliance: screen, decide, notify — on a tokenized lake"
|
|
|
3
3
|
summary: The whole payments-compliance surface in one walk. Stand up a tenant and a raw datalake, turn tokenization on because a model is going to read this data, then run two scenarios end to end — an LLM agent disambiguating gray-zone sanctions screenings into a verdict-keyed SMS fan-out, and a KYC notification fired by an account activation. Ends by reading one row back through raw, tokenized and redacted keys to show what each reader actually sees, then inspecting how far the tokenized copy has got and repairing one of its tables.
|
|
4
4
|
use_case: payments-compliance
|
|
5
5
|
slug: payments-compliance
|
|
6
|
-
vitest_source:
|
|
7
|
-
- integration-tests/tests/payments-compliance/sanctions-review-workflow.test.ts
|
|
8
|
-
- integration-tests/tests/payments-compliance/kyc-upload-workflow.test.ts
|
|
9
|
-
- integration-tests/tests/payments-compliance/interoperability-contracts.test.ts
|
|
10
|
-
- integration-tests/tests/payments-compliance/run-dac-single.test.ts
|
|
11
|
-
- integration-tests/tests/payments-compliance/create-dac.test.ts
|
|
12
|
-
- integration-tests/tests/payments-compliance/system-templates.test.ts
|
|
13
|
-
- integration-tests/tests/payments-compliance/manual-upload-tool.test.ts
|
|
14
|
-
- integration-tests/tests/payments-compliance/data-sources.test.ts
|
|
15
|
-
- integration-tests/tests/bootstrap/bootstrap.test.ts
|
|
16
6
|
status: draft
|
|
17
7
|
---
|
|
18
8
|
|
|
@@ -3,11 +3,6 @@ title: "Primary care: review, triage, extract — on a tokenized lake"
|
|
|
3
3
|
summary: "The whole primary-care surface in one walk. Stand up a tenant, a raw datalake and the tokenized and redacted copies, then run three scenarios end to end — a post-visit review request that only reaches patients whose appointment was actually fulfilled, an LLM agent triaging inbound contact-us messages into a category-keyed fan-out, and a vision agent reading a scanned document. Ends by reading one appointment back through all three lakes to show what each reader sees, then inspecting how far the tokenized copy has got and repairing one of its tables."
|
|
4
4
|
use_case: primary-care-feedback
|
|
5
5
|
slug: primary-care
|
|
6
|
-
vitest_source:
|
|
7
|
-
- integration-tests/tests/primary-care-feedback/standard-workflow.test.ts
|
|
8
|
-
- integration-tests/tests/primary-care-feedback/agent-driven-workflow.test.ts
|
|
9
|
-
- integration-tests/tests/primary-care-feedback/ai-agent-invoke.test.ts
|
|
10
|
-
- integration-tests/tests/bootstrap/bootstrap.test.ts
|
|
11
6
|
status: draft
|
|
12
7
|
---
|
|
13
8
|
|
|
@@ -3,14 +3,6 @@ title: "Subscription SaaS: one customer spine, three workflows, three ways in"
|
|
|
3
3
|
summary: "The whole subscription-billing surface in one walk. Stand up a tenant and a raw datalake, declare one customers table, and run three workflows over it — a welcome SMS gated on reachability, a dunning reminder gated on reachability and KYC, and an LLM agent that bands accounts by priority and fans out one SMS per band. Then fill the same table three more ways — a bulk CSV upload, a pull-based REST fetch, and finally a teammate invited onto the tenant."
|
|
4
4
|
use_case: subscription-saas
|
|
5
5
|
slug: subscription-saas
|
|
6
|
-
vitest_source:
|
|
7
|
-
- integration-tests/tests/subscription-saas/standard-workflow.test.ts
|
|
8
|
-
- integration-tests/tests/subscription-saas/dunning-sms-workflow.test.ts
|
|
9
|
-
- integration-tests/tests/subscription-saas/agent-lead-triage.test.ts
|
|
10
|
-
- integration-tests/tests/subscription-saas/run-dac-bulk.test.ts
|
|
11
|
-
- integration-tests/tests/subscription-saas/run-dac-fetch.test.ts
|
|
12
|
-
- integration-tests/tests/subscription-saas/invite-team.test.ts
|
|
13
|
-
- integration-tests/tests/bootstrap/bootstrap.test.ts
|
|
14
6
|
status: draft
|
|
15
7
|
---
|
|
16
8
|
|
package/.agent/datalakes.md
CHANGED
|
@@ -82,8 +82,8 @@ well-defined key prefixes inside the bucket the credentials point at:
|
|
|
82
82
|
|
|
83
83
|
```
|
|
84
84
|
{datalake_slug}/data-activations/{client_slug}/{batch_id}/{dataset_table}.ndjson
|
|
85
|
-
{datalake_slug}/workflows/{wf_slug}/{batch_id}
|
|
86
|
-
{datalake_slug}/workflows/{wf_slug}/{batch_id}/{decision_key}
|
|
85
|
+
{datalake_slug}/workflows/{wf_slug}/merged/{batch_id}.ndjson
|
|
86
|
+
{datalake_slug}/workflows/{wf_slug}/merged/{batch_id}/{decision_key}.ndjson
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
The merged NDJSONs are produced by background jobs
|
package/.agent/errors.md
CHANGED
|
@@ -108,11 +108,13 @@ don't distinguish layers from the JSON.
|
|
|
108
108
|
depends on the failure. Ordinary field validation (`"can't be blank"`,
|
|
109
109
|
`"is invalid"`, a custom `add_error` rule) is **422**. A failure that collides
|
|
110
110
|
with the current state of the resource is **409 Conflict**: a **uniqueness
|
|
111
|
-
collision** (`unique_constraint` / `unsafe_validate_unique`)
|
|
112
|
-
blocked by dependants** (a foreign-key/assoc constraint firing on delete)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
collision** (`unique_constraint` / `unsafe_validate_unique`), a **delete
|
|
112
|
+
blocked by dependants** (a foreign-key/assoc constraint firing on delete), or
|
|
113
|
+
an **action status updater refresh the updater's state refuses**: it is
|
|
114
|
+
halted, or a poll for it is already queued or running
|
|
115
|
+
(`action_status_updaters.md` §7). So do not assume every semantic conflict is
|
|
116
|
+
a tidy 422 — read `_httpStatus`, not just the body. Read `source.pointer`, fix
|
|
117
|
+
the field (or resolve the collision), resubmit.
|
|
116
118
|
|
|
117
119
|
## Concrete examples
|
|
118
120
|
|
|
@@ -174,6 +176,25 @@ comes back as a **409 Conflict** (`_httpStatus === 409`):
|
|
|
174
176
|
}
|
|
175
177
|
```
|
|
176
178
|
|
|
179
|
+
**State refusal → 409, no field** (`actionStatusUpdaters.refresh()` while a
|
|
180
|
+
poll for that updater is already queued). The refusal is about the resource,
|
|
181
|
+
not a field in your body, so `source.pointer` is empty and `title` is the
|
|
182
|
+
status phrase:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"errors": [
|
|
187
|
+
{ "source": { "pointer": "" },
|
|
188
|
+
"title": "Conflict",
|
|
189
|
+
"detail": "A poll for this updater is already queued, so this refresh was not. Retry once it finishes." }
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
A halted updater answers the same way with `Updater is halted: status is
|
|
195
|
+
<status>. Set status to active to resume polling.`, plus ` Last run error:
|
|
196
|
+
<text>` when one is recorded. Nothing was queued in either case.
|
|
197
|
+
|
|
177
198
|
## HTTP status codes (under the hood)
|
|
178
199
|
|
|
179
200
|
The SDK abstracts HTTP responses behind typed methods. Consumers mostly see
|
|
@@ -189,7 +210,7 @@ platform maps:
|
|
|
189
210
|
| 401 | Missing or invalid Bearer / `x-api-key` |
|
|
190
211
|
| 403 | Authenticated but role lacks permission |
|
|
191
212
|
| 404 | Resource doesn't exist OR cross-tenant access (404, not 403) |
|
|
192
|
-
| 409 | Uniqueness collision, delete blocked by dependants,
|
|
213
|
+
| 409 | Uniqueness collision, delete blocked by dependants, URL/body id mismatch, or an updater `refresh()` refused (halted, or a poll already queued) — same `errors[]` envelope |
|
|
193
214
|
| 422 | Validation failed; envelope above is the thrown body |
|
|
194
215
|
| 5xx | Platform-internal failure; safe to retry with backoff |
|
|
195
216
|
|
|
@@ -200,6 +221,11 @@ Treat 5xx as infra noise — retry with exponential backoff. Treat 422 (and the
|
|
|
200
221
|
resolve the collision, then resubmit. **Never retry a 422 or 409 with the same
|
|
201
222
|
body** — the failure won't clear on its own.
|
|
202
223
|
|
|
224
|
+
One 409 does clear on its own: `actionStatusUpdaters.refresh()` refused because
|
|
225
|
+
a poll for that updater is already queued or running. Retry it once that poll
|
|
226
|
+
finishes (the updater's `last_run_at` advances). The halted refusal does not
|
|
227
|
+
clear on its own: set the updater's `status` back to `'active'` first.
|
|
228
|
+
|
|
203
229
|
## Mapping `source.pointer` back to your TypeScript type
|
|
204
230
|
|
|
205
231
|
A `source.pointer` like `/db_reader_user` maps directly to the
|
|
@@ -251,13 +251,10 @@ not interchangeable.
|
|
|
251
251
|
|
|
252
252
|
Two related shapes worth keeping straight:
|
|
253
253
|
|
|
254
|
-
- **`mdm.verify`** (see `mdm.md`)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
one.
|
|
258
|
-
- **payments** additionally requires `id_type` on each entry of its own
|
|
259
|
-
verify payload. Per-domain differences are real; check the domain you are
|
|
260
|
-
writing for.
|
|
254
|
+
- **`mdm.verify`** (see `mdm.md`) asks "is this really you?" about an
|
|
255
|
+
already-known subject. Its identifier entries carry `system` and `value`,
|
|
256
|
+
both required, with `id_type` and `country` optional — the same on every
|
|
257
|
+
datalake. Its example is not a template for this one.
|
|
261
258
|
|
|
262
259
|
## 4. Error envelopes
|
|
263
260
|
|