@alvera-ai/platform-sdk 0.10.0-rc.2 → 0.10.0-rc.21

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.
Files changed (55) hide show
  1. package/.agent/AGENTS.md +440 -0
  2. package/.agent/account_management.md +455 -0
  3. package/.agent/action_status_updaters.md +262 -0
  4. package/.agent/ai_agents.md +423 -0
  5. package/.agent/ai_sandbox.md +265 -0
  6. package/.agent/async.md +111 -0
  7. package/.agent/connected_apps.md +407 -0
  8. package/.agent/cookbook/_fixtures/README.md +99 -0
  9. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
  10. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
  11. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
  12. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
  13. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
  14. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
  15. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
  16. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
  17. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
  18. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
  19. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
  20. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
  21. package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
  22. package/.agent/cookbook/_setup/foundation.md +277 -0
  23. package/.agent/cookbook/_setup/healthcare.md +279 -0
  24. package/.agent/cookbook/_setup/payment_risk.md +283 -0
  25. package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
  26. package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
  27. package/.agent/cookbook/contact-us-triage-with-llm.md +603 -0
  28. package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
  29. package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
  30. package/.agent/cookbook/sanctions-screening-with-agent-review.md +711 -0
  31. package/.agent/cookbook/score-leads-with-llm-categorization.md +602 -0
  32. package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
  33. package/.agent/data_activation_clients.md +557 -0
  34. package/.agent/data_sources.md +234 -0
  35. package/.agent/datalakes.md +712 -0
  36. package/.agent/debugging.md +137 -0
  37. package/.agent/errors.md +196 -0
  38. package/.agent/generic_tables.md +351 -0
  39. package/.agent/interoperability_contracts.md +351 -0
  40. package/.agent/mdm.md +293 -0
  41. package/.agent/mutations.md +152 -0
  42. package/.agent/templates.md +98 -0
  43. package/.agent/tool-call-configs.md +90 -0
  44. package/.agent/tools.md +546 -0
  45. package/.agent/type_naming.md +131 -0
  46. package/.agent/workflows.md +601 -0
  47. package/README.md +46 -0
  48. package/dist/bin/platform-sdk.d.mts +1 -0
  49. package/dist/bin/platform-sdk.mjs +106 -0
  50. package/dist/bin/platform-sdk.mjs.map +1 -0
  51. package/dist/index.d.mts +1200 -43201
  52. package/dist/index.d.mts.map +1 -1
  53. package/dist/index.mjs +1859 -7319
  54. package/dist/index.mjs.map +1 -1
  55. package/package.json +19 -10
