FlowDrop Workflow Specification 1.0-draft

Normalizing a conversation makes it sendable to a provider

Stored history and provider history are not the same shape. Normalization is the one place that reconciles them, and it reports what it had to drop.

The rule

Normative: this is the rule
  1. A conversation-normalization node takes one message list and emits a normalized message list plus the number of entries dropped, applying four rules in order.
  2. (1) Each entry is coerced to the flat message shape a reasoner consumes; a non-list entry or one without a role is dropped, and content that is not a scalar is serialized to text rather than blanked, falling back to the empty string if it cannot be serialized.
  3. (2) system-role messages move to the front, with the system group and the remaining group each keeping their own relative order; a system message is never dropped.
  4. (3) Tool pairing is repaired by the same procedure a reasoner applies mid-loop: an unanswered call gets a synthetic interrupted result adjacent to it, an orphan or duplicate result is dropped, and a tool call id declared a second time is dropped from the later turn, so the first declaration owns the pairing.
  5. (4) Any message still leading the list with role tool is stripped, and because stripping it can leave a call unanswered, rule (3) is applied again; repair is idempotent.
  6. The reported drop count comes from the repair pass itself, never from a separate reimplementation of its rules.

What it means

The reported drop count has to come from the repair pass itself, never from a second count kept alongside it — the two can disagree on a case a reader would not think to try. A tool call id can be present in the buffer and still be unusable: "0" is a legal id string but a falsy value, and once the call carrying it is treated as unusable, the turn that declared it, the id itself, and the result answering it all have to go — one unusable id costing three drops, not one, from a buffer of two entries.

The other clause worth reading closely is what happens at the front of the list. A tool result placed before the assistant turn that declares its id is not caught by id-matching alone — the declaring call exists somewhere in the list, just not before its result — so a separate leading-orphan guard strips it. Stripping it un-answers the assistant turn it was answering, so tool pairing is applied again; the result is provider-sendable, not merely orphan-free.

Example

A call id that is technically declared but falsy:

A call id and its result, both spelled unusable
[
  { "role": "assistant", "content": "", "tool_calls": [{ "tool_call_id": "0", "name": "search" }] },
  { "role": "tool", "content": "result", "tool_call_id": "0" }
]
What normalization resolves it todropped
[]

A tool result appearing before the call that declares its id:

A result placed ahead of its declaring callmisordered
[
  { "role": "tool", "content": "out of order", "tool_call_id": "c1" },
  { "role": "assistant", "content": "", "tool_calls": [{ "tool_call_id": "c1", "name": "search" }] }
]
What normalization resolves it torepaired
[
  { "role": "assistant", "content": "", "tool_calls": [{ "tool_call_id": "c1", "name": "search" }] },
  { "role": "tool", "content": "Tool call was interrupted; no result.", "tool_call_id": "c1" }
]
Rule identifiers are permanent and are never renumbered. Each implementation publishes its own standing against these rules; this specification does not.spec 1.0-draft · MEM-5 · changed in spec 1.0