ATL-2026-002ai_agent_experimentexperimental

Genkit Dart 0.16: when a tool result becomes part of the protocol

A reproducible comparison of the Genkit Dart 0.15.1 and 0.16.1 Tool contracts, including multipart responses, interrupts, Session boundaries, and the responsibilities that still belong to the application.

Published
September 7, 2026
Verified
September 3, 2026

Genkit Dart 0.16: when a tool result becomes part of the protocol

An agent calls a tool, receives a string, and continues. This makes a good README example: the model asks for the weather, a Dart function returns "sunny", and the next response uses that value. As long as the tool only reads data and its complete result fits into one value, very little else needs to be explained.

The boundary changes when a tool has to return text and an image, pause for human confirmation, or modify an external resource that must not be changed twice. A plain Future<Output> can no longer describe every relevant outcome. The caller needs to know whether the tool produced a response, interrupted the agent loop, or carried additional content alongside its structured output.

The Tool API redesign in Genkit Dart 0.16 matters at this boundary. The changelog describes it in one line: ToolResult plus multipart. The architectural consequence is larger than the line suggests.

A coordinated release line, but not a 1.0 contract

As of September 3, 2026, the current release is genkit 0.16.1. Its main provider and integration packages have matching releases: genkit_google_genai 0.3.1, genkit_anthropic 0.3.1, genkit_openai 0.4.1, genkit_middleware 0.6.1, and genkit_shelf 0.1.13. Each of these packages depends on genkit ^0.16.1 and declares the Dart SDK constraint ^3.10.0 (>=3.10.0 <4.0.0).

That is enough to establish a coherent dependency snapshot. It does not turn the API into a stable 1.0 surface. The original March 2026 announcement called Genkit Dart an early preview, the major version is still zero, and the release history continues to contain breaking changes.

The current release line is coherent enough for bounded, realistic experiments. An update still deserves more than an unattended dependency bump.

Before 0.16, a tool returned its declared value

On Genkit 0.15.1, a minimal tool can return its output directly:

final uppercase = ai.defineTool<String, String>(
  name: 'uppercase',
  description: 'Returns an uppercase string.',
  fn: (input, _) async => input.toUpperCase(),
);

final String output = await uppercase('dart');

The handler returns the declared Output, and invoking the Tool directly produces the same type. In the controlled experiment, the runtime type was String and the value was DART.

There is nothing inherently wrong with this contract. It accurately represents a function with one normal result. It becomes insufficient when the Tool also participates in the communication protocol of an agent loop.

On 0.16, the output is carried by a tool outcome

The equivalent 0.16.1 handler has a different return boundary:

final uppercase = ai.defineTool<String, String>(
  name: 'uppercase',
  description: 'Returns an uppercase string and multipart metadata.',
  fn: (input, _) => .response(
    input.toUpperCase(),
    parts: [TextPart(text: 'Transformed locally: $input')],
    metadata: const {'source': 'local-fixture'},
  ),
);

final ToolResult<String> result = await uppercase('dart');

The String remains the structured payload, but it is no longer the complete Tool result. A normal execution is represented by ToolResponseResult<String>. It can also contain multipart content and metadata. A different valid outcome is ToolInterruptResult<String>.

The difference was tested as a compilation contract rather than inferred only from documentation:

  • the 0.15.1 fixture returned a plain String;
  • the 0.16.1 fixture returned ToolResponseResult<String> with one TextPart and metadata;
  • .response(...) failed to compile on 0.15.1;
  • a handler returning a plain string failed to compile on 0.16.1 because it must now produce ToolResult<String>.

The contract is genuinely incompatible across the two releases. This is not an alternative spelling of the same operation.

Multipart does not erase the boundaries inside a response

Multipart can be understood as a way to place an image next to text. That is true, but it is not the most important distinction. A Tool can now produce at least three kinds of information:

  1. typed output consumed by application code;
  2. Part objects used in model-facing or communication content;
  3. metadata describing the execution.

Moving all three back into an unstructured map would discard most of the benefit. Application data, message content, and technical metadata would again share one accidental contract—only inside a larger object.

They also require different validation. An output schema can validate the shape of structured data. It cannot prove that media is safe to fetch, that metadata contains no private information, or that a text part should be trusted. A multipart response is more expressive precisely because its parts do not all mean the same thing.

Interrupt is a mechanism, not an authorization policy

A 0.16.1 Tool can return an interrupt instead of a normal response:

final approval = ai.defineTool<String, String>(
  name: 'approval',
  description: 'Interrupts before a synthetic side effect.',
  fn: (input, _) => .interrupt({'question': input}),
);

In the generation loop documented by the current README, Genkit returns control to the caller with FinishReason.interrupted. The application can continue in two ways. interruptRespond supplies the missing answer. interruptRestart asks Genkit to execute the tool again and can attach information about an approval or an environmental change.

That supports a human-in-the-loop workflow. It does not authenticate the human or make a repeated side effect safe.

The application still has to define:

  • who is allowed to approve the operation;
  • which execution the approval belongs to;
  • how long the approval remains valid;
  • whether the Tool can be executed again;
  • whether the first attempt may already have modified an external resource;
  • where the interrupted state survives until resumption.

interruptRestart deserves particular attention for a side-effecting Tool. If the Tool sends a message, creates a payment, or edits a document, restarting it is part of the business semantics. It needs an idempotency key, reconciliation, or another explicit duplicate-prevention mechanism.

