Skip to content

Intent AI client: stream the upstream call, send adaptive thinking, raise the max-tokens default - #6959

Merged
delchev merged 1 commit into
masterfrom
issue-6955-ai-streaming
Aug 27, 2026
Merged

Intent AI client: stream the upstream call, send adaptive thinking, raise the max-tokens default#6959
delchev merged 1 commit into
masterfrom
issue-6955-ai-streaming

Conversation

@delchev

@delchev delchev commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #6955.

Three compounding limits in the one shared AI client (components/engine/engine-intent/.../ai/ModelClient.java), all of which tightened exactly as the authored application grew:

  1. The call was not streamed - one blocking request whose 120-second window had to contain the entire generated document.
  2. No reasoning pass - requestBody() sent no thinking parameter, and on the configured default model (claude-opus-4-8) omitting it means running with no thinking at all (adaptive must be sent explicitly on the 4.7/4.8 family; the old budget_tokens form is rejected with a 400). A few hundred lines of structured YAML were emitted cold.
  3. The output ceiling - DIRIGIBLE_INTENT_AI_MAX_TOKENS defaulted to 8192, against a tool contract that re-emits the COMPLETE app.intent every turn and every repair round.

Fixed in that order, deliberately: without streaming, a larger budget or a longer reasoning pass converts a truncated answer into a timed-out one.

What changed

  • Streaming. "stream": true, consumed as server-sent events. assembleReply concatenates the text_deltas as the answer and re-joins the matching tool_use block's input_json_delta fragments into the proposal - matched by the block's index, never "the last tool_use seen", so a fragment of another block can never corrupt it. Thinking deltas, ping and unknown event types are skipped (the API's versioning policy is explicit that new ones may appear). The per-request timeout becomes an outer bound (10 min) rather than a window the answer has to fit into.
  • thinking: {"type": "adaptive"}, with the display default (omitted) left alone - the reasoning is wanted for the answer's sake, not for anyone to read.
  • _MAX_TOKENS default 8192 -> 32768. The environment variable is unchanged; only the fallback moved. The default model is deliberately left alone - with thinking sent explicitly, reasoning no longer depends on which model is configured.

Both assistant surfaces (the Intent Editor agent and the Workbench Java assistant) go through this one client, so all three changes reach both.

Error contract

Unchanged (412 AssistantNotConfiguredException / 502 AssistantUpstreamException), including a mid-stream error event - an overload the API reports inside a 200 response - mapping to the same AssistantUpstreamException a non-2xx status does. Two new diagnostics for causes that are otherwise invisible from the symptom: a proposal cut off mid-JSON says so and names the ceiling, and a stop_reason: "max_tokens" is logged.

Tests

ModelClientStreamTest (new, network-free) scripts a local SSE upstream:

  • the text and the proposal assembled from many narrow fragments, with a thinking block, a comment line, a ping and an unknown event type interleaved;
  • the request body actually carrying stream: true, thinking.type == adaptive and the configured ceiling (plus the raised default itself);
  • a chunked answer flushed event by event with the connection held open in between - the shape that replaces the single buffered read;
  • a fragment of an interleaved other tool_use block never landing in the proposal;
  • a mid-stream error event, a proposal truncated mid-JSON, a non-2xx status, and a blank API key.

IntentBuilderShellIT's stub now answers with the event stream, splitting the tool input across several fragments on purpose - a stub that sent it in one piece would pass even if the client only ever read the first one. Its client-side agent timeout, which was 7 minutes because it multiplied the old 120-second wall by three calls, is raised accordingly.

Green locally: commons-config (10) + engine-intent (934) unit tests, formatter:validate, javadoc under -P release, and tests-integrations compiles.

🤖 Generated with Claude Code

…6955)

Three limits compounded in the one shared AI client, and they tightened
exactly as the authored application grew:

1. The call was not streamed - a single blocking request whose 120-second
   window had to contain the entire generated document.
2. No `thinking` parameter was sent. On the configured default model
   (`claude-opus-4-8`) omitting it means running with NO thinking at all
   (adaptive must be sent explicitly on the 4.7/4.8 family; the old
   `budget_tokens` form is rejected with a 400), so a few hundred lines of
   structured YAML were emitted cold.
3. `DIRIGIBLE_INTENT_AI_MAX_TOKENS` defaulted to 8192, against a tool
   contract that re-emits the COMPLETE `app.intent` every turn AND every
   repair round.

Fixed in that order, deliberately: without streaming, a larger budget or a
longer reasoning pass turns a truncated answer into a timed-out one.

- `"stream": true`, consumed as server-sent events. `assembleReply`
  concatenates the `text_delta`s as the answer and re-joins the matching
  `tool_use` block's `input_json_delta` fragments into the proposal -
  matched by the block's `index`, never "the last tool_use seen", so a
  fragment of another block can never corrupt it. Thinking deltas, `ping`
  and unknown event types are skipped: the API's versioning policy says new
  ones may appear. The per-request timeout becomes an outer bound (10 min)
  rather than a window the answer must fit into.
- `thinking: {"type": "adaptive"}`, with the display default left alone -
  the reasoning is wanted for the answer's sake, not for anyone to read.
- The `_MAX_TOKENS` default moves to 32768. The environment variable is
  unchanged; only the fallback moved. The default model is left alone: with
  thinking sent explicitly, reasoning no longer depends on it.

The 412/502 error contract is unchanged, including a mid-stream `error`
event (an overload the API reports inside a 200 response) mapping to the
same `AssistantUpstreamException` a non-2xx status does. A proposal cut off
mid-JSON now says so and names the ceiling, and a `stop_reason: "max_tokens"`
is logged - neither cause is visible from either symptom otherwise.

Both assistant surfaces (the Intent Editor agent and the Workbench Java
assistant) go through this one client, so all three changes reach both.

`ModelClientStreamTest` scripts a local SSE upstream: the assembly, the
request body, a chunked answer flushed event by event, the interleaved-block
case, a mid-stream error, truncation and a non-2xx status. The Builder
shell's `IntentBuilderShellIT` stub now streams its proposal in narrow
fragments for the same reason, and its client-side agent timeout - which was
7 minutes because it multiplied the old 120-second wall by three calls - is
raised accordingly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@delchev
delchev merged commit 653c9f6 into master Aug 27, 2026
10 checks passed
@delchev
delchev deleted the issue-6955-ai-streaming branch August 27, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Intent AI client: stream the upstream call, send adaptive thinking, raise the max-tokens default

1 participant