release: promote develop → master (v0.2.0)#21
Merged
Merged
Conversation
* ci: add auto-release workflow on merge to stable Tags stable with current version, creates GitHub Release, and bumps patch version on source branch. Supports bump:major and bump:minor PR labels for controlling version increment. * ci: split release workflow into bump-on-PR and tag-on-merge - bump-version-on-pr-to-stable: auto-bumps patch version on main/* when PR is opened if source version <= stable version - release-on-merge-to-stable: tags + creates GitHub Release only - For major/minor bumps, manually edit pyproject.toml before PR * chore: bump version to 0.1.2 * feat: add embedding support to LLM providers Add centralized embedding functionality to dana.common.llm: - EmbeddingResponse type, EmbeddingNotSupportedError exception - embed()/embed_batch() on OpenAI, Gemini, Azure providers - Embedder class with sync/async support, auto-provider selection - Providers without embedding (Anthropic, Moonshot) raise clear errors - Config: embedding_models per provider in config.json - Unit tests (18 tests) and live integration tests --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Bug: An Azure APIConnectionError exited the STAR loop after ~7.5min without any visible retry, despite the LLMCaller having retry logic. Three converging bugs were fixed: 1. LLMCaller retry was opt-in via fallback_providers, so the primary provider had no retry path. Now retry/backoff always runs; failover stays gated on fallback_providers. 2. _is_transient_error did not classify "Connection error." as transient (no matching keyword) and could not see openai.APIConnectionError buried in __cause__. Added "connection" to the keyword list and an exception-class-name walk over the __cause__ chain. 3. STAR loop exited on any exception. Added one bounded retry per iteration for transient LLM errors (sync + async paths), with full tracebacks via exc_info=True on all error sites. Observability: OpenAI/Azure/Moonshot providers now use a logging httpx.AsyncClient that emits "LLM HTTP request" per attempt, surfacing SDK-internal retries that were previously silent. New structured events: llm_retries_exhausted, STAR transient retry warnings, and an APIConnectionError-specific log line with traceback. Tests: updated test_no_fallbacks_exception_propagates (split into transient/permanent variants), added regression tests for the connection-error classification and __cause__ chain detection. Full unit + regression suites pass.
* feat(timeline): compression parity upgrades (P2+P3+P6)
Close three compression gaps vs OpenClaude while staying single-tier,
LLM-agnostic, KISS. Always uses len(str)/4 heuristic — zero provider
coupling. System+tools coverage via optional caller-supplied callbacks.
Phase 1 — Heuristic threshold (P3)
- New env knob DANA_COMPACT_TRIGGER_TOKENS (default 150000, clamp [8k, 2M])
- CompressedTimeline accepts optional system_tokens_fn / tools_tokens_fn
callbacks; folded into needs_compression() estimate
- star_agent passes len(system_prompt)//4 + len(json.dumps(tools))//4
Phase 2 — Cheap client-side shrink (P6)
- cheap_shrink_tool_results() stubs old tool_result bodies to
"[cleared for context budget]" preserving tool_call_id
- Predictive gate blocks mutation when savings insufficient (avoids
vacuous summary over stubs)
- Idempotent via content-equality (no metadata flag)
- Opt-in via enable_cheap_shrink_tool_results; off by default
Phase 3 — Reactive compact + circuit breaker (P2)
- PromptTooLongError typed exception; provider mapping for Anthropic
(invalid_request_error + "prompt is too long"), OpenAI-compat
(context_length_exceeded), Gemini (post-hoc WARNING on MAX_TOKENS)
- llm_caller._invoke_llm_sync/async wraps with PTL catch →
reactive_compact(attempt) → retry with 1s/3s backoff
- reactive_compact drops 5→10→20 oldest + _remove_forward_orphans +
full re-summary (no shrink-bypass)
- Per-session circuit breaker with cooldown recovery
(DANA_CIRCUIT_COOLDOWN_SECONDS, default 300s) + half-open probe
- Kill switch via DANA_DISABLE_REACTIVE_COMPACT=1
- star_agent._maybe_compress_timeline re-raises PTL explicitly
Phase 4 — Telemetry & polish
- CompressionLogFields TypedDict allowlist + new_compaction_id()
- AST-based test asserts log extra={} keys stay within allowlist
Known gaps (documented in review report, follow-up PRs):
- PTL retry closes over captured messages list; post-compact retry
re-sends stale oversized payload
- Recent huge tool_result cannot be reclaimed (shrink keep_recent
blocks it; reactive_compact drops oldest only)
- Multi-compression does not preserve prior summary text
- Stubbed content persisted across reload produces vacuous summaries
* fix(timeline): compression review blockers + snapshot persistence
Addresses the code review in
plans/reports/code-review-260420-1112-compression-parity-triggering.md.
Scope: two CRITICAL merge-blockers, one HIGH bug, and the user-requested
snapshot-based persistence. HIGH-2/HIGH-3 and MEDIUM/LOW items are
intentionally deferred.
CRITICAL-1 — stale messages on PTL retry (llm_caller):
_invoke_llm_sync/async closed over `messages`, so after reactive_compact
trimmed the timeline, retries re-sent the same oversized payload and the
circuit opened on genuinely-recoverable sessions. Added a
`messages_fn: Callable[[], list[LLMMessage]]` parameter threaded through
call_llm → _call_with_failover → _invoke_llm_sync/async. After each
reactive_compact, the factory is re-invoked so the retry observes the
compacted payload. star_agent passes a factory that re-calls
runtime.build_prompt. Backwards-compatible (parameter defaults to None).
CRITICAL-2 — unreclaimable giant tool_result:
Single huge tool_result in the keep-recent window couldn't be stubbed
(cheap_shrink skips recent) nor dropped (reactive_compact drops oldest),
wedging sessions after one big call. Fixed at ingest time:
maybe_dump_oversized_content writes bodies >50KB (env-tunable via
DANA_TOOL_RESULT_DUMP_THRESHOLD_CHARS) to
{session}/tool_results/{tool_call_id}.txt and replaces the timeline
content with a compact marker that preserves tool_call_id. A new
ToolResultDumpResource exposes a read_tool_result tool with
offset/limit slicing; auto-wired into STARAgent (opt-out via
DANA_DISABLE_TOOL_RESULT_DUMP_RESOURCE=1).
HIGH-1 — vacuous summaries on reload with stubs:
_format_entries_for_compression now emits
[Tool result id=X: previously cleared — content unavailable] instead
of feeding the literal [cleared for context budget] stub back into
the LLM, so re-summarization after reload is not dominated by
"the agent cleared tool results".
New — snapshot-based persistence (user requested):
timeline.json is frozen at the first compression. Each compression
rolls timeline-after-compress-{ISO-ts}.json; subsequent saves within
a generation update the same snapshot in place. Full audit retention
— older snapshots are never deleted. Repository read and the
serializer loader both prefer the newest snapshot and fall back to
timeline.json, then legacy path. Reload rehydrates the active
snapshot so a fresh process does not roll a new file on every save.
Tests: 4 new CRITICAL-1 tests (assert retry token count < first attempt),
14 new tool-result dump tests, 7 new snapshot persistence tests. Two
existing failover tests updated for the new messages_fn parameter. Full
suite: 1148 unit + 72 integration passing.
* fix(timeline): decouple context-window budget from compression trigger
`StarAgent` unconditionally aliased `max_context_tokens` as the compression
trigger, so any agent that set a context budget (e.g. `EnergyWasteAnalyst`
at 200k) transparently overrode `DANA_COMPACT_TRIGGER_TOKENS` — ops had no
reachable knob through the agent path.
Split the two concerns:
- `CompressedTimeline.__init__` gains a dedicated `max_context_tokens` kwarg.
Trigger resolution: explicit → env → 150k default. Budget resolution:
explicit → falls back to resolved trigger for callers that haven't split
yet. `cutoff_when_token_reach` stays pinned to the trigger.
- `StarAgent.__init__` gains `compress_trigger_tokens: int | None = None`
and threads the two knobs separately. `compress_trigger_tokens=None`
(default) defers to the env var, which is the intended ops contract.
Regression guards cover: budget set alone leaves trigger on env, env wins
when only the budget is explicit, explicit trigger still beats env, and
legacy single-knob callers still alias (backward compat).
* fix(timeline): resume via load_from_entries must not clobber timeline.json
After compression rolls a snapshot, resuming a session via
`CompressedTimeline.load_from_entries(entries)` (without native_messages)
skipped the snapshot-state rehydration that `read_since` does. The save
path then fell through to `session_folder / "timeline.json"` and
overwrote it with post-compression state on every turn — leaving the
snapshot file frozen and the canonical `timeline.json` polluted.
The Honeywell Django caller (agent_service.py) takes exactly this path:
reads entries via snapshot-aware `read_session_entries`, then calls
`load_from_entries(entries)` without the `native_messages` arg, so the
load-side rehydration in `_try_load_native_messages_from_repository`
never runs.
Fix in `_resolve_snapshot_write_path`: when no active snapshot is
tracked but the session folder already contains one or more
`timeline-after-compress-*.json`, adopt the newest as the write target
and stamp `_active_snapshot_compression_at` from its filename. Symmetric
with `LocalTimelineRepository.read_session_entries` which already
prefers newest snapshot for reads.
Regression test (`test_resume_via_load_from_entries_adopts_newest_snapshot_on_save`)
mirrors the Honeywell caller: compress, fresh timeline, load_from_entries,
add entry, save, assert write landed in the snapshot and timeline.json
stayed frozen.
* refactor(timeline): decouple compression from filesystem via repo.list_sessions (GH-1)
Timeline serializer now goes through the repository interface only — no
direct file I/O, no Path/glob, no _events_path access. Compaction mints
sibling logical sessions {base}__compact__{ISO-ts} instead of rolling
snapshot files, making the compression feature usable with any
TimelineRepositoryProtocol implementation (including in-memory and
remote repos, not just filesystem-backed).
- Add list_sessions(prefix) to TimelineRepositoryProtocol + local impl.
- Add TimelineRepositoryDefaultsMixin so external repos without
list_sessions get a no-op default (empty list → single-session fallback).
- Rewrite TimelineSerializerMixin (448→347 LOC) to use only save/
read_session_entries/list_sessions.
- Drop native_messages persistence — recomputed from entries on load.
- Rename CompressedTimeline state: _active_snapshot_path →
_active_compact_session_id, _active_snapshot_compression_at →
_active_compact_compression_at.
- Use microsecond precision in compact session timestamps to prevent
same-second collisions that would break audit retention.
- Add in-memory test fixture + parity test proving behavior matches
across local-fs and in-memory backends.
- Keep LocalTimelineRepository._resolve_timeline_file_for_read for
backward-compat reads of legacy timeline-after-compress-*.json.
Refs: GH-1
Plan: plans/260420-2141-GH-1-timeline-repository-compatibility/
Tests: 132/132 timeline+compression tests pass.
* fix(grep): auto-promote files_with_matches to content on single-file path
When SearchResource.grep receives a file path with the default
output_mode=files_with_matches, the engine returns only the bare path
— which LLM callers frequently misread as an empty result. Auto-promote
to content mode with an explanatory header note so the output actually
conveys what matched. Also refactors the AUTO-mode engine chain into a
for/break/else loop so the capture-then-prepend flow stays clean.
#9) * fix(llm): surface Azure/OpenAI gpt-5 thinking blocks via Responses API The OpenAI-compatible streaming wrapper listened for the event type "response.reasoning.delta", which the openai SDK never emits. Real events are response.reasoning_summary_text.delta and response.reasoning_text.delta, so every reasoning delta was silently dropped. With reasoning.summary unset by default, the API also wouldn't emit summary events at all, even when a reasoning model was reasoning internally. Separately, Azure unconditionally routed gpt-5/o3/o4 to /openai/responses, which returns HTTP 400 BadRequest for api-version < 2025-03-01-preview. Changes: - openai_compatible_base: handle the real reasoning event names; default reasoning.summary="auto" so summary deltas stream; add _responses_api_supported() hook to gate routing on endpoint capability. - azure: override _responses_api_supported() to require api-version date >= 2025-03-01, falling back to Chat Completions on older versions instead of crashing. - tests: 23 new routing cases (version gate, prefix matching, config-flag override) plus updated reasoning-delta test to assert real SDK event names. 115/115 unit tests pass. - scripts/verify-azure-thinking.py: live-Azure verification of streaming thinking chunks and non-streaming reasoning_tokens. * fix(llm): route gpt-5/o3/o4 chat() through Responses API Mirrors the routing already used by stream(). When _should_use_responses_api() is True (reasoning model + endpoint capability), non-streaming chat() now calls client.responses.create instead of client.chat.completions.create, and parses the heterogeneous output[] array to populate LLMResponse.reasoning_content with the model's reasoning summary text — previously always None on this path. Implementation: - chat() becomes a thin dispatcher with shared error handling. - _chat_via_chat_completions: existing path, extracted unchanged. - _chat_via_responses: new path. Reuses _convert_to_responses_input and _prepare_tools_for_responses helpers. Builds reasoning={"summary":"auto"} by default so summary text actually streams back. Maps response.status + incomplete_details.reason to a Chat-Completions-style finish_reason (stop/tool_calls/length/incomplete). Maps usage input/output_tokens to prompt/completion_tokens for caller compatibility. Constructs ChatCompletionMessageToolCall objects for function_call items so downstream parsers (response_parser._to_tool_call_dicts) see the same Pydantic shape regardless of API path. json_mode maps to text.format. Tests: - 11 new unit tests in test_chat_via_responses.py covering reasoning extraction, content extraction (skipping refusals), tool-call shape, finish_reason mapping (stop/tool_calls/length/incomplete), usage mapping, dispatch routing (both directions), and json_mode mapping. - 126/126 unit tests pass. Live Azure gpt-5.2: chat() now returns reasoning_content with 1044+ chars of summary text (was None). reasoning_tokens=455. Verified via scripts/verify-azure-thinking.py. * fix(llm): default reasoning.effort=medium for gpt-5/o3/o4 Without an explicit effort, gpt-5* sometimes skips reasoning entirely on a given call — leaving reasoning_content empty and reasoning summary deltas unfiring even with summary='auto'. Observed live with Azure gpt-5.2: identical requests yielded 0 thinking chunks on some calls and 200+ on others, purely model nondeterminism. The wrapper is the right place to set this: it already routes reasoning-model traffic to the Responses API, so it knows when reasoning is the expected mode. Callers can still override (e.g. effort='low' for cheaper turns). Both _chat_via_responses and _stream_responses now setdefault: effort = "medium" summary = "auto" (already there) Tests: 3 new cases covering default behavior, effort override, and summary override. 129/129 unit tests pass. Live verification (Azure gpt-5.2 via DanaCodingAgent): wrapper now deterministically returns 967 chars of reasoning_content. Tracking confirms the wrapper-side issue is fully fixed; remaining agent-side issue (star_agent.py:750-757 drops reasoning on direct-answer turns) is a separate concern not addressed here. Adds scripts/verify-thinking-persisted-via-coding-agent.py for end-to-end inspection of timeline persistence. * fix(agent): persist reasoning to timeline on direct-answer turns When the model answered without invoking a tool, _record_think_results took the no-tool-calls branch (star_agent.py:750) which only added AGENT_RESPONSE — silently dropping the parsed reasoning. The else branch (tool-calls path) already added AGENT_THOUGHTS for non-empty reasoning; the no-tool-calls branch was the asymmetric outlier. This dropped reasoning text on: - direct-answer turns (model solves the puzzle without tools) - the final turn of any tool-using session (after tools, model answers) Affects all reasoning surfaces routed through codec_with_native_tool_use: - LLMResponse.reasoning_content (gpt-5/o3/o4 via Responses API, DeepSeek-R1, future Anthropic extended thinking) - <thinking> XML tags - JSON {"reasoning": ...} fields Live verification (Azure gpt-5.2 via DanaCodingAgent): - direct scenario: 1373 chars reasoning persisted as AGENT_THOUGHTS (was 0) - tool scenario: AGENT_THOUGHTS entry now appears even on the final answer turn For non-reasoning models, parsed.reasoning is None, the new block is a no-op, behavior is unchanged. 129/129 unit tests pass. * feat(llm): env-driven reasoning.effort with low default Add OPENAI_THINKING_EFFORT and AZURE_THINKING_EFFORT env vars (plus generic LLM_REASONING_EFFORT fallback) so operators can dial reasoning cost/latency without code changes. Precedence: 1. caller's reasoning.effort kwarg 2. provider env var (AZURE_THINKING_EFFORT / OPENAI_THINKING_EFFORT) 3. LLM_REASONING_EFFORT 4. "low" (was "medium") Default flipped to "low" so callers opt into deeper reasoning rather than paying for medium implicitly. Invalid env values are logged and ignored. Applies to both _chat_via_responses and _stream_responses.
…#10) * feat(llm,timeline): cross-turn reasoning state replay (Option C, phases 1-4) Persist raw reasoning items from the OpenAI Responses API in TimelineEntry.metadata; on subsequent calls splice them back into input[] when provider:model:endpoint matches, so gpt-5+ retains structured reasoning state across turns instead of re-deriving it from flattened assistant text. Reduces token churn and improves reasoning continuity. Native-tool codec only. Provider capture (Phase 1): - LLMResponse.reasoning_items + response_id (raw output[] items) - _chat_via_responses opts in to include=reasoning.encrypted_content with sticky one-shot 400 fallback for accounts/api-versions that reject the flag - endpoint_hash + fingerprint properties on Azure / OpenAI providers (sha256 of base_url, first 8 chars) Agent persistence (Phase 2): - ParsedResponse carries reasoning_items + response_id; both codecs populate them - star_agent _record_think_results writes {reasoning_items, fingerprint, response_id} into AGENT_THOUGHTS metadata on the reasoning entry only (not the response/tool_call entries) Codec replay (Phase 3): - LLMMessage gains reasoning_items / reasoning_fingerprint / response_id carriers - Timeline.to_llm_messages propagates entry metadata onto assistant messages; merge step skips when items present so 1:1 fingerprint provenance is preserved - prepare_messages threads carriers via underscore-prefixed keys - _convert_to_responses_input splices raw items before assistant message when fingerprint matches the active provider; carriers always stripped before API call - LLM_REASONING_REPLAY=0 kill switch (default ON, env-resolved per call) Compression policy (Phase 4): - NativeMessage.to_llm_message propagates reasoning fields - closes a Phase 3 gap where CompressedTimeline-routed traffic silently dropped them - compression_engine.compress composes summary metadata from a single explicit dict so reasoning items never survive the boundary Tests: 49 new unit tests across capture, agent persistence, codec replay (match/mismatch/kill switch/tool-call ordering/multi-turn), and compression policy. 422/422 across LLM + core + compression suites green. * fix(llm): strip output-only fields from replayed reasoning items + stale-id fallback Surfaced by Phase 5 live verify (Azure gpt-5.2): replaying reasoning items captured via model_dump() caused HTTP 400 because output-side fields like 'status' aren't valid on input. Without a fallback, the rejection silently killed every subsequent turn (user_message saved, no agent_response). Two fixes: 1. _serialize_output_item now whitelists input-side fields only: type, id, summary, encrypted_content. No more model_dump() leak of output-only metadata into replayed input[]. 2. _chat_via_responses gains a one-shot stale-id / item-shape rejection fallback. On BadRequestError matching the rejection heuristic, strip reasoning items from input[] and retry once. Degrades to flat-text replay rather than failing the user's turn entirely. Verify script (scripts/verify-reasoning-replay.py) confirms: - replay fires when ON (3 fires across turns 2-4) - kill switch (LLM_REASONING_REPLAY=0) cleanly disables replay - reasoning_items accumulate cumulatively turn-over-turn - all 4 turns complete cleanly with replay enabled - 329/329 unit tests still green Findings recorded in plans/.../verify-260507-1905-reasoning-replay.md: replay does NOT save tokens in steady-state - model reasons more when prior state is present (~200-300 tokens per accumulated item). Real benefit is reasoning continuity, not cost. Cost benefit materializes when paired with ZDR/encrypted_content (smaller payload). * test(llm): cover disk-resume reasoning replay path User flagged the gap: Phase 5 verified in-process multi-turn replay, but did not exercise the fresh-process disk-load → resume → replay path. This commit closes it with both unit tests and a live verify. Findings: 1. The replay machinery works end-to-end across a process boundary. Persisted timeline.json round-trips through the legacy + native load formats, metadata survives, items splice into Responses API input[] when fingerprint matches. 2. DanaCodingAgent does NOT auto-resume from disk - each new process gets a fresh empty session. Resume requires explicit re-injection of persisted entries via agent._timeline.load_from_entries(...). Documented in the verify report. Tests (4): - Legacy-format disk resume → splice fires with persisted items - Native-format disk resume → NativeMessage.from_dict preserves metadata.reasoning_items end-to-end - Cross-provider fingerprint mismatch → no replay (carriers stripped) - Kill switch overrides matching fingerprint after resume Live verify (scripts/verify-disk-resume-replay.py): fresh process loaded a 3-item timeline, replayed all 3 into input[] on the next turn (1 splice fire), LLM call completed. 333/333 unit tests pass.
…+ tool-batch isolation (#11) * fix(timeline): persist reasoning items on empty-summary turns GPT-5/o3/o4 turns return a reasoning item (rs_… + encrypted_content) with an empty summary on low-summary turns — typically single-call tool continuations. The AGENT_THOUGHTS gate in _record_think_results keyed on summary text, so these turns produced no timeline entry and the encrypted reasoning item was silently dropped. On resume the affected turns replay with no reasoning state, breaking cross-turn reasoning continuity for GPT-5/o3/o4. Gate now keys on reasoning_items presence, not summary text. Entry is emitted with empty content when only the item is present; metadata still carries the item for replay. Add TestEmptySummaryReasoningPersistence covering tool-call and direct-answer branches plus the no-items negative case. * fix(tools): isolate tool-call failures within a batch A dispatch-phase exception (registry getattr, name parsing, object lookup) escaped before the inner try block in _execute_single_call and _execute_single_call_async. In the async path it propagated out of asyncio.gather, discarding the entire batch's results — including calls that succeeded. The TOOL_CALL entry still recorded N tool_call_ids, so the next OpenAI turn 400s on the unanswered tool calls. - Wrap each single-call dispatcher in one outer guard covering dispatch and execution; both are now non-raising. Removes the redundant inner try blocks. - execute_tools_async: asyncio.gather(return_exceptions=True) and convert any escaped exception to an error result — defense-in-depth. Every tool_call_id in a batch now always gets a result. Add isolation tests covering async and sync paths: a failing call yields an isolated error result, siblings succeed, tool_call_ids preserved.
… reload (#12) * feat(agent,timeline): subagent factory pattern + per-session timeline reload TaskResource dispatches to agent factories instead of shared instances. Each Task call constructs a fresh agent with a disjoint object graph (timeline, star loop count, session id, EventLog), eliminating both the concurrent-same-type state corruption and the sequential timeline- accumulation bug. Legacy instance registration still works but warns. set_session_id is now a real context boundary: it flushes the outgoing session, rebuilds the timeline via the extracted _build_timeline() (which resets all session-scoped state including compaction-tracking fields), and rehydrates from disk via the new CompressedTimeline.rehydrate() wrapper over read_since. Unknown session yields an empty timeline; same id is a no-op. - task_resource.py: factory registration, _descriptions cache, per-spawn construction, notifiable propagation at spawn; drop _get_agent_tools - star_agent.py: extract _build_timeline(); rewrite set_session_id - timeline_serializer.py: add CompressedTimeline.rehydrate() - dana_coding_agent.py: register explore subagent as functools.partial - tests: 9 TaskResource + 6 set_session_id unit tests * fix(resource): mark TaskResource session failed on aquery exception Previously a sub-agent whose aquery() raised left its session stuck at status='running', so task_output reported it running indefinitely. task() now catches the exception, records status='failed' plus the error, and re-raises so the caller still observes the failure. task_output surfaces the stored error for failed sessions. Addresses code-review finding N3. * refactor(agent): rename _compress_timeline attr to _compression_enabled The persisted config flag sat one line above _timeline with a near- identical name, reading as if it held a timeline. Rename to match CompressedTimeline.compression_enabled. Constructor param unchanged. * feat(agent): reload_timeline flag to opt out of per-session timeline reload set_session_id gains reload_timeline (default True). When False it is a pure relabel — the current in-memory timeline is kept and carried into the new session id instead of being flushed, rebuilt, and rehydrated. This lets a subclass that seeds its own timeline (e.g. a persona entry before the STAR loop) keep that timeline across a session switch. Gating only the rehydrate() call is insufficient — the _build_timeline() rebuild also discards the caller's timeline — so the flag gates the whole reload. Threaded through aquery, aquery_stream, and aconverse (-> Communicator. aconverse -> aquery). Default True everywhere preserves the session-boundary contract for subagents and existing callers; opt out with reload_timeline= False. Directed @agent messages keep the default. * feat(agent): default reload_timeline to False; TaskResource opts in to True Flip the reload_timeline default to False across set_session_id, aquery, aquery_stream, and aconverse. Most callers — including subclasses that seed their own timeline before the STAR loop — manage their own context, so a pure relabel is the safer default. TaskResource.task() now passes reload_timeline=True explicitly: a sub-agent's session_id is a hard context boundary, so each spawn gets a disjoint, disk-accurate timeline (fresh for a new session, rehydrated on resume). Tests exercising the reload path updated to pass reload_timeline=True. * refactor(agent): replace reload_timeline flag with public resume() reload_timeline was a flag argument threaded through aquery, aquery_stream, aconverse, and communicator.aconverse, toggling two distinct behaviors (pure relabel vs. flush+rebuild+rehydrate). Replace it with an explicit STARAgent.resume(session_id) wrapper over set_session_id(reload_timeline=True); the query methods now always relabel. TaskResource calls agent.resume() per spawn, then aquery(). set_session_id stays the internal primitive. * fix(timeline): map SUB_AGENT_RESPONSE with tool_call_id to role=tool SUB_AGENT_RESPONSE defaulted to role=assistant and dropped tool_call_id, leaving the prior assistant tool_calls unanswered on resume — OpenAI 400 "tool_call_ids did not have response messages". Now SUB_AGENT_RESPONSE with a tool_call_id normalizes to role=tool (native tool flow); without one it stays assistant (legacy XML flow). * fix(llm): drop redundant thought text when native reasoning replayed When an agent_thoughts entry carries reasoning_items and the provider fingerprint matches, _convert_to_responses_input spliced the native reasoning item AND re-emitted the same thought as an assistant message. The duplicate text is redundant with the item's summary+encrypted_content and trips Azure's invalid_prompt / Prompt Shield filter, which scans message-role content but not reasoning.summary. Blank the assistant content on the native-splice path (non-tool-call messages only); the fallback path keeps flat text for cross-provider replay. Fixes invalid_prompt rejection replaying atlas-q33 timeline.
…alogue (#14) `resume` is the only mechanism for multi-turn dialogue with a subagent, but the calling agent often skips it — spawning fresh subagents that lose all prior context. Promote it from one bullet in `Usage notes` to a dedicated `=== CRITICAL ===` section in the dynamic tool description, with an explicit NEW-vs-EXISTING decision rule. Expand the placeholder docstring's `resume:` arg from one line to a paragraph covering channel role, failure mode of omission, and when to legitimately omit. Pure docstring change — no behavioral or API impact.
Add OPENAI_USE_RESPONSES_API / AZURE_USE_RESPONSES_API (plus generic LLM_USE_RESPONSES_API fallback) to override Responses API routing without editing config.json. Precedence: provider env > generic env > use_responses_api config flag > model-prefix auto-detect. Resolved at call time so changes apply without restart. Explicit override bypasses the endpoint capability gate (mirrors config-flag semantics); warns when forced on against an unsupported endpoint (e.g. Azure api-version < 2025-03-01). Invalid values logged and ignored, falling through to auto-detect.
* docs(spec): design for injecting LLMProvider instance through STARAgent * docs(spec): use dedicated llm_provider_instance param, keep llm_provider as str * docs(spec): document runtime/LLMCaller sink mechanics and set_llm necessity * feat(rlm): accept injected LLM instance and add set_llm * feat(ltmemory): forward injected LLM to RLMResource and add set_llm * feat(agent): inject LLMProvider instance via llm_provider_instance + set_llm_provider * fix(agent): sync _llm_config on re-point and cover ltmemory sink in test - _apply_llm_provider now writes _llm_config alongside _llm_client so the lazy llm_client property cannot silently revert to the old provider - Add divergence-warning comment in ctor instance-handling block - Strengthen test_set_llm_provider_repoints_all_sinks: construct agent with ltmemory_path and assert Sink 3 (_ltmemory._rlm._llm.provider) * test(agent): cover injected-provider ltmemory path and legacy lazy regression * fix(agent): fan llm_client setter to ltmemory sink; cover set_llm_provider string path
Two unrelated groups, both pre-existing on develop: 1. tests/unit/core/test_inject_llm_provider.py (3 failures) Tests pass llm_provider="openai"/"anthropic" (string) which eagerly builds a real provider requiring OPENAI_API_KEY/ANTHROPIC_API_KEY. They only check identity wiring and never call the LLM. Add an autouse fixture that supplies dummy env keys so construction succeeds offline. 2. tests/unit/test_llm_providers.py::TestOpenAIReasoningTokens (3 failures) Thinking models (gpt-5*) route through the Responses API since #9/#15, but these tests still mocked client.chat.completions.create — so the awaited call hit an un-mocked MagicMock ("can't be awaited"). Rewrite to mock client.responses.create with a Responses-API-shaped response (output items + usage.output_tokens_details.reasoning_tokens) via a shared helper.
Drop the develop → main/<version> → stable stabilization layer (solo
maintainer; the ceremony isn't earning its keep). New release train is
develop → master, with master as the single release branch.
Changes:
- branch-policy.yml: allow only develop/hotfix → master (was stable ←
main/hotfix). Triggers on PRs to master.
- bump-version-on-pr-to-{stable→master}.yml: trigger on PR to master,
gate on head_ref == 'develop'. Compares develop vs master; auto-bumps
patch only if develop isn't already ahead (manual minor/major bumps
set on develop pass through).
- release-on-merge-to-{stable→master}.yml: trigger on merge to master,
checkout master, tag vX.Y.Z + GitHub Release targeting master.
- pr-lint-and-test.yml: trigger on [develop, master] (was
[develop, stable, main, main/**]).
- docs/branching-strategy.md: rewrite for the two-branch model.
- pyproject.toml: 0.1.3 → 0.2.0 (next release is a minor bump).
Follow-up (not in this PR): rename the stable branch → master via
`gh api .../branches/stable/rename`. The existing ruleset is keyed on
~DEFAULT_BRANCH so it auto-covers master without reconfiguration.
Orphaned asset: docs/assets/gitflow-vertical-branching.svg still depicts
the old 5-branch model and is no longer referenced — safe to delete later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release: develop → master (v0.2.0)
First release on the flattened
develop → mastergitflow. Replaces #18 (closed whenstablewas renamed →master). 14 commits, merge clean.Highlights
resumeemphasis (docs(task-resource): strengthenresumearg emphasis for subagent dialogue #14)Release machinery (landed on develop via #19, #20)
branch-policy/bump-version/release-on-mergenow targetmaster;pyproject→0.2.0Versioning
developpyproject =0.2.0,master=0.1.3→ develop is ahead, sobump-version-on-pr-to-masterwill not auto-bump (correct — this is an intentional minor).release-on-merge-to-mastertagsv0.2.0and creates the GitHub Release.Commits (14)
Pre-merge
check-branch-policygreen (develop → master allowed)lint-and-testgreen