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

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 CHANGED
@@ -36,9 +36,9 @@ The one-line summaries in the Resources section are
36
36
  > The index line `workflows.md — Filter + decision + action;
37
37
  > standard + agent-driven variants` **cannot** tell you whether
38
38
  > `variant` is a body field. It is not. `workflows.md` §1 shows
39
- > the variant is **structural** — via the presence of the
40
- > `ai_agents[]` array on the request body **not** a top-level
41
- > discriminator.
39
+ > the variant is **structural** — an agent-driven workflow simply
40
+ > has one or more AI agents *nested* on its body (the
41
+ > `workflow_ai_agents` array) — **not** a top-level `variant` discriminator.
42
42
 
43
43
  An agent that paraphrases the index into a wire-level claim is
44
44
  **hallucinating**. The index summary is too compressed to encode
@@ -184,40 +184,75 @@ distinct names. A naming convention like `"<purpose>-<env>"` or
184
184
  including the datalake slug in the agent name keeps the
185
185
  namespace clean.
186
186
 
187
- ## 3. Workflow attachment
187
+ ## 3. Workflow & contract nesting
188
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):
189
+ An agent runs as an **enrichment** step on a workflow or an
190
+ interoperability contract. The agent is **nested directly on the
191
+ parent body** — `agentic-workflows` and `interoperability-contracts`
192
+ create/PUT bodies accept a `workflow_ai_agents` /
193
+ `interoperability_contract_ai_agents` array (a `cast_assoc`). There is
194
+ no separate attach/detach endpoint, and **no per-join checksum**: the
195
+ nested agents fold into the parent's own checksum.
192
196
 
193
197
  ```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"}',
198
+ // Nest on create. `position` is REQUIRED.
199
+ const { data: workflow } = await api.workflows.create(
200
+ tenantSlug, datalakeSlug,
201
+ {
202
+ name: 'Contact-Us Triage',
203
+ dataset_type: 'generic_table',
204
+ // …other workflow fields…
205
+ workflow_ai_agents: [
206
+ {
207
+ ai_agent_id: createdAgentId,
208
+ position: 0, // ordering when multiple agents fire; REQUIRED
209
+ context_mapping_config: {
210
+ type: 'custom',
211
+ // Liquid renders to a JSON object whose keys match the agent's
212
+ // input_schema. Values reference the inbound row via `event_dataset.<field>`.
213
+ body: JSON.stringify({
214
+ msg: '{{ event_dataset.message }}',
215
+ submission_id: '{{ event_dataset.submission_id }}',
216
+ }),
217
+ // Workflow joins OMIT output_schema — the server pins it from
218
+ // the agent's input_schema. Interop joins author it (below).
219
+ },
210
220
  },
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"}}',
221
+ ],
220
222
  },
223
+ )
224
+ workflow.checksum // the PARENT checksum — the nested agents fold into it
225
+ ```
226
+
227
+ Interoperability contracts nest the same way, via the
228
+ `interoperability_contract_ai_agents` array on the contract create/PUT
229
+ body. The one difference: a contract join's `context_mapping_config`
230
+ **does** carry `output_schema` (the server does not pin it there).
231
+
232
+ Adding, repositioning, and removing agents are all expressed as a
233
+ **full-set PUT** on the parent (`on_replace: :delete`):
234
+
235
+ | Operation | How | Result |
236
+ |------------|------------------------------------------------------|--------|
237
+ | add / nest | include in `*_ai_agents` on create or PUT | parent checksum reflects the agents |
238
+ | reposition | PUT with the same agent at a new `position` | full-set replace |
239
+ | detach | PUT omitting that agent (or `[]` to clear all) | `on_replace: :delete` removes it |
240
+ | drift | parent `checksum()` endpoint over the full body | the would-be parent checksum (no persist) |
241
+
242
+ The parent surfaces its nested agents on GET: a workflow GET returns
243
+ `workflow_ai_agents: [...]` and a contract GET returns
244
+ `interoperability_contract_ai_agents: [...]`. Both arrays are writable
245
+ on the parent body (create + PUT).
246
+
247
+ The workflow's own `decision_config` reads the agent's parsed output
248
+ at `additional_context.<agent_slug>.<field>` (property paths match the
249
+ agent's `llm_response_schema`):
250
+
251
+ ```typescript
252
+ decision_config: {
253
+ type: 'custom',
254
+ body: '["{{ additional_context.contact-us-triage-categorizer.category }}"]',
255
+ output_schema: '{"type":"array","items":{"type":"string"}}',
221
256
  }
222
257
  ```
223
258
 
@@ -256,10 +256,11 @@ ctx.agentSlug = agentResp.data.slug!
256
256
 
257
257
  The workflow has the standard shape — filter, decision, actions —
258
258
  but two things distinguish it from a static workflow. First, the
259
- `ai_agents` array binds the Triage agent into the workflow's
260
- enrichment phase, with a Liquid `context_mapping_config` that
261
- projects each submission row's fields into the agent's input
262
- schema. Second, the `decision_config` body is a Liquid template
259
+ Triage agent is **nested** in the workflow create body (the
260
+ `workflow_ai_agents` array a `cast_assoc`, not a separate attach
261
+ call), binding it into the workflow's enrichment phase with a Liquid
262
+ `context_mapping_config` that projects each submission row's fields
263
+ into the agent's input schema. Second, the `decision_config` body is a Liquid template
263
264
  that interpolates the agent's `category` output (read from
264
265
  `additional_context["<agent-slug>"].category`) into a
