Skip to content

Release v0.1.0: production hardening#7

Merged
folathecoder merged 17 commits into
mainfrom
agentling/release/v0.1.0
Jul 7, 2026
Merged

Release v0.1.0: production hardening#7
folathecoder merged 17 commits into
mainfrom
agentling/release/v0.1.0

Conversation

@folathecoder

Copy link
Copy Markdown
Owner

Path A hardening (FOL-47) plus packaging for the first real release.

  • Public API + exception hierarchy (FOL-48)
  • AgentSession run-state isolation (FOL-49)
  • Model-output robustness (FOL-45 / FOL-53 status), timeouts + cancellation (FOL-50)
  • Tool execution hardening (FOL-51), memory hardening (FOL-52)
  • Events + terminal run status (FOL-53)
  • Packaging: ruff/mypy config, CI format-check + build, README/SECURITY/CONTRIBUTING/CHANGELOG, version 0.1.0

133 tests; ruff + mypy clean; uv build produces agentling-0.1.0.

Closes FOL-44.

Introduce errors.py with an AgentlingError base and domain subclasses, and
reparent ToolCallError under it so one except catches everything the framework
raises. Replace the placeholder __init__.py with real re-exports and __all__,
and read the version from installed package metadata so it cannot drift from
pyproject.
Move mutable run state (memory, the interrupt token, and dynamically loaded
skill tools) off Agent onto a new AgentSession, so one Agent can be built once
and shared safely across concurrent runs. Agent becomes config plus a factory:
agent.start() mints a session, and agent.run() is a one-shot convenience that
runs on a fresh session. Each session copies the base tool set, so a skill
loaded in one run never leaks into another.

Breaking: agent.memory -> session.memory, agent.interrupt() -> session.interrupt().
Migrate the affected tests and add session-isolation tests.
Route both the streaming and non-streaming paths through one
parse_tool_arguments that raises ModelOutputError on invalid JSON or a
non-object arguments payload, and reject a streamed tool call with no name.
The loop catches ModelOutputError, re-prompts the model with a correction up
to a small cap, then raises ModelError if it keeps failing. Previously a bad
JSON tool-call argument raised JSONDecodeError straight out of the loop and
killed the run.
Wrap tool.call in asyncio.wait_for (a per-tool Tool.timeout overrides the
agent's tool_timeout); a timed-out tool becomes a ToolTimeoutError observation
the model can recover from, while a genuine task cancellation propagates and
stops the run. Bound each model turn with asyncio.timeout (model_timeout) and
check the interrupt token between streamed chunks, so a long or hung stream can
be stopped without waiting for it to finish.
Run sync @tool functions via asyncio.to_thread so a blocking call cannot stall
the event loop. Add per-tool metadata (timeout, parallel_safe, max_output_chars)
through the @tool decorator, which now works bare or parameterized. Truncate
long tool observations head and tail (max_tool_output_chars). Redact unexpected
tool exceptions to the type only, logging the detail, when redact_errors is set,
while ToolCallError stays fully visible so the model can fix its arguments.
Validate tool names against provider naming constraints.
… wording

Validate the version and shape in Memory.from_dict, raising MemoryLoadError
instead of a raw KeyError. Accumulate token usage across turns and report the
run total on the terminal FinalEvent (Usage gains __add__). Add an optional
context_manager hook on Agent to trim or summarize the prompt before each
model call. Tag tool failures with an error_kind so an observation says "fix
the arguments" only for a validation error and "try a different approach"
otherwise.
FinalEvent gains a status field (completed / interrupted / max_steps) so a
stream consumer can tell how a run ended; failures still raise. Interrupt keeps
writing no FinalStep, so the resume contract holds and the distinction lives in
the event. print_events takes a file argument for testing and redirection, and
the module documents the event ordering guarantees.
Declare ruff and mypy configuration in pyproject and pin them as dev
dependencies so CI runs the same versions instead of fetching them ad hoc. CI
now also checks formatting and builds the package. Reformat the tree with ruff
format. Bump the version to 0.1.0.
Extend the README config reference with the timeout, redaction, truncation, and
context-manager knobs, refresh the dev commands, and add a security note for
skill-provided tools. Add SECURITY.md (trust model and reporting),
CONTRIBUTING.md, and CHANGELOG.md.
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Release v0.1.0: production hardening (sessions, robustness, packaging)

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Introduce stable public API exports and a unified exception hierarchy.
• Split immutable Agent config from per-run AgentSession for concurrency-safe runs.
• Harden model/tool execution with retries, timeouts, cancellation, and safer outputs.
• Add docs + CI/build packaging checks for the v0.1.0 release.
Diagram

graph TD
  Caller{{"Caller / App"}} --> Agent["Agent (config)" ] --> Session["AgentSession (run)" ] --> Model{{"Model"}}
  Session --> Tools[["Tools" ]]
  Session --> Memory[("Memory")]
  Session --> Events(["Event stream" ])

  subgraph Legend
    direction LR
    _ext{{"External / caller"}} ~~~ _cfg["Config object"] ~~~ _state[("Mutable state")] ~~~ _comp[["Component set"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Per-run cloning without an AgentSession type
  • ➕ Avoids introducing a new public type (AgentSession) to the API surface
  • ➕ Keeps the 'Agent is the thing you run' mental model simpler
  • ➖ Harder to support multi-turn/inspectable state and interruption cleanly
  • ➖ Makes isolation implicit (easy to regress) rather than explicit ownership
  • ➖ Still needs a structured place to put per-run tools/interrupt/memory
2. Use contextvars to store per-run state
  • ➕ Keeps Agent methods signature stable while still isolating state
  • ➕ Can simplify passing state through internal helper functions
  • ➖ More implicit behavior; harder to reason about in concurrency scenarios
  • ➖ Context propagation across tasks/threads can be surprising
  • ➖ Does not naturally model multi-turn sessions as a first-class object
3. Adopt a structured cancellation scope (nursery/task group) for runs
  • ➕ Cleaner semantics for interruption/cancellation across model + tools
  • ➕ Makes timeout and cancellation composition more uniform
  • ➖ More invasive refactor and potentially adds dependency/complexity
  • ➖ Current approach already provides bounded timeouts and propagating cancellation

Recommendation: Keep the PR’s Agent/AgentSession split: it makes concurrency safety and state ownership explicit, enables multi-turn/resume workflows, and cleanly isolates dynamically loaded skill tools. The timeout/cancellation and malformed-output recovery logic fits naturally into the session-runner and is well covered by the expanded tests.

Files changed (19) +2044 / -202

Enhancement (5) +552 / -97
__init__.pyDefine public API exports, __all__, and metadata-based version +89/-3

Define public API exports, all, and metadata-based version

• Replaces placeholder module with a curated public API surface (re-exports and __all__). Reads __version__ from installed package metadata with a source-tree fallback.

src/agentling/init.py

agent.pySplit Agent into config + AgentSession; harden run loop behavior +308/-62

Split Agent into config + AgentSession; harden run loop behavior

• Introduces AgentSession to hold per-run state (memory, interrupt, tool set) and makes Agent a concurrency-safe config/factory. Adds model timeouts and mid-stream interruption handling, malformed tool-call recovery with capped retries, cumulative usage reporting, tool execution timeouts with error typing, output truncation, optional error redaction, optional prompt context_manager hook, and sequential tool execution when any tool is not parallel_safe.

src/agentling/agent.py

errors.pyAdd framework-wide exception hierarchy rooted at AgentlingError +56/-0

Add framework-wide exception hierarchy rooted at AgentlingError

• Adds a consistent set of domain exceptions (model, tool execution/timeout, memory load, skill load, run lifecycle). Enables host applications to catch all framework errors via a single base type.

src/agentling/errors.py

events.pyAdd terminal run status and improve print_events output control +26/-10

Add terminal run status and improve print_events output control

• Extends FinalEvent with a status field (completed/interrupted/max_steps) and clarifies deterministic ordering. Updates print_events to support writing to an injected file handle rather than always stdout.

src/agentling/events.py

tools.pyAdd tool metadata, validate tool names, and run sync tools in threads +73/-22

Add tool metadata, validate tool names, and run sync tools in threads

• Extends Tool with timeout/parallel_safe/max_output_chars metadata and enhances @tool to accept those options. Validates function-derived tool names, makes ToolCallError part of the AgentlingError hierarchy, and ensures synchronous tools run via asyncio.to_thread to avoid blocking the event loop.

src/agentling/tools.py

Bug fix (2) +86 / -31
memory.pyHarden memory serialization and improve error observation hints +42/-10

Harden memory serialization and improve error observation hints

• Introduces MemoryLoadError and validates serialized memory shape/version. Adds error_kind to ToolResult and varies recovery hints so only validation errors instruct the model to retry arguments.

src/agentling/memory.py

models.pyCentralize tool-argument parsing and reject malformed streamed tool calls +44/-21

Centralize tool-argument parsing and reject malformed streamed tool calls

• Adds parse_tool_arguments that raises ModelOutputError on invalid/non-object JSON and uses it across streaming and non-streaming paths. Agglomeration now validates tool call name presence and argument parsing, enabling recoverable handling upstream.

src/agentling/models.py

Tests (6) +650 / -71
test_agent.pyMigrate to sessions and add extensive hardening/robustness tests +491/-40

Migrate to sessions and add extensive hardening/robustness tests

• Updates tests to use AgentSession for per-run state, and adds coverage for session isolation, malformed streamed tool calls recovery, tool/model timeouts, cancellation propagation, mid-stream interruption, sequential execution for non-parallel-safe tools, output truncation, error redaction, cumulative usage reporting, prompt context_manager, FinalEvent status, and print_events file output.

tests/test_agent.py

test_memory.pyAdd MemoryLoadError coverage and error_kind behavior tests +38/-3

Add MemoryLoadError coverage and error_kind behavior tests

• Updates error rendering tests to include error_kind semantics and adds validation for version/shape handling in Memory.from_dict, expecting MemoryLoadError for invalid payloads.

tests/test_memory.py

test_models.pyUpdate model parsing tests for ModelOutputError and stricter agglomeration +43/-10

Update model parsing tests for ModelOutputError and stricter agglomeration

• Switches expected exceptions from ValueError to ModelOutputError and adds tests ensuring agglomeration rejects invalid JSON, non-object arguments, and missing tool-call names.

tests/test_models.py

test_public_api.pyAdd public API surface tests for __all__ and version +30/-0

Add public API surface tests for all and version

• Validates that every symbol in agentling.__all__ is importable, that key re-exports point to the intended implementations, that ToolCallError is under AgentlingError, and that __version__ is non-empty.

tests/test_public_api.py

test_skills.pyMinor formatting adjustments in skills tests +3/-18

Minor formatting adjustments in skills tests

• Condenses long strings/paths for readability without changing test intent or coverage.

tests/test_skills.py

test_tools.pyAdd @tool metadata and tool-name validation tests +45/-0

Add @tool metadata and tool-name validation tests

• Adds unit tests asserting default metadata values, metadata override behavior, and rejection of invalid tool names.

tests/test_tools.py

Documentation (4) +733 / -0
CHANGELOG.mdAdd v0.1.0 changelog entry for first release +29/-0

Add v0.1.0 changelog entry for first release

• Introduces a Keep-a-Changelog formatted changelog documenting v0.1.0 features and hardening work, with a release link.

CHANGELOG.md

CONTRIBUTING.mdDocument local dev workflow and required checks +35/-0

Document local dev workflow and required checks

• Adds contributor guidance for uv-based setup and the exact CI-aligned commands for tests, lint, format check, type-check, and build verification.

CONTRIBUTING.md

README.mdAdd comprehensive README (usage, architecture, configuration) +633/-0

Add comprehensive README (usage, architecture, configuration)

• Adds end-to-end documentation including quickstart, tools, streaming events, sessions/concurrency, skills, memory persistence, interruption, provider model design, and configuration reference.

README.md

SECURITY.mdAdd security policy and trust model documentation +36/-0

Add security policy and trust model documentation

• Documents how to report vulnerabilities, clarifies trust boundaries for tools and skills, and provides guidance on error redaction and tool output bounding.

SECURITY.md

Other (2) +23 / -3
ci.ymlExpand CI with format check and package build verification +8/-2

Expand CI with format check and package build verification

• Updates CI to run ruff lint without --with, adds ruff format --check, runs mypy directly, and verifies distribution builds via uv build. This ensures formatting and packaging regressions are caught before release.

.github/workflows/ci.yml

pyproject.tomlBump version to 0.1.0 and add ruff/mypy configuration +15/-1

Bump version to 0.1.0 and add ruff/mypy configuration

• Updates project version to 0.1.0, adds ruff and mypy tool config, and includes ruff/mypy as dev dependencies to standardize local and CI checks.

pyproject.toml

Declare ruff and mypy configuration in pyproject and pin them as dev
dependencies so CI runs the same versions instead of fetching them ad hoc. CI
now also checks formatting and builds the package. Reformat the tree with ruff
format. Bump the version to 0.1.0.
@qodo-code-review

qodo-code-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. load_skill tool shadowing 🐞 Bug ≡ Correctness
Description
AgentSession registers the built-in load_skill tool via _register_tool(), which silently no-ops
if a tool with that name already exists, so a user-supplied load_skill tool can disable skill
loading without warning. The system prompt still instructs the model to call load_skill(name), so
affected runs become confusing and non-functional for skills.
Code

src/agentling/agent.py[R198-212]

+        # Per-session tool view. Copying the agent's base set keeps any skill
+        # tools loaded during this run isolated to this session.
+        self.tools: dict[str, Tool] = dict(agent._base_tools)
+        self._tool_schemas: list[ToolSpec] = list(agent._base_schemas)
+        if agent.skills:
+            self._register_tool(self._build_load_skill_tool())
+
    def _register_tool(self, new_tool: Tool) -> None:
-        """Add a tool to the live tool set, skipping names already registered.
+        """Add a tool to this session's tool set, skipping names already present.

        Registration is idempotent so a skill can be loaded more than once, or
-        declare a tool the agent already has, without raising mid-run.
+        declare a tool the session already has, without raising mid-run.
        """

        if new_tool.name in self.tools:
Evidence
The session copies user/base tools into self.tools before registering the built-in load_skill,
and _register_tool returns early on name collisions; meanwhile _skill_catalog appends
instructions explicitly telling the model to call load_skill(name).

src/agentling/agent.py[192-216]
src/agentling/agent.py[205-216]
src/agentling/agent.py[606-612]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `skills` are enabled, a user-provided tool named `load_skill` prevents the built-in skill loader from being registered because `_register_tool()` skips duplicates. This silently breaks skill loading while the prompt still advertises `load_skill(name)`.

## Issue Context
- Session init copies base tools, then conditionally registers the built-in `load_skill`.
- `_register_tool()` intentionally skips duplicates for idempotency, but for built-ins this should be a hard error to avoid silent feature loss.

## Fix Focus Areas
- src/agentling/agent.py[192-216]
- src/agentling/agent.py[205-216]
- src/agentling/agent.py[537-565]

### Suggested implementation direction
- If `agent.skills` is non-empty and `'load_skill' in agent._base_tools`, raise a clear `ValueError` (or `AgentRunError`) explaining that `load_skill` is reserved when skills are configured.
- Alternatively, register the built-in `load_skill` before user tools (and fail on collision), but the simplest/clearest is reserving the name when skills are enabled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Truncation limit edge-case ✓ Resolved 🐞 Bug ≡ Correctness
Description
_truncate_middle() fails to truncate when limit is 0 or 1 because keep = limit // 2 becomes 0
and text[-keep:] becomes text[0:] (the entire string). This defeats max_tool_output_chars /
max_output_chars and can unexpectedly feed large tool outputs into the model context.
Code

src/agentling/agent.py[R578-590]

+def _truncate_middle(text: str, limit: int) -> str:
+    """Trim the middle of an over-long string, keeping the head and tail.
+
+    Large tool outputs can flood the context window; keeping both ends preserves
+    the most useful parts (a summary at the top, a result at the bottom) while
+    bounding size.
+    """
+
+    if len(text) <= limit:
+        return text
+    keep = limit // 2
+    omitted = len(text) - 2 * keep
+    return f"{text[:keep]}\n... [{omitted} characters omitted] ...\n{text[-keep:]}"
Evidence
_truncate_middle computes keep = limit // 2 and uses text[-keep:], which returns the entire
string when keep is 0; _execute_tool applies this truncation whenever a max-output limit is set.

src/agentling/agent.py[522-530]
src/agentling/agent.py[578-590]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_truncate_middle()` uses `keep = limit // 2`, which becomes 0 for limits 0 or 1. In Python, `text[-0:]` equals `text[0:]`, so the function returns the full string (plus marker) instead of truncating.

## Issue Context
This helper is used to enforce `Agent(max_tool_output_chars=...)` and per-tool `max_output_chars` caps. With a tiny limit, the cap is effectively disabled.

## Fix Focus Areas
- src/agentling/agent.py[522-530]
- src/agentling/agent.py[578-590]

### Suggested implementation direction
- Handle tiny/invalid limits explicitly, e.g.:
 - if `limit <= 0`: return "" (or a fixed short marker)
 - if `limit == 1`: return `text[:1]`
 - else: proceed with middle truncation
- Additionally (optional but safer): validate `max_tool_output_chars` / `max_output_chars` at configuration time (Agent init and `@tool(...)`) to require `None` or an integer `>= 1`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/agentling/agent.py
Comment thread src/agentling/agent.py
agglomerate_deltas now rejects a streamed tool call with no id, as it already
did for a missing name, since an empty id breaks the tool-result round-trip.
Skill loading validates that `tools:` is a list of strings and requires exact
`---` fence lines, so a stray string or a leading `---` in the body is not
mistaken for frontmatter.
Extend the README config reference with the timeout, redaction, truncation, and
context-manager knobs, note the sync-tool timeout caveat, refresh the dev
commands, and add a security note for skill-provided tools. Add SECURITY.md,
CONTRIBUTING.md, and CHANGELOG.md.
agglomerate_deltas now rejects a streamed tool call with no id, as it already
did for a missing name, since an empty id breaks the tool-result round-trip.
Skill loading validates that `tools:` is a list of strings and requires exact
`---` fence lines, so a stray string or a leading `---` in the body is not
mistaken for frontmatter.
Correct the ModelOutputError docstring (now also a missing id), the event
ordering docstring (all tool-call events precede all tool-result events), the
malformed-call comment, and a test section label. Update the README with a
value-first opener, an alpha status note, real CI and PyPI badges, an errors.py
module row, the two-hint self-healing wording, a sync-tool timeout caveat, the
production knobs, and a Limitations section. Add SECURITY.md, CONTRIBUTING.md,
and CHANGELOG.md.
Reserve the load_skill tool name when skills are configured, so a caller tool
cannot silently shadow the built-in skill loader (raise at construction).
Fix _truncate_middle to cut the head for tiny limits (0 or 1), where keep=0 and
text[-0:] would otherwise return the whole string.
Six self-contained examples that progress from a basic tool agent to failure
recovery, memory replay, and a full event observer. The three offline examples
(failure_recovery, memory_chat, advanced_observer) run with a scripted model and
no API key; the API-backed three (math_tutor, repo_assistant, notes_agent) take
an injectable model so tests drive them offline too. Adds tests/test_examples.py
(no network), a README examples section with a feature matrix, a pytest
pythonpath so examples import, and extends CI's ruff/mypy to cover examples.
@folathecoder folathecoder merged commit 5b68478 into main Jul 7, 2026
2 checks passed
@folathecoder folathecoder deleted the agentling/release/v0.1.0 branch July 7, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant