refactor: extract LLM and loop runtime contracts - #1
Conversation
`deadline_remaining_s` accepts either a renewable-lease view exposing `remaining_s()` or a bare monotonic instant, and the probe is deliberately structural: importing the concrete lease type to run an `isinstance` check would pull a product dependency into this frozen contract module and break the `agent_core` import closure. The port compressed that reasoning away, leaving the next reader with an apparent cleanup opportunity that is actually a constraint. Restore it. Also export the three dispatch helpers a host must call — `notify_tool_call`, `notify_tool_result`, `drain_background_observers` — which `__all__` omitted while listing `notify_observers` and `merge_interventions`, and repair the README scope list punctuation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review notes — I checked all six review findings against Five are pre-existing defects ported verbatim, not caused by this refactor, so they are tracked separately rather than blocking this PR (which is an equivalence-preserving move; mixing behavioural fixes in would make the diff unreviewable):
#2 and #5 are live bugs in the products, not contract tidiness. A sixth finding claimed Addressed in this PR (8462a85 → latest), both items genuinely introduced by the port:
Plus a README list-punctuation repair (the new bullets were appended after a list item ending in a period). Re-verified after the change: |
There was a problem hiding this comment.
Pull request overview
This PR extracts and formalizes provider-neutral contracts for LLM clients/responses and agent-loop runtime/observer types, along with standalone tests and README updates to support ongoing migration into agent_core.
Changes:
- Added provider-agnostic
LLMClient,LLMResponse, andStreamDeltacontracts. - Added loop runtime contract types plus observer dispatch/merge helpers (
LoopConfig, contexts, interventions, observer protocol). - Added contract-focused tests for LLM and loop types; updated README migration notes.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
agent_core/llm.py |
Introduces provider-neutral LLM protocol + response/stream delta containers. |
agent_core/loop_types.py |
Adds loop contract datatypes and observer dispatch + intervention merge helpers. |
agent_core/__init__.py |
Re-exports new LLM contract types at package top-level. |
tests/test_llm.py |
Adds runtime-protocol and mutable-default regression tests for LLM contracts. |
tests/test_loop_types.py |
Adds tests for intervention merging, observer dispatch, tool hook semantics, and deadline helper. |
README.md |
Updates migration boundary documentation to reflect newly shared contracts. |
Suppressed comments (1)
agent_core/loop_types.py:492
merge_interventionscurrently treatsinject_messages=[]the same asNone(the truthiness check skips the empty list and the return converts an emptyall_messagesback toNone). That contradicts the contract note thatNoneand[]must not be conflated; if any observer explicitly requests an empty list, the merged intervention should preserve[].
for iv in interventions:
if iv.inject_messages:
all_messages.extend(iv.inject_messages)
if stop_reason is None and iv.stop_reason is not None:
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if not isinstance(deadline, (int, float)): | ||
| return None | ||
| return float(deadline) - time.monotonic() |
| """Stream a completion as a sequence of incremental ``StreamDelta``s. | ||
|
|
||
| The terminal ``LLMResponse`` (with assembled content + finalised | ||
| tool_calls + usage) is accessible via :meth:`last_response` after the | ||
| stream is exhausted. | ||
| """ |
| def test_no_inject_messages_stays_none_not_empty_list(): | ||
| """``None`` and ``[]`` must not be conflated: the loop treats a list as | ||
| 'inject this', so an empty list would be a request to inject nothing.""" | ||
| assert merge_interventions([Intervention(), Intervention()]).inject_messages is None |
Summary
LLMClient,LLMResponse, andStreamDeltacontractsValidation
This is the next slice after the foundation package consumed by ApodexHarness #502 and FrontierAgentInternal #93.