265
266
  single-element decision array. The three `actions` are keyed
@@ -299,17 +300,6 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
299
300
  body: DECISION_CONFIG_BODY,
300
301
  output_schema: DECISION_OUTPUT_SCHEMA,
301
302
  },
302
- ai_agents: [
303
- {
304
- ai_agent_id: aiAgentId,
305
- position: 0,
306
- context_mapping_config: {
307
- type: 'custom',
308
- body: CONTEXT_MAPPING_BODY,
309
- output_schema: '{"type":"object"}',
310
- },
311
- },
312
- ],
313
303
  actions: BUCKETS.map((bucket) => ({
314
304
  decision_key: bucket,
315
305
  action_type: 'sms',
@@ -327,6 +317,15 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
327
317
  sms_type: 'transactional',
328
318
  },
329
319
  })),
320
+ // Nest the triage agent inline. Workflow joins omit output_schema —
321
+ // the server pins it from the agent's input_schema.
322
+ workflow_ai_agents: [
323
+ {
324
+ ai_agent_id: aiAgentId,
325
+ position: 0,
326
+ context_mapping_config: { type: 'custom', body: CONTEXT_MAPPING_BODY },
327
+ },
328
+ ],
330
329
  })
331
330
  workflowId = workflowResp.data.id!
332
331
  ctx.workflowSlug = workflowResp.data.slug!
@@ -201,8 +201,10 @@ ctx.agentSlug = agentResp.data.slug!
201
201
  ## 004 — create the Sanctions Review workflow
202
202
 
203
203
  The workflow has the standard shape — filter, decision, actions —
204
- plus an `ai_agents` array binding the Sanctions Review agent into
205
- the enrichment phase. Three pieces are worth noting.
204
+ plus the Sanctions Review agent **nested** in the create body (the
205
+ `workflow_ai_agents` array a `cast_assoc`, not a separate attach
206
+ call) to bind it into the enrichment phase. Three pieces are worth
207
+ noting.
206
208
 
207
209
  The **filter** is the gray-zone band: it passes only screenings
208
210
  whose `screening_score` is at least `0.80` and below `0.95`. A
@@ -256,17 +258,6 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
256
258
  body: DECISION_CONFIG_BODY,
257
259
  output_schema: DECISION_OUTPUT_SCHEMA,
258
260
  },
259
- ai_agents: [
260
- {
261
- ai_agent_id: aiAgentId,
262
- position: 0,
263
- context_mapping_config: {
264
- type: 'custom',
265
- body: CONTEXT_MAPPING_BODY,
266
- output_schema: '{"type":"object"}',
267
- },
268
- },
269
- ],
270
261
  actions: VERDICTS.map((verdict) => ({
271
262
  decision_key: verdict,
272
263
  action_type: 'sms',
@@ -284,6 +275,15 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
284
275
  sms_type: 'transactional',
285
276
  },
286
277
  })),