@@ -0,0 +1,262 @@
1
+ # Action status updaters
2
+
3
+ An **action status updater** is a scheduled job that polls an
4
+ external system (e.g. a cloud-provider log group, a delivery
5
+ provider's API) for status updates on actions the platform
6
+ previously fired, and writes those updates back onto the
7
+ matching message rows.
8
+
9
+ The typical use case: a workflow's SMS action fires through a
10
+ provider (a sender tool), and a status updater periodically
11
+ polls the provider's log group to mark each message as
12
+ `delivered` / `failed` / `bounced` on its tracked record.
13
+
14
+ SDK namespace: `api.actionStatusUpdaters`.
15
+
16
+ Action status updaters are **Datalake-DB-resident** — the
17
+ parent datalake must be `status: 'ready'` and every
18
+ referenced tool must exist before POST.
19
+
20
+ ## 1. Wire shape
21
+
22
+ ```typescript
23
+ import type {
24
+ ActionStatusUpdaterRequestWritable,
25
+ ActionStatusUpdaterResponse,
26
+ } from '@alvera-ai/platform-sdk'
27
+
28
+ const { data: created } = await api.actionStatusUpdaters.create(
29
+ tenantSlug,
30
+ datalakeSlug,
31
+ {
32
+ name: 'SMS Delivery Updater',
33
+ cron_expression: '*/30 * * * *', // every 30 minutes
34
+ updater_type: 'cloud_watch', // polling source family
35
+ updater_tool_id: cloudWatchToolId, // the tool that issues the poll
36
+ sender_tool_ids: [smsToolId], // tools whose sent messages this updater tracks
37
+ datalake_id: datalakeId,
38
+
39
+ // Polymorphic — discriminator: updater_body_type
40
+ updater_body: {
41
+ updater_body_type: 'cloud_watch_request',
42
+ log_group_name: 'sns/us-east-1/...',
43
+ start_time: '{{ now | minutes_ago: 45 }}', // Liquid window
44
+ end_time: '{{ now }}',
45
+ },
46
+
47
+ // Liquid template emitting JSON that maps poll events to message updates
48
+ message_config: {
49
+ type: 'custom',
50
+ body:
51
+ '{% for event in events %}' +
52
+ '{"external_id": "{{ event.message_id }}", "set_params": {"status": "{{ event.status }}"}}' +
53
+ '{% endfor %}',
54
+ },
55
+ },
56
+ )
57
+ ```
58
+
59
+ ## 2. Rules the type cannot encode
60
+
61
+ ### `updater_body` is polymorphic on `updater_body_type`
62
+
63
+ The body's discriminator selects the polling shape. Each
64
+ variant declares the fields the platform needs to construct
65
+ the poll request:
66
+
67
+ ```
68
+ updater_body_type: 'cloud_watch_request'
69
+ log_group_name string — the log group to scan
70
+ start_time string — Liquid template producing a timestamp
71
+ end_time string — Liquid template producing a timestamp
72
+ ```
73
+
74
+ (See the SDK type for the full set of body types — others
75
+ follow the same `<verb>_request` naming and carry the fields
76
+ their target API requires.)
77
+
78
+ `start_time` / `end_time` accept Liquid expressions using the
79
+ `now` variable plus relative-time filters (`minutes_ago: N`,
80
+ `hours_ago: N`, ...). A literal ISO 8601 string also works for
81
+ fixed windows.
82
+
83
+ ### `message_config.body` emits a JSON event stream
84
+
85
+ The template runs against an `events` array — the parsed
86
+ response from the polling tool. Each iteration must render
87
+ one JSON object with two required fields:
88
+
89
+ ```
90
+ external_id string — the message identifier to update (typically
91
+ the provider's id, matching the recipient
92
+ record's external_id column)
93
+ set_params object — fields to update on the matching message row
94
+ (e.g. { status, delivered_at, error_code })
95
+ ```
96
+
97
+ The platform parses the rendered output, joins each event to a
98
+ message row by `external_id`, and applies `set_params` as the
99
+ update. Events that don't match any message are silently
100
+ discarded.
101
+
102
+ ### `cron_expression` is a standard 5-field cron string
103
+
104
+ ```
105
+ */30 * * * * — every 30 minutes
106
+ 0 * * * * — every hour on the minute
107
+ 0 9 * * 1-5 — 9am Mon-Fri
108
+ ```
109
+
110
+ Sub-minute cadences are NOT supported. The minimum interval
111
+ the scheduler honors depends on platform-level configuration;
112
+ default minimum is 5 minutes.
113
+
114
+ ### `updater_type` and `updater_body.updater_body_type` must correspond
115
+
116
+ The body carries TWO discriminator fields that name the same
117
+ choice from different angles:
118
+
119
+ ```
120
+ updater_type outer field on the request body
121
+ (top-level)
122
+ updater_body.updater_body_type inner field on the polymorphic
123
+ body embed
124
+ ```
125
+
126
+ They must agree according to a fixed mapping:
127
+
128
+ ```
129
+ updater_type: "cloud_watch" ↔ updater_body_type: "cloud_watch_request"
130
+ updater_type: "restapi" ↔ updater_body_type: "restapi_request"
131
+ ```
132
+
133
+ A mismatched pair (e.g. `updater_type: "cloud_watch"` +
134
+ `updater_body_type: "restapi_request"`) returns 422 — typically
135
+ on `/updater_type` with a "type / body type mismatch" message.
136
+ The full mapping is exported by the SDK; the `updater_type` enum
137
+ and the `updater_body_type` enum are kept in lockstep by the
138
+ generated types.
139
+
140
+ ### `updater_tool_id` and `sender_tool_ids` serve different roles
141
+
142
+ - `updater_tool_id` (single) — the tool the updater uses to
143
+ ISSUE the poll. Must be a tool whose `intent` is
144
+ `status_poller` (e.g. a `cloud_watch_log_group` body type).
145
+ - `sender_tool_ids` (array) — the tools whose sent messages
146
+ this updater is responsible for tracking. The match logic
147
+ joins each polled event back to a message row that was
148
+ sent through one of these tools.
149
+
150
+ A single status updater can track messages from multiple
151
+ sender tools (e.g. an SMS tool plus a voice tool routed
152
+ through the same provider).
153
+
154
+ ## 3. Field ownership
155
+
156
+ **Server-derived (Response-only).** Universal set from
157
+ `type_naming.md`.
158
+
159
+ **Caller-supplied (round-trip).**
160
+
161
+ ```
162
+ name required string
163
+ cron_expression required string — standard cron
164
+ updater_type required enum — e.g. 'cloud_watch'
165
+ updater_tool_id required UUID — the polling tool
166
+ sender_tool_ids required UUID[] — tools whose messages this updater tracks
167
+ datalake_id required UUID
168
+ updater_body required embed — { updater_body_type, ... }
169
+ message_config required embed — { type, body }
170
+ ```
171
+
172
+ **Write-only (Request-only).** None.
173
+
174
+ ## 4. Error envelopes
175
+
176
+ Standard JSON:API envelopes per `errors.md`. Common rejections:
177
+
178
+ | `source.pointer` | Cause |
179
+ |-------------------------------------|----------------------------------------------------|
180
+ | `/cron_expression` | invalid cron syntax or below minimum interval |
181
+ | `/updater_tool_id` | tool's intent isn't `status_poller` |
182
+ | `/sender_tool_ids/0` | tool id doesn't exist or wrong datalake |
183
+ | `/updater_body/updater_body_type` | enum mismatch |
184
+ | `/updater_type` | mismatch with `updater_body.updater_body_type` |
185
+ | `/message_config/body` | empty when `type: 'custom'` |
186
+ | `/base` | `cron_management_failed` — body validated and saved but scheduler registration failed; the row is rolled back |
187
+
188
+ ## 5. Lifecycle
189
+
190
+ ### Create
191
+
192
+ Synchronous. A successful create registers a scheduled poll
193
+ under the supplied `cron_expression`; the first run fires at
194
+ the next cron slot at-or-after create time.
195
+
196
+ The cron registration is part of the create transaction — if
197
+ the body validates but scheduler registration fails, the
198
+ platform rolls the row back and returns 422 with
199
+ `cron_management_failed` on `/base` (see §4). Conversely, a
200
+ body that fails structural validation never reaches the
201
+ scheduler; cron-job registration only happens on the success
202
+ branch.
203
+
204
+ ### Read shapes
205
+
206
+ ```
207
+ .list(tenantSlug, datalakeSlug) Paged: { data, meta }
208
+ .get(tenantSlug, datalakeSlug, idOrSlug) One row
209
+ .metadata(tenantSlug, datalakeSlug) Markdown — catalog
210
+ .metadataDetails(tenantSlug, datalakeSlug, Markdown — one updater
211
+ idOrSlug)
212
+ ```
213
+
214
+ ### Update
215
+
216
+ `PUT` replays the full body. The polymorphic `updater_body`
217
+ must keep the same `updater_body_type` on update — changing
218
+ the discriminator requires delete + recreate.
219
+
220
+ ### Delete
221
+
222
+ `DELETE` removes the row and cancels future scheduled polls.
223
+ In-flight polls complete; their writebacks land normally.
224
+
225
+ The delete is **graceful** with respect to scheduler state: if
226
+ the row exists but its cron-job entry is somehow missing or
227
+ the scheduler raises during cancellation, the row is still
228
+ deleted and the DELETE returns 204. Consumers do not need to
229
+ check or retry for partial-delete states.
230
+
231
+ ## 6. Gotchas
232
+
233
+ 1. **The polling tool's `intent` must be `status_poller`.**
234
+ A tool with `intent: 'sms'` or any other non-poller intent
235
+ is rejected at create time. The poller intent gates the
236
+ tool's eligibility as an `updater_tool_id`.
237
+
238
+ 2. **`message_config.body` must emit valid JSON.** The
239
+ template is rendered against the polling tool's parsed
240
+ response and the output is then JSON-parsed. A render that
241
+ produces malformed JSON (e.g. trailing comma, unquoted
242
+ string) makes the entire poll cycle silently no-op for
243
+ that updater — events stay unprocessed.
244
+
245
+ 3. **`external_id` join is exact-match.** Events whose
246
+ `external_id` doesn't match a message row are discarded
247
+ silently. If your provider returns a prefixed id (e.g.
248
+ `provider-prefix/12345`) but your message rows carry
249
+ the bare id, transform inside the Liquid template (string
250
+ filters) so the rendered `external_id` matches.
251
+
252
+ 4. **`set_params` keys must be writable columns on the
253
+ message row.** Unknown keys are silently dropped during
254
+ the update. Refer to the message dataset's schema to
255
+ confirm which columns are writable from updater output.
256
+
257
+ 5. **Cron windows + polling-window overlap.** Choose
258
+ `start_time` / `end_time` to overlap consecutive cron
259
+ intervals (e.g. cron every 30 min, window of 45 min) so
260
+ transient delivery delays don't fall between two polls.
261
+ The defaults in the wire-shape example illustrate this
262
+ pattern: 30-minute cron, 45-minute window.
@@ -0,0 +1,423 @@
1
+ # AI agents
2
+
3
+ An **AI agent** is a registered LLM caller — a configured pairing
4
+ of a chat-completion tool, a model identifier, a system prompt
5
+ template, and JSON Schemas declaring the agent's input shape and
6
+ the LLM's required output shape. Workflows attach agents at
7
+ their enrichment stage; the agent's structured output then feeds
8
+ the workflow's decision template.
9
+
10
+ SDK namespace: `api.aiAgents`.
11
+
12
+ AI agents are **Datalake-DB-resident** — the parent datalake
13
+ must be `status: 'ready'` and the bound tool must exist before
14
+ POST.
15
+
16
+ ## 1. Wire shape
17
+
18
+ ```typescript
19
+ import type {
20
+ AiAgentRequestWritable,
21
+ AiAgentResponse,
22
+ } from '@alvera-ai/platform-sdk'
23
+
24
+ const { data: created } = await api.aiAgents.create(
25
+ tenantSlug,
26
+ datalakeSlug,
27
+ {
28
+ name: 'Contact Us Triage Categorizer',
29
+ description: 'Triages inbound submissions into appointment / job / spam',
30
+
31
+ tool_id: chatCompletionToolId, // see §2 — must be a tool with intent: 'llm_enrichment'
32
+
33
+ model: 'llama3.2:3b', // provider-specific model identifier
34
+ data_access: 'unregulated', // 'unregulated' | 'regulated' (compliance gate)
35
+ temperature: 0.0,
36
+ max_tokens: 2048,
37
+ enabled: true,
38
+
39
+ input_schema: { // JSON Schema — fields the prompt receives
40
+ type: 'object',
41
+ properties: {
42
+ msg: { type: 'string' },
43
+ submission_id: { type: 'string' },
44
+ },
45
+ required: ['msg', 'submission_id'],
46
+ },
47
+
48
+ llm_response_schema: { // JSON Schema — what the LLM MUST return
49
+ type: 'object',
50
+ properties: {
51
+ category: {
52
+ type: 'string',
53
+ enum: ['appointment_request', 'job_inquiry', 'flag_spam'],
54
+ },
55
+ },
56
+ required: ['category'],
57
+ additionalProperties: false,
58
+ },
59
+
60
+ prompt_config: {
61
+ type: 'custom',
62
+ body: `You are a triage assistant. Categorize this submission ...
63
+
64
+ Submission ID: {{ submission_id }}
65
+ Message: {{ msg }}
66
+
67
+ Respond with JSON: {"category": "..."}`,
68
+ },
69
+ },
70
+ )
71
+ // created.id, created.slug — server-derived
72
+ // created.name === body.name
73
+ ```
74
+
75
+ ## 2. Rules the type cannot encode
76
+
77
+ ### `tool_id` must point to a chat-completion tool
78
+
79
+ The bound tool's `intent` MUST be `llm_enrichment`. Tools with
80
+ any other intent (`sms`, `data_exchange`, `status_poller`, …)
81
+ are rejected at create time as a 422 on `/tool_id`.
82
+
83
+ The chat-completion tool is typically a `tool_body_type:
84
+ 'rest_api'` configured against an OpenAI-compatible endpoint
85
+ (OpenAI itself, Ollama, vLLM, etc.). The platform appends
86
+ `/chat/completions` to the tool's `base_url`, so:
87
+
88
+ ```
89
+ tool.base_url = 'http://localhost:11434/v1'
90
+ → actual request URL = 'http://localhost:11434/v1/chat/completions'
91
+ ```
92
+
93
+ A tool whose `base_url` already contains `/chat/completions`
94
+ will produce a doubled path and 404.
95
+
96
+ ### `llm_response_schema` is REQUIRED and load-bearing
97
+
98
+ The schema is mandatory at create time — a body omitting it
99
+ returns a 422 on `/llm_response_schema`. The schema serves two
100
+ roles:
101
+
102
+ 1. **Provider-side decoding constraint**: the platform forwards
103
+ the schema in the chat-completion request as
104
+ `response_format: json_schema`, instructing the provider to
105
+ constrain decoding so the LLM cannot return prose, markdown
106
+ code fences, or chat preamble around the JSON. Without this
107
+ constraint, small models often wrap output in fences and the
108
+ platform's JSON parse step fails with `json_decode_failed`.
109
+ 2. **Output binding**: the parsed LLM output is exposed in the
110
+ workflow's `additional_context` under the agent's slug, with
111
+ property paths matching the schema (see §3 below).
112
+
113
+ ### Prompt receives Liquid-rendered fields from `input_schema`
114
+
115
+ The agent's `prompt_config.body` is a Liquid template that
116
+ references the agent's `input_schema` fields via `{{ <field> }}`.
117
+ At workflow runtime, the workflow's `context_mapping_config`
118
+ populates these fields from the inbound dataset row (see §3).
119
+
120
+ The platform validates at create time that every `{{ <field> }}`
121
+ referenced by the prompt body resolves to a property declared in
122
+ `input_schema`. Drift between the two surfaces as a 422 on
123
+ `/prompt_config/body`.
124
+
125
+ ### `data_access` is the compliance gate
126
+
127
+ ```
128
+ data_access: 'unregulated' — the LLM call receives tokenized,
129
+ redacted data; safe for any LLM
130
+ (third-party, on-prem, …).
131
+ Default for triage / categorization
132
+ / lead-scoring workflows.
133
+ data_access: 'regulated' — the LLM call receives raw values;
134
+ MUST be paired with an LLM the
135
+ operator trusts with this tier
136
+ (typically an on-prem deployment).
137
+ Use for compliance workflows that
138
+ need raw values — sanctions
139
+ screening, manual review, KYC.
140
+ ```
141
+
142
+ The setting is **immutable post-create**. Flipping the access
143
+ tier requires recreating the agent (delete + create).
144
+
145
+ ### `enabled` gates whether workflows can invoke the agent
146
+
147
+ A workflow that references a `disabled` agent fails at run time
148
+ with `error_code: 'ai_agent_disabled'` on the workflow execution
149
+ log. `enabled: false` is the operator's kill switch for an agent
150
+ without removing it from the workflow's `ai_agents` array.
151
+
152
+ ### Numeric clamps on `temperature` and `max_tokens`
153
+
154
+ The platform validates the numeric ranges at create / update time
155
+ — BEFORE any chat-completion call to the bound tool. Out-of-range
156
+ values surface as field-level rejections, not as runtime
157
+ provider errors:
158
+
159
+ ```
160
+ temperature required number ∈ [0.0, 2.0] inclusive on both ends
161
+ max_tokens required integer ∈ [1, 100_000] inclusive on both ends
162
+ ```
163
+
164
+ These bounds are the platform's, not the provider's. Picking a
165
+ value the provider doesn't support (e.g. `temperature: 1.5` on
166
+ a model that caps at 1.0) surfaces later as a provider error in
167
+ the workflow execution log, not at create.
168
+
169
+ ### Name uniqueness is per-tenant, not per-datalake
170
+
171
+ The agent's `name` (and the server-derived `slug`) must be
172
+ unique within the tenant — collisions across **different
173
+ datalakes of the same tenant** are rejected with a 422 on
174
+ `/name` or `/slug`. This is unusual: most other resources scoped
175
+ under a datalake (tools, data sources, interoperability
176
+ contracts) carry per-datalake uniqueness. AI agents are the
177
+ exception because their `slug` is the workflow-template
178
+ reference key (`additional_context.<slug>`) and the platform
179
+ needs a stable cross-datalake namespace for it.
180
+
181
+ Practical consequence: if you want the same logical agent in
182
+ two datalakes (e.g. dev + prod inside one tenant), give them
183
+ distinct names. A naming convention like `"<purpose>-<env>"` or
184
+ including the datalake slug in the agent name keeps the
185
+ namespace clean.
186
+
187
+ ## 3. Workflow attachment
188
+
189
+ A workflow attaches an AI agent at its **enrichment** stage via
190
+ the inline `ai_agents` array on the workflow body (see
191
+ `workflows.md` §3):
192
+
193
+ ```typescript
194
+ {
195
+ // ... other workflow fields ...
196
+ ai_agents: [
197
+ {
198
+ ai_agent_id: createdAgentId,
199
+ position: 0, // ordering when multiple agents fire
200
+ context_mapping_config: {
201
+ type: 'custom',
202
+ // Liquid renders to a JSON object whose keys match the
203
+ // agent's input_schema. The values reference the inbound
204
+ // dataset row via `event_dataset.<field>`.
205
+ body: JSON.stringify({
206
+ msg: '{{ event_dataset.message }}',
207
+ submission_id: '{{ event_dataset.submission_id }}',
208
+ }),
209
+ output_schema: '{"type":"object"}',
210
+ },
211
+ },
212
+ ],
213
+ decision_config: {
214
+ type: 'custom',
215
+ // The agent's parsed output is exposed at
216
+ // `additional_context.<agent_slug>.<field>`. Property paths
217
+ // match the agent's llm_response_schema.
218
+ body: '["{{ additional_context.contact-us-triage-categorizer.category }}"]',
219
+ output_schema: '{"type":"array","items":{"type":"string"}}',
220
+ },
221
+ }
222
+ ```
223
+
224
+ The flow at run time:
225
+
226
+ ```
227
+ inbound row
228
+ │
229
+ ▼
230
+ workflow filter
231
+ │
232
+ ▼
233
+ for each ai_agents[i] (ordered by position):
234
+ • render context_mapping_config.body with Liquid (event_dataset.*, etc.)
235
+ • parse rendered output as JSON
236
+ • check it conforms to agent.input_schema
237
+ • forward to the bound tool as a chat completion
238
+ • parse the LLM response per agent.llm_response_schema
239
+ • merge under additional_context[agent.slug]
240
+ │
241
+ ▼
242
+ workflow decision (reads additional_context.<agent_slug>.<field>)
243
+ │
244
+ ▼
245
+ workflow action(s)
246
+ ```
247
+
248
+ ## 4. Field ownership
249
+
250
+ **Server-derived (Response-only).** Universal set from
251
+ `type_naming.md`. The `slug` is what workflow templates
252
+ reference via `additional_context.<slug>`.
253
+
254
+ **Caller-supplied (round-trip).**
255
+
256
+ ```
257
+ name required string
258
+ description optional string
259
+ tool_id required UUID — must be intent: llm_enrichment
260
+ model required string — provider-specific identifier
261
+ data_access required enum — 'unregulated' | 'regulated'
262
+ temperature required number
263
+ max_tokens required number
264
+ enabled required bool
265
+ input_schema required JSON Schema object
266
+ llm_response_schema required JSON Schema object — see §2
267
+ prompt_config required embed — { type, body }
268
+ ```
269
+
270
+ **Write-only (Request-only).** None.
271
+
272
+ ## 5. Error envelopes
273
+
274
+ Standard JSON:API envelopes per `errors.md`. Common rejections:
275
+
276
+ | `source.pointer` | Cause |
277
+ |---------------------------------|----------------------------------------------------|
278
+ | `/tool_id` | tool's intent isn't `llm_enrichment` |
279
+ | `/llm_response_schema` | missing or not a valid JSON Schema object |
280
+ | `/input_schema` | missing or not a valid JSON Schema object |
281
+ | `/prompt_config/body` | references a field absent from `input_schema` |
282
+ | `/data_access` | value not in enum |
283
+ | `/temperature` | outside the inclusive range [0.0, 2.0] |
284
+ | `/max_tokens` | outside the inclusive range [1, 100_000] |
285
+ | `/name` or `/slug` | uniqueness collision — scope is per-tenant |
286
+
287
+ ## 6. Lifecycle
288
+
289
+ ### Create
290
+
291
+ Synchronous. The agent is immediately invocable by workflows
292
+ once `enabled: true`.
293
+
294
+ ### Read shapes
295
+
296
+ ```
297
+ .list(tenantSlug, datalakeSlug) Paged: { data, meta }
298
+ .get(tenantSlug, datalakeSlug, id) One row, BY ID (UUID — not slug)
299
+ .metadata(tenantSlug, datalakeSlug) Markdown catalog
300
+ .metadataDetails(tenantSlug, datalakeSlug, Markdown for one agent;
301
+ id) section headings: Model,
302
+ Bound tool, Prompt config
303
+ ```
304
+
305
+ `.get` and `.metadataDetails` accept the agent's UUID `id` as
306
+ the path key — NOT the slug. The slug is only used as a
307
+ runtime reference key (`additional_context.<slug>` in workflow
308
+ templates).
309
+
310
+ ### Update
311
+
312
+ `PUT` replays the full body — no PATCH. Replace-on-PUT means
313
+ all required fields must be re-supplied verbatim, including
314
+ `input_schema`, `llm_response_schema`, and `prompt_config`.
315
+ Omitting any returns a 422.
316
+
317
+ ### Delete
318
+
319
+ `DELETE` removes the row. Workflows that still reference the
320
+ agent in their `ai_agents` array fail at run time with
321
+ `error_code: 'ai_agent_not_found'`; clean up workflow
322
+ references first.
323
+
324
+ ## 7. Run-time failure modes
325
+
326
+ When a workflow run's enrichment stage fails, the per-execution
327
+ `error.json` artifact carries a structured failure record (see
328
+ `workflows.md` §6 for the artifact convention). The
329
+ agent-specific codes:
330
+
331
+ ```
332
+ tool_execution_failed — the bound chat-completion tool's
333
+ HTTP call failed (404, refused,
334
+ timeout). error.json carries the
335
+ rich detail (URL, status); the
336
+ workflow execution log carries
337
+ a coarse error_message of the
338
+ form "AI enrichment failed: <agent_name>"
339
+ json_decode_failed — provider returned a response that
340
+ didn't parse as JSON (typically
341
+ when llm_response_schema wasn't
342
+ wired through to the provider's
343
+ structured-output mode)
344
+ context_mapping_failed — context_mapping_config.body
345
+ rendered output that didn't
346
+ validate against the agent's
347
+ input_schema
348
+ ai_agent_disabled — agent's enabled flag is false
349
+ ai_agent_not_found — agent was deleted but workflow
350
+ still references it
351
+ ```
352
+
353
+ The `error.json` shape:
354
+
355
+ ```
356
+ {
357
+ error_code: string — one of the codes above
358
+ stage: string — 'enrichment' for agent-related failures
359
+ ai_agent_slug: string — slug of the failing agent
360
+ error_message: string — coarse copy of the workflow execution log's message
361
+ detail: string — rich provider context (URL, HTTP status, etc.);
362
+ ONLY present in error.json, NOT mirrored to the
363
+ workflow execution log row
364
+ }
365
+ ```
366
+
367
+ The `enrichment.json` artifact (success branch) carries the
368
+ parsed agent outputs:
369
+
370
+ ```
371
+ {
372
+ status: 'completed' | 'failed' | 'skipped',
373
+ <agent_slug_1>: {
374
+ status: 'completed',
375
+ output: { /* matches llm_response_schema */ },
376
+ },
377
+ <agent_slug_2>: { /* ... */ },
378
+ }
379
+ ```
380
+
381
+ For workflows without any `ai_agents`, `enrichment.json` lands
382
+ with `{ status: 'skipped' }` and no per-agent entries.
383
+
384
+ ## 8. Gotchas
385
+
386
+ 1. **`llm_response_schema` is REQUIRED**, not optional. Omitting
387
+ it returns 422 on `/llm_response_schema`. The platform uses
388
+ it both as a provider-side decoding constraint and as the
389
+ output-binding shape for `additional_context.<slug>`. Missing
390
+ the schema means no structured output and downstream decision
391
+ templates can't reference the agent's fields.
392
+
393
+ 2. **`tool_id` must reference an `intent: 'llm_enrichment'`
394
+ tool.** Other intents are rejected at create. The chat
395
+ completion tool is typically `tool_body_type: 'rest_api'`
396
+ against an OpenAI-compatible endpoint; the platform appends
397
+ `/chat/completions` to its `base_url`.
398
+
399
+ 3. **`additional_context.<slug>` is the workflow accessor.**
400
+ The agent's `slug` (server-derived from `name`) is what
401
+ workflow decision and action templates reference. Capture it
402
+ from the create response — don't pre-compute.
403
+
404
+ 4. **`.get` takes UUID `id`, not slug.** The SHOW route is
405
+ `/ai-agents/:id`. The slug is only a runtime template key.
406
+
407
+ 5. **Replace-on-PUT.** Update bodies must re-supply
408
+ `input_schema`, `llm_response_schema`, `prompt_config`
409
+ verbatim. Partial PUT silently drops fields and returns
410
+ 422 on the missing required field.
411
+
412
+ 6. **`data_access` is immutable post-create.** Flipping the
413
+ compliance tier requires delete + recreate.
414
+
415
+ 7. **`prompt_config.body` field references must resolve in
416
+ `input_schema`.** A `{{ field }}` that isn't declared in
417
+ `input_schema` returns 422 on `/prompt_config/body` at
418
+ create time.
419
+
420
+ 8. **The chat-completion tool's `base_url` should NOT include
421
+ `/chat/completions`.** The platform appends the path; a
422
+ doubled path 404s. Use the API root (e.g. `/v1` for OpenAI
423
+ or Ollama).