Skip to main content

kbws-forge-runtime

PyPI version Python versions License

A framework-agnostic runtime for building coding agents in Python. It manages agents, sessions and chat flows on top of LangGraph, while keeping the core API clean, typed and free of web-framework coupling.

Highlights

  • AgentRuntime — register agents, create sessions, run blocking or streaming chats with plugin hooks
  • Composable prompts — code-first prompt blocks (Prompt/Message/compose) with automatic session-history injection
  • Model middlewares — deepagents-style hooks (before_model/after_model/wrap_model_call) around every model call
  • Structured output — pass any pydantic schema, get parsed results in ChatResult.parsed / RunFinished.parsed
  • Agent directory convention — one agent = one directory (agent.py + prompts.py + tools.py), auto-discovered by load_agents
  • Workflow builders — chat, sequence, parallel and loop graphs over a typed state
  • Tools — local tools, MCP (stdio/SSE), and SKILL.md skill loading
  • Execution policies — enforce deadlines, cancellation, call/token/cost budgets, concurrency and tool permissions
  • Typed action events — ordered model/tool/run events with timing and usage metadata, ready for SSE or a trace UI

Install

pip install kbws-forge-runtime
# optional integrations
pip install "kbws-forge-runtime[mcp]"      # MCP tool loading
pip install "kbws-forge-runtime[openai]"   # OpenAI-compatible chat models

Requires Python ≥ 3.13.

v1.0.0: the public API (AgentRuntime, prompts, middlewares, structured output, workflow builders, tools, plugins) is stable.

Quick start

import asyncio

from langchain_openai import ChatOpenAI

from kbws_forge_runtime import AgentInfo, AgentRuntime
from kbws_forge_runtime.workflow import build_chat_graph


async def main() -> None:
    model = ChatOpenAI(model="deepseek-chat", api_key="sk-...")

    runtime = AgentRuntime()
    runtime.register_agent(
        AgentInfo(agent_id="assistant", name="Assistant"),
        build_chat_graph(model, instruction="You are a helpful assistant."),
    )

    # blocking chat — sessions and memory are handled for you
    result = await runtime.chat("assistant", "user-1", "Hello!")
    print(result.content)

    # streaming chat — typed events
    async for event in runtime.chat_stream("assistant", "user-1", "Tell me more"):
        print(event.type, event)


asyncio.run(main())

Core API

AgentRuntime

Method Description
register_agent(info, graph) Register an agent (any object satisfying the ChatGraph protocol)
list_agents() Registered agents as AgentInfo
create_session(agent_id, user_id) Create a session; idempotent per (agent, user)
get_session(session_id) Look up a session
chat(agent_id, user_id, message, session_id=None, variables=None) Blocking chat, returns ChatResult
chat_stream(...) Async iterator of ChatEvents
chat_parts(agent_id, user_id, parts, ...) Chat with structured content parts
cancel(run_id, reason="run cancelled") Cancel an active run
active_run_ids() IDs of runs active in this runtime process

Execution policies

Policies can be configured as the runtime default or supplied per call. Defaults are unlimited, so existing applications keep their current behavior.

from kbws_forge_runtime import AgentRuntime, RunPolicy, ToolPolicy

policy = RunPolicy(
    timeout_seconds=30,
    max_model_calls=6,
    max_tool_calls=10,
    max_total_tokens=20_000,
    max_concurrency=4,
    tool_policy=ToolPolicy(
        allowed_tools={"search", "read_file"},
        approval_required={"read_file"},
    ),
)

runtime = AgentRuntime(default_policy=policy, tool_approval_handler=approve_tool)
result = await runtime.chat("assistant", "user-1", "Investigate this issue.")

Model and tool retries are disabled by default. Tool retries additionally require the tool name in ToolPolicy.retryable_tools, preventing accidental retries of side-effecting operations. Token and cost budgets fail closed when the provider does not expose the required usage metadata; pass usage_resolver to normalize a provider-specific response.

Graphs created by build_chat_graph (including Agent.build_graph) route model calls, tool calls and structured-output repair calls through ModelExecutor / ToolExecutor. Custom ChatGraph implementations must use those executors themselves when they need call budgets, retries or tool-policy enforcement; runtime-level timeout and cancellation still apply to the whole graph. InMemoryEventSink is available for tests and local trace viewers; implement EventSink.emit(event) to export events elsewhere. Sink failures are logged and isolated so observability outages cannot fail an agent run.

Stream events

All events share run_id / agent_id / session_id and a discriminated type:

run_started · message_created · model_started · text_delta · model_finished · model_failed · tool_started · tool_finished · tool_failed · run_finished · run_failed · run_cancelled

Events also carry event_id, UTC created_at and a run-local sequence. Action events include call/parent IDs and duration; terminal events include model/tool call counts and accumulated usage. These fields form the data contract for a future ADK-style run/evaluation UI.

Composable prompts

Prompts are plain Python objects — compose, partially apply, reuse:

from kbws_forge_runtime.prompts import Message, Prompt, compose

persona = Prompt(name="persona", messages=[Message.system("You are a {role}.")])
task = Prompt(name="task", messages=[Message.human("Weekly data:\n{data}")])

weekly = compose(persona, task, name="weekly", extra_messages=[Message.history()])

# pass variables per call; history is injected at the placeholder automatically
result = await runtime.chat(
    "assistant", "user-1", "Write the report.",
    variables={"role": "engineering lead", "data": "...git stats..."},
)