278
+ // Nest the sanctions-review agent inline. Workflow joins omit
279
+ // output_schema — the server pins it from the agent's input_schema.
280
+ workflow_ai_agents: [
281
+ {
282
+ ai_agent_id: aiAgentId,
283
+ position: 0,
284
+ context_mapping_config: { type: 'custom', body: CONTEXT_MAPPING_BODY },
285
+ },
286
+ ],
287
287
  })
288
288
  workflowId = workflowResp.data.id!
289
289
  ctx.workflowSlug = workflowResp.data.slug!
@@ -264,9 +264,11 @@ ctx.agentSlug = agentResp.data.slug!
264
264
 
265
265
  The workflow has the standard shape — filter, decision, actions — but
266
266
  two things distinguish it from the birthday cookbook's static
267
- workflow. First, the `ai_agents` array binds the Score Lead agent into
268
- the workflow's enrichment phase, with a Liquid `context_mapping_config`
269
- that projects each lead row's fields into the agent's input schema.
267
+ workflow. First, the Score Lead agent is **nested** in the workflow
268
+ create body (the `workflow_ai_agents` array a `cast_assoc`, not a
269
+ separate attach call), binding it into the workflow's enrichment phase
270
+ with a Liquid `context_mapping_config` that projects each lead row's
271
+ fields into the agent's input schema.
270
272
  Second, the `decision_config` body is a Liquid template that
271
273
  interpolates the agent's `band` output (read from
272
274
  `additional_context["<agent-slug>"].band`) into a single-element
@@ -307,17 +309,6 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
307
309
  body: DECISION_CONFIG_BODY,
308
310
  output_schema: DECISION_OUTPUT_SCHEMA,
309
311
  },
310
- ai_agents: [
311
- {
312
- ai_agent_id: aiAgentId,
313
- position: 0,
314
- context_mapping_config: {
315
- type: 'custom',
316
- body: CONTEXT_MAPPING_BODY,
317
- output_schema: '{"type":"object"}',
318
- },
319
- },
320
- ],
321
312
  actions: BANDS.map((band) => ({
322
313
  decision_key: band,
323
314
  action_type: 'sms',
@@ -335,6 +326,15 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
335
326
  sms_type: 'transactional',
336
327
  },
337
328
  })),
329
+ // Nest the scoring agent inline. Workflow joins omit output_schema —
330
+ // the server pins it from the agent's input_schema.
331
+ workflow_ai_agents: [
332
+ {
333
+ ai_agent_id: aiAgentId,
334
+ position: 0,
335
+ context_mapping_config: { type: 'custom', body: CONTEXT_MAPPING_BODY },
336
+ },
337
+ ],
338
338
  })
339
339
  workflowId = workflowResp.data.id!
340
340
  ctx.workflowSlug = workflowResp.data.slug!
