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
  • Typed events — a discriminated union of stream events, easy to serialize (e.g. to SSE)

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.

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

Stream events

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

run_started · message_created · text_delta · tool_started · tool_finished · run_finished · run_failed

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

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-0.2.0.tar.gz (34.5 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-0.2.0-py3-none-any.whl (33.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: kbws_forge_runtime-0.2.0.tar.gz
  • Upload date:
  • Size: 34.5 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-0.2.0.tar.gz
Algorithm Hash digest
SHA256 638984fbfb53509a405892e212fdf8f5318a85af5ed51351e21a0bf32a690516
MD5 81b6e2d7dbf7f38029c79a30b5ae6991
BLAKE2b-256 61e05e74233b58c4a09d2dfc823b27e56db18d51c211e5a59483a657171c6130

See more details on using hashes here.

File details

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

File metadata

  • Download URL: kbws_forge_runtime-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 33.2 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-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bcad104b3c729869d14a4be43ac64f66c073edbf3d1e4e2f9cfb3c1f5d94fc30
MD5 173c5d7672ff8d8cf2d2fffb3da5e14d
BLAKE2b-256 2812184a70719b782c18e7bd80acab374c6ed10dce5e28492d0f16ce4b8d737c

See more details on using hashes here.

Release history Release notifications | RSS feed

1.2.0

2 files

1.1.0

2 files

1.0.0

2 files

This release

0.2.0 This release

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