An approval label does not create that mechanism.

Retry policy follows the side-effect contract

Genkit includes retry middleware with exponential backoff and jitter. In the current API, model failures are retried by default while Tool execution errors are not: retryModel is true, and retryTools is false.

The conservative default is useful, but the application still owns the decision. A failed weather lookup and a payment request can expose the same technical status while having completely different retry costs.

The policy should follow the Tool semantics:

  • a read-only query can often be retried after a transient failure;
  • an idempotent write needs a stable operation key and result verification;
  • an irreversible action may have to disable automatic retry;
  • an unknown outcome after a network break requires reconciliation rather than another blind call.

Genkit supplies the interception point. It cannot infer the meaning of the external operation.

Session is another lifetime, not the entire application state

The 0.15 release line introduced typed Agent State, a server-side Agent runtime, a transport-independent client, session and snapshot storage, and a file-backed SessionStore. In the 0.16.1 API, Session<State> tracks messages, custom state, and artifacts. Its sessionId correlates traces across agent turns.

Four lifetimes now need to remain visible:

Object What completes it
Model call one provider response or error
Agent turn one cycle of reasoning and Tool calls
Genkit Session the retained conversation history, state, and artifacts
Product operation a business outcome such as save, search, or approval

A prototype can make all four lifetimes coincide. One screen sends one prompt, receives one answer, and discards the history. A continuing conversation or an external side effect quickly breaks that assumption.

Session state is not automatically the source of truth for an order, profile, or document. It stores the context of an agent interaction. Domain state can change independently through another user, a backend process, or an operation that outlives the current turn. Storing both under one vague idea of "state" creates an implicit synchronization protocol and gives the model a potentially stale view of the product.

The changelog line that needed a code check

The 0.16.1 changelog says that a context parameter was added to session-store operations. A natural migration assumption is that a custom SessionStore must adopt new method signatures when moving from 0.15.1.

The published package archives did not support that interpretation. In both 0.15.1 and 0.16.1, getSnapshot and saveSnapshot already accept Map<String, dynamic>? context. Implementations with the same signatures compiled in both fixtures and received the synthetic tenant context.

This does not prove that the changelog is wrong. The change may concern context propagation inside the runtime, additional store operations, or related tests. It does mean that the public difference between the two selected versions cannot be described as the first appearance of those parameters.

Reviewing a release therefore needs a small experiment. An official source remains primary evidence, but a short sentence should not be expanded beyond what the code reproduces.

The ArkTelos boundary

The ArkTelos approach keeps the lifecycle and terminal result of a business operation outside the Tool. In ArkTelos terminology, a use case owns that operation, while the Tool acts as an adapter for one bounded capability. It translates the agent protocol into a product command and returns only the information the agent is allowed to receive.

The responsibilities remain distinct:

  • the use case protects business invariants and owns the operation outcome;
  • the Tool defines the capability exposed to the agent;
  • the Genkit Session stores the context of agent turns;
  • the provider adapter calls the external model;
  • the application owns authorization, approval, retry, and compensation.

This experiment does not establish an ArkTelos integration with Genkit. No ArkTelos package was used. The distinction matters because a more expressive Tool API should not become an accidental owner of every neighboring concern.

A bounded adoption decision

For a new prototype, 0.16.1 is a reasonable point for investigation: the current provider packages resolve together, multipart and interrupt outcomes are explicit, and the contract can be explored without calling a hosted model.

An existing application should perform at least four checks before updating:

  1. find every Tool handler that returns a plain output;
  2. decide where parts, metadata, or interrupts are actually required;
  3. review side-effecting Tools for duplicate execution and unknown outcomes;
  4. compile custom SessionStore and middleware implementations instead of relying only on the release notes.

Applications that already allow agents to modify external systems need more than a successful build. They require tests of the complete generation loop, session persistence, interrupt/resume behavior, and provider failures. None of those was exercised by this compile-only experiment.

The evidence supports a narrow conclusion: Genkit 0.16.1 gives Tool execution a more expressive protocol result. It does not own the meaning of the business operation, the authority to perform it, or the cost of repeating it. Those boundaries still belong to the application.

Sources and reproduction


ArkTelos Lab: website | Telegram EN | Telegram RU

ArkTelos: website | Telegram EN | Telegram RU

Result boundaries

  • The result is limited to the pinned Genkit Dart 0.15.1 and 0.16.1 package contracts and must be rechecked after dependency updates.
  • A successful compile-only migration does not establish production readiness, provider compatibility, safe retries, or interrupt recovery after process failure.

CODE / DATA / AGENTS

Related experiments

experimentgenkit-016-contract-boundaries

Reproduce the migration-relevant Tool result boundary between Genkit Dart 0.15.1 and 0.16.1 without a hosted model.

The experiment invokes Tools directly and does not reproduce a complete model and tool loop. No hosted model, provider plugin behavior, persistence failure, latency, cost, or output quality was tested.

Open the experiment record →

Telegram edition

Read in ArkTelos Lab

Read in ArkTelos Lab ↗

ARKTELOS CHANNELS

News and engineering material, without mixing languages.

The main channels cover ArkTelos development. ArkTelos Lab publishes architecture analysis, experiments, and reproducible research.