build_chat_graph accepts a plain str, a composable Prompt, or any langchain chat prompt template — so full flexibility (few-shot, example selectors, …) is one import away.

Model middlewares

Intercept every model call (deepagents-style):

from kbws_forge_runtime.middleware import (
    AppendSystemContextMiddleware,
    CallCountMiddleware,
    LoggingMiddleware,
)

runtime.register_agent(
    AgentInfo(agent_id="assistant", name="Assistant"),
    build_chat_graph(
        model,
        instruction="...",
        middleware=[
            LoggingMiddleware(),
            AppendSystemContextMiddleware("Reply in Chinese."),
        ],
    ),
)

Implement your own via ModelMiddleware (before_model / after_model / wrap_model_call).

Structured output

Pass any pydantic schema; the model answers via a tool call whose arguments are parsed into the schema:

from pydantic import BaseModel, Field


class Scientist(BaseModel):
    name: str = Field(description="全名")
    birth_year: int = Field(description="出生年份")
    fields: list[str] = Field(description="研究领域")


result = await runtime.chat("sci", "u1", "介绍牛顿")
assert isinstance(result.parsed, Scientist)   # ChatResult.parsed
# streaming: RunFinished.parsed

Schemas are supplied by the caller — nothing is hard-coded; nested models, lists and enums all work. Structured output coexists with regular tools and model middlewares (via Agent(middleware=..., output_schema=...) too).

Agent directory convention

from kbws_forge_runtime import AgentRuntime
from kbws_forge_runtime.agent import load_agents

runtime = AgentRuntime()
await load_agents("agents", runtime, model_factory=lambda: model)

Any directory under agents/ containing an agent.py that exports an agent object is registered automatically. Directories without one (e.g. shared/) are skipped:

agents/
├── weekly-report/
│   ├── agent.py       # agent = Agent(agent_id=..., prompt=..., tools=...)
│   ├── prompts.py     # Prompt components
│   └── tools.py       # this agent's tools
└── shared/            # shared components, not loaded as an agent

An Agent aggregates its prompt, tools, MCP config and skills, and builds its own graph:

from kbws_forge_runtime.agent import Agent

agent = Agent(
    agent_id="weekly-report",
    name="Weekly Report",
    prompt=weekly,
    tools=[git_summary, jira_stats],
    mcp=[StdioMcpServer(name="jira", command="jira-mcp", args=[])],
)

Workflow builders

build_chat_graph(model, *, instruction, tools=(), checkpointer=None) builds a single chat agent (with a tool loop when tools are given). build_sequence, build_parallel and build_loop compose multiple agents over a typed WorkflowState. Memory is on by default (InMemorySaver) keyed by session_id; pass your own langgraph saver to persist elsewhere.

Tools, MCP & skills

from kbws_forge_runtime.tools import McpToolLoader, SkillLoader, ToolBox, tool

@tool
def current_time() -> str:
    """Return the current UTC time in ISO 8601 format."""
    return datetime.now(UTC).isoformat()

tools = ToolBox([current_time])
tools.extend(await McpToolLoader([StdioMcpServer(name="db", command="mcp-db", args=[]) ]).load())

Plugins

from kbws_forge_runtime.plugins import LoggingPlugin, Plugin

runtime = AgentRuntime(plugins=[LoggingPlugin()])

Plugin hooks: on_user_message · before_agent · on_event · after_agent · on_error.

Errors

Exceptions inherit ForgeRuntimeError and carry a stable code:

E0001 agent not found · 0002 session not found · 0003 session ownership · 0004 illegal parameter · 0005 run error · 0006 MCP config · 0007 cancelled · 0008 timeout · 0009 budget exceeded · 0010 tool denied · 0011 approval required · 0012 usage unavailable

Development

uv sync --all-extras
uv run pytest packages/forge-runtime/tests          # unit + API (fake models)
RUN_REAL_PROVIDER_TESTS=1 uv run pytest packages/forge-runtime/tests  # + real LLM calls

License

MIT License

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kbws_forge_runtime-1.2.0.tar.gz (64.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kbws_forge_runtime-1.2.0-py3-none-any.whl (57.4 kB view details)

Uploaded Python 3

File details

Details for the file kbws_forge_runtime-1.2.0.tar.gz.

File metadata

  • Download URL: kbws_forge_runtime-1.2.0.tar.gz
  • Upload date:
  • Size: 64.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for kbws_forge_runtime-1.2.0.tar.gz
Algorithm Hash digest
SHA256 0437b9aa01af8a84c1d9d84eb0c9d11c1f25697bc30ab44535be581ff68b4a7f
MD5 b95288233f1f9433e0841777c446211e
BLAKE2b-256 df91e252d57cf2f26ab65416437778aae05e2dd5c0325a518832803cbc2f2254

See more details on using hashes here.

File details

Details for the file kbws_forge_runtime-1.2.0-py3-none-any.whl.

File metadata

  • Download URL: kbws_forge_runtime-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 57.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for kbws_forge_runtime-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2c2b561c4e7adf715594ef7791a5cc3e8ff747086e0906f9acd523a4eb82dc8b
MD5 2aebc28fe2ff64f6e9c2e2164a91611a
BLAKE2b-256 505787b20f7359da07d9e20733c3cb1eed65f3b695456dc6b2000a2cb0c9880b

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 files

1.1.0

2 files

1.0.0

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page