Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ Version `0.1.x` contains the converged foundation layer:
- token-estimation helpers;
- compaction policy and deterministic compactor;
- context-budget estimation and non-blocking tokenizer access;
- message trimming.
- message trimming;
- provider-neutral LLM response, stream, and client contracts;
- loop configuration, lifecycle contexts, observer protocol, intervention
merging, and observer dispatch helpers.

The initial extraction is based on the already-merged integration branches:

Expand All @@ -26,11 +29,10 @@ The initial extraction is based on the already-merged integration branches:
Those revisions are provenance, not runtime dependencies. AgentCore tests and
builds without either product checkout.

The LLM call runtime and agent loop are the next migration slice. They remain
in the products until their remaining dependencies (`loop_types`, tool
execution, model profiles, retry classification, and runtime hooks) have a
product-neutral boundary. Moving those files before that boundary exists would
only hide product coupling inside this package.
The LLM call runtime and agent loop remain in the products until tool
execution, model profiles, retry classification, execution-context storage,
and runtime hooks have product-neutral boundaries. Moving those files before
that boundary exists would only hide product coupling inside this package.

## Repository boundary

Expand Down Expand Up @@ -106,8 +108,9 @@ edit both products' core copies, that is evidence it belongs here.

1. **Foundation** (this version): messages, token estimation, compaction,
context budget, trimming.
2. **Runtime contracts:** errors, LLM/tool protocols, loop types, execution
context, retry classification, and explicit product hooks.
2. **Runtime contracts** (in progress): LLM protocols and loop types are now
shared; errors, execution-context storage, retry classification, and
explicit product hooks remain.
3. **LLM runtime:** binding, calls, streaming, response normalization, runaway
recovery, and the public `llm_client` facade.
4. **Agent loop:** model/tool parsing, tool execution, and `agent_loop`.
Expand Down
4 changes: 4 additions & 0 deletions agent_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Product-neutral building blocks for Apodex agent runtimes."""

from agent_core.llm import LLMClient, LLMResponse, StreamDelta
from agent_core.messages import (
Message,
ToolCall,
Expand All @@ -10,7 +11,10 @@
)

__all__ = [
"LLMClient",
"LLMResponse",
"Message",
"StreamDelta",
"ToolCall",
"assistant_msg",
"system_msg",
Expand Down
92 changes: 92 additions & 0 deletions agent_core/llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""LLM client contracts — provider-agnostic chat completion interface."""

from __future__ import annotations

from collections.abc import AsyncIterator
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable

from agent_core.messages import Message, ToolCall


@dataclass
class LLMResponse:
"""One non-streaming completion result."""

content: Any = "" # str | list[dict]
tool_calls: list[ToolCall] = field(default_factory=list[ToolCall])
reasoning_content: str = ""
finish_reason: str = ""
model: str = ""
usage: dict[str, int] = field(default_factory=dict[str, int])
response_metadata: dict[str, Any] = field(default_factory=dict[str, Any])


@dataclass
class StreamDelta:
"""Incremental update during a streaming completion."""

content: str = ""
reasoning_content: str = ""
tool_call_deltas: list[dict[str, Any]] = field(default_factory=list[dict[str, Any]])
# Terminal metadata. Providers send these late in the stream — usage on a
# separate ``choices=[]`` chunk (OpenAI ``include_usage``), finish_reason on
# the last content chunk. Carried here so the stream assembler can put them
# on the final ``LLMResponse`` (else streaming usage/billing reads 0 and
# ``finish_reason="length"`` is invisible to truncation/rollback observers).
usage: dict[str, int] = field(default_factory=dict[str, int])
finish_reason: str = ""
model: str = ""
# Vendor label of the leg serving this stream, stamped by
# ``LLMFallbackChain.stream`` (constant once the chain commits to an
# entry — failover only fires before the first yield). The stream
# assembler folds it into ``LLMResponse.response_metadata`` so per-call
# billing attribution works for streamed calls too — without this the
# streaming path had no channel for the provider and every billing
# consumer read an empty vendor, which split one model's usage across
# a ``provider=""`` bucket and a named bucket.
provider: str = ""


@runtime_checkable
class LLMClient(Protocol):
"""Minimal async chat completion client."""

# Stays a settable attribute: LLMClient is not purely structural — concrete
# clients such as OpenAIClient subclass it and assign ``self.model`` in
# __init__, so a read-only property here would break them at runtime.
model: str

async def chat(
self,
messages: list[Message],
*,
tools: list[dict[str, Any]] | None = None,
temperature: float | None = None,
max_tokens: int | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | None = None,
) -> LLMResponse:
"""Send one non-streaming completion request. ``tools`` is a list
of OpenAI function-schema dicts; ``None`` runs without tools."""
...

def stream(
self,
messages: list[Message],
*,
tools: list[dict[str, Any]] | None = None,
temperature: float | None = None,
max_tokens: int | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | None = None,
) -> AsyncIterator[StreamDelta]:
"""Stream a completion as a sequence of incremental ``StreamDelta``s.

Terminal metadata is carried by late deltas; the consuming runtime is
responsible for assembling those deltas into its final response.
"""
...


__all__ = ["LLMClient", "LLMResponse", "StreamDelta"]
Loading