Skip to main content

guides/developer/directives_runtime_contract.md

# Directives Runtime Contract

You need to modify runtime side effects (LLM/tool/embed/lifecycle) without breaking strategy semantics.

After this guide, you can add directive behavior while preserving correlation, retries, and signal contracts.

## Core Directives

- `Jido.AI.Directive.LLMStream`
- `Jido.AI.Directive.LLMGenerate`
- `Jido.AI.Directive.LLMEmbed`
- `Jido.AI.Directive.ToolExec`
- `Jido.AI.Directive.EmitToolError`
- `Jido.AI.Directive.EmitRequestError`

## Directive-To-Signal Contract Map

- `LLMStream` / `LLMGenerate` -> `ai.llm.delta`, `ai.llm.response`, `ai.usage`
- `LLMEmbed` -> `ai.embed.result`
- `ToolExec` -> `ai.tool.started`, `ai.tool.result`
- `EmitToolError` -> `ai.tool.result` (error payload)
- `EmitRequestError` -> `ai.request.error`

## Result Envelope Contract

For `ai.llm.response` and `ai.tool.result`, `data.result` should be treated as a canonical triple:

- `{:ok, payload, effects}`
- `{:error, reason, effects}`

Legacy 2-tuples may appear at boundaries but are normalized by runtime/policy helpers.

## Canonical Error Envelope

Runtime-emitted failures for `ai.llm.response` and `ai.tool.result` normalize to:

```elixir
%{
  type: atom(),
  message: String.t(),
  details: map(),
  retryable?: boolean()
}
```

Legacy error shapes may still enter at boundaries, but runtime helpers normalize
them before the signal leaves the runtime layer.

`details` is sanitized to JSON-safe values at this boundary so tuple/pid/ref terms
cannot break downstream envelope encoding.

## Tool Result Content Contract

For model follow-up turns, the canonical tool result semantics should be
represented in the content body:

- success: `%{ok: true, result: ...}`
- failure: `%{ok: false, error: %{type: ..., message: ..., details: ..., retryable?: ...}}`

Runtime may also preserve native outputs in metadata for adapters and local
tooling, but metadata is supplementary and should not be the only place the
result meaning exists.

## Contract Rules

- Directives describe work; they do not own strategy state transitions.
- Every side effect emits a matching signal with correlation IDs.
- Retry/timeout metadata must remain explicit in directive fields.
- Errors must resolve to structured signal payloads, not silent drops.

## Example: ToolExec Fields That Matter

```elixir
%Jido.AI.Directive.ToolExec{
  id: "tool_call_1",
  tool_name: "multiply",
  arguments: %{a: 2, b: 3},
  timeout_ms: 15_000,
  max_retries: 1,
  retry_backoff_ms: 200,
  request_id: "req_123",
  iteration: 2
}
```

`ToolExec.context` reserves one runtime-managed snapshot key for action execution:

- `:state` (canonical, core Jido-compatible)

This key is populated by strategy/runtime orchestration and overrides same-named values from user tool context.

## Failure Mode: Deadlock Waiting For Tool Result

Symptom:
- strategy remains in `:awaiting_tool`

Fix:
- ensure runtime always emits either `ai.tool.result` or `EmitToolError`
- preserve `id` correlation from tool call to result signal

## Contract Parity Tests

If you change directive fields or emitted signal payloads, update directive/runtime parity tests in the same change.

## Defaults You Should Know

- `LLM*` directives support either direct `model` or `model_alias`
- `ToolExec` retries default to `0` unless set
- metadata fields are designed for observability and debugging
- action-origin LLM telemetry shares the canonical `[:jido, :ai, :llm, ...]` namespace and is distinguished by metadata such as `origin` and `operation`

## When To Use / Not Use

Use this guide when:
- changing execution semantics, timeout policy, or signal emission behavior

Do not use this guide when:
- changing only strategy heuristics or prompts

## Next

- [Signals, Namespaces, Contracts](signals_namespaces_contracts.md)
- [Security And Validation](security_and_validation.md)
- [Error Model And Recovery](error_model_and_recovery.md)