Skip to content

Commit d4db523

Browse files
committed
release: v0.5.2 — hardened no-tools recovery + release-workflow guard
PyPI 0.5.1 was published from the pre-review commit (the first v0.5.1 tag push), and PyPI versions are immutable, so the corrected build can't reuse 0.5.1. Ship the review-hardened code from main as 0.5.2. - openai.py: recovery stream that errors after emitting text now preserves the streamed text in its terminal DONE result (review finding #2). - RELEASE_NOTES.md: the agent-layer nudge is a user-role message, not a system message (review finding #1); add v0.5.2 notes. - version 0.5.2 across pyproject.toml, src/hive/__init__.py, uv.lock; CHANGELOG.md and docs/changelog.md 0.5.2 entries (folds in Phase 3, already on main). - .github/workflows/release.yml: skip publishing when the version already exists on PyPI, so a moved/re-pushed tag can't trigger a conflicting re-upload. - tests: recovery-stream-error-preserves-streamed-text.
1 parent b5485f7 commit d4db523

9 files changed

Lines changed: 114 additions & 7 deletions

File tree

.github/workflows/release.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,20 @@ jobs:
2121
- name: Build package
2222
run: uv build
2323

24+
- name: Check whether this version is already on PyPI
25+
id: pypi_check
26+
run: |
27+
version="$(grep -m1 -E '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/')"
28+
echo "version=$version"
29+
if curl -sf -o /dev/null "https://pypi.org/pypi/hive-agent/$version/json"; then
30+
echo "hive-agent $version is already on PyPI -- skipping publish (a moved/re-pushed tag can't re-upload an immutable version)."
31+
echo "exists=true" >> "$GITHUB_OUTPUT"
32+
else
33+
echo "exists=false" >> "$GITHUB_OUTPUT"
34+
fi
35+
2436
- name: Publish to PyPI
37+
if: steps.pypi_check.outputs.exists == 'false'
2538
run: uv publish --token "$PYPI_TOKEN" --check-url https://pypi.org/simple/
2639
env:
2740
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## [0.5.2] — 2026-05-31
4+
5+
### Fixed
6+
- **Hardened no-tools recovery** (review follow-up to 0.5.1) — streaming recovery now fires only when the `tool_use_failed` occurs *before* any text reaches the caller (no duplicated output), and a recovery stream that errors after emitting text preserves that text in its terminal result. A `tool_use_failed` on a request that *did* offer tools is no longer swallowed. The agent-layer wrap-up nudge and the adapter's text-only recovery nudge are now `user`-role messages (some strict providers reject mid-thread `system` messages); the agent nudge is sent only for the wrap-up call and isn't written to the logged conversation.
7+
8+
### Added
9+
- **Phase 3 simulation core** — registry-driven world catalogs (events & jobs) and wired simulation feedback loops.
10+
11+
### CI
12+
- Release workflow skips publishing when the version already exists on PyPI, so moving or re-pushing a tag can't trigger a conflicting re-upload.
13+
314
## [0.5.1] — 2026-05-31
415

516
### Fixed

RELEASE_NOTES.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
# Hive v0.5.2 Release Notes
2+
3+
## Headline: Hardened No-Tools Recovery
4+
5+
A follow-up to v0.5.1 that incorporates review feedback on the no-tools recovery
6+
path, plus the Phase 3 simulation core that landed on `main`.
7+
8+
## What's Fixed
9+
10+
- **Streaming recovery is scoped to pre-content failures**: the OpenAI-compatible
11+
adapter now recovers a `tool_use_failed` only when it happens *before* any text has
12+
reached the caller, so a mid-stream error can never produce duplicated output. If a
13+
recovery stream errors after emitting text, the terminal result preserves the text
14+
already streamed.
15+
- **Recovery no longer masks real errors**: a `tool_use_failed` on a request that
16+
*did* offer tools (e.g. a malformed tool schema) is surfaced instead of being
17+
swallowed by the recovery retry.
18+
- **No mid-thread system messages**: both the agent-layer wrap-up nudge and the
19+
adapter's text-only recovery nudge are sent as `user`-role messages, which strict
20+
providers accept mid-conversation (some reject mid-thread `system` messages). The
21+
agent nudge is sent only for the wrap-up call and is not written to the logged
22+
conversation.
23+
24+
## Also Included
25+
26+
- **Phase 3 simulation core**: registry-driven world catalogs (events & jobs) and
27+
wired simulation feedback loops.
28+
29+
## Upgrade
30+
31+
```bash
32+
pip install --upgrade hive-agent
33+
```
34+
35+
No code changes required. The happy path (tools provided, or no error) is unchanged.
36+
37+
---
38+
139
# Hive v0.5.1 Release Notes
240

