@alvera-ai/platform-sdk 0.17.4-next.gc508af1 → 0.17.4
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/action_status_updaters.md +15 -26
- package/.agent/cookbook/action-status-updaters.md +6 -9
- package/.agent/cookbook/ai-agent-invoke.md +0 -10
- package/.agent/cookbook/appointment-review-sms-workflow.md +0 -14
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -14
- package/.agent/cookbook/contact-us-triage-with-llm.md +0 -15
- package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -14
- package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -14
- package/.agent/cookbook/marketing-campaign-send.md +2 -7
- package/.agent/cookbook/paginated-restapi-poller.md +4 -5
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -15
- package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -15
- package/.agent/cookbook/triage-prospects-by-priority.md +0 -15
- package/.agent/cookbook/welcome-sms-for-customers.md +0 -14
- package/.agent/data_sources.md +0 -3
- package/.agent/tools.md +18 -34
- package/.agent/type_naming.md +1 -2
- package/dist/index.d.mts +14 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -14,8 +14,8 @@ polls the provider's log group to mark each message as
|
|
|
14
14
|
SDK namespace: `api.actionStatusUpdaters`.
|
|
15
15
|
|
|
16
16
|
Action status updaters are **Datalake-DB-resident** — the
|
|
17
|
-
parent datalake must be `status: 'ready'
|
|
18
|
-
|
|
17
|
+
parent datalake must be `status: 'ready'` and every
|
|
18
|
+
referenced tool must exist before you `create()`.
|
|
19
19
|
|
|
20
20
|
## 1. Wire shape
|
|
21
21
|
|
|
@@ -33,7 +33,7 @@ const { data: created } = await api.actionStatusUpdaters.create(
|
|
|
33
33
|
cron_expression: '*/30 * * * *', // every 30 minutes
|
|
34
34
|
updater_type: 'cloud_watch', // polling source family
|
|
35
35
|
updater_tool_id: cloudWatchToolId, // the tool that issues the poll
|
|
36
|
-
|
|
36
|
+
sender_tool_ids: [smsToolId], // tools whose sent messages this updater tracks
|
|
37
37
|
datalake_id: datalakeId,
|
|
38
38
|
|
|
39
39
|
// Polymorphic — discriminator: updater_body_type
|
|
@@ -354,7 +354,7 @@ anyway — a future release may start enforcing it, and a lying
|
|
|
354
354
|
`updater_type` is a debugging trap — but don't expect a 422 to catch
|
|
355
355
|
a mismatch for you today.
|
|
356
356
|
|
|
357
|
-
### `updater_tool_id` and `
|
|
357
|
+
### `updater_tool_id` and `sender_tool_ids` serve different roles
|
|
358
358
|
|
|
359
359
|
- `updater_tool_id` (single) — the tool the updater uses to
|
|
360
360
|
ISSUE the poll. By convention this is a tool whose `intent` is
|
|
@@ -364,25 +364,14 @@ a mismatch for you today.
|
|
|
364
364
|
wrong-intent tool is accepted and only fails later, when the poll
|
|
365
365
|
actually runs. Wire a `status_poller` tool anyway; just don't rely
|
|
366
366
|
on a create-time 422 to enforce it.
|
|
367
|
-
- `
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
So the updater is not tied to one sender tool: a message sent
|
|
377
|
-
through any tool on its data source is a candidate for its match.
|
|
378
|
-
One data source may hold SMS, MMS and voice tools, and nothing
|
|
379
|
-
checks what else is on it.
|
|
380
|
-
|
|
381
|
-
`data_source_id` is required on create AND update (PUT replaces the
|
|
382
|
-
whole body). The response reads it back nullable: updaters created
|
|
383
|
-
before it existed carry none, skip every run, and report
|
|
384
|
-
`no data source: …` as their last run error until an update names
|
|
385
|
-
one.
|
|
367
|
+
- `sender_tool_ids` (array) — the tools whose sent messages
|
|
368
|
+
this updater is responsible for tracking. The match logic
|
|
369
|
+
joins each polled event back to a message row that was
|
|
370
|
+
sent through one of these tools.
|
|
371
|
+
|
|
372
|
+
A single status updater can track messages from multiple
|
|
373
|
+
sender tools (e.g. an SMS tool plus a voice tool routed
|
|
374
|
+
through the same provider).
|
|
386
375
|
|
|
387
376
|
## 3. Field ownership
|
|
388
377
|
|
|
@@ -396,7 +385,7 @@ name required string
|
|
|
396
385
|
cron_expression required string — standard cron
|
|
397
386
|
updater_type required enum — e.g. 'cloud_watch'
|
|
398
387
|
updater_tool_id required UUID — the polling tool
|
|
399
|
-
|
|
388
|
+
sender_tool_ids required UUID[] — tools whose messages this updater tracks
|
|
400
389
|
datalake_id required UUID
|
|
401
390
|
updater_body required embed — { updater_body_type, ... }
|
|
402
391
|
message_config required embed — { type, body }
|
|
@@ -413,7 +402,7 @@ Standard JSON:API envelopes per `errors.md`. Common rejections:
|
|
|
413
402
|
|-------------------------------------|----------------------------------------------------|
|
|
414
403
|
| `/cron_expression` | invalid cron syntax or below minimum interval |
|
|
415
404
|
| `/updater_tool_id` | tool id doesn't exist or wrong datalake (intent is NOT checked — see §2) |
|
|
416
|
-
| `/
|
|
405
|
+
| `/sender_tool_ids/0` | tool id doesn't exist or wrong datalake |
|
|
417
406
|
| `/updater_body/updater_body_type` | value not in the polymorphic embed's type enum |
|
|
418
407
|
| `/message_config/body` | empty when `type: 'custom'` |
|
|
419
408
|
| `/action_log_config` | missing entirely — `Missing field: action_log_config` (create AND update) |
|
|
@@ -637,7 +626,7 @@ is the part that matters:
|
|
|
637
626
|
- **No updater configured → no delivery truth.** Rows stay `sent`
|
|
638
627
|
forever; a delivery-rate number computed from them is fiction.
|
|
639
628
|
Updaters are created manually (this page §1) — the platform does
|
|
640
|
-
not auto-create them per
|
|
629
|
+
not auto-create them per sender tool.
|
|
641
630
|
- **The join is `{external_id, set_params}`,** exact-match, silent
|
|
642
631
|
on misses (gotcha 3). "Eventually honest": a just-sent row is
|
|
643
632
|
neither delivered nor failed yet — show the in-between state.
|
|
@@ -22,8 +22,7 @@ An **action status updater (ASU)** is a scheduled reconciler:
|
|
|
22
22
|
- the ASU runs on a `cron_expression`,
|
|
23
23
|
- its `message_config` Liquid maps each log event back to the message it came
|
|
24
24
|
from (by `external_id`) and the new `status`,
|
|
25
|
-
- `
|
|
26
|
-
that carry that data source's `uri` — every message a tool on it sends.
|
|
25
|
+
- `sender_tool_ids` scopes it to the messages a given sender tool produced.
|
|
27
26
|
|
|
28
27
|
This is **global** — the same shape reconciles any sender's delivery status. See
|
|
29
28
|
`action_status_updaters.md` for the wire reference and `tools.md` for the
|
|
@@ -105,8 +104,7 @@ ctx.cwToolId = cwTool.id!
|
|
|
105
104
|
## 004 — create the action status updater
|
|
106
105
|
|
|
107
106
|
The ASU ties it together: a `cron_expression` schedule, the `updater_tool_id`
|
|
108
|
-
(the poller), the `
|
|
109
|
-
SMS sender tool names — and an `updater_body`
|
|
107
|
+
(the poller), the `sender_tool_ids` it reconciles for, and an `updater_body`
|
|
110
108
|
naming the log group + the time window. On `start_time`/`end_time`, use
|
|
111
109
|
`now_msec` — the injected variable holding unix milliseconds — piped through
|
|
112
110
|
`minutes_ago`; the bare `now` is a DateTime and raises here. The
|
|
@@ -124,7 +122,7 @@ const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalake
|
|
|
124
122
|
cron_expression: '*/30 * * * *',
|
|
125
123
|
updater_type: 'cloud_watch',
|
|
126
124
|
updater_tool_id: ctx.cwToolId,
|
|
127
|
-
|
|
125
|
+
sender_tool_ids: [ctx.smsToolId],
|
|
128
126
|
datalake_id: ctx.datalakeId,
|
|
129
127
|
updater_body: {
|
|
130
128
|
updater_body_type: 'cloud_watch_request',
|
|
@@ -161,7 +159,7 @@ const { data: ck } = await api.actionStatusUpdaters.checksum(tenantSlug, datalak
|
|
|
161
159
|
cron_expression: row.cron_expression,
|
|
162
160
|
updater_type: 'cloud_watch',
|
|
163
161
|
updater_tool_id: ctx.cwToolId,
|
|
164
|
-
|
|
162
|
+
sender_tool_ids: [ctx.smsToolId],
|
|
165
163
|
datalake_id: ctx.datalakeId,
|
|
166
164
|
updater_body: {
|
|
167
165
|
updater_body_type: 'cloud_watch_request',
|
|
@@ -237,9 +235,8 @@ blind.
|
|
|
237
235
|
# Gotchas
|
|
238
236
|
|
|
239
237
|
- **The poller tool's intent is `status_poller`, not the sender's intent.** The
|
|
240
|
-
reconciler reads logs; it doesn't send.
|
|
241
|
-
|
|
242
|
-
source, and the messages it sends carry that data source's `uri`.
|
|
238
|
+
reconciler reads logs; it doesn't send. `sender_tool_ids` is the separate list
|
|
239
|
+
of sender tools whose messages it updates.
|
|
243
240
|
- **`message_config` renders once per event, and emits a FLAT object.** The event
|
|
244
241
|
is the assigns — there is no `events` list to loop — and every top-level key
|
|
245
242
|
that is not `external_id` IS the update (`status`, `delivered_at`, …). There is
|
|
@@ -99,22 +99,12 @@ const OPENAI_BASE_BODY =
|
|
|
99
99
|
const OPENAI_EXTRACTOR =
|
|
100
100
|
'{"output_json": "{{ msg.choices[0].message.content | json_escape }}", "explanation": "{{ msg.choices[0].message.reasoning_content | json_escape }}", "input_tokens": {{ msg.usage.prompt_tokens | default: 0 }}, "output_tokens": {{ msg.usage.completion_tokens | default: 0 }}, "total_tokens": {{ msg.usage.total_tokens | default: 0 }}}'
|
|
101
101
|
|
|
102
|
-
// Every tool names a data source; this one exists for the LLM tool alone.
|
|
103
|
-
const { data: toolSource } = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
104
|
-
name: `Extraction Tool Source ${runSuffix}`,
|
|
105
|
-
uri: 'openai.local',
|
|
106
|
-
description: 'Data source the LLM-enrichment tool names.',
|
|
107
|
-
status: 'active',
|
|
108
|
-
is_default: false,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
102
|
const { data: tool } = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
112
103
|
name: `Legal-Entity Extraction Tool ${runSuffix}`,
|
|
113
104
|
description: 'Multimodal LLM-enrichment adapter for legal_entity extraction.',
|
|
114
105
|
intent: 'llm_enrichment',
|
|
115
106
|
status: 'active',
|
|
116
107
|
datalake_id: ctx.datalakeId,
|
|
117
|
-
data_source_id: toolSource.id!,
|
|
118
108
|
response_extractor: { type: 'custom', body: OPENAI_EXTRACTOR, output_schema: ENRICHMENT_OUTPUT_SCHEMA },
|
|
119
109
|
body: {
|
|
120
110
|
tool_body_type: 'rest_api',
|
|
@@ -77,27 +77,13 @@ dev points it at LocalStack on `http://localhost:4566` via
|
|
|
77
77
|
`intent: 'sms'` tags this tool for workflow actions that send SMS
|
|
78
78
|
(versus `data_exchange` for ingestion tools).
|
|
79
79
|
|
|
80
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
81
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
82
|
-
creates one for the SMS tool first.
|
|
83
|
-
|
|
84
80
|
```typescript
|
|
85
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
86
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
87
|
-
uri: 'sns.local',
|
|
88
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
89
|
-
status: 'active',
|
|
90
|
-
is_default: false,
|
|
91
|
-
})
|
|
92
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
93
|
-
|
|
94
81
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
95
82
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
96
83
|
description: 'SNS-backed SMS dispatcher for the Review SMS workflow, wired to LocalStack.',
|
|
97
84
|
intent: 'sms',
|
|
98
85
|
status: 'active',
|
|
99
86
|
datalake_id: ctx.datalakeId,
|
|
100
|
-
data_source_id: ctx.toolSourceId,
|
|
101
87
|
body: {
|
|
102
88
|
tool_body_type: 'sns',
|
|
103
89
|
auth_method: 'access_key',
|
|
@@ -70,27 +70,13 @@ local dev points it at LocalStack on `http://localhost:4566` via
|
|
|
70
70
|
`intent: 'sms'` tags this tool for workflow actions that send SMS
|
|
71
71
|
(versus `data_exchange` for ingestion tools).
|
|
72
72
|
|
|
73
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
74
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
75
|
-
creates one for the SMS tool first.
|
|
76
|
-
|
|
77
73
|
```typescript
|
|
78
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
79
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
80
|
-
uri: 'sns.local',
|
|
81
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
82
|
-
status: 'active',
|
|
83
|
-
is_default: false,
|
|
84
|
-
})
|
|
85
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
86
|
-
|
|
87
74
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
88
75
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
89
76
|
description: 'SNS-backed SMS dispatcher for the Happy Birthday workflow, wired to LocalStack.',
|
|
90
77
|
intent: 'sms',
|
|
91
78
|
status: 'active',
|
|
92
79
|
datalake_id: ctx.datalakeId,
|
|
93
|
-
data_source_id: ctx.toolSourceId,
|
|
94
80
|
body: {
|
|
95
81
|
tool_body_type: 'sns',
|
|
96
82
|
auth_method: 'access_key',
|
|
@@ -136,27 +136,13 @@ per-bucket SMS body lives on each workflow action, not on the tool.
|
|
|
136
136
|
SNS-backed LocalStack wiring — `intent: 'sms'` tags it for
|
|
137
137
|
workflow-action use.
|
|
138
138
|
|
|
139
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
140
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
141
|
-
creates one for the SMS tool first.
|
|
142
|
-
|
|
143
139
|
```typescript
|
|
144
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
145
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
146
|
-
uri: 'sns.local',
|
|
147
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
148
|
-
status: 'active',
|
|
149
|
-
is_default: false,
|
|
150
|
-
})
|
|
151
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
152
|
-
|
|
153
140
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
154
141
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
155
142
|
description: 'SNS-backed SMS dispatcher for the Contact Us Triage workflow, wired to LocalStack.',
|
|
156
143
|
intent: 'sms',
|
|
157
144
|
status: 'active',
|
|
158
145
|
datalake_id: ctx.datalakeId,
|
|
159
|
-
data_source_id: ctx.toolSourceId,
|
|
160
146
|
body: {
|
|
161
147
|
tool_body_type: 'sns',
|
|
162
148
|
auth_method: 'access_key',
|
|
@@ -208,7 +194,6 @@ const llmToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
208
194
|
intent: 'llm_enrichment',
|
|
209
195
|
status: 'active',
|
|
210
196
|
datalake_id: ctx.datalakeId,
|
|
211
|
-
data_source_id: ctx.toolSourceId,
|
|
212
197
|
response_extractor: { type: 'custom', body: OLLAMA_EXTRACTOR, output_schema: ENRICHMENT_OUTPUT_SCHEMA },
|
|
213
198
|
body: {
|
|
214
199
|
tool_body_type: 'rest_api',
|
|
@@ -79,27 +79,13 @@ dev points it at LocalStack on `http://localhost:4566` via
|
|
|
79
79
|
`intent: 'sms'` tags this tool for workflow actions that send SMS
|
|
80
80
|
(versus `data_exchange` for ingestion tools).
|
|
81
81
|
|
|
82
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
83
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
84
|
-
creates one for the SMS tool first.
|
|
85
|
-
|
|
86
82
|
```typescript
|
|
87
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
88
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
89
|
-
uri: 'sns.local',
|
|
90
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
91
|
-
status: 'active',
|
|
92
|
-
is_default: false,
|
|
93
|
-
})
|
|
94
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
95
|
-
|
|
96
83
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
97
84
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
98
85
|
description: 'SNS-backed SMS dispatcher for the Dunning SMS workflow, wired to LocalStack.',
|
|
99
86
|
intent: 'sms',
|
|
100
87
|
status: 'active',
|
|
101
88
|
datalake_id: ctx.datalakeId,
|
|
102
|
-
data_source_id: ctx.toolSourceId,
|
|
103
89
|
body: {
|
|
104
90
|
tool_body_type: 'sns',
|
|
105
91
|
auth_method: 'access_key',
|
|
@@ -77,27 +77,13 @@ local dev points it at LocalStack on `http://localhost:4566` via
|
|
|
77
77
|
`intent: 'sms'` tags this tool for workflow actions that send SMS
|
|
78
78
|
(versus `data_exchange` for ingestion tools).
|
|
79
79
|
|
|
80
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
81
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
82
|
-
creates one for the SMS tool first.
|
|
83
|
-
|
|
84
80
|
```typescript
|
|
85
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
86
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
87
|
-
uri: 'sns.local',
|
|
88
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
89
|
-
status: 'active',
|
|
90
|
-
is_default: false,
|
|
91
|
-
})
|
|
92
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
93
|
-
|
|
94
81
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
95
82
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
96
83
|
description: 'SNS-backed SMS dispatcher for the KYC-notification workflow, wired to LocalStack.',
|
|
97
84
|
intent: 'sms',
|
|
98
85
|
status: 'active',
|
|
99
86
|
datalake_id: ctx.datalakeId,
|
|
100
|
-
data_source_id: ctx.toolSourceId,
|
|
101
87
|
body: {
|
|
102
88
|
tool_body_type: 'sns',
|
|
103
89
|
auth_method: 'access_key',
|
|
@@ -512,9 +512,7 @@ The sender lives in the tool's Base Configuration: `base_from_email`
|
|
|
512
512
|
rendered when the tool is saved with the tool's own variables only
|
|
513
513
|
(`tool.name`, `tool.provider`, …). An email action's own `from_email` /
|
|
514
514
|
`from_name` / `reply_to` still win, field by field, when they render
|
|
515
|
-
non-blank.
|
|
516
|
-
must; the messages they send carry its `uri`, which is what §009's
|
|
517
|
-
status updater matches on.
|
|
515
|
+
non-blank.
|
|
518
516
|
|
|
519
517
|
The connected app is the campaign's booking form. It is registered
|
|
520
518
|
here, and §010's actions deep-link into it.
|
|
@@ -528,7 +526,6 @@ const { data: smsTool } = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
528
526
|
intent: ToolIntent.SMS,
|
|
529
527
|
status: 'active',
|
|
530
528
|
datalake_id: ctx.datalakeId,
|
|
531
|
-
data_source_id: dataSourceId,
|
|
532
529
|
body: {
|
|
533
530
|
tool_body_type: 'sns',
|
|
534
531
|
auth_method: 'access_key',
|
|
@@ -547,7 +544,6 @@ const { data: emailTool } = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
547
544
|
intent: ToolIntent.EMAIL,
|
|
548
545
|
status: 'active',
|
|
549
546
|
datalake_id: ctx.datalakeId,
|
|
550
|
-
data_source_id: dataSourceId,
|
|
551
547
|
body: {
|
|
552
548
|
tool_body_type: 'email',
|
|
553
549
|
provider: 'mock',
|
|
@@ -663,7 +659,6 @@ const { data: sesTool } = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
663
659
|
intent: ToolIntent.EMAIL,
|
|
664
660
|
status: 'active',
|
|
665
661
|
datalake_id: ctx.datalakeId,
|
|
666
|
-
data_source_id: dataSourceId,
|
|
667
662
|
body: {
|
|
668
663
|
tool_body_type: 'email',
|
|
669
664
|
provider: 'ses',
|
|
@@ -760,7 +755,7 @@ const { data: sesUpdater } = await api.actionStatusUpdaters.create(tenantSlug, d
|
|
|
760
755
|
cron_expression: '*/30 * * * *',
|
|
761
756
|
updater_type: 'cloud_watch',
|
|
762
757
|
updater_tool_id: sesReader.id!,
|
|
763
|
-
|
|
758
|
+
sender_tool_ids: [ctx.sesToolId],
|
|
764
759
|
datalake_id: ctx.datalakeId,
|
|
765
760
|
updater_body: {
|
|
766
761
|
updater_body_type: 'cloud_watch_request',
|
|
@@ -57,9 +57,8 @@ dataSourceId = ds.id!
|
|
|
57
57
|
|
|
58
58
|
## 002 — create the sender tool whose messages get reconciled
|
|
59
59
|
|
|
60
|
-
The ASU updates
|
|
61
|
-
|
|
62
|
-
SNS-backed SMS sender (LocalStack locally) plays that role.
|
|
60
|
+
The ASU updates messages a sender produced; `sender_tool_ids` will point here.
|
|
61
|
+
An SNS-backed SMS sender (LocalStack locally) plays that role.
|
|
63
62
|
|
|
64
63
|
```typescript
|
|
65
64
|
const { data: smsTool } = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
@@ -127,7 +126,7 @@ try {
|
|
|
127
126
|
cron_expression: '*/30 * * * *',
|
|
128
127
|
updater_type: 'restapi',
|
|
129
128
|
updater_tool_id: ctx.restToolId,
|
|
130
|
-
|
|
129
|
+
sender_tool_ids: [ctx.senderToolId],
|
|
131
130
|
datalake_id: ctx.datalakeId,
|
|
132
131
|
// FLOOR the platform enforces: an array whose items are objects listing
|
|
133
132
|
// `external_id` in `required`. A bare `{ type: 'array' }` is a 422 at
|
|
@@ -202,7 +201,7 @@ const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalake
|
|
|
202
201
|
cron_expression: '*/30 * * * *',
|
|
203
202
|
updater_type: 'restapi',
|
|
204
203
|
updater_tool_id: ctx.restToolId,
|
|
205
|
-
|
|
204
|
+
sender_tool_ids: [ctx.senderToolId],
|
|
206
205
|
datalake_id: ctx.datalakeId,
|
|
207
206
|
events_output_schema: {
|
|
208
207
|
type: 'array',
|
|
@@ -82,27 +82,13 @@ local dev points it at LocalStack on `http://localhost:4566` via
|
|
|
82
82
|
`intent: 'sms'` tags this tool for workflow actions that send SMS
|
|
83
83
|
(versus `data_exchange` for ingestion tools).
|
|
84
84
|
|
|
85
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
86
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
87
|
-
creates one for the SMS tool first.
|
|
88
|
-
|
|
89
85
|
```typescript
|
|
90
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
91
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
92
|
-
uri: 'sns.local',
|
|
93
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
94
|
-
status: 'active',
|
|
95
|
-
is_default: false,
|
|
96
|
-
})
|
|
97
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
98
|
-
|
|
99
86
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
100
87
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
101
88
|
description: 'SNS-backed SMS dispatcher for the Sanctions Review workflow, wired to LocalStack.',
|
|
102
89
|
intent: 'sms',
|
|
103
90
|
status: 'active',
|
|
104
91
|
datalake_id: ctx.datalakeId,
|
|
105
|
-
data_source_id: ctx.toolSourceId,
|
|
106
92
|
body: {
|
|
107
93
|
tool_body_type: 'sns',
|
|
108
94
|
auth_method: 'access_key',
|
|
@@ -155,7 +141,6 @@ const llmToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
155
141
|
intent: 'llm_enrichment',
|
|
156
142
|
status: 'active',
|
|
157
143
|
datalake_id: ctx.datalakeId,
|
|
158
|
-
data_source_id: ctx.toolSourceId,
|
|
159
144
|
response_extractor: { type: 'custom', body: OLLAMA_EXTRACTOR, output_schema: ENRICHMENT_OUTPUT_SCHEMA },
|
|
160
145
|
body: {
|
|
161
146
|
tool_body_type: 'rest_api',
|
|
@@ -146,27 +146,13 @@ per-band SMS body lives on each workflow action, not on the tool. Same
|
|
|
146
146
|
SNS-backed LocalStack wiring as the birthday cookbook — `intent: 'sms'`
|
|
147
147
|
tags it for workflow-action use.
|
|
148
148
|
|
|
149
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
150
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
151
|
-
creates one for the SMS tool first.
|
|
152
|
-
|
|
153
149
|
```typescript
|
|
154
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
155
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
156
|
-
uri: 'sns.local',
|
|
157
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
158
|
-
status: 'active',
|
|
159
|
-
is_default: false,
|
|
160
|
-
})
|
|
161
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
162
|
-
|
|
163
150
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
164
151
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
165
152
|
description: 'SNS-backed SMS dispatcher for the Score Lead workflow, wired to LocalStack.',
|
|
166
153
|
intent: 'sms',
|
|
167
154
|
status: 'active',
|
|
168
155
|
datalake_id: ctx.datalakeId,
|
|
169
|
-
data_source_id: ctx.toolSourceId,
|
|
170
156
|
body: {
|
|
171
157
|
tool_body_type: 'sns',
|
|
172
158
|
auth_method: 'access_key',
|
|
@@ -218,7 +204,6 @@ const llmToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
218
204
|
intent: 'llm_enrichment',
|
|
219
205
|
status: 'active',
|
|
220
206
|
datalake_id: ctx.datalakeId,
|
|
221
|
-
data_source_id: ctx.toolSourceId,
|
|
222
207
|
response_extractor: { type: 'custom', body: OLLAMA_EXTRACTOR, output_schema: ENRICHMENT_OUTPUT_SCHEMA },
|
|
223
208
|
body: {
|
|
224
209
|
tool_body_type: 'rest_api',
|
|
@@ -65,27 +65,13 @@ A single SMS tool dispatches every band's message; the per-band body lives on
|
|
|
65
65
|
each workflow action. SNS-backed, pointed at LocalStack locally; `intent: 'sms'`
|
|
66
66
|
tags it for workflow actions.
|
|
67
67
|
|
|
68
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
69
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
70
|
-
creates one for the SMS tool first.
|
|
71
|
-
|
|
72
68
|
```typescript
|
|
73
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
74
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
75
|
-
uri: 'sns.local',
|
|
76
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
77
|
-
status: 'active',
|
|
78
|
-
is_default: false,
|
|
79
|
-
})
|
|
80
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
81
|
-
|
|
82
69
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
83
70
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
84
71
|
description: 'SNS-backed SMS dispatcher for the priority-triage workflow, wired to LocalStack.',
|
|
85
72
|
intent: 'sms',
|
|
86
73
|
status: 'active',
|
|
87
74
|
datalake_id: ctx.datalakeId,
|
|
88
|
-
data_source_id: ctx.toolSourceId,
|
|
89
75
|
body: {
|
|
90
76
|
tool_body_type: 'sns',
|
|
91
77
|
auth_method: 'access_key',
|
|
@@ -133,7 +119,6 @@ const llmToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
133
119
|
intent: 'llm_enrichment',
|
|
134
120
|
status: 'active',
|
|
135
121
|
datalake_id: ctx.datalakeId,
|
|
136
|
-
data_source_id: ctx.toolSourceId,
|
|
137
122
|
response_extractor: { type: 'custom', body: OLLAMA_EXTRACTOR, output_schema: ENRICHMENT_OUTPUT_SCHEMA },
|
|
138
123
|
body: {
|
|
139
124
|
tool_body_type: 'rest_api',
|
|
@@ -76,27 +76,13 @@ dev points it at LocalStack on `http://localhost:4566` via
|
|
|
76
76
|
`intent: 'sms'` tags this tool for workflow actions that send SMS
|
|
77
77
|
(versus `data_exchange` for ingestion tools).
|
|
78
78
|
|
|
79
|
-
Every tool names a data source, and the messages a sender tool sends carry
|
|
80
|
-
that data source's `uri` — what a status updater matches on — so the step
|
|
81
|
-
creates one for the SMS tool first.
|
|
82
|
-
|
|
83
79
|
```typescript
|
|
84
|
-
const toolSourceResp = await api.dataSources.create(tenantSlug, datalakeSlug, {
|
|
85
|
-
name: `Cookbook SMS Source ${runSuffix}`,
|
|
86
|
-
uri: 'sns.local',
|
|
87
|
-
description: 'Origin of the messages the SMS tool sends.',
|
|
88
|
-
status: 'active',
|
|
89
|
-
is_default: false,
|
|
90
|
-
})
|
|
91
|
-
ctx.toolSourceId = toolSourceResp.data.id!
|
|
92
|
-
|
|
93
80
|
const smsToolResp = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
94
81
|
name: `Cookbook SMS Tool ${runSuffix}`,
|
|
95
82
|
description: 'SNS-backed SMS dispatcher for the Welcome SMS workflow, wired to LocalStack.',
|
|
96
83
|
intent: 'sms',
|
|
97
84
|
status: 'active',
|
|
98
85
|
datalake_id: ctx.datalakeId,
|
|
99
|
-
data_source_id: ctx.toolSourceId,
|
|
100
86
|
body: {
|
|
101
87
|
tool_body_type: 'sns',
|
|
102
88
|
auth_method: 'access_key',
|
package/.agent/data_sources.md
CHANGED
|
@@ -210,9 +210,6 @@ asymmetric cascade behavior by dependent resource:
|
|
|
210
210
|
restricting FK (`on_delete: :restrict`), so the delete is
|
|
211
211
|
**rejected** while any action still points at the source —
|
|
212
212
|
detach those actions first.
|
|
213
|
-
- Every tool names its data source (`tools.md` §2), and that FK
|
|
214
|
-
restricts too: a data source that still has tools answers
|
|
215
|
-
`409` (`still has tools`). Delete or move its tools first.
|
|
216
213
|
|
|
217
214
|
The auto-created default Manual Upload source (see §2 "A
|
|
218
215
|
default Manual Upload data source is auto-created per
|
package/.agent/tools.md
CHANGED
|
@@ -12,11 +12,10 @@ dimensions:
|
|
|
12
12
|
- **body type** declares HOW the tool connects (S3, SFTP, SQL,
|
|
13
13
|
REST, AWS Lambda, …) via a polymorphic discriminator
|
|
14
14
|
|
|
15
|
-
A datalake can own many tools
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
action status updaters match on.
|
|
15
|
+
A datalake can own many tools. Some tools nest under a data source
|
|
16
|
+
(`data_source_id` populated — typical for ingestion-receiving
|
|
17
|
+
tools like `manual_upload`); others are standalone and reachable
|
|
18
|
+
to any workflow on the datalake.
|
|
20
19
|
|
|
21
20
|
## 1. Wire shape
|
|
22
21
|
|
|
@@ -120,7 +119,6 @@ happened to it** — no delivery status, no reconciliation.
|
|
|
120
119
|
await api.tools.create(tenantSlug, datalakeSlug, {
|
|
121
120
|
name: 'Campaign Sender — Main Line',
|
|
122
121
|
intent: 'sms',
|
|
123
|
-
data_source_id: dataSourceId, // every tool names one; its messages carry this source's uri
|
|
124
122
|
tool_body_type: 'twilio',
|
|
125
123
|
variant_type: 'primary',
|
|
126
124
|
account_sid: 'AC…', // "AC" + 32 hex
|
|
@@ -231,29 +229,16 @@ create time:
|
|
|
231
229
|
A misaligned pair (e.g. `intent: sms` + `tool_body_type: sftp`)
|
|
232
230
|
returns a `422` with `body.tool_body_type` in `errors`.
|
|
233
231
|
|
|
234
|
-
### `data_source_id` is
|
|
232
|
+
### `data_source_id` is optional but discriminator-aware
|
|
235
233
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
marks the field optional and nullable — the server's rule is the one
|
|
241
|
-
that holds.
|
|
234
|
+
When set, the tool is **embedded under a data source** — its
|
|
235
|
+
lifecycle is tied to the parent data source, and ingestion paths
|
|
236
|
+
expect it at that scope. When omitted, the tool is **standalone**
|
|
237
|
+
on the datalake — available to any workflow that names it.
|
|
242
238
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
action status updater names one data source and matches polled
|
|
247
|
-
events to messages and action logs by that `uri` plus the vendor's
|
|
248
|
-
id (`action_status_updaters.md` §2). One data source may hold SMS,
|
|
249
|
-
MMS and voice tools; nothing checks what else is on it.
|
|
250
|
-
- **A data source cannot be deleted while it still has tools.** The
|
|
251
|
-
delete answers `409` (`still has tools`); delete or move its tools
|
|
252
|
-
first. The foreign key restricts, so nothing cascades.
|
|
253
|
-
|
|
254
|
-
The body type still signals the typical pattern: a `manual_upload`
|
|
255
|
-
tool's data source IS the upload sink; a sender's data source is the
|
|
256
|
-
one its messages, and its status updater, belong to.
|
|
239
|
+
The body type itself signals the typical pattern: `manual_upload`
|
|
240
|
+
is almost always embedded (the data source IS the upload sink);
|
|
241
|
+
`sns`, `rest_api`, `sql_database` are typically standalone.
|
|
257
242
|
|
|
258
243
|
### Body-type sub-fields are validated against the matching discriminator branch (server-side)
|
|
259
244
|
|
|
@@ -377,7 +362,6 @@ const { data: sesTool } = await api.tools.create(tenantSlug, datalakeSlug, {
|
|
|
377
362
|
intent: ToolIntent.EMAIL,
|
|
378
363
|
status: 'active',
|
|
379
364
|
datalake_id: datalakeId,
|
|
380
|
-
data_source_id: dataSourceId,
|
|
381
365
|
body: {
|
|
382
366
|
tool_body_type: 'email',
|
|
383
367
|
provider: 'ses',
|
|
@@ -562,8 +546,8 @@ silent OIDC-provider outage can fail an otherwise-cosmetic
|
|
|
562
546
|
|
|
563
547
|
scoping
|
|
564
548
|
datalake_id parent datalake (UUID)
|
|
565
|
-
data_source_id
|
|
566
|
-
|
|
549
|
+
data_source_id (optional) parent data source for embedded
|
|
550
|
+
tools; omit for standalone
|
|
567
551
|
|
|
568
552
|
body (polymorphic — required keys depend on tool_body_type)
|
|
569
553
|
tool_body_type discriminator — names which branch shape
|
|
@@ -597,8 +581,8 @@ Capture both `id` and `slug` from the create response; never
|
|
|
597
581
|
pre-compute either client-side. The **UUID `id`** is the CRUD path
|
|
598
582
|
key (`.get` / `.update` / `.delete`) AND the cross-resource
|
|
599
583
|
reference — workflows and data-activation clients bind a tool by
|
|
600
|
-
`id` (`tool_id`), and action status updaters by `updater_tool_id`
|
|
601
|
-
(see §7
|
|
584
|
+
`id` (`tool_id`), and action status updaters by `updater_tool_id` /
|
|
585
|
+
`sender_tool_ids` (see §7). The `slug` is the human handle; it does
|
|
602
586
|
NOT key a CRUD `.get` (passing it 422s `Expected :uuid`). See
|
|
603
587
|
`type_naming.md` for the id/slug/name convention.
|
|
604
588
|
|
|
@@ -860,9 +844,9 @@ downstream resource that names a tool must apply AFTER it.
|
|
|
860
844
|
`status: "error"` + non-empty `error_message`. If you see a
|
|
861
845
|
5xx, file a platform bug, do not retry.
|
|
862
846
|
|
|
863
|
-
5. **
|
|
847
|
+
5. **Standalone vs embedded tool routing differs at list time**
|
|
864
848
|
— `tools.list(tenant, datalake, query?)` returns ALL tools on
|
|
865
|
-
the datalake
|
|
849
|
+
the datalake (standalone + embedded). The Flop query
|
|
866
850
|
(type_naming.md) filters on `intent`, `status`, `name`, etc. —
|
|
867
851
|
but `data_source_id` is NOT in the filterable set, so tools
|
|
868
852
|
owned by a specific data source must still be filtered
|