@@ -290,7 +290,72 @@ const { data: list } = await api.interoperabilityContracts.list(
290
290
  const editable = list.data.filter((c) => !c.system_created)
291
291
  ```
292
292
 
293
- ## 6. Gotchas
293
+ ## 6. Nesting AI agents on the contract body
294
+
295
+ A contract can run AI-agent **enrichment** during ingestion. Agents
296
+ are **nested directly on the contract body** — the create/PUT body
297
+ accepts an `interoperability_contract_ai_agents` array (a `cast_assoc`).
298
+ There is no separate attach/detach endpoint, and **no per-join
299
+ checksum**: the nested agents fold into the contract's own checksum.
300
+
301
+ ```typescript
302
+ // Nest on create. `position` is REQUIRED.
303
+ const { data: contract } = await api.interoperabilityContracts.create(
304
+ tenantSlug, datalakeSlug,
305
+ {
306
+ name: 'MOA Enrichment Contract',
307
+ resource_type: 'legal_entity',
308
+ template_config: { type: 'custom', body: '{{ name }}' },
309
+ mdm_input_config: { type: 'null' },
310
+ // …other contract fields…
311
+ interoperability_contract_ai_agents: [
312
+ {
313
+ ai_agent_id: createdAgentId,
314
+ position: 0, // ordering when multiple agents fire; REQUIRED
315
+ context_mapping_config: {
316
+ type: 'custom',
317
+ // Liquid renders to a JSON object whose keys match the agent's
318
+ // input_schema; values reference the inbound row via `msg.<field>`.
319
+ body: JSON.stringify({ name: '{{ msg.company_name }}' }),
320
+ // A contract join AUTHORS output_schema — unlike a workflow
321
+ // join, the server does NOT pin it here.
322
+ output_schema: '{"type":"object","properties":{"lead":{"type":"string"}}}',
323
+ },
324
+ },
325
+ ],
326
+ },
327
+ )
328
+ contract.checksum // the PARENT checksum — the nested agents fold into it
329
+ ```
330
+
331
+ Adding, repositioning, and removing agents are all expressed as a
332
+ **full-set PUT** on the contract (`on_replace: :delete`):
333
+
334
+ | Operation | How | Result |
335
+ |------------|--------------------------------------------------|--------|
336
+ | add / nest | include in the array on create or PUT | contract checksum reflects the agents |
337
+ | reposition | PUT with the same agent at a new `position` | full-set replace |
338
+ | detach | PUT omitting that agent (or `[]` to clear all) | `on_replace: :delete` removes it |
339
+ | drift | contract `checksum()` endpoint over the full body| the would-be contract checksum (no persist) |
340
+
341
+ A contract GET surfaces its nested agents as
342
+ `interoperability_contract_ai_agents: [...]`. The array is writable on
343
+ the contract body (create + PUT).
344
+
345
+ **Runtime.** Enrichment fires on the ingest path (`execute_contract`),
346
+ **not** on the sandbox `.run`. Attached agents run in `position`
347
+ order: each agent's `context_mapping_config` renders against the row,
348
+ the result is validated against the agent's `input_schema`, sent to
349
+ the bound `llm_enrichment` tool, and the parsed response is merged
350
+ under `msg.enrichment.<agent_slug>` for the contract's
351
+ `template_config` to read. So attaching an agent to a DAC-wired
352
+ contract makes ingestion run enrichment per row — keep agents off
353
+ contracts whose only job is a straight-through upsert.
354
+
355
+ See `ai_agents.md` §3 for the agent create body and the shared attach
356
+ surface, and `workflows.md` for the workflow side of the same join.
357
+
358
+ ## 7. Gotchas
294
359
 
295
360
  1. **`filter_template` is empty-passes, non-empty-skips.**
296
361
  Already documented in §2 — repeated here because it's the
@@ -92,9 +92,9 @@ const { data: created } = await api.workflows.create(
92
92
  )
93
93
  ```
94
94
 
95
- `actions`, `context_datasets`, and (for agent-driven workflows)
96
- `ai_agents` are all inline arrays — one POST builds the whole
97
- workflow.
95
+ `actions`, `context_datasets`, and `workflow_ai_agents` are all
96
+ inline arrays — one POST builds the entire workflow body, agents
97
+ included (see §3).
98
98
 
99
99
  ## 2. Rules the type cannot encode
100
100
 
@@ -115,9 +115,9 @@ to validate the rendered output before invoking the next stage.
115
115
 
116
116
  ### Replace-on-PUT semantics for inline arrays
117
117
 
118
- `actions`, `context_datasets`, and `ai_agents` (when present)
119
- use **replace semantics on PUT**. Submitting an update body that
120
- omits any of these arrays silently disassociates ALL of its
118
+ `actions` and `context_datasets` use **replace semantics on PUT**.
119
+ Submitting an update body that omits either array silently
120
+ disassociates ALL of its
121
121
  entries. Always re-supply the full array on every update, even
122
122
  when you're only changing `description`.
123
123
 
@@ -185,29 +185,43 @@ When `decision_config.type` references an AI agent (instead of
185
185
  action(s)
186
186
  ```
187
187
 
188
- Bound agents are declared in `ai_agents: []` on the workflow
189
- body. The full AI agent surface (create body, prompt config,
190
- input_schema / llm_response_schema, runtime failure codes) lives
191
- in `ai_agents.md`. The workflow-side embed shape is:
188
+ Agents are **nested directly on the workflow body** — the create and
189
+ PUT body accept a `workflow_ai_agents` array (a `cast_assoc`). There is
190
+ no separate attach/detach endpoint. The agents **fold into the
191
+ workflow's own checksum** — there is no per-join checksum. `position` is
192
+ required, and workflow joins **omit `output_schema`** (the server pins
193
+ it from the agent's `input_schema`). The agent create body (prompt
194
+ config, `input_schema` / `llm_response_schema`, runtime failure codes)
195
+ lives in `ai_agents.md` §3. A workflow GET surfaces the nested agents as
196
+ `workflow_ai_agents: [...]`.
197
+
198
+ PUT is a **full-set replace** (`on_replace: :delete`): the body must
199
+ re-supply every agent you want to keep — agents absent from the array
200
+ are detached, and re-supplied ones are repositioned.
192
201
 
193
202
  ```typescript
194
- ai_agents: [
195
- {
196
- ai_agent_id: aiAgentId, // UUID from api.aiAgents.create
197
- position: 0, // ordering when multiple agents fire
198
- context_mapping_config: {
199
- type: 'custom',
200
- // Liquid renders to a JSON object whose keys match the
201
- // referenced agent's input_schema. The values reference
202
- // the inbound dataset row's fields via `event_dataset.<field>`.
203
- body: JSON.stringify({
204
- msg: '{{ event_dataset.message }}',
205
- submission_id: '{{ event_dataset.submission_id }}',
206
- }),
207
- output_schema: '{"type":"object"}',
203
+ await api.workflows.create(tenantSlug, datalakeSlug, {
204
+ name: 'Contact-Us Triage',
205
+ dataset_type: 'generic_table',
206
+ // …other workflow fields…
207
+ workflow_ai_agents: [
208
+ {
209
+ ai_agent_id: aiAgentId, // UUID from api.aiAgents.create
210
+ position: 0, // ordering when multiple agents fire; REQUIRED
211
+ context_mapping_config: {
212
+ type: 'custom',
213
+ // Liquid renders to a JSON object whose keys match the referenced
214
+ // agent's input_schema. Values reference the inbound row's fields
215
+ // via `event_dataset.<field>`. Workflow joins OMIT output_schema
216
+ // — the server pins it from the agent's input_schema.
217
+ body: JSON.stringify({
218
+ msg: '{{ event_dataset.message }}',
219
+ submission_id: '{{ event_dataset.submission_id }}',
220
+ }),
221
+ },
208
222
  },
209
- },
210
- ],
223
+ ],
224
+ })
211
225
  ```
212
226
 
213
227
  The agent's parsed output is exposed in the workflow's runtime
@@ -219,9 +233,9 @@ action templates reference it via Liquid:
219
233
  {{ additional_context.contact-us-triage-categorizer.category }}
220
234
  ```
221
235
 
222
- Standard (non-agent) workflows omit `ai_agents` the enrichment
223
- stage observably runs but emits `{ status: 'skipped' }` in the
224
- per-step artifact (see §6).
236
+ A workflow with no attached agents observably runs the enrichment
237
+ stage but emits `{ status: 'skipped' }` in the per-step artifact
238
+ (see §6).
225
239
 
226
240
  ### `event_dataset` is the workflow accessor for the inbound row
227
241
 
@@ -273,7 +287,7 @@ filter_config required embed — { type, body, output_schema }
273
287
  decision_config required embed — same shape
274
288
  context_datasets required array — inline; replace-on-PUT
275
289
  actions required array — inline; replace-on-PUT
276
- ai_agents optional array — inline; replace-on-PUT (§3)
290
+ workflow_ai_agents optional array — inline; replace-on-PUT (see §3)
277
291
  ```
278
292
 
279
293
  **Write-only (Request-only).** None.
@@ -550,10 +564,11 @@ record `opened_at` / `form_submitted_at`.
550
564
  `interoperability_contracts.md` §2 for the worked example.
551
565
 
552
566
  2. **Replace-on-PUT silently drops omitted arrays.** Workflow
553
- updates must re-supply `actions`, `context_datasets`, and
554
- `ai_agents` verbatim — otherwise the existing rows are
555
- deleted. Factor the body construction so create and update
556
- share one builder.
567
+ updates must re-supply `actions` and `context_datasets`
568
+ verbatim — otherwise the existing rows are deleted. Factor the
569
+ body construction so create and update share one builder.
570
+ (Nested `workflow_ai_agents` follow the same replace-on-PUT
571
+ rule — re-supply the full set on every PUT; see §3.)
557
572
 
558
573
  3. **`sql_where_clause` ids require an alias prefix.** `id` is
559
574
  ambiguous across the multi-JOIN base query; use