Skip to content
44 changes: 44 additions & 0 deletions docs/learnings/structured-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,47 @@ Structured outputs are the primary way to measure voice agent performance. Commo
**Recommendation:** For squad-wide KPIs that must populate on **every** ending (VM-only, classifier-only, and live-agent), use **`squad.membersOverrides.analysisPlan.structuredDataPlan`** plus **`membersOverrides.artifactPlan.fullMessageHistoryEnabled: true`**. Remove per-assistant duplicate plans. Keep standalone structured outputs for evals, dashboard analytics, or schemas you want versioned as separate resources.

Full YAML pattern and merge-order notes: [squads.md → Squad-level post-call extraction via `membersOverrides`](squads.md#squad-level-post-call-extraction-via-membersoverrides-multi-member-squads).

---

## assistant_ids and the update/linking cycle (dependency cycle)

Structured outputs are pushed before assistants, same as tools — see
[tools.md → "Handoff/transfer tools reference assistants (dependency
cycle)"](tools.md#handofftransfer-tools-reference-assistants-dependency-cycle)
for the general shape. A structured output's `assistant_ids` references an
assistant, which inverts the push order for that field. The engine handles it
the same two-pass way: on create, `assistantIds` is stripped entirely (no
assistants exist yet); on update, it's resolved to whatever already exists in
state. Either way, a linking pass then sets the real value once every
assistant in the push has been applied.

The difference from tool destinations is what happens when a reference still
doesn't resolve. `assistantIds` is a flat array with no shape to signal "leave
this one for later" — so instead of sending a shorter array, the engine omits
the whole `assistantIds` key from the update PATCH whenever any authored
`assistant_ids` entry fails to resolve. Sending a filtered array would PATCH-
replace the platform's current list, silently unlinking every live-but-
untracked assistant the array left out — the update never shrinks the
dashboard's array as a side effect of an unrelated edit.

The linking pass (which runs once every assistant in the push has been
applied) can still find an unresolved reference — genuinely absent from both
state and the local repo, not just not-yet-applied. When that happens it
**skips that structured output** rather than PATCHing the partial list, and
logs a warning naming the unresolved reference(s):

```
⚠️ Structured output "<so-id>" still references unresolved assistant(s): <ref>. Leaving this structured output's assistant links untouched on the platform — they will link on a future push once those assistants exist.
```

**One asymmetry worth knowing versus tool destinations:** for structured
outputs, an **untracked raw UUID counts as unresolved**, not as a legitimate
platform-only reference. Tool destinations let a raw UUID through even when
it's untracked in state (it might be a real dashboard assistant the repo
just doesn't manage). Structured outputs can't take that risk — there's no
later repair pass for a structured output's assistant links the way there is
for tool destinations, so if the linking pass got it wrong here, the wipe
would be permanent until someone noticed and re-pushed by hand. Treating an
untracked UUID as unresolved means it skips-and-warns instead of silently
shipping a wipe.
29 changes: 29 additions & 0 deletions docs/learnings/sync-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,35 @@ UUIDs → names):
| `apply` (default `--resolve=defer`) | pull defers → push prompts for **exactly the conflicted resources**; clean ones flow silently |
| `apply --resolve=ours` | no questions: pull re-baselines, push runs with `--overwrite` (CI semantics; dashboard edits lose) |

### Conflict timing context (advisory)

When a 3-way conflict is reported, each entry carries a timing line beside the
hashes:

```
- assistants/intake
local-hash: 3f9a1c2b… platform-hash: 8e01d4aa… last-pulled: 3f9a1c2b…
dashboard changed 2026-08-01 15:00Z, your file 2026-08-01 12:00Z — dashboard is 3h newer
```

It is a hint, never a verdict, and the engine still refuses to choose. Three
reasons it cannot be trusted as one:

- `updatedAt` is bumped by **our own pushes**, so a "newer" dashboard often just
means you pushed a few minutes ago.
- The local mtime is reset by `git clone` and `git checkout`, so on a fresh
checkout every file looks edited seconds ago.
- **Later does not mean supersedes.** If you changed the prompt and a teammate
changed the voice, both edits deserve to survive; last-write-wins would discard
one silently.

Sub-minute gaps are reported as "within a minute of each other" rather than
picking a winner, because clock skew is the same order of magnitude as the gap.

A previous state schema stored `lastPulledAt` for this purpose and it was
deliberately removed in favour of content hashes. This line reads `updatedAt`
from the live response and the file's mtime at report time; it persists nothing.

### 5. Both changed identically (L = D, stale baseline)

Both `pull` and `push` treat this as clean (live sides agree — nothing to
Expand Down
31 changes: 31 additions & 0 deletions docs/learnings/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,34 @@ The LLM produces only `name` and `email` (what the caller spoke). The orchestrat
For belt-and-braces, pair static parameters with HMAC body signing so the backend verifies sender + content, not just channel.

For the trust-tier breakdown of which Liquid variables are safe to use here (`{{ customer.number }}`, `{{ call.id }}`, etc.) vs. which are LLM-derived and not, see [assistants.md → Liquid Variable Bag and Trust Tiers](assistants.md#liquid-variable-bag-and-trust-tiers).

## Handoff/transfer tools reference assistants (dependency cycle)

Tools are pushed before assistants, because assistants reference tools. A
handoff or transfer tool references an assistant, which inverts that for the
tool in question. The engine handles it in two passes: the tool is created (or
updated) without its unresolved assistant destinations, then a linking pass
PATCHes the real destinations once every assistant exists.

Consequence worth knowing: on a push where the referenced assistant is not in
state, the tool's `destinations` are deliberately **not** sent by the main
create/update. They are set by the linking pass at the end of the same push,
once every assistant in the push has been applied. If you scope a push to
`--type tools` while the assistant is untracked, the destinations on the
dashboard are left as-is rather than cleared — the engine omits the key instead
of sending a partial array, because PATCH replaces whatever it receives.

The linking pass itself can still find an unresolved destination — not
"not yet applied this push" but genuinely absent from both state and the
local repo. In that case it does **not** send the raw slug. It **skips that
tool** and logs a warning naming the unresolved reference(s):

```
⚠️ Tool "<tool-id>" still references unresolved assistant destination(s): <slug>. Leaving this tool's destinations untouched on the platform — they will link on a future push once those assistants exist.
```

The rest of the push continues normally, and the destinations link
automatically on a later push once the assistant exists. Previously the
linking pass sent the raw slug straight through, and the API's `400
Assistant not found` aborted the whole push at the very end — after every
other resource had already applied.
Loading