341
## Headline: Resilient No-Tools Wrap-Up Across Strict Providers
@@ -17,8 +55,9 @@ even though the tools that ran during the loop already persisted.
1755
of raising -- the tools already ran. Covers both `generate_with_metadata` and
1856
`generate_stream`.
1957
- **Belt-and-suspenders nudge** at the agent layer: `Agent.run_once` now appends a
20-
"tool budget exhausted, reply in plain text" system message before the final
21-
wrap-up call, reducing the chance of hitting the error at all.
58+
"tool budget exhausted, reply in plain text" user message before the final
59+
wrap-up call, reducing the chance of hitting the error at all. (A user-role message
60+
is used deliberately -- some strict providers reject mid-thread system messages.)
2261

2362
A multi-action turn (e.g. "make three notes") now completes on Groq without surfacing
2463
a 400.

docs/changelog.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## [0.5.2] -- 2026-05-31
4+
5+
### Fixed
6+
- **Hardened no-tools recovery** (review follow-up to 0.5.1): streaming recovery now fires only when the `tool_use_failed` occurs *before* any text reaches the caller (no duplicated output), and a recovery stream that errors after emitting text preserves that text in its terminal result. A `tool_use_failed` on a request that *did* offer tools is no longer swallowed. The agent-layer wrap-up nudge and the adapter's text-only recovery nudge are now `user`-role messages (some strict providers reject mid-thread `system` messages); the agent nudge is sent only for the wrap-up call and isn't written to the logged conversation.
7+
8+
### Added
9+
- **Phase 3 simulation core**: registry-driven world catalogs (events & jobs) and wired simulation feedback loops.
10+
11+
### CI
12+
- Release workflow skips publishing when the version already exists on PyPI, so moving or re-pushing a tag can't trigger a conflicting re-upload.
13+
314
## [0.5.1] -- 2026-05-31
415

516
### Fixed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "hive-agent"
3-
version = "0.5.1"
3+
version = "0.5.2"
44
description = "Local-first agent OS. Spawn persistent AI agents that collaborate, write code, and use tools autonomously."
55
authors = [{ name = "chiruu12" }]
66
license = { text = "MIT" }

src/hive/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Hive - Autonomous agent OS."""
22

3-
__version__ = "0.5.1"
3+
__version__ = "0.5.2"
44

55
from hive.agents.existence import ExistenceLoop
66
from hive.agents.goal_strategy import Goal, GoalContext, GoalStrategy

src/hive/models/openai.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,19 +299,25 @@ async def generate_stream(
299299
"stream": True,
300300
"stream_options": {"include_usage": True},
301301
}
302+
recovered_text: list[str] = []
302303
try:
303304
stream = await self._client.chat.completions.create(**recovery_kwargs)
304305
async for event in self._consume_stream(stream, t0):
306+
if event.type == StreamEventType.TEXT and event.text:
307+
recovered_text.append(event.text)
305308
yield event
306309
except Exception:
307310
logger.warning(
308-
"No-tools recovery stream for model %s failed; returning empty text",
311+
"No-tools recovery stream for model %s failed; returning streamed text so far",
309312
self._model,
310313
)
314+
# If the recovery stream errored mid-flight, _consume_stream never emitted its
315+
# terminal DONE -- synthesize one carrying whatever text already reached the
316+
# caller, so consumers reconstructing from DONE.result don't lose it.
311317
yield StreamEvent(
312318
type=StreamEventType.DONE,
313319
result=GenerateResult(
314-
message=Message.assistant(""),
320+
message=Message.assistant("".join(recovered_text)),
315321
model=self._model,
316322
duration_ms=int((time.time() - t0) * 1000),
317323
),

tests/models/test_tool_use_recovery.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,33 @@ async def fake_create(**kwargs: Any) -> Any:
175175
assert done.type == StreamEventType.DONE
176176
assert done.result.message.content == ""
177177

178+
@pytest.mark.asyncio
179+
async def test_recovery_stream_error_preserves_streamed_text(self) -> None:
180+
# Double failure: initial no-tools call rejects, then the recovery stream emits
181+
# some text and errors mid-flight. The terminal DONE must carry the text already
182+
# streamed (not empty), so consumers reconstructing from DONE.result don't lose it.
183+
p = OpenAI(api_key="sk-test")
184+
185+
async def fake_create(**kwargs: Any) -> Any:
186+
if not _has_nudge(kwargs["messages"]):
187+
raise _ToolUseFailedError()
188+
189+
async def gen() -> Any:
190+
yield _stream_chunk(content="Saved the ")
191+
yield _stream_chunk(content="notes")
192+
raise _ToolUseFailedError()
193+
194+
return gen()
195+
196+
p._client.chat.completions.create = fake_create
197+
events = await _collect(p.generate_stream([Message.user("make three notes")], None))
198+
199+
texts = [e.text for e in events if e.type == StreamEventType.TEXT]
200+
assert texts == ["Saved the ", "notes"]
201+
done = events[-1]
202+
assert done.type == StreamEventType.DONE
203+
assert done.result.message.content == "Saved the notes"
204+
178205
@pytest.mark.asyncio
179206
async def test_error_after_text_propagates_without_duplicate(self) -> None:
180207
# If the stream fails *after* text already reached the caller, recovery would

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)