Skip to content

release: 0.10.3#330

Merged
declan-scale merged 119 commits intomainfrom
release-please--branches--main--changes--next
Apr 30, 2026
Merged

release: 0.10.3#330
declan-scale merged 119 commits intomainfrom
release-please--branches--main--changes--next

Conversation

@stainless-app
Copy link
Copy Markdown
Contributor

@stainless-app stainless-app Bot commented Apr 21, 2026

Automated Release PR

0.10.3 (2026-04-30)

Full Changelog: v0.10.2...v0.10.3

Features

Bug Fixes

  • adk: Always inject headers on execute activity (#337) (9d80e0b)
  • allow litellm security patch (#336) (c980948)
  • tests: repair test_streaming_model so all 28 tests run and pass (#334) (7e5e69c)
  • use correct field name format for multipart file arrays (bd6d362)

Performance Improvements

  • streaming: coalesce per-token publishes to Redis (50ms / 128-char window) (#333) (e6f11c4)

Chores

  • internal: more robust bootstrap script (f004301)

This pull request is managed by Stainless's GitHub App.

The semver version number is based on included commit messages. Alternatively, you can manually set the version number in the title of this pull request.

For a better experience, it is recommended to use either rebase-merge or squash-merge when merging this pull request.

🔗 Stainless website
📚 Read the docs
🙋 Reach out for help or questions

Greptile Summary

This release bundles several independent fixes and one performance feature: a CoalescingBuffer that batches per-token Redis publishes into 50 ms / 128-char windows, three execute_activity_method → execute_activity corrections so headers are always injected, a BaseHTTPMiddleware → pure ASGI swap that fixes streaming response buffering, and an AGENTEX_CUSTOM_HEADERS env-var feature.

  • The expanded header-injection scope in context_interceptor.py now fires logger.warning for every non-agentex activity that lacks _task_id, which will flood logs in multi-activity workflows.
  • send_message (sync + async) silently discards JSON-RPC error payloads when the server returns an error envelope, returning an empty result list instead.

Confidence Score: 4/5

Safe to merge with minor follow-up; no data loss or critical runtime failures introduced.

Both findings are P2: the warning log noise in the interceptor is annoying but not functionally breaking, and the silent error discard in send_message was a pre-existing gap now made slightly more likely to hit. All P0/P1 bugs (execute_activity fix, BaseHTTPMiddleware streaming fix, multipart field naming) are addressed with tests.

src/agentex/resources/agents.py (silent error swallowing in send_message) and src/agentex/lib/core/temporal/plugins/openai_agents/interceptors/context_interceptor.py (warning log level).

Important Files Changed

Filename Overview
src/agentex/lib/core/services/adk/streaming.py Major addition: CoalescingBuffer (50ms/128-char windowed batching), StreamingMode literal, and delta merge helpers; StreamingTaskMessageContext updated to dispatch through the new buffer.
src/agentex/resources/agents.py send_message (sync + async) refactored to consume a streaming response and reconstruct the final result; JSON-RPC error chunks are silently discarded with an empty task_messages list.
src/agentex/lib/core/temporal/plugins/openai_agents/interceptors/context_interceptor.py Header injection scope widened from model-only activities to all activities; logger.warning fires for every non-agentex workflow activity that lacks _task_id/_trace_id/_parent_span_id.
src/agentex/lib/sdk/fastacp/base/base_acp_server.py RequestIDMiddleware replaced from BaseHTTPMiddleware (which buffers streaming responses) to a pure ASGI middleware; fixes streaming response truncation.
src/agentex/_utils/_utils.py extract_files now accepts an array_format parameter and delegates suffix generation to _array_suffix; fixes bracket/indices/repeat/comma naming for multipart file arrays.
src/agentex/_client.py Added AGENTEX_CUSTOM_HEADERS env var support (newline-delimited key:value pairs) in both sync and async clients; env headers take precedence over default_headers argument.
src/agentex/lib/core/temporal/plugins/openai_agents/hooks/hooks.py Switched execute_activity_method to execute_activity for stream_lifecycle_content calls; ensures header injection by the context interceptor.
pyproject.toml Bumped httpx (0.27 → 0.28), litellm minimum (1.83.0 → 1.83.7 for security patch), pinned starlette>=0.49.1 and tornado>=6.5.5 explicitly, and relaxed fastapi upper bound.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[stream_update called] --> B{streaming_mode?}
    B -->|off| C[Feed accumulator only\nno publish]
    B -->|per_token| D[Publish immediately\nvia stream_update]
    B -->|coalesced| E[CoalescingBuffer.add]

    E --> F{first delta OR\nbuf_chars >= 128?}
    F -->|yes| G[Signal flush_event]
    F -->|no| H[Wait for ticker]

    G --> I[_run background task\nawakens immediately]
    H --> J[50ms timeout\nexpires]
    I --> K[_drain_locked:\nmerge consecutive same-channel deltas]
    J --> K
    K --> L[Publish merged batch\nvia on_flush]

    M[context.close] --> N[buffer.close:\ncancel ticker\ndrain remainder]
    N --> L
    L --> O[stream_update:\nStreamTaskMessageDone]
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/agentex/lib/core/temporal/plugins/openai_agents/interceptors/context_interceptor.py:780-781
**Warning log will flood for every non-agentex activity**

The previous code only ran this block for model-specific activities. Now it runs for every activity in every workflow. Any workflow that schedules a plain activity (signal handler, cleanup, etc.) without `_task_id`/`_trace_id`/`_parent_span_id` will emit a `WARNING` on every call. This will produce significant log noise in production and could make the warning meaningless when a genuine misconfiguration occurs.

Consider downgrading the "not found" branch to `logger.debug` so it doesn't drown out real issues.

### Issue 2 of 2
src/agentex/resources/agents.py:1139-1146
**JSON-RPC errors silently discarded**

When the server returns a JSON-RPC error response (e.g., `{"id": 1, "jsonrpc": "2.0", "error": {"code": -32600, "message": "..."}, "result": null}`), `SendMessageResponse.model_validate(chunk)` fails (non-optional `result: list[TaskMessage]` is absent), and `SendMessageStreamResponse.model_validate` succeeds with `result=None`. The loop finishes with `task_messages = []`, and the function returns a `SendMessageResponse` with an empty `result` and `error=None` — the server's error payload is never surfaced to the caller.

The same pattern appears in the sync path. Consider checking `chunk.get("error")` and raising (or preserving it) before the stream-parsing logic.

Reviews (113): Last reviewed commit: "release: 0.10.3" | Re-trigger Greptile

@stainless-app stainless-app Bot force-pushed the release-please--branches--main--changes--next branch 29 times, most recently from 78f998a to 48d5770 Compare April 24, 2026 07:20
@stainless-app
Copy link
Copy Markdown
Contributor Author

stainless-app Bot commented Apr 30, 2026

Release version edited manually

The Pull Request version has been manually set to 0.10.3 and will be used for the release.

If you instead want to use the version number 0.11.0 generated from conventional commits, just remove the label autorelease: custom version from this Pull Request.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 30, 2026

declan-scale and others added 5 commits April 30, 2026 15:59
…ar window) (#333)

* perf(streaming): coalesce per-token publishes to Redis (50ms / 128-char window)

Per-token Redis publishes from TemporalStreamingModel were adding ~45s
(56-62%) overhead to agent response latency, mostly from head-of-line
blocking on the model's event loop: each `await streaming_context.stream_update(...)`
inside the OpenAI stream `async for` paused token consumption until the
publish round-trip completed.

This change introduces a `CoalescingBuffer` driven by an `asyncio.Event`,
so the producer never awaits on Redis. Deltas are merged consecutive-only
(preserving character order in every (type, index) channel) and flushed
on a 50ms timer, on a 128-char size threshold, or immediately for the
first delta to keep perceived responsiveness high. The buffer's `close()`
drains remaining deltas before the DONE event, so consumers see the full
sequence in order.

A new `StreamingMode = Literal["off", "per_token", "coalesced"]` lives
in `streaming.py` as the single source of truth and is plumbed through
the adk streaming module, `StreamingService.streaming_task_message_context`,
and `StreamingTaskMessageContext`. Default is `"coalesced"` everywhere,
so all 13+ existing context callers (claude_agents, langgraph, litellm
provider, openai sync provider, etc.) benefit automatically.

* chore(streaming): fix import ordering (ruff I001)

* fix(streaming): address greptile review findings

- _run: when CancelledError is raised mid-flush in the for-loop, re-enqueue
  the in-flight item plus any remaining items in the local `drained` list
  back into self._buf so close()'s final drain can recover them. Previously
  the local `drained` list was unreachable after CancelledError exited the
  for-loop, causing the last coalesced batch to be silently dropped on
  close-during-flush races. Trade-off: the in-flight item may be duplicated
  on the consumer side (Redis pub may have completed before cancel was
  delivered), which is preferable to silent loss for streaming UX.

- _merge_pair: replace `return b` fallback with AssertionError. All six
  current TaskMessageDelta variants have explicit isinstance branches, so
  the fallback is unreachable today. But _can_merge returns True for any
  same-type pair, so adding a 7th delta variant without updating
  _merge_pair would silently drop `a`'s accumulated content. Asserting
  turns a future silent data-loss into an immediate, diagnosable crash.

* test(streaming): add coalescing-layer tests; loosen one model assertion

After merging the test-suite repair from main (#334) into this branch, one
model test (test_responses_api_streaming) regressed because its
assert_called_with strict-matched all kwargs of streaming_task_message_context
and didn't tolerate the new `streaming_mode='coalesced'` kwarg this PR
adds. Switched to assert_called() + targeted kwarg checks so the test
verifies what it cares about (task_id threading) without locking in
implementation details.

Replaced the ad-hoc smoke scripts that lived in conversation with a real
pytest module at tests/lib/core/services/adk/test_streaming.py covering:

- _delta_char_len, _can_merge, _merge_pair: per-channel correctness +
  None-handling
- _merge_consecutive: pure-text collapse, cross-channel order preservation,
  per-channel reconstruction matches per-token semantics
- CoalescingBuffer: first-delta-immediate flush within ~20ms,
  size-threshold flush before timer fires, multi-delta coalescing within
  one window, idle close, add-after-close no-op
- CoalescingBuffer cancel-during-flush regression test for the P1 fix:
  five queued chunks must all surface across publishes when close()
  cancels mid-flush (asserts substring presence rather than exact
  ordering, since the documented trade-off allows duplicates of the
  in-flight item)
- StreamingTaskMessageContext mode dispatch: "off" suppresses publishes
  but persists full content, "per_token" publishes each delta synchronously,
  "coalesced" batches and persists full content

* chore(streaming): route TemporalStreamingModel logger through make_logger

The model file used raw ``logging.getLogger("agentex.temporal.streaming")``,
which returns a logger with no handler attached and no level configured —
so the existing ``[TemporalStreamingModel] Initialized ... streaming_mode=...``
INFO log was silently dropped, making it impossible to verify at runtime
that a coalesced (or any) streaming mode was actually wired.

Switch to the SDK's ``make_logger`` helper (level=INFO, RichHandler in
local mode, StreamHandler otherwise) used everywhere else in the SDK.
The explicit logger name ``agentex.temporal.streaming`` is preserved so
any external logging configuration targeting that name keeps working.
@stainless-app
Copy link
Copy Markdown
Contributor Author

stainless-app Bot commented Apr 30, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants