Skip to content

feat: AgentKit — FastAPI-style ergonomics layer (0.4.0)#39

Merged
ebarti merged 5 commits into
mainfrom
feat/agentkit
Jul 2, 2026
Merged

feat: AgentKit — FastAPI-style ergonomics layer (0.4.0)#39
ebarti merged 5 commits into
mainfrom
feat/agentkit

Conversation

@ebarti

@ebarti ebarti commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

A FastAPI-flavored ergonomics layer over the existing runtime API — zero new dependencies, everything additive on the 0.3.0 surface.

from dataclasses import dataclass
from agent_runtime_kit import AgentKit

@dataclass
class RepoSummary:
    name: str
    languages: list[str]

async with AgentKit() as kit:
    result = await kit.run(
        "claude",                     # alias; "claude-agent-sdk" works too
        goal="Summarize this repository",
        permissions="strict",         # literal -> real enum member
        output_type=RepoSummary,      # type -> wire schema -> typed .parsed
    )
    print(result.parsed.languages if result.parsed else result.error)

Commits (each green: pytest, mypy --strict, ruff)

  1. PermissionProfile literal coercionmode="strict" now coerces to the real enum member in __post_init__. Previously the bare string silently matched none of the adapters' is-identity checks and the run proceeded at DEFAULT posture; unknown values now raise with the valid vocabulary.
  2. _schema.py + OutputTypeError — stdlib bridge between Python types and JSON schema: dataclasses, TypedDict, Literal, str-Enums, X | None, list[X], dict[str, X]. Fail-closed on anything else; strict parsing with $-path breadcrumbs; Pydantic v2 models used via their own model_json_schema/model_validate through a structural check (no import, no dependency).
  3. AgentKit + ParsedResult[T] — kwargs-native run() assembling the frozen AgentTask (prebuilt task= passes through), KIND_ALIASES for the built-in kinds, per-kind runtime cache closed by aclose()/async with, and output_type= returning ParsedResult[T] (a runtime-identical AgentResult subclass; AgentResult stays non-generic so downstream disallow_any_generics users are unaffected). Non-conforming payloads yield finish_reason="failed" — the adapters' own structured-output convention — never an exception.
  4. @kit.on(...) + @kit.runtime(...) — sync/async event handlers tee'd alongside the task's own sink (per-handler exception swallowing, the safe_emit contract), and decorator registration for third-party kinds.
  5. Docs + 0.4.0 — quickstart/README lead with the hub, api-stability pins the posture (hub is sugar, aliases bounded, output_type subset documented and fail-closed).

Test delta

232 → 258 tests (test_schema.py, test_kit.py, coercion tests in test_core.py). No existing test, adapter, or protocol changed.

ebarti added 5 commits July 2, 2026 21:43
PermissionProfile(mode="strict") previously stored the bare string: it
type-checks against downstream strictness only, and every adapter gates
posture with identity checks (mode is PermissionMode.STRICT), so the
string silently matched nothing and the run proceeded at DEFAULT
posture — a quiet permission loosening.

mode/filesystem now coerce to real enum members in __post_init__ (the
enums are str subclasses, so lookup-by-value is native); unknown values
raise ValueError listing the valid vocabulary. Enum members pass
through untouched, preserving all identity comparisons.
…osed

json_schema_for(tp) and parse_as(tp, value) cover a bounded subset —
scalars, X | None, list[X], dict[str, X], Literal, str-valued Enums,
dataclasses (additionalProperties: false, required = fields without
defaults), TypedDict — and raise the new OutputTypeError on anything
else rather than emitting a half-true schema. Parsing is strict (no
cross-type coercion, extra keys rejected, bool never satisfies int)
with $-path breadcrumbs in every error.

Types shaped like Pydantic v2 models (model_json_schema +
model_validate) are delegated to those methods via a structural check:
users who have Pydantic get its full coverage, the core gains no
dependency. OutputTypeError is exported top-level.
…utput

kit = AgentKit() wraps the registry (fake + vendor adapters registered;
adapters stay lazy so no extra is required) behind a FastAPI-flavored
surface:

- await kit.run("claude", goal=..., permissions="strict") assembles
  the frozen AgentTask internally; every task field is a keyword
  argument, permissions/filesystem accept string literals (coerced to
  real enum members), and a prebuilt task= passes through for advanced
  use. Short aliases for the built-in kinds live in one documented
  KIND_ALIASES dict; exact kind strings keep working.
- output_type=SomeType derives the wire schema via the stdlib bridge
  (or the type's own model_json_schema) and validates parsed_output
  back into the type. Non-conforming payloads yield a
  finish_reason="failed" result — the adapters' own convention for
  unsatisfied structured output — never an exception. The return is
  ParsedResult[T], a runtime-identical AgentResult subclass adding the
  typed .parsed accessor, so AgentResult itself stays non-generic and
  bare annotations survive downstream disallow_any_generics.
- Hub-resolved runtimes are constructed zero-arg, cached per kind, and
  closed by aclose()/async with; instances passed directly to run()
  remain caller-managed.
…ator

@kit.on("agent.tool.completed") (or bare @kit.on() for everything)
attaches sync or async handlers to every task the hub assembles,
including task= passthroughs, via a tee sink that composes with the
user's own event_sink. Failures are swallowed per the safe_emit
contract — independently per handler, so one bad handler starves
neither the others nor the downstream sink.

@kit.runtime("x-myorg-agent") registers a factory in decorator form;
factories stay zero-arg constructible so availability/capabilities
diagnostics keep working.
Quickstart and README now lead with the AgentKit surface (kwargs-native
run, string-literal permissions, output_type=) while keeping the
standalone-adapter path documented for vendor-specific configuration.
api-stability.md pins the compatibility posture: the hub is sugar over
the same task/result objects, aliases are bounded to KIND_ALIASES, and
output_type supports a documented subset that fails closed.

Everything in 0.4.0 is additive on the 0.3.0 surface.
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