framework-agents 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. framework_agents-0.1.0/.gitignore +47 -0
  2. framework_agents-0.1.0/PKG-INFO +118 -0
  3. framework_agents-0.1.0/README.md +87 -0
  4. framework_agents-0.1.0/examples/01-streaming-tools.py +49 -0
  5. framework_agents-0.1.0/examples/02-orchestration.py +27 -0
  6. framework_agents-0.1.0/examples/03-swarm.py +25 -0
  7. framework_agents-0.1.0/examples/04-workflows.py +47 -0
  8. framework_agents-0.1.0/examples/05-memory-sessions.py +32 -0
  9. framework_agents-0.1.0/examples/06-hitl-resume.py +64 -0
  10. framework_agents-0.1.0/examples/07-guardrails-budgets.py +53 -0
  11. framework_agents-0.1.0/examples/08-semantic-graph.py +38 -0
  12. framework_agents-0.1.0/examples/09-a2a-remote.py +49 -0
  13. framework_agents-0.1.0/examples/10-middleware-suite.py +47 -0
  14. framework_agents-0.1.0/examples/11-rag-retrieval.py +24 -0
  15. framework_agents-0.1.0/examples/12-providers.py +32 -0
  16. framework_agents-0.1.0/examples/13-serve-framework.py +59 -0
  17. framework_agents-0.1.0/examples/README.md +22 -0
  18. framework_agents-0.1.0/pyproject.toml +53 -0
  19. framework_agents-0.1.0/src/framework_agents/__init__.py +345 -0
  20. framework_agents-0.1.0/src/framework_agents/a2a_client.py +360 -0
  21. framework_agents-0.1.0/src/framework_agents/a2a_server.py +495 -0
  22. framework_agents-0.1.0/src/framework_agents/agent.py +920 -0
  23. framework_agents-0.1.0/src/framework_agents/checkpoint.py +231 -0
  24. framework_agents-0.1.0/src/framework_agents/config.py +200 -0
  25. framework_agents-0.1.0/src/framework_agents/conversation.py +249 -0
  26. framework_agents-0.1.0/src/framework_agents/errors.py +47 -0
  27. framework_agents-0.1.0/src/framework_agents/evals.py +127 -0
  28. framework_agents-0.1.0/src/framework_agents/mcp_client.py +477 -0
  29. framework_agents-0.1.0/src/framework_agents/memory.py +103 -0
  30. framework_agents-0.1.0/src/framework_agents/memory_compaction.py +63 -0
  31. framework_agents-0.1.0/src/framework_agents/middleware.py +1302 -0
  32. framework_agents-0.1.0/src/framework_agents/models/__init__.py +100 -0
  33. framework_agents-0.1.0/src/framework_agents/models/anthropic.py +339 -0
  34. framework_agents-0.1.0/src/framework_agents/models/capabilities.py +45 -0
  35. framework_agents-0.1.0/src/framework_agents/models/ollama.py +47 -0
  36. framework_agents-0.1.0/src/framework_agents/models/openai_compatible.py +409 -0
  37. framework_agents-0.1.0/src/framework_agents/observability.py +84 -0
  38. framework_agents-0.1.0/src/framework_agents/ontology_review.py +321 -0
  39. framework_agents-0.1.0/src/framework_agents/orchestration.py +169 -0
  40. framework_agents-0.1.0/src/framework_agents/pricing.py +63 -0
  41. framework_agents-0.1.0/src/framework_agents/prompts.py +132 -0
  42. framework_agents-0.1.0/src/framework_agents/rag.py +123 -0
  43. framework_agents-0.1.0/src/framework_agents/remote_sparql.py +213 -0
  44. framework_agents-0.1.0/src/framework_agents/runtime.py +88 -0
  45. framework_agents-0.1.0/src/framework_agents/sandbox.py +327 -0
  46. framework_agents-0.1.0/src/framework_agents/semantic.py +1614 -0
  47. framework_agents-0.1.0/src/framework_agents/serve.py +436 -0
  48. framework_agents-0.1.0/src/framework_agents/sessions.py +249 -0
  49. framework_agents-0.1.0/src/framework_agents/skills.py +213 -0
  50. framework_agents-0.1.0/src/framework_agents/state.py +89 -0
  51. framework_agents-0.1.0/src/framework_agents/streaming.py +74 -0
  52. framework_agents-0.1.0/src/framework_agents/swarm.py +318 -0
  53. framework_agents-0.1.0/src/framework_agents/tools.py +114 -0
  54. framework_agents-0.1.0/src/framework_agents/tracing.py +131 -0
  55. framework_agents-0.1.0/src/framework_agents/types.py +262 -0
  56. framework_agents-0.1.0/src/framework_agents/workflows.py +130 -0
  57. framework_agents-0.1.0/tests/conftest.py +8 -0
  58. framework_agents-0.1.0/tests/test_agent_live_together.py +117 -0
  59. framework_agents-0.1.0/tests/test_anthropic_offline.py +194 -0
  60. framework_agents-0.1.0/tests/test_conversation_sessions.py +448 -0
  61. framework_agents-0.1.0/tests/test_core_parity.py +273 -0
  62. framework_agents-0.1.0/tests/test_cross_language.py +114 -0
  63. framework_agents-0.1.0/tests/test_gaps.py +722 -0
  64. framework_agents-0.1.0/tests/test_live_providers.py +177 -0
  65. framework_agents-0.1.0/tests/test_middleware.py +646 -0
  66. framework_agents-0.1.0/tests/test_middleware_extra.py +559 -0
  67. framework_agents-0.1.0/tests/test_multi_and_evals.py +705 -0
  68. framework_agents-0.1.0/tests/test_review_fixes.py +1165 -0
  69. framework_agents-0.1.0/tests/test_semantic_parity.py +860 -0
  70. framework_agents-0.1.0/tests/test_serve.py +338 -0
  71. framework_agents-0.1.0/tests/test_servers_and_clients.py +767 -0
@@ -0,0 +1,47 @@
1
+ # deps
2
+ node_modules/
3
+ .pnpm-store/
4
+ .turbo/
5
+
6
+ # build output
7
+ dist/
8
+ build/
9
+ *.tsbuildinfo
10
+
11
+ # test / coverage
12
+ coverage/
13
+ .nyc_output/
14
+
15
+ # env
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+
20
+ # OS / editor
21
+ .DS_Store
22
+ *.log
23
+ .vscode/*
24
+ !.vscode/extensions.json
25
+ .idea/
26
+
27
+ # python (future package)
28
+ __pycache__/
29
+ *.pyc
30
+ .venv/
31
+ .uv/
32
+
33
+ # docs build
34
+ site/
35
+ .venv-docs/
36
+
37
+ # python package build
38
+ packages/python/dist/
39
+ packages/python/build/
40
+ *.egg-info/
41
+ uv.lock
42
+
43
+ # npm tarballs
44
+ *.tgz
45
+
46
+ # doc scripts / temp
47
+ claude.md
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.5
2
+ Name: framework-agents
3
+ Version: 0.1.0
4
+ Summary: Python agent framework: ReAct loop, tools, memory, streaming, budgets, middleware, multi-agent, semantic graph, A2A serving - parity with the TypeScript package
5
+ Project-URL: Homepage, https://github.com/fab679/framework
6
+ Project-URL: Documentation, https://fab679.github.io/framework/
7
+ Project-URL: Repository, https://github.com/fab679/framework
8
+ Project-URL: Changelog, https://github.com/fab679/framework/releases
9
+ Author: Framework Agents
10
+ License: MIT
11
+ Keywords: a2a,agents,ai,asyncio,llm,mcp,semantic-web,streaming
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: pydantic>=2.7
26
+ Provides-Extra: dev
27
+ Requires-Dist: anyio>=4; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Python package — TS parity port (`framework_agents`)
33
+
34
+ The TypeScript package (`packages/typescript`) is the reference implementation.
35
+ This port targets API parity with the **fixed** TS semantics — every behavior
36
+ that was a real bug during the TS line (and is regression-pinned there) is
37
+ encoded here from day one:
38
+
39
+ | Pinned semantic | Why |
40
+ |---|---|
41
+ | `reasoning_tokens` is a SUBSET of `output_tokens` | never double-billed in budgets/cost |
42
+ | `cost_usd` only when a price is known | unknown models → `None`, never a fake $0 |
43
+ | `save()` APPENDS; the loop does clear-then-save | history never duplicates across runs |
44
+ | Structured `'auto'` resolves by capability | unresolved auto never throws STRUCTURED_NO_MATCH |
45
+ | `finish_reason=length` + zero content → loud `MODEL_ERROR` | a thinking model burning its whole budget is not a silent empty answer |
46
+ | Tool parameter schemas ride the wire | models see real argument shapes |
47
+ | HITL decisions: exact key THEN bare tool name | resume converges even when models regenerate arg text |
48
+ | Local `ollama:` pricing wins over name collisions | never bill cloud prices for free local models |
49
+ | Storage keys/threadIds reject traversal (`..`) | no writes outside the root |
50
+ | Remote SPARQL: descriptive UA + 429/5xx retry honoring Retry-After | public endpoints reject and rate-limit otherwise |
51
+
52
+ No personal or brand names appear anywhere in this package.
53
+
54
+ ## Port status (mirroring the TS module map)
55
+
56
+ | TS module | Python | State |
57
+ |---|---|---|
58
+ | types (messages/blocks/usage) | `types.py` | ✅ |
59
+ | errors + retryable verdicts | `errors.py` | ✅ |
60
+ | tools (define_tool/registry/schemas) | `tools.py` | ✅ |
61
+ | memory (+compaction) | `memory.py`, `memory_compaction.py` | ✅ (MEMORY_CORRUPT loud, atomic unique-tmp writes) |
62
+ | conversation managers | `conversation.py` | ✅ (order-preserving pin; model or fn summarizer) |
63
+ | sessions + snapshots | `sessions.py` | ✅ (validated keys, TS-tested preset semantics) |
64
+ | checkpoint + file locks | `checkpoint.py` | ✅ (O_EXCL + stale reclaim, threadId validation) |
65
+ | runtime (abort/timeout) | `runtime.py` | ✅ |
66
+ | models (mock + OpenAI-compatible + Anthropic + Ollama) | `models/` | ✅ (Together+OpenAI field unions, native Anthropic Messages API w/ thinking budgets + SSE machine, vision capability fail-fast, stream usage, truncation guard) |
67
+ | state (AgentState + invocation) | `state.py`, `agent.py` | ✅ (validated JSON KV, custom-prototype rejection, snapshots/session restore, pause/resume, edit/branch/timelines, v1 reasoning blocks, `with_tools`) |
68
+ | pricing | `pricing.py` | ✅ |
69
+ | config + factory | `config.py` | ✅ (together/openai/ollama/custom) |
70
+ | agent loop (budgets/structured/streaming) | `agent.py` | ✅ (calls stream via drain; budget price refusal; synthesized final answer) |
71
+ | middleware (hooks, HITL, retries, limits, policy, todos, pii) | `middleware.py` | ✅ |
72
+ | orchestration | `orchestration.py` | ✅ (sequential/parallel/router; routed result semantics) |
73
+ | workflows | `workflows.py` | ✅ (dedup'd indegree, retries, propagation) |
74
+ | swarm | `swarm.py` | ✅ (handoff tool + stream events + loop detection default) |
75
+ | evals | `evals.py` | ✅ (JSONL + pass-rate reporter) |
76
+ | skills + TF-IDF routing | `skills.py` | ✅ ($mention word-boundary wins) |
77
+ | prompts (+A/B) | `prompts.py` | ✅ (CRLF/BOM tolerance) |
78
+ | rag (tf-idf retriever + tool) | `rag.py` | ✅ |
79
+ | semantic (store/SHACL/rules/graph memory/linking/JSON-LD/evolution) | `semantic.py`, `ontology_review.py` | ✅ (version graphs under `urn:framework:ontology:*`) |
80
+ | remote SPARQL | `remote_sparql.py` | ✅ (UA + Retry-After retry) |
81
+ | A2A clients (SSE + JSON-RPC) | `a2a_client.py` | ✅ (envelope + contextId chain + retry) |
82
+ | A2A server (contexts/LRU/disconnect/SSE) | `a2a_server.py` | ✅ (stdio-of-asyncio raw HTTP, body caps, CORS) |
83
+ | MCP clients + bridging | `mcp_client.py` | ✅ (stdio child lifecycle, no-id notification, per-tool degrade) |
84
+ | observability + tracing sinks | `observability.py`, `tracing.py` | ✅ |
85
+ | sandbox (JS + shell) | `sandbox.py` | ✅ (caveat: exec timeouts abandon a thread, no hard kill) |
86
+ | serve (framework.json + CLI) | `serve.py` | ✅ (lazy model init, argparse CLI, per-context isolated agents) |
87
+ | React SDK | — | n/a (React-specific; browser clients use A2A/SSE) |
88
+
89
+ **Batch-2 middleware** (`model_fallback`, `summarization`, `context_editing` +
90
+ `ClearToolUsesEdit`, `llm_tool_selector`, `tool_emulator`, `filesystem`,
91
+ `create_sub_agent`, `pii` tool-results) is ported with deviations pinned in
92
+ `middleware.py`'s docstring (feedback-strings instead of raises on escape
93
+ misses; `older_than_steps` interpreted as a step gate).
94
+
95
+ ## Running
96
+
97
+ ```bash
98
+ python3 -m venv .venv
99
+ .venv/bin/pip install -e ".[dev]"
100
+ .venv/bin/python -m pytest tests -q # offline parity suite (233 tests)
101
+ E2E_MODEL=1 .venv/bin/python -m pytest tests -q -m live
102
+ # live Together parity runs; OpenAI + Anthropic suites skip cleanly and go
103
+ # live as soon as OPENAI_API_KEY / ANTHROPIC_API_KEY appear in framework/.env
104
+ ```
105
+
106
+ ## Known deviations (documented in module docstrings)
107
+
108
+ - `Agent.stream()` yields a `done` event and stores the result (generators
109
+ can't return values); run() drains — wire contracts identical.
110
+ - Async file IO runs via `asyncio.to_thread` in filesystem tools; storage
111
+ classes use blocking IO inside async methods (single-process locality).
112
+ - Sandbox JS cannot hard-interrupt a thread mid-statement (node:vm can) —
113
+ the timeout surfaces per contract, the abandoned thread dies at exit.
114
+ - Semantic version graphs use `urn:framework:ontology:<version>` (the TS
115
+ default `urn:fabisch:*` embeds a personal name; both are configurable).
116
+ - Cross-language conformance: the Python A2A client speaks the exact wire
117
+ contract of the TS A2A server (envelope, SSE frames, [DONE]) and vice
118
+ versa — see `tests/test_servers_and_clients.py`.
@@ -0,0 +1,87 @@
1
+ # Python package — TS parity port (`framework_agents`)
2
+
3
+ The TypeScript package (`packages/typescript`) is the reference implementation.
4
+ This port targets API parity with the **fixed** TS semantics — every behavior
5
+ that was a real bug during the TS line (and is regression-pinned there) is
6
+ encoded here from day one:
7
+
8
+ | Pinned semantic | Why |
9
+ |---|---|
10
+ | `reasoning_tokens` is a SUBSET of `output_tokens` | never double-billed in budgets/cost |
11
+ | `cost_usd` only when a price is known | unknown models → `None`, never a fake $0 |
12
+ | `save()` APPENDS; the loop does clear-then-save | history never duplicates across runs |
13
+ | Structured `'auto'` resolves by capability | unresolved auto never throws STRUCTURED_NO_MATCH |
14
+ | `finish_reason=length` + zero content → loud `MODEL_ERROR` | a thinking model burning its whole budget is not a silent empty answer |
15
+ | Tool parameter schemas ride the wire | models see real argument shapes |
16
+ | HITL decisions: exact key THEN bare tool name | resume converges even when models regenerate arg text |
17
+ | Local `ollama:` pricing wins over name collisions | never bill cloud prices for free local models |
18
+ | Storage keys/threadIds reject traversal (`..`) | no writes outside the root |
19
+ | Remote SPARQL: descriptive UA + 429/5xx retry honoring Retry-After | public endpoints reject and rate-limit otherwise |
20
+
21
+ No personal or brand names appear anywhere in this package.
22
+
23
+ ## Port status (mirroring the TS module map)
24
+
25
+ | TS module | Python | State |
26
+ |---|---|---|
27
+ | types (messages/blocks/usage) | `types.py` | ✅ |
28
+ | errors + retryable verdicts | `errors.py` | ✅ |
29
+ | tools (define_tool/registry/schemas) | `tools.py` | ✅ |
30
+ | memory (+compaction) | `memory.py`, `memory_compaction.py` | ✅ (MEMORY_CORRUPT loud, atomic unique-tmp writes) |
31
+ | conversation managers | `conversation.py` | ✅ (order-preserving pin; model or fn summarizer) |
32
+ | sessions + snapshots | `sessions.py` | ✅ (validated keys, TS-tested preset semantics) |
33
+ | checkpoint + file locks | `checkpoint.py` | ✅ (O_EXCL + stale reclaim, threadId validation) |
34
+ | runtime (abort/timeout) | `runtime.py` | ✅ |
35
+ | models (mock + OpenAI-compatible + Anthropic + Ollama) | `models/` | ✅ (Together+OpenAI field unions, native Anthropic Messages API w/ thinking budgets + SSE machine, vision capability fail-fast, stream usage, truncation guard) |
36
+ | state (AgentState + invocation) | `state.py`, `agent.py` | ✅ (validated JSON KV, custom-prototype rejection, snapshots/session restore, pause/resume, edit/branch/timelines, v1 reasoning blocks, `with_tools`) |
37
+ | pricing | `pricing.py` | ✅ |
38
+ | config + factory | `config.py` | ✅ (together/openai/ollama/custom) |
39
+ | agent loop (budgets/structured/streaming) | `agent.py` | ✅ (calls stream via drain; budget price refusal; synthesized final answer) |
40
+ | middleware (hooks, HITL, retries, limits, policy, todos, pii) | `middleware.py` | ✅ |
41
+ | orchestration | `orchestration.py` | ✅ (sequential/parallel/router; routed result semantics) |
42
+ | workflows | `workflows.py` | ✅ (dedup'd indegree, retries, propagation) |
43
+ | swarm | `swarm.py` | ✅ (handoff tool + stream events + loop detection default) |
44
+ | evals | `evals.py` | ✅ (JSONL + pass-rate reporter) |
45
+ | skills + TF-IDF routing | `skills.py` | ✅ ($mention word-boundary wins) |
46
+ | prompts (+A/B) | `prompts.py` | ✅ (CRLF/BOM tolerance) |
47
+ | rag (tf-idf retriever + tool) | `rag.py` | ✅ |
48
+ | semantic (store/SHACL/rules/graph memory/linking/JSON-LD/evolution) | `semantic.py`, `ontology_review.py` | ✅ (version graphs under `urn:framework:ontology:*`) |
49
+ | remote SPARQL | `remote_sparql.py` | ✅ (UA + Retry-After retry) |
50
+ | A2A clients (SSE + JSON-RPC) | `a2a_client.py` | ✅ (envelope + contextId chain + retry) |
51
+ | A2A server (contexts/LRU/disconnect/SSE) | `a2a_server.py` | ✅ (stdio-of-asyncio raw HTTP, body caps, CORS) |
52
+ | MCP clients + bridging | `mcp_client.py` | ✅ (stdio child lifecycle, no-id notification, per-tool degrade) |
53
+ | observability + tracing sinks | `observability.py`, `tracing.py` | ✅ |
54
+ | sandbox (JS + shell) | `sandbox.py` | ✅ (caveat: exec timeouts abandon a thread, no hard kill) |
55
+ | serve (framework.json + CLI) | `serve.py` | ✅ (lazy model init, argparse CLI, per-context isolated agents) |
56
+ | React SDK | — | n/a (React-specific; browser clients use A2A/SSE) |
57
+
58
+ **Batch-2 middleware** (`model_fallback`, `summarization`, `context_editing` +
59
+ `ClearToolUsesEdit`, `llm_tool_selector`, `tool_emulator`, `filesystem`,
60
+ `create_sub_agent`, `pii` tool-results) is ported with deviations pinned in
61
+ `middleware.py`'s docstring (feedback-strings instead of raises on escape
62
+ misses; `older_than_steps` interpreted as a step gate).
63
+
64
+ ## Running
65
+
66
+ ```bash
67
+ python3 -m venv .venv
68
+ .venv/bin/pip install -e ".[dev]"
69
+ .venv/bin/python -m pytest tests -q # offline parity suite (233 tests)
70
+ E2E_MODEL=1 .venv/bin/python -m pytest tests -q -m live
71
+ # live Together parity runs; OpenAI + Anthropic suites skip cleanly and go
72
+ # live as soon as OPENAI_API_KEY / ANTHROPIC_API_KEY appear in framework/.env
73
+ ```
74
+
75
+ ## Known deviations (documented in module docstrings)
76
+
77
+ - `Agent.stream()` yields a `done` event and stores the result (generators
78
+ can't return values); run() drains — wire contracts identical.
79
+ - Async file IO runs via `asyncio.to_thread` in filesystem tools; storage
80
+ classes use blocking IO inside async methods (single-process locality).
81
+ - Sandbox JS cannot hard-interrupt a thread mid-statement (node:vm can) —
82
+ the timeout surfaces per contract, the abandoned thread dies at exit.
83
+ - Semantic version graphs use `urn:framework:ontology:<version>` (the TS
84
+ default `urn:fabisch:*` embeds a personal name; both are configurable).
85
+ - Cross-language conformance: the Python A2A client speaks the exact wire
86
+ contract of the TS A2A server (envelope, SSE frames, [DONE]) and vice
87
+ versa — see `tests/test_servers_and_clients.py`.
@@ -0,0 +1,49 @@
1
+ """LIVE: streaming — real token/thinking deltas through Agent.stream(), and
2
+ tool calls streamed live (tool_call event before execution, tool_result when
3
+ it lands)."""
4
+
5
+ import asyncio
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from framework_agents import Agent, AgentOptions, define_tool, load_config
10
+ from framework_agents.config import init_chat_model
11
+ from framework_agents.middleware import model_retry
12
+
13
+
14
+ class Count(BaseModel):
15
+ count: int
16
+
17
+
18
+ async def main() -> None:
19
+ cfg = load_config()
20
+ model = init_chat_model(f"together:{cfg.together_model}", max_tokens=2500)
21
+
22
+ async def digits(args: Count, ctx) -> str:
23
+ await asyncio.sleep(0.7) # the 700ms proves tool_result lands mid-run
24
+ return "3.14159"
25
+
26
+ agent = Agent(AgentOptions(
27
+ model=model,
28
+ max_steps=4,
29
+ middleware=[model_retry(max_retries=3, initial_delay_ms=500)],
30
+ tools=[define_tool(name="get_pi_digits", description="Returns digits of pi.", schema=Count, execute=digits)],
31
+ ))
32
+
33
+ kinds: dict[str, int] = {}
34
+ text = ""
35
+ async for event in agent.stream("Use the get_pi_digits tool with count 5, then reply with exactly the digits it returned."):
36
+ kinds[event["type"]] = kinds.get(event["type"], 0) + 1
37
+ if event["type"] in ("token", "thinking"):
38
+ pass # deltas stream live through hooks; batched per call here
39
+ if event["type"] == "done":
40
+ text = event["text"]
41
+ print("EVENT KINDS:", kinds)
42
+ print("TEXT:", text.strip())
43
+ assert agent._last_result is not None
44
+ print("USAGE:", agent._last_result.usage)
45
+ ok = kinds.get("tool_call") and kinds.get("tool_result") and "3.14159" in text
46
+ print("PASS: tools streamed live" if ok else "NOTE: inspect trace (model variance)")
47
+
48
+
49
+ asyncio.run(main())
@@ -0,0 +1,27 @@
1
+ """LIVE: orchestration — sequential chain, parallel fan-out, model router."""
2
+ import asyncio
3
+ from framework_agents import Agent, AgentOptions, load_config
4
+ from framework_agents.orchestration import OrchestrationPlan, run_orchestrated
5
+
6
+ cfg = load_config()
7
+ model = lambda: __import__("framework_agents").init_chat_model(f"together:{cfg.together_model}", max_tokens=8000)
8
+
9
+ async def main():
10
+ retry = [__import__("framework_agents.middleware", fromlist=["model_retry"]).model_retry(max_retries=3, initial_delay_ms=500)]
11
+ mk = lambda name, sys, desc="": Agent(AgentOptions(model=model(), name=name, description=desc, system_prompt=sys, middleware=retry, max_steps=2))
12
+ seq = await run_orchestrated(OrchestrationPlan(strategy="sequential", agents=[
13
+ mk("math", "Answer with just the number."), mk("halver", "Answer with just half of the number you are given."),
14
+ ]), "What is 100 + 24?")
15
+ print("SEQUENTIAL:", seq.text.strip(), "(expect 62)")
16
+ par = await run_orchestrated(OrchestrationPlan(strategy="parallel", concurrency=2, agents=[
17
+ mk("a", 'Reply with exactly: AAA'), mk("b", 'Reply with exactly: BBB'), mk("c", 'Reply with exactly: CCC'),
18
+ ]), "Respond as instructed.")
19
+ print("PARALLEL:", [r.text.strip() for r in par.results])
20
+ router = await run_orchestrated(OrchestrationPlan(strategy="router", agents=[
21
+ mk("poet", "Reply with a one-line poem.", "writes poetry"),
22
+ mk("calculator", "Answer with just the number.", "does arithmetic"),
23
+ ]), "What is 12 times 12?")
24
+ print("ROUTER:", router.routed_to, router.results[0].text.strip(), "(expect 1 / 144)")
25
+ print("PASS" if "62" in seq.text and router.routed_to == 1 and "144" in router.results[0].text else "NOTE: model variance")
26
+
27
+ asyncio.run(main())
@@ -0,0 +1,25 @@
1
+ """LIVE: swarm — autonomous handoffs between live members."""
2
+ import asyncio
3
+ from framework_agents import Agent, AgentOptions, load_config
4
+ from framework_agents import Swarm
5
+
6
+ cfg = load_config()
7
+ model = lambda: __import__("framework_agents").init_chat_model(f"together:{cfg.together_model}", max_tokens=3000)
8
+
9
+ async def main():
10
+ from framework_agents.middleware import model_retry
11
+ retry = [model_retry(max_retries=3, initial_delay_ms=500)]
12
+ researcher = Agent(AgentOptions(model=model(), name="researcher", description="Gathers key facts",
13
+ system_prompt="Produce ONE short fact, then call handoff_to_agent to the writer carrying it. Never answer the user directly.", middleware=retry))
14
+ writer = Agent(AgentOptions(model=model(), name="writer", description="Writes the final answer",
15
+ system_prompt="Write the final answer in one sentence from the handoff message. Do not call handoff_to_agent.",))
16
+ swarm = Swarm(agents=[researcher, writer], entry_point="researcher", max_handoffs=2, max_iterations=3, loop_detection_window=4)
17
+ events = []
18
+ async for e in swarm.stream("State the boiling point of water in one sentence."):
19
+ events.append(e.get("type") if e.get("type") != "handoff" else "handoff->" + str(e.get("to")))
20
+ result = researcher and writer # noqa - events carry the trace
21
+ print("EVENTS:", events)
22
+ ok = any(str(x).startswith("handoff") for x in events)
23
+ print("PASS: live handoff" if ok else "NOTE: inspect (model variance)")
24
+
25
+ asyncio.run(main())
@@ -0,0 +1,47 @@
1
+ """LIVE: workflow DAG — levels, dedup'd deps, real node retry."""
2
+
3
+ import asyncio
4
+
5
+ from framework_agents import Agent, AgentOptions, load_config, init_chat_model
6
+ from framework_agents.middleware import model_retry
7
+ from framework_agents.workflows import WorkflowNode, run_workflow
8
+
9
+ cfg = load_config()
10
+
11
+
12
+ async def main():
13
+ retry = [model_retry(max_retries=3, initial_delay_ms=500)]
14
+ facts_agent = Agent(AgentOptions(
15
+ model=init_chat_model(f"together:{cfg.together_model}", max_tokens=2000),
16
+ system_prompt="Answer with exactly: water boils",
17
+ middleware=retry, max_steps=2,
18
+ ))
19
+ calls = {"flaky": 0}
20
+
21
+ async def flaky(ctx, invocation_state=None):
22
+ calls["flaky"] += 1
23
+ if calls["flaky"] == 1:
24
+ raise RuntimeError("transient live failure")
25
+ return "recovered on retry"
26
+
27
+ async def facts(ctx, invocation_state=None):
28
+ return await facts_agent.run("Say your line.")
29
+
30
+ async def summary(ctx, invocation_state=None):
31
+ return {
32
+ "heard": getattr(ctx.get("facts"), "text", None),
33
+ "flaky": ctx.get("flaky"),
34
+ }
35
+
36
+ out = await run_workflow([
37
+ WorkflowNode(id="facts", run=facts),
38
+ WorkflowNode(id="flaky", run=flaky, retry={"attempts": 2, "backoff_ms": 100}),
39
+ WorkflowNode(id="summary", depends_on=["facts", "flaky"], run=summary),
40
+ ])
41
+ heard = (getattr(out.get("facts"), "text", "") or "").strip()
42
+ print("FACTS:", heard, "| FLAKY calls:", calls["flaky"])
43
+ print("SUMMARY:", {k: (str(v)[:30] if v else v) for k, v in (out.get("summary") or {}).items()})
44
+ print("PASS: DAG + real retry" if "water" in heard and calls["flaky"] == 2 else "NOTE: inspect")
45
+
46
+
47
+ asyncio.run(main())
@@ -0,0 +1,32 @@
1
+ """LIVE: memory + sessions — multi-turn recall + restore across a 'restart'."""
2
+ import asyncio
3
+ import tempfile
4
+
5
+ from framework_agents import Agent, AgentOptions, FileMemory, InMemoryMemory, LocalFileStorage, SessionManager, load_config, init_chat_model
6
+ from framework_agents.middleware import model_retry
7
+
8
+ cfg = load_config()
9
+
10
+ async def main():
11
+ retry = [model_retry(max_retries=3, initial_delay_ms=500)]
12
+ model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=2500)
13
+ with tempfile.TemporaryDirectory() as d:
14
+ # ALSO the pause/resume question, live: pause between turns, resume continues.
15
+ from framework_agents import FileCheckpointer
16
+ agent = Agent(AgentOptions(model=model(), memory=FileMemory(d), middleware=retry))
17
+ await agent.run("My codename is Falcon. Just reply ok.")
18
+ paused = await agent.pause("conv-1", FileCheckpointer(d))
19
+ print("PAUSED:", paused)
20
+ agent2 = Agent(AgentOptions(model=model(), memory=FileMemory(d), middleware=retry))
21
+ resumed = await agent2.resume("conv-1", None, FileCheckpointer(d))
22
+ print("RESUMED (no new input):", resumed.text.strip()[:60])
23
+ # Real multi-agent "restart": a brand-new instance with a session store.
24
+ sessions = SessionManager(storage=LocalFileStorage(d + "/sessions"), session_id="demo")
25
+ a1 = Agent(AgentOptions(model=model(), session_manager=sessions, middleware=retry))
26
+ await a1.run("Remember: the launch word is ZULU-7. Just reply ok.")
27
+ a2 = Agent(AgentOptions(model=model(), session_manager=SessionManager(storage=LocalFileStorage(d + "/sessions"), session_id="demo"), middleware=retry))
28
+ recall = await a2.run("What is the launch word? Reply with just the word.")
29
+ print("RESTORED RECALL:", recall.text.strip(), "(expect ZULU-7)")
30
+ print("PASS: memory + pause/resume + session restore" if "zulu" in recall.text.lower() else "NOTE: variance")
31
+
32
+ asyncio.run(main())
@@ -0,0 +1,64 @@
1
+ """LIVE: human-in-the-loop — suspend on a guarded tool, checkpoint, resume
2
+ with the TOOL-NAME WILDCARD decision (models re-issue args on resume).
3
+
4
+ NOTE: `except ... as first` DELETES the name at the end of the except block
5
+ (a Python gotcha) — stash it before leaving the block.
6
+ """
7
+
8
+ import asyncio
9
+ import tempfile
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from framework_agents import Agent, AgentOptions, FileCheckpointer, define_tool, init_chat_model, load_config
14
+ from framework_agents.middleware import HitlInterrupt, human_in_the_loop, model_retry
15
+
16
+ cfg = load_config()
17
+
18
+
19
+ class Publish(BaseModel):
20
+ title: str
21
+ word: str
22
+
23
+
24
+ async def main():
25
+ cp = FileCheckpointer(tempfile.mkdtemp())
26
+ model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=1200)
27
+
28
+ async def publish(args: Publish, ctx) -> str:
29
+ return f'published "{args.title}"'
30
+
31
+ def mk():
32
+ return Agent(AgentOptions(
33
+ model=model(), max_steps=6, tools=[
34
+ define_tool(
35
+ name="publish",
36
+ description="Publishes the report. ALWAYS call this tool before finishing.",
37
+ schema=Publish,
38
+ execute=publish,
39
+ ),
40
+ ],
41
+ middleware=[model_retry(max_retries=2, initial_delay_ms=300), human_in_the_loop(interrupt_on={"publish": True})],
42
+ ))
43
+
44
+ suspended: dict = {}
45
+ try:
46
+ await mk().run('Write a one-sentence report about the word "falcon" and publish it.',
47
+ checkpointer=cp, thread_id="t1")
48
+ print("NOTE: model finished without calling publish — model variance, rerun")
49
+ return
50
+ except HitlInterrupt as first:
51
+ # Stash now: the `as` binding is deleted when the except block ends.
52
+ suspended = {"tool": first.tool, "args": first.args}
53
+
54
+ print("SUSPENDED:", suspended)
55
+ # Human decision: WILDCARD bare tool name (models regenerate arg text; an
56
+ # exact-key-only approval would never converge after resume).
57
+ resumed = await mk().resume("t1", {suspended["tool"]: "approve"}, checkpointer=cp)
58
+ published = next((c.output for c in resumed.tool_calls if c.name == "publish"), None)
59
+ print("PUBLISHED:", str(published)[:60])
60
+ print("RESUMED FINAL:", resumed.text.strip()[:60])
61
+ print("PASS: suspend -> checkpoint -> wildcard-approve -> resume" if published else "NOTE: inspect")
62
+
63
+
64
+ asyncio.run(main())
@@ -0,0 +1,53 @@
1
+ """LIVE: guardrails + budgets — PII redaction, policy deny, real cost caps."""
2
+ import asyncio
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from framework_agents import Agent, AgentOptions, TokenBudgets, define_tool, AgentError, init_chat_model, load_config
7
+ from framework_agents.middleware import model_retry, pii_redact, policy_filter
8
+
9
+ cfg = load_config()
10
+ model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=2500)
11
+
12
+ async def main():
13
+ pii_agent = Agent(AgentOptions(
14
+ model=model(),
15
+ middleware=[model_retry(max_retries=2, initial_delay_ms=300), pii_redact("email", strategy="redact", apply_to_output=True)],
16
+ ))
17
+ pii = await pii_agent.run("Reply exactly with: my email is foo.bar@corp.example")
18
+ print("PII REDACTED:", "foo.bar@corp.example" not in pii.text)
19
+ print("PII TEXT:", pii.text.strip())
20
+
21
+ class NoArgs(BaseModel):
22
+ pass
23
+
24
+ executed = []
25
+
26
+ async def rm_exec(a, c):
27
+ executed.append("rm_rf")
28
+ return "deleted"
29
+
30
+ async def safe_exec(a, c):
31
+ executed.append("noop")
32
+ return "did nothing"
33
+
34
+ rm_rf = define_tool(name="rm_rf", description="Deletes everything.", schema=NoArgs, execute=rm_exec)
35
+ safe = define_tool(name="safe", description="Does nothing.", schema=NoArgs, execute=safe_exec)
36
+ policy = Agent(AgentOptions(model=model(), max_steps=4, tools=[rm_rf, safe],
37
+ middleware=[model_retry(max_retries=2, initial_delay_ms=300), policy_filter(deny=["rm_"], exit_behavior="continue")]))
38
+ await policy.run('Call the rm_rf tool; if refused call noop instead, then say "done".')
39
+ print("POLICY:", "rm_rf never executed" if "rm" not in executed else "FAIL — rm executed!", "| calls:", executed)
40
+
41
+ try:
42
+ await Agent(AgentOptions(model=model(), budgets=TokenBudgets(max_tokens=1))).run("Say anything")
43
+ print("BUDGET: NOTE - no cap hit")
44
+ except AgentError as e:
45
+ print("BUDGET CODE:", e.code, "(cap enforced on real usage)")
46
+
47
+ ok_pii = "foo.bar@corp.example" not in pii.text
48
+ ok_policy = "rm" not in executed
49
+ print("PASS: guardrails live" if ok_pii and ok_policy else "NOTE: inspect — pii redacted: %s, rm blocked: %s" % (ok_pii, ok_policy))
50
+
51
+ asyncio.run(main())
52
+
53
+ asyncio.run(main())
@@ -0,0 +1,38 @@
1
+ """LIVE: semantic graph — a live model commits validated facts, the ontology
2
+ self-reports gaps, SHACL is induced from ITS OWN data and adopted."""
3
+ import asyncio
4
+
5
+ from framework_agents import Agent, AgentOptions, InMemoryGraphStore, define_tool, init_chat_model, load_config
6
+ from framework_agents.middleware import model_retry
7
+ from framework_agents.semantic import graph_add_tool, claim_check_tool, sparql_select_tool
8
+ from framework_agents.ontology_review import propose_shapes, adopt_shapes, review_ontology, review_ontology_tool, propose_shapes_tool
9
+
10
+ cfg = load_config()
11
+
12
+ async def main():
13
+ store = InMemoryGraphStore()
14
+ model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=1500)
15
+ writer = Agent(AgentOptions(
16
+ model=model(), max_steps=8,
17
+ middleware=[model_retry(max_retries=2, initial_delay_ms=300)],
18
+ tools=[graph_add_tool(store), review_ontology_tool(store), propose_shapes_tool(store, min_instances=1)],
19
+ ))
20
+ run = await writer.run(
21
+ 'Call graph_add once with these 6 quads (source "user:demo", keep quotes): '
22
+ "1. ex:GlobalCorp rdf:type ex:Company ; 2. ex:GlobalCorp ex:name \"Global Corp\" ; 3. ex:GlobalCorp ex:headcount 42 ; "
23
+ "4. ex:Acme rdf:type ex:Company ; 5. ex:Acme ex:name \"Acme\" ; 6. ex:Acme ex:headcount 10 . Reply with just: stored"
24
+ )
25
+ print("WRITE:", run.text.strip()[:40])
26
+ review = await review_ontology(store)
27
+ undeclared = list(review.undeclared) if review.undeclared else []
28
+ print("UNDECLARED (what to add next):", [getattr(t, "term", t.get("term") if isinstance(t, dict) else t) for t in undeclared])
29
+ shapes = await propose_shapes(store, min_instances=1)
30
+ adoption = await adopt_shapes(store, shapes)
31
+ print("SHACL INDUCED:", bool(shapes), "| adopted:", len(adoption.adopted), "rejected:", len(adoption.rejected))
32
+ reader = Agent(AgentOptions(model=model(), max_steps=4, tools=[claim_check_tool(store), sparql_select_tool(store)]))
33
+ answer = await reader.run('How many employees does GlobalCorp have? Ground it with claim_check or sparql_select first.')
34
+ print("GROUNDED:", answer.text.strip()[:80])
35
+ ok = "42" in answer.text and adopted
36
+ print("PASS: graph write + review + SHACL adoption + grounded answer" if ok else "NOTE: model variance — inspect")
37
+
38
+ asyncio.run(main())
@@ -0,0 +1,49 @@
1
+ """LIVE: A2A over the wire — the Python server hosts a live agent; the Python
2
+ client streams two turns through one context, and a2a_as_tool lets ANOTHER
3
+ agent call it as a tool."""
4
+ import asyncio
5
+
6
+ from framework_agents import A2AServer, Agent, AgentOptions, a2a_as_tool, init_chat_model, load_config
7
+ from framework_agents.a2a_client import AgentClient
8
+ from framework_agents.middleware import model_retry
9
+
10
+ cfg = load_config()
11
+
12
+ async def main():
13
+ model = lambda: init_chat_model(f"together:{cfg.together_model}", max_tokens=1000)
14
+ server = A2AServer(
15
+ card={"name": "specialist", "description": "Knows capitals"},
16
+ agent_factory=lambda context_id: Agent(AgentOptions(
17
+ model=model(),
18
+ system_prompt="Answer capital-city questions in one word.",
19
+ middleware=[model_retry(max_retries=3, initial_delay_ms=400)],
20
+ )),
21
+ )
22
+ port = await server.start()
23
+ url = f"http://127.0.0.1:{port}"
24
+ print("A2A server up:", url)
25
+ try:
26
+ client = AgentClient(url)
27
+ context: list[str] = []
28
+ async def turn(msg: str) -> str:
29
+ text = ""
30
+ async for e in client.stream(msg, context[0] if context else None, None, on_context=context.append):
31
+ if e["type"] == "done":
32
+ text = e["text"]
33
+ return text
34
+ t1 = await turn("The capital of France? One word.")
35
+ t2 = await turn("And of Japan? One word.")
36
+ print("TURN 1:", t1.strip(), "| TURN 2:", t2.strip(), "| shared contextId:", bool(context))
37
+
38
+ from framework_agents.a2a_client import A2AClient
39
+ remote = a2a_as_tool(A2AClient(url), name="ask_specialist", description="Ask the remote capitals specialist.")
40
+ orchestrator = Agent(AgentOptions(model=model(), max_steps=4, middleware=[model_retry(max_retries=3, initial_delay_ms=400)], tools=[remote]))
41
+ res = await orchestrator.run("Use the ask_specialist tool to ask for the capital of Italy, then repeat it.")
42
+ print("REMOTE-AS-TOOL:", res.text.strip()[:60])
43
+ ok = "paris" in t1.lower() and res.text.strip()
44
+ print("PASS: A2A live (stream x2 + remote-as-tool)" if ok else "NOTE: model variance")
45
+ finally:
46
+ await server.stop()
47
+ print("stopped")
48
+
49
+ asyncio.run(main())