All notable changes to SwiftAgentX are documented here. This project adheres to Semantic Versioning and uses Keep a Changelog format.
- Repositioned the public README around dynamic Scenario chains: repeated Agent reasoning can become reviewed, reusable, low-latency Scenario chains instead of paying the full ReAct cost forever.
- Updated package metadata to match the new Scenario-chain positioning.
- Added
httpx[socks]to thedevextra so a cleanpip install -e ".[dev]"environment can run provider payload tests without separately installing the OpenAI-compatible optional dependency. - Reworked no-key cookbook examples so they demonstrate the behavior their titles promise: Scenario routing now really hits Scenarios, customer service shows KB / Scenario / direct paths, RAG hits the local KB, and tool-calling executes real tools through a scripted ReAct flow.
- Cleaned stale
SwiftAgentreferences in quickstart/package text.
EmbeddingRetriever— semantic scenario prefiltering. Abovescenario_prefilter_top_kscenarios the router already prefilters via retrieval; it can now rank semantically instead of lexically: passAgent(scenario_retriever=EmbeddingRetriever(embedder))with anyEmbedder(built-in:providers.embedding. OpenAICompatibleEmbeddingProvider, works with DashScopetext-embedding-v4). Scenario doc vectors are embedded once and cached — each request costs one query embedding; any embedding failure degrades to the lexical fallback instead of breaking classification.ScenarioRetriever.rankis now async to allow the network hop. Live verification on a 30-scenario pool: two zero-lexical-overlap semantic queries ("明天出门要带伞吗" → weather, "欧元现在什么价" → exchange rate) that the lexical prefilter misses both rank top-K and classify correctly via embeddings; doc-vector caching confirmed (1 doc batch + 1 query call per request, first request 2548ms, subsequent ~800ms).
-
A successful plan's afterlife now has two independently configurable gates, both defaulting to manual. Gate 1 — reuse (
plan_auto_reuse, defaultFalse): generated plans are one-shot accelerators; same-shape regenerations dedupe into a single candidate that keeps score, but nothing is matched against future requests untilAgent.approve_plan()opens the gate (orplan_auto_reuse=Truemakes it a rule). Gate 2 — promotion to Scenario (plan_auto_promote, defaultFalse): manual viapromote_plan/export_plan_scenario, or rule-based afterplan_promote_afterclean successes. Promotion implies reuse approval. Defaults mean the framework never persists LLM-generated behavior behind the developer's back. -
Planner fast path (opt-in:
enable_planner) — one light-model call turns a REACT-level request into a deterministic tool plan, and plans that keep succeeding graduate into Scenarios. Where ReAct spends N+1 LLM calls deciding each step, the Planner emits the whole templated chain up front (core/planner.py), validates it (known tools, closed template vars, step cap), and executes it through the same engine that runs Scenarios (ScenarioEngine.execute_config). Any failure falls back to the ReAct loop. Successful plans enter a probation cache: new phrasings match via slot-value-stripped anchors (one slot-extraction call instead of a planning call), and promotion is dual-track — rule-based auto-promotion afterplan_promote_afterclean successes (plan_auto_promote), plus a manual track (Agent.list_plan_candidates/promote_plan/export_plan_scenario) for codifying a reviewed plan as a permanent Scenario. A promoted plan registers its real user phrasings as retrieval triggers and is routed by the classifier like any human-authored scenario. Live lifecycle verification (qwen3.6-flash): the same intent across three phrasings ran 2303ms/3-calls (fresh plan) → 1592ms/3 (cache reuse + auto-promote) → 1260ms/2 (scenario route). Caveat: a plan failing mid-chain after a side-effectful tool ran means the ReAct fallback may re-run that tool — prefer read-mostly toolsets. -
Classifier prompt: scenario descriptions + explicit scenario-first rule. Candidates now render as
id(name: description) [slots: ...]and the prompt instructs "prefer level=2 over level=1 whenever a scenario covers the request" — without these, multi-tool requests kept classifying as REACT even when a registered scenario covered them exactly (surfaced by the Planner promotion lifecycle test). -
OpenAICompatibleProvider(extra_params=...)— vendor-specific request fields (constructor default + per-call override) now reach the wire. The motivating case: DashScope hybrid-thinking models need{"enable_thinking": False}or every classification pays reasoning latency — measured 2716ms → 465ms on qwen3.6-flash, and even qwen-flash drops 1041ms → 444ms. README quickstart now recommends qwen3.6-flash with thinking disabled.
-
Classifier slot extraction noise. Slot values must now be the shortest literal span copied verbatim from the input and are omitted entirely when absent — previously the classifier emitted whole-phrase values like
from_city='下周去上海'or guessedtracking_no='我的包裹'. Strict-equality slot verification against live qwen3.6-flash: 6/6 (qwen-flash scores 4/6 on the same prompt — the model upgrade is load-bearing). -
Scenario retrieval pre-filter — the classifier prompt stays O(K) as the scenario pool grows. Classification accuracy of a light model degrades as more candidates are listed in its prompt; retrieval doesn't. Above
scenario_prefilter_top_k(default 8) registered scenarios, the router now ranks the pool against the user input with a zero-dependency, CJK-aware lexical retriever (core/retrieval.py: Latin word tokens + CJK character bigrams, IDF-weighted overlap) and only shows the top-K candidates to the classifier. Scenariotriggersdouble as retrieval anchors. Pools at or below K see the exact same prompt as before; a wrongly filtered scenario degrades to ReAct (slower but correct), never to a misfire; a failing custom retriever falls back to the full pool. Swap in an embedding-based retriever via theScenarioRetrieverprotocol (IntentRouter(retriever=...)). Verified against live DashScope qwen-flash with a 30-scenario pool: 6/6 natural-language queries classified into the correct scenario at unchanged latency.
- README: added a rendered tiered-execution architecture diagram
(
docs/assets/architecture-flow.png), turned the Features list into an icon table, and refreshed the benchmark numbers + chart against measured swiftagentx 0.3.3 data (20 iterations/scenario). The latency chart now labels every bar, so the two sub-millisecond tiers (cache / KB hit) are finally legible on the log scale instead of squashed to the axis floor. - Corrected the stale "195 tests" comparison-table figure to 218.
- Scenario-level result cache now actually works
(#1).
ScenarioConfig.cache_key_template/cache_ttlandScenarioEngine.build_cache_key()were declared and documented (the README scenario examples setcache_ttl) but never read — pure dead code.Agent._execute_scenario()now checks the scenario cache on entry and stores successful results with the scenario'scache_ttl. Caching is opt-in: only scenarios that declared acache_key_template(or a custom key builder) are cached, so scenarios that didn't ask for it keep their previous no-cache behavior. The semantic key (e.g.order_status_$user_id) dedups across different phrasings of the same intent — something the request-level cache (keyed on raw input text) cannot do. Keys are user-scoped, so one user's cached result never leaks to another.
- Scenarios now extract their template slots from natural language.
A
ToolChainStep(tool="weather", kwargs_template={"city": "$city"})previously only worked if the caller pre-parsed the city and passed it asagent.run(text, city="北京"). Now the intent classifier extracts declared slots in the same classification call, so a user typing "北京天气怎么样" fires the scenario withcity=北京automatically — the headline Scenario feature finally works end-to-end from a chat UI, still at one LLM call. NewScenarioConfig.required_vars()computes which$slotsa scenario needs (excluding reserved keys and vars produced mid-chain byextract_to). If the classifier can't fill a required slot, the request gracefully falls back to ReAct instead of firing a step with an unsubstituted$var.
0.3.1 — 2026-05-28
A dogfood-driven patch release. Took the v0.3.0 README and tried to
build a real chatbot from it as a first-time user, then spawned four
parallel sub-agents to verify every documented feature. Six rounds of
fixes later, the same walkthrough plus all six examples/cookbook/
scripts, Flask, FastAPI, real-LLM benchmarks, and a literal smoke of
every Python block in the README run clean end-to-end. No new
framework features — only sharper defaults, fewer footguns, clearer
errors.
Agent.run()withoutsession_idnow shares one stable default session per Agent instance. Previously every call generated a fresh random UUID, which silently disabled the v0.3 LayeredMemory feature for the most natural usage pattern (single Agent, multipleawait agent.run(text)calls). Multi-user servers still pass an explicitsession_id. (Dogfood Friction #5 — the headline bug.)Agent.run()now accepts anAgentRequestpolymorphically. Previouslyagent.run(AgentRequest(...))crashed inside_validate_inputwithTypeError: object of type 'AgentRequest' has no len()— a reasonable user expectation givenrun_stream(request, adapter)takes one. Mixing AgentRequest + kwargs raises a clearTypeErrorexplaining the two calling styles.- Three-level cache now actually writes.
cache.set_level_2()was never called fromrun()/run_stream()— only reads were wired. Cache hits worked the second time only by accident (Scenario short circuit's own cache layer). Now every successful turn populates L2. Agent.use(middleware)now actually runs the chain. The chain was built and middlewares were appended, butrun()never invoked it — every middleware was silently dropped. Wired into bothrun()andrun_stream(), with short-circuit support.- Six lifecycle
HookEvents now actually dispatch: BEFORE / AFTER TOOL_CALL, BEFORE / AFTER SCENARIO_STEP, BEFORE / AFTER REACT_ITER. They were declared in the enum and documented but never fired — HookRegistry handlers attached to them would never run. Added a contract test (test_every_lifecycle_hook_event_fires_at_least_once) that exercises every enum value to keep this from regressing again. - FastAPI admin router no longer requires a double-prefix. Users
who followed the README pattern
app.include_router(router, prefix="/admin")got endpoints at/admin/admin/statusinstead of/admin/status. The router now declares no internal prefix and defers entirely to the caller's mount path. - ReAct loop refuses to call the same tool twice in a row with
semantically-equivalent args.
calculator(12*34)and `calculator(12- 34)` previously counted as different actions, so qwen-flash gladly ran them both. Dedup key now normalises whitespace inside string params. Real measured impact: step9_hooks_middleware latency 5463ms → 2831ms on the same prompt.
- Scenario engine's template substitution and
directoutput. Multi-arg MCP-shaped tools were unreachable from a Scenario chain (only single-stringquery_templateworked) — addedToolChainStep.kwargs_template: dict[str, str]so MCPadd(a, b)- style tools work inside a Scenario. Thedirectoutput type branch incorrectly returned the initialextra_varsdict (user_id / session_id metadata) instead of the tool's real result when no step declaredextract_to. Fixed; matches README contract. - SSE wire format now emits the standard
event: <type>field in front of everydata:payload. BrowserEventSourceandaiohttp-sse-clientboth dispatch on this field, but it was buried inside the JSON payload only. README claimed "12 event types" — consumers couldn't actually pick which type they wanted without JSON-parsing every frame. Backwards-compatible — old consumers that only readdata:lines still work. SSEStreamAdaptersurvives a disconnected consumer. The producer used to block 5s persend_event(and the queue'sput(None)infinish()blocked forever) when the HTTP client vanished mid-stream.put_timeoutis now 1s, the adapter silently no-ops further events on first timeout, andfinish()'s sentinel is best-effort. Net effect:run_streamreturns in ~0.1s with a dead consumer instead of deadlocking.- SSE answer duplication. Streamed answers were emitted once
during the direct/streaming path AND again at the end via
_stream_answer. Now the framework tracks whether the chosen execution path already streamed and skips the re-emit, only sendinganswer_endto mark completion. LayeredMemory.flush_l2_to_l3()andTopicChangeHooknow actually flush. The hook detected topic changes and calledsummarize(), but with L2 not yet overflowed L3 was empty and summarize silently no-op'd. Old topic kept bleeding into the new one via L2 replay. The hook now flushes L2 → L3 before calling summarize so the topic boundary actually fires.LLMHook.parse_responsetolerates real-world LLM output. Models wrap JSON in```json … ```` fences or surround it with prose; parsing now tries three strategies (raw, fenced, embedded{...}`) before giving up. Total parse failure logs a WARNING (was silent).- MCP error format.
MCPClientErrorfor tools/call errors now reads(code -32000): intentional server errorinstead of the dict repr{'code': -32000, 'message': '…'}— easier for the LLM observation channel to reason about.
OpenAICompatibleProvidernow fails fast at construction time with a clear actionable message —pip install 'swiftagentx[openai]'— whenhttpxis missing, instead of crashing on the first chat() call with the misleadingModuleNotFoundError: No module named 'requests'. (Dogfood Friction #3.)AgentResponse.metadatanow exposeserror_classand (only whenconfig.debug=True)error_message+tracebackon every exception. Previously the user-facing answer was"Sorry, an internal error occurred"and metadata was empty.debug=Falseno longer leaks raw exception strings into metadata (regression caught by Round 3 dogfood).- Input-validation failures now return an
AgentResponsewithmetadata={"input_rejected": True, "error_class": "ValueError"}instead of raisingValueErrorout ofrun()/run_stream(). A web handler that didn't wrap the call in try/except would otherwise return a 500 with a leaky stack trace. StageActionis now exported from the top-level package. README pointed atfrom swiftagentx import StageActionbut onlyPipelineStage,RequestPipeline,StageResultwere re-exported — the example was broken.- Skill markdown with unclosed YAML frontmatter raises a clear
ValueErrornaming the missing closing---. Previously the whole frontmatter block was silently treated as body, the skill'snamedefaulted to its filename, anddescriptionwas lost without warning.
- The
[openai]extra now installshttpx[socks]>=0.25.0instead of plainhttpx. This pulls insocksioso users behind a SOCKS proxy (common in mainland China deployments — explicitly called out in the project'sCLAUDE.mdpolicies) don't crash on the first request withImportError: Using SOCKS proxy, but the 'socksio' package is not installed.
- README's "OpenAI-Compatible API" Quick Start now prefaces with the required extras install and the China-mainland proxy gotcha — the two things that block a new user inside 60 seconds.
- README gains a "Multi-turn conversations" section explicitly showing
the default-session pattern and when to pass an explicit
session_id. - README's "LLM_API_KEY" placeholder replaced with three concrete provider examples (OpenAI / DashScope active / DeepSeek), so users can copy-paste a working configuration.
- README's Lifecycle Hooks section split into "A. Subclass Agent" (the 7 subclass hooks) and "B. HookRegistry" (12 declarative event names) so both extension patterns are discoverable.
- Both English and Chinese sections updated in lockstep.
195 (v0.3.0) → 211 (v0.3.1). Sixteen new regression tests cover the
default session, error metadata, OpenAI provider import-error path,
ReAct duplicate-action guard, Scenario template substitution + direct
output, middleware short-circuit, FastAPI admin mounting, every
HookEvent firing, multi-kwarg ToolChainStep, disconnected SSE
consumer, input validation, and stream send_event post-finish
silent-drop.
- The
[openai]extra now installshttpx[socks]>=0.25.0instead of plainhttpx. This pulls insocksioso users behind a SOCKS proxy (common in mainland China deployments — explicitly called out in the project'sCLAUDE.mdpolicies) don't crash on the first request withImportError: Using SOCKS proxy, but the 'socksio' package is not installed.
- README's "OpenAI-Compatible API" Quick Start now prefaces with the required extras install and the China-mainland proxy gotcha — the two things that block a new user inside 60 seconds.
- README gains a "Multi-turn conversations" section explicitly showing
the default-session pattern and when to pass an explicit
session_id. - Both English and Chinese sections updated in lockstep.
195 (v0.3.0) → 201 (v0.3.1). Six new regression tests in
tests/test_default_session.py cover the default session, error
metadata behavior, and OpenAI provider import-error path.
0.3.0 — 2026-05-26
This release brings the framework into the 2026 generation of agent patterns (memory, hooks, MCP, sub-agents, skills) while keeping Scenarios as the headline abstraction. Every new subsystem is a building block a Scenario or ReAct iteration can use — Scenarios are not replaced.
- 4-layer Memory (
LayeredMemory,LayeredMemoryStore) — L1 current question / L2 last-4-turns verbatim / L3 reference window / L4 incremental rolling summary. Cadence-based (every N turns) and semantic-hook-triggered summarization paths both exist. PluggableMemoryBackend; ships withInMemoryBackend(production-ready future: Redis backend). - Hook system (
HookRegistry,HookEvent) — 12 lifecycle events and 4 semantic events with four handler kinds (PythonHook,LLMHook,ShellHook, semantic hooks). The v0.2 subclass-override pattern still works alongside. TopicChangeHook— built-in semantic hook that asks the LIGHT model whether the current input starts a new topic; on detection, callsmemory.summarize()so the layered memory stays coherent across topic switches. Auto-registered; opt-out via config.- MCP server support (
Agent.register_mcp_server,MCPClient,MCPTool) — Scenarios and ReAct can call any Model Context Protocol server's tools by name. Stdio + SSE transports. - Sub-agent dispatch (
SubAgentRole,Agent.dispatch_subagents) — parallel focused agents with isolated context, structured results, and one-failed-doesn't-break-others fan-out. - Skill-in-ReAct (
Skill,Agent.invoke_skill,Agent.load_skills) — markdown-defined workflows the ReAct loop can invoke. Complement to Scenarios; not a replacement. - Session workspace (
Workspace,Agent.workspace) — per-session file sandbox withLocalDiskWorkspaceBackend+InMemoryWorkspaceBackend, path-escape protection, optional cleanup-on-exit. - Cache-friendly prompt layout (
PromptLayout) — assembles prompts in least-changing → most-changing order (tools → system → L4 → L3 → L2 → L1) for Anthropic/OpenAI prompt-cache friendliness. - Lazy tool loading (
ToolRegistry.select_tools_for_query,schemas_for_query) — when a registry exceeds a threshold, score tools against the query and return only the top-K. Important when many MCP servers contribute hundreds of tools. - Real-LLM benchmark runner (
benchmarks/real_runner.py) — exercises the four execution tiers against any OpenAI-compatible endpoint (defaults to DashScope qwen-flash + qwen-turbo), emits JSON + matplotlib chart. 30 iterations per scenario costs well under one yuan. docs/architecture-v0.3.md— binding construction blueprint for the release, including OUT-of-scope items (no permissions, slash commands, CLI, output styles, dashboards).- README headline visual embeds the measured benchmark chart from
docs/assets/v0.3-benchmark-qwen.png.
Agent.memoryis now aLayeredMemoryStore(per-session multiplexer) instead of the singletonSessionMemorythat pooled every session's history together (latent v0.2 bug — sessions could see each other's context). The standaloneSessionMemoryclass itself is unchanged for users who construct it directly.Agent.run()/Agent.run_stream()now dispatch lifecycle hooks at every boundary in addition to calling the subclass-override methods (on_request_startetc.). The two paths are additive._direct_response()now injects layered memory into the chat prompt viamem.to_chat_messages(l2_rounds=5)rather than the oldget_conversation_for_reply.- README test-suite count updated from 111 to 195. Tiered-execution table replaced with measured P50/P95 numbers from the new benchmark runner.
- The undocumented patterns
agent.memory.add_message(...)andagent.memory.get_conversation_for_reply(...)no longer work on the agent attribute (it's no longer aSessionMemory). Importing theSessionMemoryclass directly fromswiftagentx.core.memorystill works as before.
DashScope Qwen, 30 iterations per scenario, LIGHT=qwen-flash,
HEAVY=qwen-turbo:
| Tier | P50 | P95 | LLM calls |
|---|---|---|---|
| KB exact match | 0 ms | 0 ms | 0 |
| Scenario shortcut | 517 ms | 802 ms | 1 |
| Cache hit | 0 ms | 0 ms | 0 |
| Simple QA (DIRECT) | 1.4 s | 2.4 s | 2 |
| ReAct complex | 3.1 s | 4.0 s | 3 |
Reproduce: python benchmarks/real_runner.py --iterations 30.
105 (v0.2.0) → 195 (v0.3.0). Full suite runs in <0.5 s.
0.2.0 — 2026-05-25
This release focuses on production readiness, observability, and benchmark transparency. The framework's tiered execution architecture is unchanged; this release hardens the surrounding engineering.
- Benchmark suite (
benchmarks/) with 10 scenarios across cache, scenario routing, ReAct, RAG, concurrency, and error recovery. Supports both a mock LLM mode (deterministic, CI-friendly) and a real OpenAI-compatible mode. Outputs per-scenario P50/P95/P99 latency, LLM call counts, and token usage, plus an auto-generated comparison chart. - Cookbook (
examples/cookbook/) — six runnable end-to-end examples: customer service agent, RAG chatbot, tool-calling workflow, streaming dashboard, dual-model optimization, and scenario routing. pytest-covand a coverage configuration inpyproject.toml.benchmarkoptional dependency group (pip install swiftagentx[benchmark]) bundlingmatplotlibandtabulate.IssuesandChangelogURLs topyproject.toml.- GitHub Actions CI workflow running tests on Python 3.10 / 3.11 / 3.12 /
3.13 plus
ruffandmypygates. CONTRIBUTING.mdandCODE_OF_CONDUCT.md.
- Minimum Python version raised to 3.10 (3.9 reached EOL in October 2025 and the codebase already used 3.10-only typing features).
pyproject.tomlruff rules extended withB(bugbear) andUP(pyupgrade); target bumped topy310.Development Statusclassifier moved from3 - Alphato4 - Beta.- README rewritten with a sharper positioning, head-to-head comparison against LangChain, and embedded benchmark results.
- Author metadata corrected to point at the actual maintainer.
- Pipeline stages were never executed.
Agent.pipelinewas created in__init__butAgent.run()skipped the pipeline entirely, soagent.pipeline.add_stage(...)was a silent no-op. The README documentedKnowledgeBaseStageas a way to get exact-match short-circuits — that promise was broken. Nowrun()executes all pipeline stages before the cache check, and a stage returningSHORT_CIRCUITreturns immediately without any LLM call. Tests added intests/test_pipeline_integration.py. set_knowledge_basenow auto-installs aKnowledgeBaseStage(opt-out withauto_short_circuit=False). Previously, callingagent.set_knowledge_base(kb)only registered aKnowledgeBaseToolfor use inside the ReAct loop; users had to remember to also add a pipeline stage to get the documented "zero LLM calls on exact match" behavior. Callingset_knowledge_basetwice is now idempotent.- Version mismatch between
pyproject.toml(0.1.1) andswiftagentx.__version__(0.1.0). pyproject.tomlproject URLs previously pointed to a non-existentgithub.com/swiftagent/swiftagentrepository.stream/adapter.pyusedasyncio.timeout()(Python 3.11+) while declaringrequires-python = ">=3.9". Rewritten with await_for-based approach compatible with Python 3.10+.tools/termination.pysilently swallowed exceptions raised by custom termination checkers; now logged atWARNINGwith checker name and exception type.core/memory.pycleanup comparedOptional[datetime]againstdatetimewithout aNoneguard.- Removed 18 unused imports and reformatted 38 import blocks via
ruff --fix.
- Empty
examples/customer_service/placeholder directory replaced byexamples/cookbook/customer_service_agent.py.
0.1.1 — 2026-02-25
Initial public release on PyPI.
- Core
Agentwith tiered execution: cache hit → scenario toolchain → ReAct loop → direct LLM response. - Three-level cache (
CacheManager): global KB, per-user tool result, per-session dynamic variables. - Dual-model abstraction (
ModelTier.LIGHT/ModelTier.HEAVY). - Pluggable
KnowledgeBaseABC with built-in TF-IDFMemoryKnowledgeBase. KnowledgeBaseStagepipeline stage for exact-match short-circuit.- SSE streaming adapter with twelve event types.
- Flask and FastAPI adapters; framework-agnostic
AdminService. - Middleware chain with built-in
TracingMiddleware. - 105 tests covering core, cache, KB, admin, tools, streaming.